Merge commit '863052b53e3051bb7e0fd4904d348fa294835821'

This commit is contained in:
drone 2022-09-29 13:16:56 +00:00
commit 3a90d17649
10 changed files with 491 additions and 435 deletions

View file

@ -42,7 +42,7 @@ android {
namespace 'com.nextcloud.talk'
defaultConfig {
minSdkVersion 21
minSdkVersion 23
targetSdkVersion 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View file

@ -1,371 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Andy Scherzinger
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters.items;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.view.View;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.interfaces.DraweeController;
import com.nextcloud.talk.R;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.data.user.model.User;
import com.nextcloud.talk.databinding.RvItemConversationWithLastMessageBinding;
import com.nextcloud.talk.models.json.chat.ChatMessage;
import com.nextcloud.talk.models.json.conversations.Conversation;
import com.nextcloud.talk.models.json.status.Status;
import com.nextcloud.talk.ui.StatusDrawable;
import com.nextcloud.talk.ui.theme.ViewThemeUtils;
import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.DisplayUtils;
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import androidx.core.content.ContextCompat;
import androidx.core.content.res.ResourcesCompat;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
import eu.davidea.flexibleadapter.items.IFilterable;
import eu.davidea.flexibleadapter.items.IFlexible;
import eu.davidea.flexibleadapter.items.ISectionable;
import eu.davidea.viewholders.FlexibleViewHolder;
public class ConversationItem extends AbstractFlexibleItem<ConversationItem.ConversationItemViewHolder> implements
ISectionable<ConversationItem.ConversationItemViewHolder, GenericTextHeaderItem>, IFilterable<String> {
public static final int VIEW_TYPE = R.layout.rv_item_conversation_with_last_message;
private static final float STATUS_SIZE_IN_DP = 9f;
private final Conversation conversation;
private final User user;
private final Context context;
private GenericTextHeaderItem header;
private final Status status;
private final ViewThemeUtils viewThemeUtils;
public ConversationItem(Conversation conversation, User user, Context activityContext, Status status, final ViewThemeUtils viewThemeUtils) {
this.conversation = conversation;
this.user = user;
this.context = activityContext;
this.status = status;
this.viewThemeUtils = viewThemeUtils;
}
public ConversationItem(Conversation conversation, User user,
Context activityContext, GenericTextHeaderItem genericTextHeaderItem, Status status,
final ViewThemeUtils viewThemeUtils) {
this(conversation, user, activityContext, status, viewThemeUtils);
this.header = genericTextHeaderItem;
}
@Override
public boolean equals(Object o) {
if (o instanceof ConversationItem) {
ConversationItem inItem = (ConversationItem) o;
return conversation.equals(inItem.getModel()) && Objects.equals(status, inItem.status);
}
return false;
}
public Conversation getModel() {
return conversation;
}
@Override
public int hashCode() {
int result = conversation.hashCode();
result = 31 * result + (status != null ? status.hashCode() : 0);
return result;
}
@Override
public int getLayoutRes() {
return R.layout.rv_item_conversation_with_last_message;
}
@Override
public int getItemViewType() {
return VIEW_TYPE;
}
@Override
public ConversationItemViewHolder createViewHolder(View view, FlexibleAdapter<IFlexible> adapter) {
return new ConversationItemViewHolder(view, adapter);
}
@SuppressLint("SetTextI18n")
@Override
public void bindViewHolder(FlexibleAdapter<IFlexible> adapter,
ConversationItemViewHolder holder,
int position,
List<Object> payloads) {
Context appContext =
NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext();
holder.binding.dialogAvatar.setController(null);
holder.binding.dialogName.setTextColor(ResourcesCompat.getColor(context.getResources(),
R.color.conversation_item_header,
null));
if (adapter.hasFilter()) {
viewThemeUtils.platform.highlightText(holder.binding.dialogName,
conversation.getDisplayName(),
String.valueOf(adapter.getFilter(String.class)));
} else {
holder.binding.dialogName.setText(conversation.getDisplayName());
}
if (conversation.getUnreadMessages() > 0) {
holder.binding.dialogName.setTypeface(holder.binding.dialogName.getTypeface(), Typeface.BOLD);
holder.binding.dialogLastMessage.setTypeface(holder.binding.dialogLastMessage.getTypeface(), Typeface.BOLD);
holder.binding.dialogUnreadBubble.setVisibility(View.VISIBLE);
if (conversation.getUnreadMessages() < 1000) {
holder.binding.dialogUnreadBubble.setText(Long.toString(conversation.getUnreadMessages()));
} else {
holder.binding.dialogUnreadBubble.setText(R.string.tooManyUnreadMessages);
}
ColorStateList lightBubbleFillColor = ColorStateList.valueOf(
ContextCompat.getColor(context,
R.color.conversation_unread_bubble));
int lightBubbleTextColor = ContextCompat.getColor(
context,
R.color.conversation_unread_bubble_text);
if (conversation.getType() == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble);
} else if (conversation.getUnreadMention()) {
if (CapabilitiesUtilNew.hasSpreedFeatureCapability(user, "direct-mention-flag")) {
if (conversation.getUnreadMentionDirect()) {
viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble);
} else {
viewThemeUtils.material.colorChipOutlined(holder.binding.dialogUnreadBubble, 6.0f);
}
} else {
viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble);
}
} else {
holder.binding.dialogUnreadBubble.setChipBackgroundColor(lightBubbleFillColor);
holder.binding.dialogUnreadBubble.setTextColor(lightBubbleTextColor);
}
} else {
holder.binding.dialogName.setTypeface(null, Typeface.NORMAL);
holder.binding.dialogDate.setTypeface(null, Typeface.NORMAL);
holder.binding.dialogLastMessage.setTypeface(null, Typeface.NORMAL);
holder.binding.dialogUnreadBubble.setVisibility(View.GONE);
}
if (conversation.getFavorite()) {
holder.binding.favoriteConversationImageView.setVisibility(View.VISIBLE);
} else {
holder.binding.favoriteConversationImageView.setVisibility(View.GONE);
}
if (status != null && Conversation.ConversationType.ROOM_SYSTEM != conversation.getType()) {
float size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, appContext);
holder.binding.userStatusImage.setVisibility(View.VISIBLE);
holder.binding.userStatusImage.setImageDrawable(new StatusDrawable(
status.getStatus(),
status.getIcon(),
size,
context.getResources().getColor(R.color.bg_default),
appContext));
} else {
holder.binding.userStatusImage.setVisibility(View.GONE);
}
if (conversation.getLastMessage() != null) {
holder.binding.dialogDate.setVisibility(View.VISIBLE);
holder.binding.dialogDate.setText(
DateUtils.getRelativeTimeSpanString(conversation.getLastActivity() * 1000L,
System.currentTimeMillis(),
0,
DateUtils.FORMAT_ABBREV_RELATIVE));
if (!TextUtils.isEmpty(conversation.getLastMessage().getSystemMessage()) ||
Conversation.ConversationType.ROOM_SYSTEM == conversation.getType()) {
holder.binding.dialogLastMessage.setText(conversation.getLastMessage().getText());
} else {
String authorDisplayName = "";
conversation.getLastMessage().setActiveUser(user);
String text;
if (conversation.getLastMessage().getCalculateMessageType() == ChatMessage.MessageType.REGULAR_TEXT_MESSAGE) {
if (conversation.getLastMessage().getActorId().equals(user.getUserId())) {
text = String.format(appContext.getString(R.string.nc_formatted_message_you),
conversation.getLastMessage().getLastMessageDisplayText());
} else {
authorDisplayName = !TextUtils.isEmpty(conversation.getLastMessage().getActorDisplayName()) ?
conversation.getLastMessage().getActorDisplayName() :
"guests".equals(conversation.getLastMessage().getActorType()) ?
appContext.getString(R.string.nc_guest) : "";
text = String.format(appContext.getString(R.string.nc_formatted_message),
authorDisplayName,
conversation.getLastMessage().getLastMessageDisplayText());
}
} else {
text = conversation.getLastMessage().getLastMessageDisplayText();
}
holder.binding.dialogLastMessage.setText(text);
}
} else {
holder.binding.dialogDate.setVisibility(View.GONE);
holder.binding.dialogLastMessage.setText(R.string.nc_no_messages_yet);
}
holder.binding.dialogAvatar.setVisibility(View.VISIBLE);
boolean shouldLoadAvatar = true;
String objectType;
if (!TextUtils.isEmpty(objectType = conversation.getObjectType())) {
switch (objectType) {
case "share:password":
shouldLoadAvatar = false;
holder.binding.dialogAvatar.setImageDrawable(
ContextCompat.getDrawable(context,
R.drawable.ic_circular_lock));
break;
case "file":
shouldLoadAvatar = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
holder.binding.dialogAvatar.setImageDrawable(
DisplayUtils.getRoundedDrawable(
viewThemeUtils.talk.themePlaceholderAvatar(holder.binding.dialogAvatar,
R.drawable.ic_avatar_document)));
} else {
holder.binding.dialogAvatar.setImageDrawable(
ContextCompat.getDrawable(context, R.drawable.ic_circular_document));
}
break;
default:
break;
}
}
if (Conversation.ConversationType.ROOM_SYSTEM.equals(conversation.getType())) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Drawable[] layers = new Drawable[2];
layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background);
layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground);
LayerDrawable layerDrawable = new LayerDrawable(layers);
holder.binding.dialogAvatar.getHierarchy().setPlaceholderImage(
DisplayUtils.getRoundedDrawable(layerDrawable));
} else {
holder.binding.dialogAvatar.getHierarchy().setPlaceholderImage(R.mipmap.ic_launcher);
}
shouldLoadAvatar = false;
}
if (shouldLoadAvatar) {
switch (conversation.getType()) {
case ROOM_TYPE_ONE_TO_ONE_CALL:
if (!TextUtils.isEmpty(conversation.getName())) {
DraweeController draweeController = Fresco.newDraweeControllerBuilder()
.setOldController(holder.binding.dialogAvatar.getController())
.setAutoPlayAnimations(true)
.setImageRequest(DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForAvatar(user.getBaseUrl(),
conversation.getName(),
true),
user))
.build();
holder.binding.dialogAvatar.setController(draweeController);
} else {
holder.binding.dialogAvatar.setVisibility(View.GONE);
}
break;
case ROOM_GROUP_CALL:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
holder.binding.dialogAvatar.setImageDrawable(
DisplayUtils.getRoundedDrawable(
viewThemeUtils.talk.themePlaceholderAvatar(holder.binding.dialogAvatar,
R.drawable.ic_avatar_group)));
} else {
holder.binding.dialogAvatar.setImageDrawable(
ContextCompat.getDrawable(context, R.drawable.ic_circular_group));
}
break;
case ROOM_PUBLIC_CALL:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
holder.binding.dialogAvatar.setImageDrawable(
DisplayUtils.getRoundedDrawable(
viewThemeUtils.talk.themePlaceholderAvatar(holder.binding.dialogAvatar,
R.drawable.ic_avatar_link)));
} else {
holder.binding.dialogAvatar.setImageDrawable(
ContextCompat.getDrawable(context, R.drawable.ic_circular_link));
}
break;
default:
holder.binding.dialogAvatar.setVisibility(View.GONE);
}
}
}
@Override
public boolean filter(String constraint) {
return conversation.getDisplayName() != null &&
Pattern
.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
.matcher(conversation.getDisplayName().trim())
.find();
}
@Override
public GenericTextHeaderItem getHeader() {
return header;
}
@Override
public void setHeader(GenericTextHeaderItem header) {
this.header = header;
}
static class ConversationItemViewHolder extends FlexibleViewHolder {
RvItemConversationWithLastMessageBinding binding;
ConversationItemViewHolder(View view, FlexibleAdapter adapter) {
super(view, adapter);
binding = RvItemConversationWithLastMessageBinding.bind(view);
}
}
}

View file

@ -0,0 +1,352 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Andy Scherzinger
* @author Marcel Hibbe
* @author Tim Krüger
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters.items
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Build
import android.text.TextUtils
import android.text.format.DateUtils
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.items.ConversationItem.ConversationItemViewHolder
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.RvItemConversationWithLastMessageBinding
import com.nextcloud.talk.extensions.loadAvatar
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.Conversation.ConversationType
import com.nextcloud.talk.models.json.status.Status
import com.nextcloud.talk.ui.StatusDrawable
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFilterable
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.ISectionable
import eu.davidea.viewholders.FlexibleViewHolder
import java.util.regex.Pattern
class ConversationItem(
val model: Conversation,
private val user: User,
private val context: Context,
private val status: Status?,
private val viewThemeUtils: ViewThemeUtils
) : AbstractFlexibleItem<ConversationItemViewHolder>(),
ISectionable<ConversationItemViewHolder, GenericTextHeaderItem?>,
IFilterable<String?> {
private var header: GenericTextHeaderItem? = null
constructor(
conversation: Conversation,
user: User,
activityContext: Context,
genericTextHeaderItem: GenericTextHeaderItem?,
status: Status?,
viewThemeUtils: ViewThemeUtils
) : this(conversation, user, activityContext, status, viewThemeUtils) {
header = genericTextHeaderItem
}
override fun equals(other: Any?): Boolean {
if (other is ConversationItem) {
return model == other.model && status == other.status
}
return false
}
override fun hashCode(): Int {
var result = model.hashCode()
result = 31 * result + (status?.hashCode() ?: 0)
return result
}
override fun getLayoutRes(): Int {
return R.layout.rv_item_conversation_with_last_message
}
override fun getItemViewType(): Int {
return VIEW_TYPE
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>?>?): ConversationItemViewHolder {
return ConversationItemViewHolder(view, adapter)
}
@SuppressLint("SetTextI18n")
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<*>?>,
holder: ConversationItemViewHolder,
position: Int,
payloads: List<Any>
) {
val appContext = sharedApplication!!.applicationContext
holder.binding.dialogName.setTextColor(
ResourcesCompat.getColor(
context.resources,
R.color.conversation_item_header,
null
)
)
if (adapter.hasFilter()) {
viewThemeUtils.platform.highlightText(
holder.binding.dialogName,
model.displayName!!, adapter.getFilter(String::class.java).toString()
)
} else {
holder.binding.dialogName.text = model.displayName
}
if (model.unreadMessages > 0) {
holder.binding.dialogName.setTypeface(holder.binding.dialogName.typeface, Typeface.BOLD)
holder.binding.dialogLastMessage.setTypeface(holder.binding.dialogLastMessage.typeface, Typeface.BOLD)
holder.binding.dialogUnreadBubble.visibility = View.VISIBLE
if (model.unreadMessages < 1000) {
holder.binding.dialogUnreadBubble.text = model.unreadMessages.toLong().toString()
} else {
holder.binding.dialogUnreadBubble.setText(R.string.tooManyUnreadMessages)
}
val lightBubbleFillColor = ColorStateList.valueOf(
ContextCompat.getColor(
context,
R.color.conversation_unread_bubble
)
)
val lightBubbleTextColor = ContextCompat.getColor(
context,
R.color.conversation_unread_bubble_text
)
if (model.type === ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
} else if (model.unreadMention) {
if (hasSpreedFeatureCapability(user, "direct-mention-flag")) {
if (model.unreadMentionDirect!!) {
viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
} else {
viewThemeUtils.material.colorChipOutlined(holder.binding.dialogUnreadBubble, 6.0f)
}
} else {
viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
}
} else {
holder.binding.dialogUnreadBubble.chipBackgroundColor = lightBubbleFillColor
holder.binding.dialogUnreadBubble.setTextColor(lightBubbleTextColor)
}
} else {
holder.binding.dialogName.setTypeface(null, Typeface.NORMAL)
holder.binding.dialogDate.setTypeface(null, Typeface.NORMAL)
holder.binding.dialogLastMessage.setTypeface(null, Typeface.NORMAL)
holder.binding.dialogUnreadBubble.visibility = View.GONE
}
if (model.favorite) {
holder.binding.favoriteConversationImageView.visibility = View.VISIBLE
} else {
holder.binding.favoriteConversationImageView.visibility = View.GONE
}
if (status != null && ConversationType.ROOM_SYSTEM !== model.type) {
val size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, appContext)
holder.binding.userStatusImage.visibility = View.VISIBLE
holder.binding.userStatusImage.setImageDrawable(
StatusDrawable(
status.status,
status.icon,
size,
context.resources.getColor(R.color.bg_default),
appContext
)
)
} else {
holder.binding.userStatusImage.visibility = View.GONE
}
if (model.lastMessage != null) {
holder.binding.dialogDate.visibility = View.VISIBLE
holder.binding.dialogDate.text = DateUtils.getRelativeTimeSpanString(
model.lastActivity * 1000L,
System.currentTimeMillis(),
0,
DateUtils.FORMAT_ABBREV_RELATIVE
)
if (!TextUtils.isEmpty(model.lastMessage!!.systemMessage) ||
ConversationType.ROOM_SYSTEM === model.type
) {
holder.binding.dialogLastMessage.text = model.lastMessage!!.text
} else {
model.lastMessage!!.activeUser = user
val text: String
if (model.lastMessage!!.getCalculateMessageType() === ChatMessage.MessageType.REGULAR_TEXT_MESSAGE) {
if (model.lastMessage!!.actorId == user.userId) {
text = String.format(
appContext.getString(R.string.nc_formatted_message_you),
model.lastMessage!!.lastMessageDisplayText
)
} else {
val authorDisplayName =
if (!TextUtils.isEmpty(model.lastMessage!!.actorDisplayName)) {
model.lastMessage!!.actorDisplayName
} else if ("guests" == model.lastMessage!!.actorType) {
appContext.getString(R.string.nc_guest)
} else {
""
}
text = String.format(
appContext.getString(R.string.nc_formatted_message),
authorDisplayName,
model.lastMessage!!.lastMessageDisplayText
)
}
} else {
text = model.lastMessage!!.lastMessageDisplayText
}
holder.binding.dialogLastMessage.text = text
}
} else {
holder.binding.dialogDate.visibility = View.GONE
holder.binding.dialogLastMessage.setText(R.string.nc_no_messages_yet)
}
holder.binding.dialogAvatar.visibility = View.VISIBLE
var shouldLoadAvatar = true
var objectType: String?
if (!TextUtils.isEmpty(model.objectType.also { objectType = it })) {
when (objectType) {
"share:password" -> {
shouldLoadAvatar = false
holder.binding.dialogAvatar.setImageDrawable(
ContextCompat.getDrawable(
context,
R.drawable.ic_circular_lock
)
)
}
"file" -> {
shouldLoadAvatar = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
holder.binding.dialogAvatar.setImageDrawable(
DisplayUtils.getRoundedDrawable(
viewThemeUtils.talk.themePlaceholderAvatar(
holder.binding.dialogAvatar,
R.drawable.ic_avatar_document
)
)
)
} else {
holder.binding.dialogAvatar.setImageDrawable(
ContextCompat.getDrawable(context, R.drawable.ic_circular_document)
)
}
}
else -> {}
}
}
if (ConversationType.ROOM_SYSTEM == model.type) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val layers = arrayOfNulls<Drawable>(2)
layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
val layerDrawable = LayerDrawable(layers)
holder.binding.dialogAvatar.loadAvatar(DisplayUtils.getRoundedDrawable(layerDrawable))
} else {
holder.binding.dialogAvatar.loadAvatar(R.mipmap.ic_launcher)
}
shouldLoadAvatar = false
}
if (shouldLoadAvatar) {
when (model.type) {
ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty(model.name)) {
holder.binding.dialogAvatar.loadAvatar(user, model.name!!)
} else {
holder.binding.dialogAvatar.visibility = View.GONE
}
ConversationType.ROOM_GROUP_CALL -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
holder.binding.dialogAvatar.setImageDrawable(
DisplayUtils.getRoundedDrawable(
viewThemeUtils.talk.themePlaceholderAvatar(
holder.binding.dialogAvatar,
R.drawable.ic_avatar_group
)
)
)
} else {
holder.binding.dialogAvatar.setImageDrawable(
ContextCompat.getDrawable(context, R.drawable.ic_circular_group)
)
}
ConversationType.ROOM_PUBLIC_CALL -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
holder.binding.dialogAvatar.setImageDrawable(
DisplayUtils.getRoundedDrawable(
viewThemeUtils.talk.themePlaceholderAvatar(
holder.binding.dialogAvatar,
R.drawable.ic_avatar_link
)
)
)
} else {
holder.binding.dialogAvatar.setImageDrawable(
ContextCompat.getDrawable(context, R.drawable.ic_circular_link)
)
}
else -> holder.binding.dialogAvatar.visibility = View.GONE
}
}
}
override fun filter(constraint: String?): Boolean {
return model.displayName != null &&
Pattern
.compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
.matcher(model.displayName!!.trim { it <= ' ' })
.find()
}
override fun getHeader(): GenericTextHeaderItem? {
return header
}
override fun setHeader(header: GenericTextHeaderItem?) {
this.header = header
}
class ConversationItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) {
var binding: RvItemConversationWithLastMessageBinding
init {
binding = RvItemConversationWithLastMessageBinding.bind(view!!)
}
}
companion object {
const val VIEW_TYPE = R.layout.rv_item_conversation_with_last_message
private const val STATUS_SIZE_IN_DP = 9f
}
}

View file

@ -2,6 +2,8 @@
* Nextcloud Talk application
*
* @author Álvaro Brey
* @author Tim Krüger
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Nextcloud GmbH
*
@ -27,9 +29,9 @@ import androidx.recyclerview.widget.RecyclerView
import com.nextcloud.talk.R
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.RvItemSearchMessageBinding
import com.nextcloud.talk.extensions.loadThumbnail
import com.nextcloud.talk.models.domain.SearchMessageEntry
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.DisplayUtils
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFilterable
@ -72,7 +74,7 @@ data class MessageResultItem constructor(
) {
holder.binding.conversationTitle.text = messageEntry.title
bindMessageExcerpt(holder)
loadImage(holder)
holder.binding.thumbnail.loadThumbnail(messageEntry.thumbnailURL, currentUser)
}
private fun bindMessageExcerpt(holder: ViewHolder) {
@ -83,17 +85,6 @@ data class MessageResultItem constructor(
)
}
private fun loadImage(holder: ViewHolder) {
DisplayUtils.loadAvatarPlaceholder(holder.binding.thumbnail)
if (messageEntry.thumbnailURL != null) {
val imageRequest = DisplayUtils.getImageRequestForUrl(
messageEntry.thumbnailURL,
currentUser
)
DisplayUtils.loadImage(holder.binding.thumbnail, imageRequest)
}
}
override fun filter(constraint: String?): Boolean = true
override fun getItemViewType(): Int {

View file

@ -587,16 +587,16 @@ class ConversationsListController(bundle: Bundle) :
if (activity != null) {
val conversationItem = ConversationItem(
conversation,
currentUser,
activity,
currentUser!!,
activity!!,
userStatuses[conversation.name],
viewThemeUtils
)
conversationItems.add(conversationItem)
val conversationItemWithHeader = ConversationItem(
conversation,
currentUser,
activity,
currentUser!!,
activity!!,
callHeaderItems[headerTitle],
userStatuses[conversation.name],
viewThemeUtils
@ -659,8 +659,8 @@ class ConversationsListController(bundle: Bundle) :
}
val conversationItem = ConversationItem(
conversation,
currentUser,
activity,
currentUser!!,
activity!!,
callHeaderItems[headerTitle],
userStatuses[conversation.name],
viewThemeUtils

View file

@ -0,0 +1,98 @@
/*
* Nextcloud Talk application
*
* @author Tim Krüger
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.extensions
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Build
import android.widget.ImageView
import androidx.core.content.ContextCompat
import coil.load
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import com.nextcloud.talk.R
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.utils.ApiUtils
fun ImageView.loadAvatar(user: User, avatar: String): io.reactivex.disposables.Disposable {
val imageRequestUri = ApiUtils.getUrlForAvatar(
user.baseUrl,
avatar,
true
)
return DisposableWrapper(
load(imageRequestUri) {
addHeader(
"Authorization",
ApiUtils.getCredentials(user.username, user.token)
)
transformations(CircleCropTransformation())
}
)
}
fun ImageView.loadThumbnail(url: String?, user: User): io.reactivex.disposables.Disposable {
val requestBuilder = ImageRequest.Builder(context)
.data(url)
.crossfade(true)
.target(this)
.transformations(CircleCropTransformation())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val layers = arrayOfNulls<Drawable>(2)
layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
requestBuilder.placeholder(LayerDrawable(layers))
} else {
requestBuilder.placeholder(R.mipmap.ic_launcher)
}
if (url != null &&
url.startsWith(user.baseUrl!!) &&
(url.contains("index.php/core/preview?fileId=") || url.contains("/avatar/"))
) {
requestBuilder.addHeader(
"Authorization",
ApiUtils.getCredentials(user.username, user.token)
)
}
return DisposableWrapper(load(requestBuilder.build()))
}
fun ImageView.loadAvatar(any: Any?): io.reactivex.disposables.Disposable {
return DisposableWrapper(load(any))
}
private class DisposableWrapper(private val disposable: coil.request.Disposable) : io.reactivex.disposables
.Disposable {
override fun dispose() {
disposable.dispose()
}
override fun isDisposed(): Boolean {
return disposable.isDisposed
}
}

View file

@ -32,6 +32,11 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.BitmapDrawable;
@ -164,21 +169,28 @@ public class DisplayUtils {
}
}
public static Bitmap roundBitmap(Bitmap bitmap) {
Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(output);
final Paint paint = new Paint();
final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
final RectF rectF = new RectF(rect);
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
canvas.drawOval(rectF, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint);
return output;
}
public static Drawable getRoundedDrawable(Drawable drawable) {
Bitmap bitmap = getBitmap(drawable);
new RoundAsCirclePostprocessor(true).process(bitmap);
return new BitmapDrawable(bitmap);
}
public static Bitmap getRoundedBitmapFromVectorDrawableResource(Resources resources, int resource) {
VectorDrawable vectorDrawable = (VectorDrawable) ResourcesCompat.getDrawable(resources, resource, null);
Bitmap bitmap = getBitmap(vectorDrawable);
new RoundPostprocessor(true).process(bitmap);
return bitmap;
}
public static Drawable getRoundedBitmapDrawableFromVectorDrawableResource(Resources resources, int resource) {
return new BitmapDrawable(getRoundedBitmapFromVectorDrawableResource(resources, resource));
return new BitmapDrawable(roundBitmap(bitmap));
}
public static Bitmap getBitmap(Drawable drawable) {
@ -605,30 +617,6 @@ public class DisplayUtils {
avatarImageView.setController(draweeController);
}
public static void loadAvatarPlaceholder(final SimpleDraweeView targetView) {
final Context context = targetView.getContext();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Drawable[] layers = new Drawable[2];
layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background);
layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground);
LayerDrawable layerDrawable = new LayerDrawable(layers);
targetView.getHierarchy().setPlaceholderImage(
DisplayUtils.getRoundedDrawable(layerDrawable));
} else {
targetView.getHierarchy().setPlaceholderImage(R.mipmap.ic_launcher);
}
}
public static void loadImage(final SimpleDraweeView targetView, final ImageRequest imageRequest) {
final DraweeController newController = Fresco.newDraweeControllerBuilder()
.setOldController(targetView.getController())
.setAutoPlayAnimations(true)
.setImageRequest(imageRequest)
.build();
targetView.setController(newController);
}
public static @StringRes
int getSortOrderStringId(FileSortOrder sortOrder) {
switch (sortOrder.getName()) {

View file

@ -38,7 +38,6 @@ import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSources
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.image.CloseableBitmap
import com.facebook.imagepipeline.postprocessors.RoundAsCirclePostprocessor
import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R
import com.nextcloud.talk.data.user.model.User
@ -334,10 +333,7 @@ object NotificationUtils {
val closeableImageRef = DataSources.waitForFinalResult(dataSource) as CloseableReference<CloseableBitmap>?
val bitmap = closeableImageRef?.get()?.underlyingBitmap
if (bitmap != null) {
// According to Fresco documentation a copy of the bitmap should be made before closing the references.
// However, it seems to work without making a copy... ;-)
RoundAsCirclePostprocessor(true).process(bitmap)
avatarIcon = IconCompat.createWithBitmap(bitmap)
avatarIcon = IconCompat.createWithBitmap(DisplayUtils.roundBitmap(bitmap))
}
CloseableReference.closeSafely(closeableImageRef)
dataSource.close()

View file

@ -38,7 +38,7 @@
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/double_margin_between_elements">
<com.facebook.drawee.view.SimpleDraweeView
<ImageView
android:id="@id/dialogAvatar"
android:layout_width="@dimen/small_item_height"
android:layout_height="@dimen/small_item_height"

View file

@ -3,6 +3,8 @@
~
~ @author Mario Danic
~ @author Andy Scherzinger
~ @author Tim Krüger
~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
~
@ -35,7 +37,7 @@
android:layout_margin="@dimen/double_margin_between_elements"
tools:background="@color/white">
<com.facebook.drawee.view.SimpleDraweeView
<ImageView
android:id="@+id/thumbnail"
android:layout_width="@dimen/small_item_height"
android:layout_height="@dimen/small_item_height"