From 870ef03d61e204e5d9055b138d1524103c1c1547 Mon Sep 17 00:00:00 2001 From: Julius Linus Date: Fri, 8 Mar 2024 11:21:09 -0600 Subject: [PATCH 1/6] Federated Mentions - Federated mention chip - Federated message avatars - Helper functions Signed-off-by: Julius Linus --- .../items/MentionAutocompleteItem.java | 58 ++++++++++++++----- .../IncomingLinkPreviewMessageViewHolder.kt | 3 + .../IncomingLocationMessageViewHolder.kt | 3 + .../messages/IncomingPollMessageViewHolder.kt | 3 + .../messages/IncomingTextMessageViewHolder.kt | 3 + .../IncomingVoiceMessageViewHolder.kt | 3 + .../messages/PreviewMessageViewHolder.kt | 3 + .../MentionAutocompleteCallback.java | 21 ++++--- .../com/nextcloud/talk/chat/ChatActivity.kt | 2 + .../talk/extensions/ImageViewExtensions.kt | 42 ++++++++++++++ .../talk/models/json/chat/ChatMessage.kt | 4 +- .../talk/models/json/mention/Mention.kt | 9 ++- .../MentionAutocompletePresenter.java | 6 ++ .../java/com/nextcloud/talk/utils/ApiUtils.kt | 13 +++++ .../nextcloud/talk/utils/DisplayUtils.java | 31 +++++++--- .../talk/utils/message/MessageUtils.kt | 13 ++++- 16 files changed, 181 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java index 62cc33510..610758002 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java @@ -58,8 +58,10 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem= Build.VERSION_CODES.O) { - ImageViewExtensionsKt.loadUserAvatar( - holder.binding.avatarView, - viewThemeUtils.talk.themePlaceholderAvatar( + String avatarId = objectId; + switch (source) { + case SOURCE_CALLS: {} + case SOURCE_GROUPS: { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ImageViewExtensionsKt.loadUserAvatar( holder.binding.avatarView, - R.drawable.ic_avatar_group - ) - ); - } else { - ImageViewExtensionsKt.loadUserAvatar(holder.binding.avatarView, R.drawable.ic_circular_group); + viewThemeUtils.talk.themePlaceholderAvatar( + holder.binding.avatarView, + R.drawable.ic_avatar_group)); + } else { + ImageViewExtensionsKt.loadUserAvatar(holder.binding.avatarView, R.drawable.ic_circular_group); + } + break; } - } else { - String avatarId = objectId; - if (SOURCE_GUESTS.equals(source)) { + case SOURCE_FEDERATION: { + int darkTheme = (DisplayUtils.isDarkModeOn(this.context))? 1 : 0; + ImageViewExtensionsKt.loadFederatedUserAvatar(holder.binding.avatarView, + currentUser, + Objects.requireNonNull(currentUser.getBaseUrl()), + roomToken, + avatarId, + darkTheme, + true, + false); + break; + } + case SOURCE_GUESTS: { avatarId = displayName; } - ImageViewExtensionsKt.loadUserAvatar(holder.binding.avatarView, currentUser, avatarId, true, false); + default: { + ImageViewExtensionsKt.loadUserAvatar(holder.binding.avatarView, currentUser, avatarId, true, false); + } } drawStatus(holder); diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt index ed2662ff9..713361aff 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt @@ -37,6 +37,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA import com.nextcloud.talk.databinding.ItemCustomIncomingLinkPreviewMessageBinding import com.nextcloud.talk.extensions.loadBotsAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar +import com.nextcloud.talk.extensions.loadFederatedUserAvatar import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils @@ -172,6 +173,8 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) : binding.messageUserAvatar.loadChangelogBotAvatar() } else if (message.actorType == "bots") { binding.messageUserAvatar.loadBotsAvatar() + } else if (message.actorType == "federated_users" && message.messageParameters?.get("actor") != null) { + binding.messageUserAvatar.loadFederatedUserAvatar(message) } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt index aa9ab400c..6185aa8e9 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt @@ -47,6 +47,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding import com.nextcloud.talk.extensions.loadBotsAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar +import com.nextcloud.talk.extensions.loadFederatedUserAvatar import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils @@ -148,6 +149,8 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : binding.messageUserAvatar.loadChangelogBotAvatar() } else if (message.actorType == "bots") { binding.messageUserAvatar.loadBotsAvatar() + } else if (message.actorType == "federated_users" && message.messageParameters?.get("actor") != null) { + binding.messageUserAvatar.loadFederatedUserAvatar(message) } } else { if (message.isOneToOneConversation || message.isFormerOneToOneConversation) { diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt index a3ee9c61e..becb6858b 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt @@ -36,6 +36,7 @@ import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.databinding.ItemCustomIncomingPollMessageBinding import com.nextcloud.talk.extensions.loadBotsAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar +import com.nextcloud.talk.extensions.loadFederatedUserAvatar import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.polls.ui.PollMainDialogFragment import com.nextcloud.talk.ui.theme.ViewThemeUtils @@ -179,6 +180,8 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) : binding.messageUserAvatar.loadChangelogBotAvatar() } else if (message.actorType == "bots") { binding.messageUserAvatar.loadBotsAvatar() + } else if (message.actorType == "federated_users" && message.messageParameters?.get("actor") != null) { + binding.messageUserAvatar.loadFederatedUserAvatar(message) } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt index 1bfb832df..bb0b3d018 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt @@ -41,6 +41,7 @@ import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.databinding.ItemCustomIncomingTextMessageBinding import com.nextcloud.talk.extensions.loadBotsAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar +import com.nextcloud.talk.extensions.loadFederatedUserAvatar import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils @@ -182,6 +183,8 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : binding.messageUserAvatar.loadChangelogBotAvatar() } else if (message.actorType == "bots") { binding.messageUserAvatar.loadBotsAvatar() + } else if (message.actorType == "federated_users" && message.messageParameters?.get("actor") != null) { + binding.messageUserAvatar.loadFederatedUserAvatar(message) } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt index e09bae1fb..929f54407 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt @@ -45,6 +45,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding import com.nextcloud.talk.extensions.loadChangelogBotAvatar +import com.nextcloud.talk.extensions.loadFederatedUserAvatar import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils @@ -285,6 +286,8 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : ) binding.messageUserAvatar.visibility = View.VISIBLE binding.messageUserAvatar.setImageDrawable(drawable) + } else if (message.actorType == "federated_users" && message.messageParameters?.get("actor") != null) { + binding.messageUserAvatar.loadFederatedUserAvatar(message) } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt index 6674240fb..ee21d703e 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt @@ -49,6 +49,7 @@ import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding import com.nextcloud.talk.extensions.loadChangelogBotAvatar +import com.nextcloud.talk.extensions.loadFederatedUserAvatar import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.users.UserManager @@ -194,6 +195,8 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : } if (ACTOR_TYPE_BOTS == message.actorType && ACTOR_ID_CHANGELOG == message.actorId) { userAvatar.loadChangelogBotAvatar() + } else if (message.actorType == "federated_users" && message.messageParameters?.get("actor") != null) { + userAvatar.loadFederatedUserAvatar(message) } } } diff --git a/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java b/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java index fe8434393..87f85c048 100644 --- a/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java +++ b/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java @@ -27,7 +27,6 @@ import android.text.Editable; import android.text.Spanned; import android.widget.EditText; -import third.parties.fresco.BetterImageSpan; import com.nextcloud.talk.R; import com.nextcloud.talk.data.user.model.User; import com.nextcloud.talk.models.json.mention.Mention; @@ -39,7 +38,10 @@ import com.otaliastudios.autocomplete.AutocompleteCallback; import com.vanniktech.emoji.EmojiRange; import com.vanniktech.emoji.Emojis; +import java.util.Objects; + import kotlin.OptIn; +import third.parties.fresco.BetterImageSpan; public class MentionAutocompleteCallback implements AutocompleteCallback { private final ViewThemeUtils viewThemeUtils; @@ -66,26 +68,31 @@ public class MentionAutocompleteCallback implements AutocompleteCallback mention, currentUser, context, + roomToken, viewThemeUtils)); } @@ -185,9 +186,14 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter Mention mention = new Mention(); MentionAutocompleteItem mentionAutocompleteItem = (MentionAutocompleteItem) adapter.getItem(position); if (mentionAutocompleteItem != null) { + String mentionId = mentionAutocompleteItem.getMentionId(); + if(mentionId != null) { + mention.setMentionId(mentionId); + } mention.setId(mentionAutocompleteItem.getObjectId()); mention.setLabel(mentionAutocompleteItem.getDisplayName()); mention.setSource(mentionAutocompleteItem.getSource()); + mention.setRoomToken(mentionAutocompleteItem.getRoomToken()); dispatchClick(mention); } return true; diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt index 82092a00c..a5b5613e5 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt @@ -383,6 +383,19 @@ object ApiUtils { return baseUrl + "/index.php/avatar/" + Uri.encode(name) + "/" + avatarSize } + @JvmStatic + fun getUrlForFederatedAvatar( + baseUrl: String, + token: String, + cloudId: String, + darkTheme: Int, + requestBigSize: Boolean + ): String { + val avatarSize = if (requestBigSize) AVATAR_SIZE_BIG else AVATAR_SIZE_SMALL + val url = "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/proxy/$token/user-avatar/$avatarSize" + return "$url?cloudId=$cloudId&darkTheme=$darkTheme" + } + @JvmStatic fun getUrlForGuestAvatar(baseUrl: String?, name: String?, requestBigSize: Boolean): String { val avatarSize = if (requestBigSize) AVATAR_SIZE_BIG else AVATAR_SIZE_SMALL diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java index c964645e8..3e2c392c8 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java @@ -68,6 +68,7 @@ import org.greenrobot.eventbus.EventBus; import java.text.DateFormat; import java.util.Date; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -168,12 +169,14 @@ public class DisplayUtils { public static Drawable getDrawableForMentionChipSpan(Context context, String id, + String roomToken, CharSequence label, User conversationUser, String type, @XmlRes int chipResource, @Nullable EditText emojiEditText, - ViewThemeUtils viewThemeUtils) { + ViewThemeUtils viewThemeUtils, + Boolean isFederated) { ChipDrawable chip = ChipDrawable.createFromResource(context, chipResource); chip.setText(EmojiCompat.get().process(label)); chip.setEllipsize(TextUtils.TruncateAt.MIDDLE); @@ -205,13 +208,21 @@ public class DisplayUtils { chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight()); if (!isCallOrGroup) { - String url = ApiUtils.getUrlForAvatar(conversationUser.getBaseUrl(), id, true); + String url = ApiUtils.getUrlForAvatar(conversationUser.getBaseUrl(), id, false); if ("guests".equals(type) || "guest".equals(type)) { url = ApiUtils.getUrlForGuestAvatar( conversationUser.getBaseUrl(), String.valueOf(label), true); } + if (isFederated) { + int darkTheme = (DisplayUtils.isDarkModeOn(context))? 1 : 0; + url = ApiUtils.getUrlForFederatedAvatar(Objects.requireNonNull(conversationUser.getBaseUrl()), + roomToken, id, + darkTheme, false); + } + + ImageRequest imageRequest = new ImageRequest.Builder(context) .data(url) .crossfade(true) @@ -224,13 +235,12 @@ public class DisplayUtils { @Override public void onError(@Nullable Drawable drawable) { - + chip.setChipIcon(drawable); } @Override public void onSuccess(@NonNull Drawable drawable) { chip.setChipIcon(drawable); - // A hack to refresh the chip icon if (emojiEditText != null) { emojiEditText.post(() -> emojiEditText.setTextKeepState( @@ -248,10 +258,12 @@ public class DisplayUtils { } public static Spannable searchAndReplaceWithMentionSpan(String key, Context context, Spanned text, - String id, String label, String type, + String id, String roomToken, + String label, String type, User conversationUser, @XmlRes int chipXmlRes, - ViewThemeUtils viewThemeUtils) { + ViewThemeUtils viewThemeUtils, + Boolean isFederated) { Spannable spannableString = new SpannableString(text); String stringText = text.toString(); @@ -267,7 +279,7 @@ public class DisplayUtils { } }; - int lastStartIndex = -1; + int lastStartIndex = 0; Spans.MentionChipSpan mentionChipSpan; while (m.find()) { int start = stringText.indexOf(m.group(), lastStartIndex); @@ -276,13 +288,14 @@ public class DisplayUtils { Drawable drawableForChip = DisplayUtils.getDrawableForMentionChipSpan(context, id, + roomToken, label, conversationUser, type, chipXmlRes, null, - viewThemeUtils); - + viewThemeUtils, + isFederated); mentionChipSpan = new Spans.MentionChipSpan(drawableForChip, BetterImageSpan.ALIGN_CENTER, id, diff --git a/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt index 786d9f7f4..cb8787746 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt @@ -127,16 +127,24 @@ class MessageUtils(val context: Context) { } else { R.xml.chip_others } + val id = if (individualHashMap["server"] != null) { + individualHashMap["id"] + "@" + individualHashMap["server"] + } else { + individualHashMap["id"] + } + messageStringInternal = DisplayUtils.searchAndReplaceWithMentionSpan( key, themingContext, messageStringInternal, - individualHashMap["id"]!!, + id, + message.roomToken, individualHashMap["name"]!!, individualHashMap["type"]!!, message.activeUser!!, chip, - viewThemeUtils + viewThemeUtils, + individualHashMap["server"] != null ) } @@ -174,5 +182,6 @@ class MessageUtils(val context: Context) { companion object { private const val TAG = "MessageUtils" const val MAX_REPLY_LENGTH = 250 + const val HTTPS_PROTOCOL = "https://" } } From c0c671bccf9c2be88eb029a5a3900bb44e4ac87e Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 21 Mar 2024 14:20:17 +0100 Subject: [PATCH 2/6] fix to use correct cloudId. Without this fix, the avatars for "normal" messages were not able to show avatars. Only the messages that contained messageParameters with actor were able to show avatars(for example "userX invited userY") Signed-off-by: Marcel Hibbe --- .../messages/IncomingLinkPreviewMessageViewHolder.kt | 2 +- .../adapters/messages/IncomingLocationMessageViewHolder.kt | 2 +- .../talk/adapters/messages/IncomingPollMessageViewHolder.kt | 2 +- .../talk/adapters/messages/IncomingTextMessageViewHolder.kt | 2 +- .../talk/adapters/messages/IncomingVoiceMessageViewHolder.kt | 2 +- .../talk/adapters/messages/PreviewMessageViewHolder.kt | 2 +- .../com/nextcloud/talk/extensions/ImageViewExtensions.kt | 5 +---- 7 files changed, 7 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt index 713361aff..de0221835 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt @@ -173,7 +173,7 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) : binding.messageUserAvatar.loadChangelogBotAvatar() } else if (message.actorType == "bots") { binding.messageUserAvatar.loadBotsAvatar() - } else if (message.actorType == "federated_users" && message.messageParameters?.get("actor") != null) { + } else if (message.actorType == "federated_users") { binding.messageUserAvatar.loadFederatedUserAvatar(message) } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt index 6185aa8e9..4bcc90976 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt @@ -149,7 +149,7 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : binding.messageUserAvatar.loadChangelogBotAvatar() } else if (message.actorType == "bots") { binding.messageUserAvatar.loadBotsAvatar() - } else if (message.actorType == "federated_users" && message.messageParameters?.get("actor") != null) { + } else if (message.actorType == "federated_users") { binding.messageUserAvatar.loadFederatedUserAvatar(message) } } else { diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt index becb6858b..fe00b0863 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt @@ -180,7 +180,7 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) : binding.messageUserAvatar.loadChangelogBotAvatar() } else if (message.actorType == "bots") { binding.messageUserAvatar.loadBotsAvatar() - } else if (message.actorType == "federated_users" && message.messageParameters?.get("actor") != null) { + } else if (message.actorType == "federated_users") { binding.messageUserAvatar.loadFederatedUserAvatar(message) } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt index bb0b3d018..6b7806eb4 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt @@ -183,7 +183,7 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : binding.messageUserAvatar.loadChangelogBotAvatar() } else if (message.actorType == "bots") { binding.messageUserAvatar.loadBotsAvatar() - } else if (message.actorType == "federated_users" && message.messageParameters?.get("actor") != null) { + } else if (message.actorType == "federated_users") { binding.messageUserAvatar.loadFederatedUserAvatar(message) } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt index 929f54407..67b282232 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt @@ -286,7 +286,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : ) binding.messageUserAvatar.visibility = View.VISIBLE binding.messageUserAvatar.setImageDrawable(drawable) - } else if (message.actorType == "federated_users" && message.messageParameters?.get("actor") != null) { + } else if (message.actorType == "federated_users") { binding.messageUserAvatar.loadFederatedUserAvatar(message) } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt index ee21d703e..6121a30ad 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt @@ -195,7 +195,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : } if (ACTOR_TYPE_BOTS == message.actorType && ACTOR_ID_CHANGELOG == message.actorId) { userAvatar.loadChangelogBotAvatar() - } else if (message.actorType == "federated_users" && message.messageParameters?.get("actor") != null) { + } else if (message.actorType == "federated_users") { userAvatar.loadFederatedUserAvatar(message) } } diff --git a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt index ce52114aa..3d336b736 100644 --- a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt +++ b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt @@ -128,10 +128,7 @@ fun ImageView.loadUserAvatar( } fun ImageView.loadFederatedUserAvatar(message: ChatMessage): io.reactivex.disposables.Disposable { - val map = message.messageParameters?.get("actor") - val url = map?.get("server")!! - val id = map["id"] - val cloudId = "$id@$url" + val cloudId = message.actorId!! val darkTheme = if (DisplayUtils.isDarkModeOn(context)) 1 else 0 val ignoreCache = false val requestBigSize = true From c55403c023b7d7b80f656d0be1004a97dfc75c99 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 21 Mar 2024 14:27:16 +0100 Subject: [PATCH 3/6] modify log statement Signed-off-by: Marcel Hibbe --- .../java/com/nextcloud/talk/extensions/ImageViewExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt index 3d336b736..f461e7d78 100644 --- a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt +++ b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt @@ -160,7 +160,7 @@ fun ImageView.loadFederatedUserAvatar( darkTheme, requestBigSize ) - Log.d("Julius", "URL::$imageRequestUri") + Log.d(TAG, "federated avatar URL: $imageRequestUri") return loadAvatarInternal(user, imageRequestUri, ignoreCache, null) } From 4a75108557e927511bff6c5bee5e8bb0258c734e Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 21 Mar 2024 14:29:14 +0100 Subject: [PATCH 4/6] reformat code Signed-off-by: Marcel Hibbe --- .../MentionAutocompletePresenter.java | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java b/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java index 06e9fde2d..9abf5f32d 100644 --- a/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java +++ b/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java @@ -131,54 +131,54 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()), ApiUtils.getUrlForMentionSuggestions(chatApiVersion, currentUser.getBaseUrl(), roomToken), queryString, 5, queryMap) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .retry(3) - .subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) { - // no actions atm - } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .retry(3) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable d) { + // no actions atm + } - @Override - public void onNext(@NonNull MentionOverall mentionOverall) { - List mentionsList = mentionOverall.getOcs().getData(); + @Override + public void onNext(@NonNull MentionOverall mentionOverall) { + List mentionsList = mentionOverall.getOcs().getData(); - if (mentionsList.size() == 0) { - adapter.clear(); - } else { - List internalAbstractFlexibleItemList = - new ArrayList<>(mentionsList.size()); - for (Mention mention : mentionsList) { - internalAbstractFlexibleItemList.add( - new MentionAutocompleteItem( - mention, - currentUser, - context, - roomToken, - viewThemeUtils)); - } - - if (adapter.getItemCount() != 0) { - adapter.clear(); - } - - adapter.updateDataSet(internalAbstractFlexibleItemList); - } - } - - @SuppressLint("LongLogTag") - @Override - public void onError(@NonNull Throwable e) { + if (mentionsList.size() == 0) { adapter.clear(); - Log.e(TAG, "failed to get MentionAutocompleteSuggestions", e); - } + } else { + List internalAbstractFlexibleItemList = + new ArrayList<>(mentionsList.size()); + for (Mention mention : mentionsList) { + internalAbstractFlexibleItemList.add( + new MentionAutocompleteItem( + mention, + currentUser, + context, + roomToken, + viewThemeUtils)); + } - @Override - public void onComplete() { - // no actions atm + if (adapter.getItemCount() != 0) { + adapter.clear(); + } + + adapter.updateDataSet(internalAbstractFlexibleItemList); } - }); + } + + @SuppressLint("LongLogTag") + @Override + public void onError(@NonNull Throwable e) { + adapter.clear(); + Log.e(TAG, "failed to get MentionAutocompleteSuggestions", e); + } + + @Override + public void onComplete() { + // no actions atm + } + }); } @Override @@ -187,7 +187,7 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter MentionAutocompleteItem mentionAutocompleteItem = (MentionAutocompleteItem) adapter.getItem(position); if (mentionAutocompleteItem != null) { String mentionId = mentionAutocompleteItem.getMentionId(); - if(mentionId != null) { + if (mentionId != null) { mention.setMentionId(mentionId); } mention.setId(mentionAutocompleteItem.getObjectId()); From bb6af562cc510e6388f5a030c2b3bb4ddec31608 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 21 Mar 2024 14:53:52 +0100 Subject: [PATCH 5/6] convert MentionAutocompleteItem to kt Signed-off-by: Marcel Hibbe --- .../items/MentionAutocompleteItem.java | 255 ------------------ .../adapters/items/MentionAutocompleteItem.kt | 247 +++++++++++++++++ .../talk/adapters/items/ParticipantItem.java | 2 +- .../MentionAutocompletePresenter.java | 10 +- 4 files changed, 253 insertions(+), 261 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.kt diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java deleted file mode 100644 index 610758002..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * @author Marcel Hibbe - * @author Andy Scherzinger - * Copyright (C) 2021-2022 Andy Scherzinger - * Copyright (C) 2022 Marcel Hibbe - * Copyright (C) 2017 Mario Danic - * - * 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 . - */ - -package com.nextcloud.talk.adapters.items; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.Build; -import android.view.View; - -import com.nextcloud.talk.R; -import com.nextcloud.talk.data.user.model.User; -import com.nextcloud.talk.extensions.ImageViewExtensionsKt; -import com.nextcloud.talk.models.json.mention.Mention; -import com.nextcloud.talk.models.json.status.StatusType; -import com.nextcloud.talk.ui.StatusDrawable; -import com.nextcloud.talk.ui.theme.ViewThemeUtils; -import com.nextcloud.talk.utils.DisplayUtils; - -import java.util.List; -import java.util.Objects; -import java.util.regex.Pattern; - -import androidx.constraintlayout.widget.ConstraintLayout; -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; - -public class MentionAutocompleteItem extends AbstractFlexibleItem - implements IFilterable { - - private static final float STATUS_SIZE_IN_DP = 9f; - private static final String NO_ICON = ""; - public static final String SOURCE_CALLS = "calls"; - public static final String SOURCE_GUESTS = "guests"; - - public static final String SOURCE_GROUPS = "groups"; - public static final String SOURCE_FEDERATION = "federated_users"; - - private String source; - private final String mentionId; - private final String objectId; - private final String displayName; - private final String status; - private final String statusIcon; - private final String statusMessage; - private final User currentUser; - private final Context context; - private final String roomToken; - private final ViewThemeUtils viewThemeUtils; - - public MentionAutocompleteItem( - Mention mention, - User currentUser, - Context activityContext, String roomToken, ViewThemeUtils viewThemeUtils) { - this.mentionId = mention.getMentionId(); - this.objectId = mention.getId(); - this.displayName = mention.getLabel(); - this.source = mention.getSource(); - this.status = mention.getStatus(); - this.statusIcon = mention.getStatusIcon(); - this.statusMessage = mention.getStatusMessage(); - this.currentUser = currentUser; - this.context = activityContext; - this.viewThemeUtils = viewThemeUtils; - this.roomToken = roomToken; - } - - public String getSource() { - return source; - } - - public void setSource(String source) { - this.source = source; - } - - public String getMentionId() { - return mentionId; - } - - public String getObjectId() { - return objectId; - } - - public String getDisplayName() { - return displayName; - } - - public String getRoomToken() { - return roomToken; - } - - @Override - public boolean equals(Object o) { - if (o instanceof MentionAutocompleteItem inItem) { - return (objectId.equals(inItem.objectId) && displayName.equals(inItem.displayName)); - } - - return false; - } - - @Override - public int hashCode() { - return Objects.hash(objectId, displayName); - } - - @Override - public int getLayoutRes() { - return R.layout.rv_item_conversation_info_participant; - } - - @Override - public ParticipantItem.ParticipantItemViewHolder createViewHolder(View view, FlexibleAdapter adapter) { - return new ParticipantItem.ParticipantItemViewHolder(view, adapter); - } - - @SuppressLint("SetTextI18n") - @Override - public void bindViewHolder(FlexibleAdapter adapter, - ParticipantItem.ParticipantItemViewHolder holder, - int position, - List payloads) { - - holder.binding.nameText.setTextColor( - ResourcesCompat.getColor(context.getResources(), - R.color.conversation_item_header, - null)); - if (adapter.hasFilter()) { - viewThemeUtils.talk.themeAndHighlightText(holder.binding.nameText, - displayName, - String.valueOf(adapter.getFilter(String.class))); - viewThemeUtils.talk.themeAndHighlightText(holder.binding.secondaryText, - "@" + objectId, - String.valueOf(adapter.getFilter(String.class))); - } else { - holder.binding.nameText.setText(displayName); - holder.binding.secondaryText.setText("@" + objectId); - } - - String avatarId = objectId; - switch (source) { - case SOURCE_CALLS: {} - case SOURCE_GROUPS: { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - ImageViewExtensionsKt.loadUserAvatar( - holder.binding.avatarView, - viewThemeUtils.talk.themePlaceholderAvatar( - holder.binding.avatarView, - R.drawable.ic_avatar_group)); - } else { - ImageViewExtensionsKt.loadUserAvatar(holder.binding.avatarView, R.drawable.ic_circular_group); - } - break; - } - case SOURCE_FEDERATION: { - int darkTheme = (DisplayUtils.isDarkModeOn(this.context))? 1 : 0; - ImageViewExtensionsKt.loadFederatedUserAvatar(holder.binding.avatarView, - currentUser, - Objects.requireNonNull(currentUser.getBaseUrl()), - roomToken, - avatarId, - darkTheme, - true, - false); - break; - } - case SOURCE_GUESTS: { - avatarId = displayName; - } - default: { - ImageViewExtensionsKt.loadUserAvatar(holder.binding.avatarView, currentUser, avatarId, true, false); - } - } - - drawStatus(holder); - } - - private void drawStatus(ParticipantItem.ParticipantItemViewHolder holder) { - float size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, context); - holder.binding.userStatusImage.setImageDrawable(new StatusDrawable( - status, - NO_ICON, - size, - context.getResources().getColor(R.color.bg_default), - context)); - - if (statusMessage != null) { - holder.binding.conversationInfoStatusMessage.setText(statusMessage); - alignUsernameVertical(holder, 0); - } else { - holder.binding.conversationInfoStatusMessage.setText(""); - alignUsernameVertical(holder, 10); - } - - if (statusIcon != null && !statusIcon.isEmpty()) { - holder.binding.participantStatusEmoji.setText(statusIcon); - } else { - holder.binding.participantStatusEmoji.setVisibility(View.GONE); - } - - if (status != null && status.equals(StatusType.DND.getString())) { - if (statusMessage == null || statusMessage.isEmpty()) { - holder.binding.conversationInfoStatusMessage.setText(R.string.dnd); - } - } else if (status != null && status.equals(StatusType.AWAY.getString())) { - if (statusMessage == null || statusMessage.isEmpty()) { - holder.binding.conversationInfoStatusMessage.setText(R.string.away); - } - } - } - - private void alignUsernameVertical(ParticipantItem.ParticipantItemViewHolder holder, float densityPixelsFromTop) { - ConstraintLayout.LayoutParams layoutParams = - (ConstraintLayout.LayoutParams) holder.binding.nameText.getLayoutParams(); - layoutParams.topMargin = (int) DisplayUtils.convertDpToPixel(densityPixelsFromTop, context); - holder.binding.nameText.setLayoutParams(layoutParams); - } - - @Override - public boolean filter(String constraint) { - return objectId != null && - Pattern - .compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL) - .matcher(objectId) - .find() || - displayName != null && - Pattern - .compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL) - .matcher(displayName) - .find(); - } -} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.kt new file mode 100644 index 000000000..9751135ed --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.kt @@ -0,0 +1,247 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * @author Marcel Hibbe + * @author Andy Scherzinger + * Copyright (C) 2021-2022 Andy Scherzinger + * Copyright (C) 2022 Marcel Hibbe + * Copyright (C) 2017 Mario Danic + * + * 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 . + */ +package com.nextcloud.talk.adapters.items + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.res.ResourcesCompat +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.items.ParticipantItem.ParticipantItemViewHolder +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.extensions.loadFederatedUserAvatar +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.json.mention.Mention +import com.nextcloud.talk.models.json.status.StatusType +import com.nextcloud.talk.ui.StatusDrawable +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 +import eu.davidea.flexibleadapter.items.IFlexible +import java.util.Objects +import java.util.regex.Pattern + +class MentionAutocompleteItem( + mention: Mention, + private val currentUser: User, + private val context: Context, + @JvmField val roomToken: String, + private val viewThemeUtils: ViewThemeUtils +) : AbstractFlexibleItem(), IFilterable { + @JvmField + var source: String? + + @JvmField + val mentionId: String? + + @JvmField + val objectId: String? + + @JvmField + val displayName: String? + private val status: String? + private val statusIcon: String? + private val statusMessage: String? + + init { + mentionId = mention.mentionId + objectId = mention.id + displayName = mention.label + source = mention.source + status = mention.status + statusIcon = mention.statusIcon + statusMessage = mention.statusMessage + } + + override fun equals(o: Any?): Boolean { + return if (o is MentionAutocompleteItem) { + objectId == o.objectId && displayName == o.displayName + } else { + false + } + } + + override fun hashCode(): Int { + return Objects.hash(objectId, displayName) + } + + override fun getLayoutRes(): Int { + return R.layout.rv_item_conversation_info_participant + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter?>?): ParticipantItemViewHolder { + return ParticipantItemViewHolder(view, adapter) + } + + @SuppressLint("SetTextI18n") + override fun bindViewHolder( + adapter: FlexibleAdapter?>, + holder: ParticipantItemViewHolder, + position: Int, + payloads: List + ) { + holder.binding.nameText.setTextColor( + ResourcesCompat.getColor( + context.resources, + R.color.conversation_item_header, + null + ) + ) + if (adapter.hasFilter()) { + viewThemeUtils.talk.themeAndHighlightText( + holder.binding.nameText, + displayName, + adapter.getFilter(String::class.java).toString() + ) + viewThemeUtils.talk.themeAndHighlightText( + holder.binding.secondaryText, + "@$objectId", + adapter.getFilter(String::class.java).toString() + ) + } else { + holder.binding.nameText.text = displayName + holder.binding.secondaryText.text = "@$objectId" + } + var avatarId = objectId + when (source) { + SOURCE_CALLS -> { + run {} + run { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + holder.binding.avatarView.loadUserAvatar( + viewThemeUtils.talk.themePlaceholderAvatar( + holder.binding.avatarView, + R.drawable.ic_avatar_group + ) + ) + } else { + holder.binding.avatarView.loadUserAvatar(R.drawable.ic_circular_group) + } + } + } + + SOURCE_GROUPS -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + holder.binding.avatarView.loadUserAvatar( + viewThemeUtils.talk.themePlaceholderAvatar( + holder.binding.avatarView, + R.drawable.ic_avatar_group + ) + ) + } else { + holder.binding.avatarView.loadUserAvatar(R.drawable.ic_circular_group) + } + } + + SOURCE_FEDERATION -> { + val darkTheme = if (DisplayUtils.isDarkModeOn(context)) 1 else 0 + holder.binding.avatarView.loadFederatedUserAvatar( + currentUser, + currentUser.baseUrl!!, + roomToken, + avatarId!!, + darkTheme, + true, + false + ) + } + + SOURCE_GUESTS -> { + run { avatarId = displayName } + run { holder.binding.avatarView.loadUserAvatar(currentUser, avatarId!!, true, false) } + } + + else -> { + holder.binding.avatarView.loadUserAvatar(currentUser, avatarId!!, true, false) + } + } + drawStatus(holder) + } + + private fun drawStatus(holder: ParticipantItemViewHolder) { + val size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, context) + holder.binding.userStatusImage.setImageDrawable( + StatusDrawable( + status, + NO_ICON, + size, + context.resources.getColor(R.color.bg_default), + context + ) + ) + if (statusMessage != null) { + holder.binding.conversationInfoStatusMessage.text = statusMessage + alignUsernameVertical(holder, 0f) + } else { + holder.binding.conversationInfoStatusMessage.text = "" + alignUsernameVertical(holder, 10f) + } + if (!statusIcon.isNullOrEmpty()) { + holder.binding.participantStatusEmoji.setText(statusIcon) + } else { + holder.binding.participantStatusEmoji.visibility = View.GONE + } + if (status != null && status == StatusType.DND.string) { + if (statusMessage.isNullOrEmpty()) { + holder.binding.conversationInfoStatusMessage.setText(R.string.dnd) + } + } else if (status != null && status == StatusType.AWAY.string) { + if (statusMessage.isNullOrEmpty()) { + holder.binding.conversationInfoStatusMessage.setText(R.string.away) + } + } + } + + private fun alignUsernameVertical(holder: ParticipantItemViewHolder, densityPixelsFromTop: Float) { + val layoutParams = holder.binding.nameText.layoutParams as ConstraintLayout.LayoutParams + layoutParams.topMargin = DisplayUtils.convertDpToPixel(densityPixelsFromTop, context).toInt() + holder.binding.nameText.setLayoutParams(layoutParams) + } + + override fun filter(constraint: String?): Boolean { + return objectId != null && + Pattern + .compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(objectId) + .find() || + displayName != null && + Pattern + .compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(displayName) + .find() + } + + companion object { + private const val STATUS_SIZE_IN_DP = 9f + private const val NO_ICON = "" + const val SOURCE_CALLS = "calls" + const val SOURCE_GUESTS = "guests" + const val SOURCE_GROUPS = "groups" + const val SOURCE_FEDERATION = "federated_users" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java index c6bfc90cc..664062549 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java @@ -301,7 +301,7 @@ public class ParticipantItem extends AbstractFlexibleItem Mention mention = new Mention(); MentionAutocompleteItem mentionAutocompleteItem = (MentionAutocompleteItem) adapter.getItem(position); if (mentionAutocompleteItem != null) { - String mentionId = mentionAutocompleteItem.getMentionId(); + String mentionId = mentionAutocompleteItem.mentionId; if (mentionId != null) { mention.setMentionId(mentionId); } - mention.setId(mentionAutocompleteItem.getObjectId()); - mention.setLabel(mentionAutocompleteItem.getDisplayName()); - mention.setSource(mentionAutocompleteItem.getSource()); - mention.setRoomToken(mentionAutocompleteItem.getRoomToken()); + mention.setId(mentionAutocompleteItem.objectId); + mention.setLabel(mentionAutocompleteItem.displayName); + mention.setSource(mentionAutocompleteItem.source); + mention.setRoomToken(mentionAutocompleteItem.roomToken); dispatchClick(mention); } return true; From cc191578670b02283a0d9c96f60f1e403e114426 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 21 Mar 2024 15:19:55 +0100 Subject: [PATCH 6/6] convert DisplayUtils to kt Signed-off-by: Marcel Hibbe --- .../messages/PreviewMessageViewHolder.kt | 2 +- .../messages/SystemMessageViewHolder.kt | 2 +- .../com/nextcloud/talk/chat/ChatActivity.kt | 2 +- .../talk/extensions/ImageViewExtensions.kt | 1 + .../talk/location/LocationPickerActivity.kt | 2 +- .../nextcloud/talk/utils/DisplayUtils.java | 576 ------------------ .../com/nextcloud/talk/utils/DisplayUtils.kt | 540 ++++++++++++++++ .../talk/utils/message/MessageUtils.kt | 4 +- 8 files changed, 547 insertions(+), 582 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt index 6121a30ad..2b0a7f6c9 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt @@ -110,7 +110,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : @Suppress("NestedBlockDepth", "ComplexMethod", "LongMethod") override fun onBind(message: ChatMessage) { super.onBind(message) - image.minimumHeight = DisplayUtils.convertDpToPixel(MIN_IMAGE_HEIGHT, context).toInt() + image.minimumHeight = DisplayUtils.convertDpToPixel(MIN_IMAGE_HEIGHT, context!!).toInt() time.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt index 0c1feeba8..6c9980438 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt @@ -90,7 +90,7 @@ class SystemMessageViewHolder(itemView: View) : MessageHolders.IncomingTextMessa } else { individualMap["name"] } - messageString = DisplayUtils.searchAndColor(messageString, searchText, mentionColor) + messageString = DisplayUtils.searchAndColor(messageString, searchText!!, mentionColor) } } } diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 518bddde0..898dba17b 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -2142,7 +2142,7 @@ class ChatActivity : true ) - if (DisplayUtils.isDarkModeOn(supportActionBar?.themedContext)) { + if (DisplayUtils.isDarkModeOn(supportActionBar?.themedContext!!)) { url = "$url/dark" } diff --git a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt index f461e7d78..61a8aecef 100644 --- a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt +++ b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt @@ -70,6 +70,7 @@ fun ImageView.loadConversationAvatar( ) } +@Suppress("ReturnCount") fun ImageView.loadConversationAvatar( user: User, conversation: ConversationModel, diff --git a/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt index d80c7492b..973397205 100644 --- a/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt @@ -281,7 +281,7 @@ class LocationPickerActivity : locationOverlay.setPersonHotspot(PERSON_HOT_SPOT_X, PERSON_HOT_SPOT_Y) locationOverlay.setPersonIcon( DisplayUtils.getBitmap( - ResourcesCompat.getDrawable(resources!!, R.drawable.current_location_circle, null) + ResourcesCompat.getDrawable(resources!!, R.drawable.current_location_circle, null)!! ) ) binding.map.overlays.add(locationOverlay) diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java deleted file mode 100644 index 3e2c392c8..000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ /dev/null @@ -1,576 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * @author Andy Scherzinger - * @author Tim Krüger - * Copyright (C) 2022 Tim Krüger - * Copyright (C) 2021 Andy Scherzinger - * Copyright (C) 2017-2020 Mario Danic - * - * 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 . - */ - -package com.nextcloud.talk.utils; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Build; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.Spanned; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.text.method.LinkMovementMethod; -import android.text.style.AbsoluteSizeSpan; -import android.text.style.ClickableSpan; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.util.TypedValue; -import android.view.View; -import android.view.Window; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.TextView; - -import com.google.android.material.chip.ChipDrawable; -import com.nextcloud.talk.R; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.data.user.model.User; -import com.nextcloud.talk.events.UserMentionClickEvent; -import com.nextcloud.talk.extensions.ImageViewExtensionsKt; -import com.nextcloud.talk.ui.theme.ViewThemeUtils; -import com.nextcloud.talk.utils.text.Spans; - -import org.greenrobot.eventbus.EventBus; - -import java.text.DateFormat; -import java.util.Date; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import androidx.annotation.ColorInt; -import androidx.annotation.ColorRes; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.annotation.XmlRes; -import androidx.core.content.ContextCompat; -import androidx.core.content.res.ResourcesCompat; -import androidx.core.graphics.ColorUtils; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.emoji2.text.EmojiCompat; -import coil.Coil; -import coil.request.ImageRequest; -import coil.target.Target; -import coil.transform.CircleCropTransformation; -import third.parties.fresco.BetterImageSpan; - -import static com.nextcloud.talk.utils.FileSortOrder.SORT_A_TO_Z_ID; -import static com.nextcloud.talk.utils.FileSortOrder.SORT_BIG_TO_SMALL_ID; -import static com.nextcloud.talk.utils.FileSortOrder.SORT_NEW_TO_OLD_ID; -import static com.nextcloud.talk.utils.FileSortOrder.SORT_OLD_TO_NEW_ID; -import static com.nextcloud.talk.utils.FileSortOrder.SORT_SMALL_TO_BIG_ID; -import static com.nextcloud.talk.utils.FileSortOrder.SORT_Z_TO_A_ID; - -public class DisplayUtils { - private static final String TAG = DisplayUtils.class.getSimpleName(); - - private static final int INDEX_LUMINATION = 2; - private static final double MAX_LIGHTNESS = 0.92; - - private static final String TWITTER_HANDLE_PREFIX = "@"; - private static final String HTTP_PROTOCOL = "http://"; - private static final String HTTPS_PROTOCOL = "https://"; - - private static final int DATE_TIME_PARTS_SIZE = 2; - - public static Boolean isDarkModeOn(Context context) { - int currentNightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - return currentNightMode == Configuration.UI_MODE_NIGHT_YES; - } - - public static void setClickableString(String string, String url, TextView textView) { - SpannableString spannableString = new SpannableString(string); - spannableString.setSpan(new ClickableSpan() { - @Override - public void onClick(@NonNull View widget) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - NextcloudTalkApplication - .Companion - .getSharedApplication() - .getApplicationContext() - .startActivity(browserIntent); - } - - @Override - public void updateDrawState(@NonNull TextPaint ds) { - super.updateDrawState(ds); - ds.setUnderlineText(false); - } - }, 0, string.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - textView.setText(spannableString); - textView.setMovementMethod(LinkMovementMethod.getInstance()); - } - - public static Bitmap getBitmap(Drawable drawable) { - Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), - drawable.getIntrinsicHeight(), - Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - return bitmap; - } - - public static float convertDpToPixel(float dp, Context context) { - return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, - context.getResources().getDisplayMetrics()) + 0.5f); - } - - public static float convertPixelToDp(float px, Context context) { - return px / context.getResources().getDisplayMetrics().density; - } - - public static Drawable getTintedDrawable(Resources res, @DrawableRes int drawableResId, @ColorRes int colorResId) { - Drawable drawable = ResourcesCompat.getDrawable(res, drawableResId, null); - - int color = res.getColor(colorResId); - if (drawable != null) { - drawable.setTint(color); - } - return drawable; - } - - public static Drawable getDrawableForMentionChipSpan(Context context, - String id, - String roomToken, - CharSequence label, - User conversationUser, - String type, - @XmlRes int chipResource, - @Nullable EditText emojiEditText, - ViewThemeUtils viewThemeUtils, - Boolean isFederated) { - ChipDrawable chip = ChipDrawable.createFromResource(context, chipResource); - chip.setText(EmojiCompat.get().process(label)); - chip.setEllipsize(TextUtils.TruncateAt.MIDDLE); - - if (chipResource == R.xml.chip_you) { - viewThemeUtils.material.colorChipDrawable(context, chip); - } - - Configuration config = context.getResources().getConfiguration(); - chip.setLayoutDirection(config.getLayoutDirection()); - - int drawable; - - boolean isCallOrGroup = - "call".equals(type) || "calls".equals(type) || "groups".equals(type) || "user-group".equals(type); - - if (!isCallOrGroup) { - if (chipResource == R.xml.chip_you) { - drawable = R.drawable.mention_chip; - } else { - drawable = R.drawable.accent_circle; - } - - chip.setChipIconResource(drawable); - } else { - chip.setChipIconResource(R.drawable.ic_circular_group); - } - - chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight()); - - if (!isCallOrGroup) { - String url = ApiUtils.getUrlForAvatar(conversationUser.getBaseUrl(), id, false); - if ("guests".equals(type) || "guest".equals(type)) { - url = ApiUtils.getUrlForGuestAvatar( - conversationUser.getBaseUrl(), - String.valueOf(label), true); - } - - if (isFederated) { - int darkTheme = (DisplayUtils.isDarkModeOn(context))? 1 : 0; - url = ApiUtils.getUrlForFederatedAvatar(Objects.requireNonNull(conversationUser.getBaseUrl()), - roomToken, id, - darkTheme, false); - } - - - ImageRequest imageRequest = new ImageRequest.Builder(context) - .data(url) - .crossfade(true) - .transformations(new CircleCropTransformation()) - .target(new Target() { - @Override - public void onStart(@Nullable Drawable drawable) { - - } - - @Override - public void onError(@Nullable Drawable drawable) { - chip.setChipIcon(drawable); - } - - @Override - public void onSuccess(@NonNull Drawable drawable) { - chip.setChipIcon(drawable); - // A hack to refresh the chip icon - if (emojiEditText != null) { - emojiEditText.post(() -> emojiEditText.setTextKeepState( - emojiEditText.getText(), - TextView.BufferType.SPANNABLE)); - } - } - }) - .build(); - - Coil.imageLoader(context).enqueue(imageRequest); - } - - return chip; - } - - public static Spannable searchAndReplaceWithMentionSpan(String key, Context context, Spanned text, - String id, String roomToken, - String label, String type, - User conversationUser, - @XmlRes int chipXmlRes, - ViewThemeUtils viewThemeUtils, - Boolean isFederated) { - - Spannable spannableString = new SpannableString(text); - String stringText = text.toString(); - - String keyWithBrackets = "{" + key + "}"; - Matcher m = Pattern.compile(keyWithBrackets, Pattern.CASE_INSENSITIVE | Pattern.LITERAL | Pattern.MULTILINE) - .matcher(spannableString); - - ClickableSpan clickableSpan = new ClickableSpan() { - @Override - public void onClick(@NonNull View widget) { - EventBus.getDefault().post(new UserMentionClickEvent(id)); - } - }; - - int lastStartIndex = 0; - Spans.MentionChipSpan mentionChipSpan; - while (m.find()) { - int start = stringText.indexOf(m.group(), lastStartIndex); - int end = start + m.group().length(); - lastStartIndex = end; - - Drawable drawableForChip = DisplayUtils.getDrawableForMentionChipSpan(context, - id, - roomToken, - label, - conversationUser, - type, - chipXmlRes, - null, - viewThemeUtils, - isFederated); - mentionChipSpan = new Spans.MentionChipSpan(drawableForChip, - BetterImageSpan.ALIGN_CENTER, - id, - label); - - spannableString.setSpan(mentionChipSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - - if (chipXmlRes == R.xml.chip_you) { - spannableString.setSpan( - viewThemeUtils.talk.themeForegroundColorSpan(context), - start, - end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - if ("user".equals(type) && !conversationUser.getUserId().equals(id)) { - spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - } - } - - return spannableString; - } - - public static Spannable searchAndColor(Spannable text, String searchText, @ColorInt int color) { - - Spannable spannableString = new SpannableString(text); - String stringText = text.toString(); - if (TextUtils.isEmpty(text) || TextUtils.isEmpty(searchText)) { - return spannableString; - } - - Matcher m = Pattern.compile(searchText, - Pattern.CASE_INSENSITIVE | Pattern.LITERAL | Pattern.MULTILINE) - .matcher(spannableString); - - - int textSize = NextcloudTalkApplication.Companion.getSharedApplication().getResources().getDimensionPixelSize(R.dimen - .chat_text_size); - - int lastStartIndex = -1; - while (m.find()) { - int start = stringText.indexOf(m.group(), lastStartIndex); - int end = start + m.group().length(); - lastStartIndex = end; - spannableString.setSpan(new ForegroundColorSpan(color), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - spannableString.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - spannableString.setSpan(new AbsoluteSizeSpan(textSize), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - return spannableString; - } - - public static Drawable getMessageSelector(@ColorInt int normalColor, - @ColorInt int selectedColor, - @ColorInt int pressedColor, - @DrawableRes int shape) { - - Drawable vectorDrawable = ContextCompat.getDrawable(NextcloudTalkApplication.Companion.getSharedApplication() - .getApplicationContext(), - shape); - Drawable drawable = DrawableCompat.wrap(vectorDrawable).mutate(); - DrawableCompat.setTintList( - drawable, - new ColorStateList( - new int[][]{ - new int[]{android.R.attr.state_selected}, - new int[]{android.R.attr.state_pressed}, - new int[]{-android.R.attr.state_pressed, -android.R.attr.state_selected} - }, - new int[]{selectedColor, pressedColor, normalColor} - )); - return drawable; - } - - /** - * Sets the color of the status bar to {@code color}. - * - * @param activity activity - * @param color the color - */ - public static void applyColorToStatusBar(Activity activity, @ColorInt int color) { - Window window = activity.getWindow(); - boolean isLightTheme = lightTheme(color); - if (window != null) { - - View decor = window.getDecorView(); - if (isLightTheme) { - int systemUiFlagLightStatusBar; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - systemUiFlagLightStatusBar = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | - View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; - } else { - systemUiFlagLightStatusBar = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; - } - decor.setSystemUiVisibility(systemUiFlagLightStatusBar); - } else { - decor.setSystemUiVisibility(0); - } - window.setStatusBarColor(color); - } - } - - /** - * Tests if light color is set - * - * @param color the color - * @return true if primaryColor is lighter than MAX_LIGHTNESS - */ - @SuppressWarnings("CLI_CONSTANT_LIST_INDEX") - public static boolean lightTheme(int color) { - float[] hsl = colorToHSL(color); - - // spotbugs dislikes fixed index access - // which is enforced by having such an - // array from Android-API itself - return hsl[INDEX_LUMINATION] >= MAX_LIGHTNESS; - } - - private static float[] colorToHSL(int color) { - float[] hsl = new float[3]; - ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl); - - return hsl; - } - - public static void applyColorToNavigationBar(Window window, @ColorInt int color) { - window.setNavigationBarColor(color); - } - - /** - * beautifies a given URL by removing any http/https protocol prefix. - * - * @param url to be beautified url - * @return beautified url - */ - public static String beautifyURL(@Nullable String url) { - if (TextUtils.isEmpty(url)) { - return ""; - } - - if (url.length() >= 7 && HTTP_PROTOCOL.equalsIgnoreCase(url.substring(0, 7))) { - return url.substring(HTTP_PROTOCOL.length()).trim(); - } - - if (url.length() >= 8 && HTTPS_PROTOCOL.equalsIgnoreCase(url.substring(0, 8))) { - return url.substring(HTTPS_PROTOCOL.length()).trim(); - } - - return url.trim(); - } - - /** - * beautifies a given twitter handle by prefixing it with an @ in case it is missing. - * - * @param handle to be beautified twitter handle - * @return beautified twitter handle - */ - public static String beautifyTwitterHandle(@Nullable String handle) { - if (handle != null) { - String trimmedHandle = handle.trim(); - - if (TextUtils.isEmpty(trimmedHandle)) { - return ""; - } - - if (trimmedHandle.startsWith(TWITTER_HANDLE_PREFIX)) { - return trimmedHandle; - } else { - return TWITTER_HANDLE_PREFIX + trimmedHandle; - } - } else { - return ""; - } - } - - public static void loadAvatarImage(User user, ImageView avatarImageView, boolean deleteCache) { - if (user != null && avatarImageView != null) { - String avatarId; - if (!TextUtils.isEmpty(user.getUserId())) { - avatarId = user.getUserId(); - } else { - avatarId = user.getUsername(); - } - - if (avatarId != null) { - ImageViewExtensionsKt.loadUserAvatar(avatarImageView, user, avatarId, true, deleteCache); - } - } - } - - public static @StringRes - int getSortOrderStringId(FileSortOrder sortOrder) { - switch (sortOrder.getName()) { - case SORT_Z_TO_A_ID: - return R.string.menu_item_sort_by_name_z_a; - case SORT_NEW_TO_OLD_ID: - return R.string.menu_item_sort_by_date_newest_first; - case SORT_OLD_TO_NEW_ID: - return R.string.menu_item_sort_by_date_oldest_first; - case SORT_BIG_TO_SMALL_ID: - return R.string.menu_item_sort_by_size_biggest_first; - case SORT_SMALL_TO_BIG_ID: - return R.string.menu_item_sort_by_size_smallest_first; - case SORT_A_TO_Z_ID: - default: - return R.string.menu_item_sort_by_name_a_z; - } - } - - /** - * calculates the relative time string based on the given modification timestamp. - * - * @param context the app's context - * @param modificationTimestamp the UNIX timestamp of the file modification time in milliseconds. - * @return a relative time string - */ - - public static CharSequence getRelativeTimestamp(Context context, long modificationTimestamp, boolean showFuture) { - return getRelativeDateTimeString(context, - modificationTimestamp, - DateUtils.SECOND_IN_MILLIS, - DateUtils.WEEK_IN_MILLIS, - 0, - showFuture); - } - - public static CharSequence getRelativeDateTimeString(Context c, - long time, - long minResolution, - long transitionResolution, - int flags, - boolean showFuture) { - - CharSequence dateString = ""; - - // in Future - if (!showFuture && time > System.currentTimeMillis()) { - return DisplayUtils.unixTimeToHumanReadable(time); - } - // < 60 seconds -> seconds ago - long diff = System.currentTimeMillis() - time; - if (diff > 0 && diff < 60 * 1000 && minResolution == DateUtils.SECOND_IN_MILLIS) { - return c.getString(R.string.secondsAgo); - } else { - dateString = DateUtils.getRelativeDateTimeString(c, time, minResolution, transitionResolution, flags); - } - - String[] parts = dateString.toString().split(","); - if (parts.length == DATE_TIME_PARTS_SIZE) { - if (parts[1].contains(":") && !parts[0].contains(":")) { - return parts[0]; - } else if (parts[0].contains(":") && !parts[1].contains(":")) { - return parts[1]; - } - } - // dateString contains unexpected format. fallback: use relative date time string from android api as is. - return dateString.toString(); - } - - /** - * Converts Unix time to human readable format - * - * @param milliseconds that have passed since 01/01/1970 - * @return The human readable time for the users locale - */ - public static String unixTimeToHumanReadable(long milliseconds) { - Date date = new Date(milliseconds); - DateFormat df = DateFormat.getDateTimeInstance(); - return df.format(date); - } - - public static String ellipsize(String text, int maxLength) { - if (text.length() > maxLength) { - return text.substring(0, maxLength - 1) + "…"; - } - return text; - } -} diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt new file mode 100644 index 000000000..14f8680a8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt @@ -0,0 +1,540 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * @author Andy Scherzinger + * @author Tim Krüger + * Copyright (C) 2022 Tim Krüger + * Copyright (C) 2021 Andy Scherzinger + * Copyright (C) 2017-2020 Mario Danic + * + * 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 . + */ +package com.nextcloud.talk.utils + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.text.Spannable +import android.text.SpannableString +import android.text.Spanned +import android.text.TextPaint +import android.text.TextUtils +import android.text.format.DateUtils +import android.text.method.LinkMovementMethod +import android.text.style.AbsoluteSizeSpan +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.util.TypedValue +import android.view.View +import android.view.Window +import android.widget.EditText +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.annotation.XmlRes +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.drawable.DrawableCompat +import androidx.emoji2.text.EmojiCompat +import coil.Coil.imageLoader +import coil.request.ImageRequest +import coil.target.Target +import coil.transform.CircleCropTransformation +import com.google.android.material.chip.ChipDrawable +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.events.UserMentionClickEvent +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils.getUrlForAvatar +import com.nextcloud.talk.utils.ApiUtils.getUrlForFederatedAvatar +import com.nextcloud.talk.utils.ApiUtils.getUrlForGuestAvatar +import com.nextcloud.talk.utils.text.Spans.MentionChipSpan +import org.greenrobot.eventbus.EventBus +import third.parties.fresco.BetterImageSpan +import java.text.DateFormat +import java.util.Date +import java.util.regex.Pattern + +object DisplayUtils { + private val TAG = DisplayUtils::class.java.getSimpleName() + private const val INDEX_LUMINATION = 2 + private const val MAX_LIGHTNESS = 0.92 + private const val TWITTER_HANDLE_PREFIX = "@" + private const val HTTP_PROTOCOL = "http://" + private const val HTTPS_PROTOCOL = "https://" + private const val DATE_TIME_PARTS_SIZE = 2 + fun isDarkModeOn(context: Context): Boolean { + val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return currentNightMode == Configuration.UI_MODE_NIGHT_YES + } + + fun setClickableString(string: String, url: String?, textView: TextView) { + val spannableString = SpannableString(string) + spannableString.setSpan( + object : ClickableSpan() { + override fun onClick(widget: View) { + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + sharedApplication!!.applicationContext.startActivity(browserIntent) + } + + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.isUnderlineText = false + } + }, + 0, + string.length, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + textView.text = spannableString + textView.movementMethod = LinkMovementMethod.getInstance() + } + + fun getBitmap(drawable: Drawable): Bitmap { + val bitmap = Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bitmap + } + + @JvmStatic + fun convertDpToPixel(dp: Float, context: Context): Float { + return Math.round( + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, dp, + context.resources.displayMetrics + ) + 0.5f + ).toFloat() + } + + fun convertPixelToDp(px: Float, context: Context): Float { + return px / context.resources.displayMetrics.density + } + + fun getTintedDrawable(res: Resources, @DrawableRes drawableResId: Int, @ColorRes colorResId: Int): Drawable? { + val drawable = ResourcesCompat.getDrawable(res, drawableResId, null) + val color = res.getColor(colorResId) + drawable?.setTint(color) + return drawable + } + + @JvmStatic + fun getDrawableForMentionChipSpan( + context: Context, + id: String?, + roomToken: String?, + label: CharSequence, + conversationUser: User, + type: String, + @XmlRes chipResource: Int, + emojiEditText: EditText?, + viewThemeUtils: ViewThemeUtils, + isFederated: Boolean + ): Drawable { + val chip = ChipDrawable.createFromResource(context, chipResource) + chip.text = EmojiCompat.get().process(label) + chip.ellipsize = TextUtils.TruncateAt.MIDDLE + if (chipResource == R.xml.chip_you) { + viewThemeUtils.material.colorChipDrawable(context, chip) + } + val config = context.resources.configuration + chip.setLayoutDirection(config.layoutDirection) + val drawable: Int + val isCallOrGroup = "call" == type || "calls" == type || "groups" == type || "user-group" == type + if (!isCallOrGroup) { + drawable = if (chipResource == R.xml.chip_you) { + R.drawable.mention_chip + } else { + R.drawable.accent_circle + } + chip.setChipIconResource(drawable) + } else { + chip.setChipIconResource(R.drawable.ic_circular_group) + } + chip.setBounds(0, 0, chip.intrinsicWidth, chip.intrinsicHeight) + if (!isCallOrGroup) { + var url = getUrlForAvatar(conversationUser.baseUrl, id, false) + if ("guests" == type || "guest" == type) { + url = getUrlForGuestAvatar( + conversationUser.baseUrl, label.toString(), true + ) + } + if (isFederated) { + val darkTheme = if (isDarkModeOn(context)) 1 else 0 + url = getUrlForFederatedAvatar( + conversationUser.baseUrl!!, + roomToken!!, id!!, + darkTheme, false + ) + } + val imageRequest: ImageRequest = ImageRequest.Builder(context) + .data(url) + .crossfade(true) + .transformations(CircleCropTransformation()) + .target(object : Target { + override fun onStart(placeholder: Drawable?) {} + override fun onError(error: Drawable?) { + chip.chipIcon = error + } + + override fun onSuccess(result: Drawable) { + chip.chipIcon = result + // A hack to refresh the chip icon + emojiEditText?.post { + emojiEditText.setTextKeepState( + emojiEditText.getText(), + TextView.BufferType.SPANNABLE + ) + } + } + }) + .build() + imageLoader(context).enqueue(imageRequest) + } + return chip + } + + fun searchAndReplaceWithMentionSpan( + key: String, + context: Context, + text: Spanned, + id: String, + roomToken: String?, + label: String, + type: String, + conversationUser: User, + @XmlRes chipXmlRes: Int, + viewThemeUtils: ViewThemeUtils, + isFederated: Boolean + ): Spannable { + val spannableString: Spannable = SpannableString(text) + val stringText = text.toString() + val keyWithBrackets = "{$key}" + val m = Pattern.compile(keyWithBrackets, Pattern.CASE_INSENSITIVE or Pattern.LITERAL or Pattern.MULTILINE) + .matcher(spannableString) + val clickableSpan: ClickableSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + EventBus.getDefault().post(UserMentionClickEvent(id)) + } + } + var lastStartIndex = 0 + var mentionChipSpan: MentionChipSpan + while (m.find()) { + val start = stringText.indexOf(m.group(), lastStartIndex) + val end = start + m.group().length + lastStartIndex = end + val drawableForChip = getDrawableForMentionChipSpan( + context, + id, + roomToken, + label, + conversationUser, + type, + chipXmlRes, + null, + viewThemeUtils, + isFederated + ) + mentionChipSpan = MentionChipSpan( + drawableForChip, + BetterImageSpan.ALIGN_CENTER, + id, + label + ) + spannableString.setSpan(mentionChipSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + if (chipXmlRes == R.xml.chip_you) { + spannableString.setSpan( + viewThemeUtils.talk.themeForegroundColorSpan(context), + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + if ("user" == type && conversationUser.userId != id) { + spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + } + } + return spannableString + } + + fun searchAndColor(text: Spannable, searchText: String, @ColorInt color: Int): Spannable { + val spannableString: Spannable = SpannableString(text) + val stringText = text.toString() + if (TextUtils.isEmpty(text) || TextUtils.isEmpty(searchText)) { + return spannableString + } + val m = Pattern.compile( + searchText, + Pattern.CASE_INSENSITIVE or Pattern.LITERAL or Pattern.MULTILINE + ) + .matcher(spannableString) + val textSize = sharedApplication!!.resources.getDimensionPixelSize(R.dimen.chat_text_size) + var lastStartIndex = -1 + while (m.find()) { + val start = stringText.indexOf(m.group(), lastStartIndex) + val end = start + m.group().length + lastStartIndex = end + spannableString.setSpan( + ForegroundColorSpan(color), + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + spannableString.setSpan(StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + spannableString.setSpan(AbsoluteSizeSpan(textSize), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return spannableString + } + + fun getMessageSelector( + @ColorInt normalColor: Int, + @ColorInt selectedColor: Int, + @ColorInt pressedColor: Int, + @DrawableRes shape: Int + ): Drawable { + val vectorDrawable = ContextCompat.getDrawable( + sharedApplication!!.applicationContext, + shape + ) + val drawable = DrawableCompat.wrap(vectorDrawable!!).mutate() + DrawableCompat.setTintList( + drawable, + ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_selected), + intArrayOf(android.R.attr.state_pressed), + intArrayOf(-android.R.attr.state_pressed, -android.R.attr.state_selected) + ), + intArrayOf(selectedColor, pressedColor, normalColor) + ) + ) + return drawable + } + + /** + * Sets the color of the status bar to `color`. + * + * @param activity activity + * @param color the color + */ + fun applyColorToStatusBar(activity: Activity, @ColorInt color: Int) { + val window = activity.window + val isLightTheme = lightTheme(color) + if (window != null) { + val decor = window.decorView + if (isLightTheme) { + val systemUiFlagLightStatusBar: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or + View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + } else { + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } + decor.systemUiVisibility = systemUiFlagLightStatusBar + } else { + decor.systemUiVisibility = 0 + } + window.statusBarColor = color + } + } + + /** + * Tests if light color is set + * + * @param color the color + * @return true if primaryColor is lighter than MAX_LIGHTNESS + */ + fun lightTheme(color: Int): Boolean { + val hsl = colorToHSL(color) + + // spotbugs dislikes fixed index access + // which is enforced by having such an + // array from Android-API itself + return hsl[INDEX_LUMINATION] >= MAX_LIGHTNESS + } + + private fun colorToHSL(color: Int): FloatArray { + val hsl = FloatArray(3) + ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl) + return hsl + } + + fun applyColorToNavigationBar(window: Window, @ColorInt color: Int) { + window.navigationBarColor = color + } + + /** + * beautifies a given URL by removing any http/https protocol prefix. + * + * @param url to be beautified url + * @return beautified url + */ + @Suppress("ReturnCount") + fun beautifyURL(url: String?): String { + if (TextUtils.isEmpty(url)) { + return "" + } + if (url!!.length >= 7 && HTTP_PROTOCOL.equals(url.substring(0, 7), ignoreCase = true)) { + return url.substring(HTTP_PROTOCOL.length).trim { it <= ' ' } + } + return if (url.length >= 8 && HTTPS_PROTOCOL.equals(url.substring(0, 8), ignoreCase = true)) { + url.substring(HTTPS_PROTOCOL.length).trim { it <= ' ' } + } else { + url.trim { it <= ' ' } + } + } + + /** + * beautifies a given twitter handle by prefixing it with an @ in case it is missing. + * + * @param handle to be beautified twitter handle + * @return beautified twitter handle + */ + fun beautifyTwitterHandle(handle: String?): String { + return if (handle != null) { + val trimmedHandle = handle.trim { it <= ' ' } + if (TextUtils.isEmpty(trimmedHandle)) { + return "" + } + if (trimmedHandle.startsWith(TWITTER_HANDLE_PREFIX)) { + trimmedHandle + } else { + TWITTER_HANDLE_PREFIX + trimmedHandle + } + } else { + "" + } + } + + fun loadAvatarImage(user: User?, avatarImageView: ImageView?, deleteCache: Boolean) { + if (user != null && avatarImageView != null) { + val avatarId: String? = if (!TextUtils.isEmpty(user.userId)) { + user.userId + } else { + user.username + } + if (avatarId != null) { + avatarImageView.loadUserAvatar(user, avatarId, true, deleteCache) + } + } + } + + @StringRes + fun getSortOrderStringId(sortOrder: FileSortOrder): Int { + return when (sortOrder.name) { + FileSortOrder.SORT_Z_TO_A_ID -> R.string.menu_item_sort_by_name_z_a + FileSortOrder.SORT_NEW_TO_OLD_ID -> R.string.menu_item_sort_by_date_newest_first + FileSortOrder.SORT_OLD_TO_NEW_ID -> R.string.menu_item_sort_by_date_oldest_first + FileSortOrder.SORT_BIG_TO_SMALL_ID -> R.string.menu_item_sort_by_size_biggest_first + FileSortOrder.SORT_SMALL_TO_BIG_ID -> R.string.menu_item_sort_by_size_smallest_first + FileSortOrder.SORT_A_TO_Z_ID -> R.string.menu_item_sort_by_name_a_z + else -> R.string.menu_item_sort_by_name_a_z + } + } + + /** + * calculates the relative time string based on the given modification timestamp. + * + * @param context the app's context + * @param modificationTimestamp the UNIX timestamp of the file modification time in milliseconds. + * @return a relative time string + */ + fun getRelativeTimestamp(context: Context, modificationTimestamp: Long, showFuture: Boolean): CharSequence { + return getRelativeDateTimeString( + context, + modificationTimestamp, + DateUtils.SECOND_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + 0, + showFuture + ) + } + + @Suppress("ReturnCount") + private fun getRelativeDateTimeString( + c: Context, + time: Long, + minResolution: Long, + transitionResolution: Long, + flags: Int, + showFuture: Boolean + ): CharSequence { + val dateString: CharSequence + + // in Future + if (!showFuture && time > System.currentTimeMillis()) { + return unixTimeToHumanReadable(time) + } + // < 60 seconds -> seconds ago + val diff = System.currentTimeMillis() - time + dateString = if (diff > 0 && diff < 60 * 1000 && minResolution == DateUtils.SECOND_IN_MILLIS) { + return c.getString(R.string.secondsAgo) + } else { + DateUtils.getRelativeDateTimeString(c, time, minResolution, transitionResolution, flags) + } + val parts = dateString.toString().split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (parts.size == DATE_TIME_PARTS_SIZE) { + if (parts[1].contains(":") && !parts[0].contains(":")) { + return parts[0] + } else if (parts[0].contains(":") && !parts[1].contains(":")) { + return parts[1] + } + } + // dateString contains unexpected format. fallback: use relative date time string from android api as is. + return dateString.toString() + } + + /** + * Converts Unix time to human readable format + * + * @param milliseconds that have passed since 01/01/1970 + * @return The human readable time for the users locale + */ + fun unixTimeToHumanReadable(milliseconds: Long): String { + val date = Date(milliseconds) + val df = DateFormat.getDateTimeInstance() + return df.format(date) + } + + fun ellipsize(text: String, maxLength: Int): String { + return if (text.length > maxLength) { + text.substring(0, maxLength - 1) + "…" + } else { + text + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt index cb8787746..73856063c 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt @@ -134,10 +134,10 @@ class MessageUtils(val context: Context) { } messageStringInternal = DisplayUtils.searchAndReplaceWithMentionSpan( - key, + key!!, themingContext, messageStringInternal, - id, + id!!, message.roomToken, individualHashMap["name"]!!, individualHashMap["type"]!!,