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 62cc33510..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java +++ /dev/null @@ -1,227 +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"; - - private String source; - 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 ViewThemeUtils viewThemeUtils; - - public MentionAutocompleteItem( - Mention mention, - User currentUser, - Context activityContext, ViewThemeUtils viewThemeUtils) { - 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; - } - - public String getSource() { - return source; - } - - public void setSource(String source) { - this.source = source; - } - - public String getObjectId() { - return objectId; - } - - public String getDisplayName() { - return displayName; - } - - @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); - } - - if (SOURCE_CALLS.equals(source) || SOURCE_GROUPS.equals(source)) { - 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); - } - } else { - String avatarId = objectId; - if (SOURCE_GUESTS.equals(source)) { - avatarId = displayName; - } - 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 { private final ViewThemeUtils viewThemeUtils; @@ -66,26 +68,31 @@ public class MentionAutocompleteCallback implements AutocompleteCallback 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, - 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 @@ -185,9 +186,14 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter Mention mention = new Mention(); MentionAutocompleteItem mentionAutocompleteItem = (MentionAutocompleteItem) adapter.getItem(position); if (mentionAutocompleteItem != null) { - mention.setId(mentionAutocompleteItem.getObjectId()); - mention.setLabel(mentionAutocompleteItem.getDisplayName()); - mention.setSource(mentionAutocompleteItem.getSource()); + String mentionId = mentionAutocompleteItem.mentionId; + if (mentionId != null) { + mention.setMentionId(mentionId); + } + mention.setId(mentionAutocompleteItem.objectId); + mention.setLabel(mentionAutocompleteItem.displayName); + mention.setSource(mentionAutocompleteItem.source); + mention.setRoomToken(mentionAutocompleteItem.roomToken); 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 deleted file mode 100644 index c964645e8..000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ /dev/null @@ -1,563 +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.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, - CharSequence label, - User conversationUser, - String type, - @XmlRes int chipResource, - @Nullable EditText emojiEditText, - ViewThemeUtils viewThemeUtils) { - 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, true); - if ("guests".equals(type) || "guest".equals(type)) { - url = ApiUtils.getUrlForGuestAvatar( - conversationUser.getBaseUrl(), - String.valueOf(label), true); - } - - 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) { - - } - - @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 label, String type, - User conversationUser, - @XmlRes int chipXmlRes, - ViewThemeUtils viewThemeUtils) { - - 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 = -1; - 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, - label, - conversationUser, - type, - chipXmlRes, - null, - viewThemeUtils); - - 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 786d9f7f4..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 @@ -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, + 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://" } }