Federated Mentions

- Federated mention chip
- Federated message avatars
- Helper functions

Signed-off-by: Julius Linus <julius.linus@nextcloud.com>
This commit is contained in:
Julius Linus 2024-03-08 11:21:09 -06:00 committed by Andy Scherzinger
parent 22de77896a
commit 870ef03d61
16 changed files with 181 additions and 36 deletions

View File

@ -58,8 +58,10 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<ParticipantIte
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;
@ -67,12 +69,14 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<ParticipantIte
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, ViewThemeUtils viewThemeUtils) {
Context activityContext, String roomToken, ViewThemeUtils viewThemeUtils) {
this.mentionId = mention.getMentionId();
this.objectId = mention.getId();
this.displayName = mention.getLabel();
this.source = mention.getSource();
@ -82,6 +86,7 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<ParticipantIte
this.currentUser = currentUser;
this.context = activityContext;
this.viewThemeUtils = viewThemeUtils;
this.roomToken = roomToken;
}
public String getSource() {
@ -92,6 +97,10 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<ParticipantIte
this.source = source;
}
public String getMentionId() {
return mentionId;
}
public String getObjectId() {
return objectId;
}
@ -100,6 +109,10 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<ParticipantIte
return displayName;
}
public String getRoomToken() {
return roomToken;
}
@Override
public boolean equals(Object o) {
if (o instanceof MentionAutocompleteItem inItem) {
@ -147,25 +160,40 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<ParticipantIte
holder.binding.secondaryText.setText("@" + objectId);
}
if (SOURCE_CALLS.equals(source) || SOURCE_GROUPS.equals(source)) {
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
)
);
R.drawable.ic_avatar_group));
} else {
ImageViewExtensionsKt.loadUserAvatar(holder.binding.avatarView, R.drawable.ic_circular_group);
}
} else {
String avatarId = objectId;
if (SOURCE_GUESTS.equals(source)) {
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);
}

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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<Mention> {
private final ViewThemeUtils viewThemeUtils;
@ -66,26 +68,31 @@ public class MentionAutocompleteCallback implements AutocompleteCallback<Mention
}
String replacement = item.getLabel();
StringBuilder replacementStringBuilder = new StringBuilder(item.getLabel());
StringBuilder replacementStringBuilder = new StringBuilder(Objects.requireNonNull(item.getLabel()));
for (EmojiRange emojiRange : Emojis.emojis(replacement)) {
replacementStringBuilder.delete(emojiRange.range.getStart(), emojiRange.range.getEndInclusive());
}
editable.replace(range.getStart(), range.getEnd(), replacementStringBuilder + " ");
String charSequence = " ";
editable.replace(range.getStart(), range.getEnd(), charSequence + replacementStringBuilder + " ");
String id;
if (item.getMentionId() != null) id = item.getMentionId(); else id = item.getId();
Spans.MentionChipSpan mentionChipSpan =
new Spans.MentionChipSpan(DisplayUtils.getDrawableForMentionChipSpan(context,
item.getId(),
item.getRoomToken(),
item.getLabel(),
conversationUser,
item.getSource(),
R.xml.chip_you,
editText,
viewThemeUtils),
viewThemeUtils,
"federated_users".equals(item.getSource())),
BetterImageSpan.ALIGN_CENTER,
item.getId(), item.getLabel());
id, item.getLabel());
editable.setSpan(mentionChipSpan,
range.getStart(),
range.getStart() + replacementStringBuilder.length(),
range.getStart() + charSequence.length(),
range.getStart() + replacementStringBuilder.length() + charSequence.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);

View File

@ -3584,6 +3584,7 @@ class ChatActivity :
mentionSpan = mentionSpans[i]
var mentionId = mentionSpan.id
if (mentionId.contains(" ") ||
mentionId.contains("@") ||
mentionId.startsWith("guest/") ||
mentionId.startsWith("group/")
) {
@ -3798,6 +3799,7 @@ class ChatActivity :
chatMessage.isFormerOneToOneConversation =
(currentConversation?.type == ConversationType.FORMER_ONE_TO_ONE)
chatMessage.activeUser = conversationUser
chatMessage.roomToken = roomToken
}
if (adapter != null) {

View File

@ -46,6 +46,7 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.domain.ConversationType
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
@ -126,6 +127,47 @@ fun ImageView.loadUserAvatar(
return loadAvatarInternal(user, imageRequestUri, ignoreCache, null)
}
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 darkTheme = if (DisplayUtils.isDarkModeOn(context)) 1 else 0
val ignoreCache = false
val requestBigSize = true
return loadFederatedUserAvatar(
message.activeUser!!,
message.activeUser!!.baseUrl!!,
message.roomToken,
cloudId,
darkTheme,
requestBigSize,
ignoreCache
)
}
@Suppress("LongParameterList")
fun ImageView.loadFederatedUserAvatar(
user: User,
baseUrl: String,
token: String,
cloudId: String,
darkTheme: Int,
requestBigSize: Boolean = true,
ignoreCache: Boolean
): io.reactivex.disposables.Disposable {
val imageRequestUri = ApiUtils.getUrlForFederatedAvatar(
baseUrl,
token,
cloudId,
darkTheme,
requestBigSize
)
Log.d("Julius", "URL::$imageRequestUri")
return loadAvatarInternal(user, imageRequestUri, ignoreCache, null)
}
@OptIn(ExperimentalCoilApi::class)
private fun ImageView.loadAvatarInternal(
user: User?,

View File

@ -159,7 +159,9 @@ data class ChatMessage(
var hiddenByCollapse: Boolean = false,
var openWhenDownloaded: Boolean = true
var openWhenDownloaded: Boolean = true,
var roomToken: String = ""
) : Parcelable, MessageContentType, MessageContentType.Image {

View File

@ -23,12 +23,15 @@ package com.nextcloud.talk.models.json.mention
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonIgnore
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class Mention(
@JsonField(name = ["mentionId"])
var mentionId: String?,
@JsonField(name = ["id"])
var id: String?,
@JsonField(name = ["label"])
@ -41,8 +44,10 @@ data class Mention(
@JsonField(name = ["statusIcon"])
var statusIcon: String?,
@JsonField(name = ["statusMessage"])
var statusMessage: String?
var statusMessage: String?,
@JsonIgnore
var roomToken: String?
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null, null, null, null, null)
constructor() : this(null, null, null, null, null, null, null, null)
}

View File

@ -155,6 +155,7 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
mention,
currentUser,
context,
roomToken,
viewThemeUtils));
}
@ -185,9 +186,14 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
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;

View File

@ -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

View File

@ -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,

View File

@ -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://"
}
}