From b3f298db975c956d1f1cb649ecc653e1a018c845 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 5 Mar 2025 09:14:07 +0100 Subject: [PATCH 1/9] wip Signed-off-by: tobiasKaminsky --- .../java/com/nextcloud/utils/text/Spans.java | 82 ++++++++++++ .../ui/adapter/ActivityListAdapter.java | 101 +++++++++++++++ .../third_parties/fresco/BetterImageSpan.kt | 120 ++++++++++++++++++ .../main/res/layout/activity_list_item.xml | 34 +++-- app/src/main/res/values/styles.xml | 4 + app/src/main/res/xml/chip_others.xml | 13 ++ 6 files changed, 343 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/utils/text/Spans.java create mode 100644 app/src/main/java/third_parties/fresco/BetterImageSpan.kt create mode 100644 app/src/main/res/xml/chip_others.xml diff --git a/app/src/main/java/com/nextcloud/utils/text/Spans.java b/app/src/main/java/com/nextcloud/utils/text/Spans.java new file mode 100644 index 000000000000..0834c6520cbe --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/text/Spans.java @@ -0,0 +1,82 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.utils.text; + +import android.graphics.drawable.Drawable; + +import androidx.annotation.NonNull; +import third_parties.fresco.BetterImageSpan; + +public class Spans { + + public static class MentionChipSpan extends BetterImageSpan { + public String id; + public CharSequence label; + + public MentionChipSpan(@NonNull Drawable drawable, int verticalAlignment, String id, CharSequence label) { + super(drawable, verticalAlignment); + this.id = id; + this.label = label; + } + + public String getId() { + return this.id; + } + + public CharSequence getLabel() { + return this.label; + } + + public void setId(String id) { + this.id = id; + } + + public void setLabel(CharSequence label) { + this.label = label; + } + + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof MentionChipSpan)) { + return false; + } + final MentionChipSpan other = (MentionChipSpan) o; + if (!other.canEqual((Object) this)) { + return false; + } + final Object this$id = this.getId(); + final Object other$id = other.getId(); + if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + return false; + } + final Object this$label = this.getLabel(); + final Object other$label = other.getLabel(); + + return this$label == null ? other$label == null : this$label.equals(other$label); + } + + protected boolean canEqual(final Object other) { + return other instanceof MentionChipSpan; + } + + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $id = this.getId(); + result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + final Object $label = this.getLabel(); + return result * PRIME + ($label == null ? 43 : $label.hashCode()); + } + + public String toString() { + return "Spans.MentionChipSpan(id=" + this.getId() + ", label=" + this.getLabel() + ")"; + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java index d8246088ae81..dea183daff81 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java @@ -16,9 +16,12 @@ import android.content.res.Resources; import android.graphics.Color; import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.text.Spannable; +import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.TextPaint; import android.text.TextUtils; @@ -35,9 +38,17 @@ import android.widget.LinearLayout; import android.widget.TextView; +import com.bumptech.glide.GenericRequestBuilder; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.model.StreamEncoder; +import com.bumptech.glide.load.resource.file.FileToStreamDecoder; +import com.caverock.androidsvg.SVG; +import com.google.android.material.chip.ChipDrawable; import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.network.ClientFactory; import com.nextcloud.common.NextcloudClient; +import com.nextcloud.utils.text.Spans; import com.nextcloud.utils.GlideHelper; import com.owncloud.android.MainApp; import com.owncloud.android.R; @@ -57,10 +68,14 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.Optional; import androidx.annotation.NonNull; +import androidx.annotation.XmlRes; import androidx.recyclerview.widget.RecyclerView; +import third_parties.fresco.BetterImageSpan; /** * Adapter for the activity view. @@ -150,6 +165,13 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi activityViewHolder.binding.subject.setMovementMethod(LinkMovementMethod.getInstance()); activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()), TextView.BufferType.SPANNABLE); + + activityViewHolder.binding.subject.setText(searchAndReplaceWithMentionSpan("actor", + activity.getRichSubjectElement().getRichSubject(), + "1", + "label", + R.xml.chip_others)); + activityViewHolder.binding.subject.setVisibility(View.VISIBLE); } else if (!TextUtils.isEmpty(activity.getSubject())) { activityViewHolder.binding.subject.setVisibility(View.VISIBLE); @@ -275,6 +297,85 @@ private ImageView createThumbnailNew(PreviewObject previewObject, List requestBuilder = Glide.with(context) + .using(Glide.buildStreamModelLoader(Uri.class, context), InputStream.class) + .from(Uri.class) + .as(SVG.class) + .transcode(new SvgBitmapTranscoder(128, 128), Bitmap.class) + .sourceEncoder(new StreamEncoder()) + .cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder())) + .decoder(new SvgDecoder()) + .placeholder(R.drawable.ic_activity) + .error(R.drawable.ic_activity) + .animate(android.R.anim.fade_in); + + Uri uri = Uri.parse(activity.getIcon()); + requestBuilder + .diskCacheStrategy(DiskCacheStrategy.SOURCE) + .load(uri) + .into(itemViewType); + } + + /** + * c&p from Talk: DisplayUtils:227 + * + * @return Spannable + */ + private Spannable searchAndReplaceWithMentionSpan( + String key, + String text, + String id, + String label, + @XmlRes int chipXmlRes) { + 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 view) { + //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 = getDrawableForMentionChipSpan( + chipXmlRes + ); + 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" == type && conversationUser.userId != id && !isFederated) { +// spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); +// } + } + return spannableString; + } + + private Drawable getDrawableForMentionChipSpan(int chipResource) { + return ChipDrawable.createFromResource(context, chipResource); + } + private SpannableStringBuilder addClickablePart(RichElement richElement) { String text = richElement.getRichSubject(); SpannableStringBuilder ssb = new SpannableStringBuilder(text); diff --git a/app/src/main/java/third_parties/fresco/BetterImageSpan.kt b/app/src/main/java/third_parties/fresco/BetterImageSpan.kt new file mode 100644 index 000000000000..7de8994aaa5e --- /dev/null +++ b/app/src/main/java/third_parties/fresco/BetterImageSpan.kt @@ -0,0 +1,120 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package third_parties.fresco + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.text.style.ReplacementSpan +import androidx.annotation.IntDef + +/** + * A better implementation of image spans that also supports centering images against the text. + * + * In order to migrate from ImageSpan, replace `new ImageSpan(drawable, alignment)` with + * `new BetterImageSpan(drawable, BetterImageSpan.normalizeAlignment(alignment))`. + * + * There are 2 main differences between BetterImageSpan and ImageSpan: + * 1. Pass in ALIGN_CENTER to center images against the text. + * 2. ALIGN_BOTTOM no longer unnecessarily increases the size of the text: + * DynamicDrawableSpan (ImageSpan's parent) adjusts sizes as if alignment was ALIGN_BASELINE + * which can lead to unnecessary whitespace. + */ +open class BetterImageSpan @JvmOverloads constructor( + val drawable: Drawable, + @param:BetterImageSpanAlignment private val mAlignment: Int = ALIGN_BASELINE +) : ReplacementSpan() { + @IntDef(*[ALIGN_BASELINE, ALIGN_BOTTOM, ALIGN_CENTER]) + @Retention(AnnotationRetention.SOURCE) + annotation class BetterImageSpanAlignment + + private var mWidth = 0 + private var mHeight = 0 + private var mBounds: Rect? = null + private val mFontMetricsInt = Paint.FontMetricsInt() + + init { + updateBounds() + } + + /** + * Returns the width of the image span and increases the height if font metrics are available. + */ + override fun getSize( + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fontMetrics: Paint.FontMetricsInt? + ): Int { + updateBounds() + if (fontMetrics == null) { + return mWidth + } + val offsetAbove = getOffsetAboveBaseline(fontMetrics) + val offsetBelow = mHeight + offsetAbove + if (offsetAbove < fontMetrics.ascent) { + fontMetrics.ascent = offsetAbove + } + if (offsetAbove < fontMetrics.top) { + fontMetrics.top = offsetAbove + } + if (offsetBelow > fontMetrics.descent) { + fontMetrics.descent = offsetBelow + } + if (offsetBelow > fontMetrics.bottom) { + fontMetrics.bottom = offsetBelow + } + return mWidth + } + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint + ) { + paint.getFontMetricsInt(mFontMetricsInt) + val iconTop = y + getOffsetAboveBaseline(mFontMetricsInt) + canvas.translate(x, iconTop.toFloat()) + drawable.draw(canvas) + canvas.translate(-x, -iconTop.toFloat()) + } + + private fun updateBounds() { + mBounds = drawable.bounds + mWidth = mBounds!!.width() + mHeight = mBounds!!.height() + } + + private fun getOffsetAboveBaseline(fm: Paint.FontMetricsInt): Int { + return when (mAlignment) { + ALIGN_BOTTOM -> fm.descent - mHeight + ALIGN_CENTER -> { + val textHeight = fm.descent - fm.ascent + val offset = (textHeight - mHeight) / 2 + fm.ascent + offset + } + + ALIGN_BASELINE -> -mHeight + else -> -mHeight + } + } + + companion object { + const val ALIGN_BOTTOM = 0 + const val ALIGN_BASELINE = 1 + const val ALIGN_CENTER = 2 + } +} diff --git a/app/src/main/res/layout/activity_list_item.xml b/app/src/main/res/layout/activity_list_item.xml index ad17d9e40485..970ca3d18c72 100644 --- a/app/src/main/res/layout/activity_list_item.xml +++ b/app/src/main/res/layout/activity_list_item.xml @@ -32,17 +32,29 @@ android:layout_toEndOf="@id/icon" android:orientation="vertical"> - + + + + + + @style/Theme.ownCloud.Launcher + + diff --git a/app/src/main/res/xml/chip_others.xml b/app/src/main/res/xml/chip_others.xml new file mode 100644 index 000000000000..e71cc44cee5a --- /dev/null +++ b/app/src/main/res/xml/chip_others.xml @@ -0,0 +1,13 @@ + + From 84c527dcd348657b36c5bca2acf774dc8b9565a1 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 16 Apr 2025 10:43:05 +0200 Subject: [PATCH 2/9] wip Signed-off-by: tobiasKaminsky --- .../ui/adapter/ActivityListAdapter.java | 25 +++++++++++++------ app/src/main/res/drawable/accent_circle.xml | 10 ++++++++ .../main/res/layout/activity_list_item.xml | 6 ----- 3 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 app/src/main/res/drawable/accent_circle.xml diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java index dea183daff81..1707cedcf959 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java @@ -23,11 +23,11 @@ import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.TextPaint; import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; -import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; @@ -162,9 +162,9 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi if (!TextUtils.isEmpty(activity.getRichSubjectElement().getRichSubject())) { activityViewHolder.binding.subject.setVisibility(View.VISIBLE); - activityViewHolder.binding.subject.setMovementMethod(LinkMovementMethod.getInstance()); - activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()), - TextView.BufferType.SPANNABLE); + // activityViewHolder.binding.subject.setMovementMethod(LinkMovementMethod.getInstance()); +// activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()), +// TextView.BufferType.SPANNABLE); activityViewHolder.binding.subject.setText(searchAndReplaceWithMentionSpan("actor", activity.getRichSubjectElement().getRichSubject(), @@ -322,7 +322,7 @@ private void downloadIcon(Activity activity, ImageView itemViewType) { * * @return Spannable */ - private Spannable searchAndReplaceWithMentionSpan( + private Spanned searchAndReplaceWithMentionSpan( String key, String text, String id, @@ -348,8 +348,11 @@ public void onClick(@NonNull View view) { int end = start + m.group().length(); lastStartIndex = end; Drawable drawableForChip = getDrawableForMentionChipSpan( - chipXmlRes + chipXmlRes, + label ); + + mentionChipSpan = new Spans.MentionChipSpan( drawableForChip, BetterImageSpan.ALIGN_CENTER, @@ -372,8 +375,14 @@ public void onClick(@NonNull View view) { return spannableString; } - private Drawable getDrawableForMentionChipSpan(int chipResource) { - return ChipDrawable.createFromResource(context, chipResource); + private Drawable getDrawableForMentionChipSpan(int chipResource, String text) { + ChipDrawable chip = ChipDrawable.createFromResource(context, chipResource); + chip.setEllipsize(TextUtils.TruncateAt.MIDDLE); + chip.setText(text); + chip.setChipIconResource(R.drawable.accent_circle); + chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight()); + + return chip; } private SpannableStringBuilder addClickablePart(RichElement richElement) { diff --git a/app/src/main/res/drawable/accent_circle.xml b/app/src/main/res/drawable/accent_circle.xml new file mode 100644 index 000000000000..3acd7c33c465 --- /dev/null +++ b/app/src/main/res/drawable/accent_circle.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout/activity_list_item.xml b/app/src/main/res/layout/activity_list_item.xml index 970ca3d18c72..c4601ca2b5b0 100644 --- a/app/src/main/res/layout/activity_list_item.xml +++ b/app/src/main/res/layout/activity_list_item.xml @@ -36,12 +36,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - Date: Wed, 19 Nov 2025 10:50:22 +0100 Subject: [PATCH 3/9] wip Signed-off-by: tobiasKaminsky --- .../ui/adapter/ActivityListAdapter.java | 228 ++++++++---------- app/src/main/res/xml/chip_others.xml | 2 +- 2 files changed, 106 insertions(+), 124 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java index 1707cedcf959..28e1e594d2d0 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java @@ -17,17 +17,16 @@ import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.text.Spannable; -import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextPaint; import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; +import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; @@ -38,18 +37,12 @@ import android.widget.LinearLayout; import android.widget.TextView; -import com.bumptech.glide.GenericRequestBuilder; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.model.StreamEncoder; -import com.bumptech.glide.load.resource.file.FileToStreamDecoder; -import com.caverock.androidsvg.SVG; import com.google.android.material.chip.ChipDrawable; import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.network.ClientFactory; import com.nextcloud.common.NextcloudClient; -import com.nextcloud.utils.text.Spans; import com.nextcloud.utils.GlideHelper; +import com.nextcloud.utils.text.Spans; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.databinding.ActivityListItemBinding; @@ -68,19 +61,17 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.Optional; import androidx.annotation.NonNull; -import androidx.annotation.XmlRes; import androidx.recyclerview.widget.RecyclerView; import third_parties.fresco.BetterImageSpan; /** * Adapter for the activity view. */ -public class ActivityListAdapter extends RecyclerView.Adapter implements StickyHeaderAdapter { +public class ActivityListAdapter extends RecyclerView.Adapter implements StickyHeaderAdapter, + DisplayUtils.AvatarGenerationListener { static final int HEADER_TYPE = 100; static final int ACTIVITY_TYPE = 101; @@ -162,16 +153,17 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi if (!TextUtils.isEmpty(activity.getRichSubjectElement().getRichSubject())) { activityViewHolder.binding.subject.setVisibility(View.VISIBLE); - // activityViewHolder.binding.subject.setMovementMethod(LinkMovementMethod.getInstance()); -// activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()), -// TextView.BufferType.SPANNABLE); - - activityViewHolder.binding.subject.setText(searchAndReplaceWithMentionSpan("actor", - activity.getRichSubjectElement().getRichSubject(), - "1", - "label", - R.xml.chip_others)); + + + + + activityViewHolder.binding.subject.setMovementMethod(LinkMovementMethod.getInstance()); +// activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()), TextView.BufferType.SPANNABLE); + + Spanned old = addClickablePart(activity.getRichSubjectElement()); + activityViewHolder.binding.subject.setText(old); + activityViewHolder.binding.subject.setVisibility(View.VISIBLE); } else if (!TextUtils.isEmpty(activity.getSubject())) { activityViewHolder.binding.subject.setVisibility(View.VISIBLE); @@ -296,88 +288,31 @@ private ImageView createThumbnailNew(PreviewObject previewObject, List requestBuilder = Glide.with(context) - .using(Glide.buildStreamModelLoader(Uri.class, context), InputStream.class) - .from(Uri.class) - .as(SVG.class) - .transcode(new SvgBitmapTranscoder(128, 128), Bitmap.class) - .sourceEncoder(new StreamEncoder()) - .cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder())) - .decoder(new SvgDecoder()) - .placeholder(R.drawable.ic_activity) - .error(R.drawable.ic_activity) - .animate(android.R.anim.fade_in); - - Uri uri = Uri.parse(activity.getIcon()); - requestBuilder - .diskCacheStrategy(DiskCacheStrategy.SOURCE) - .load(uri) - .into(itemViewType); - } - - /** - * c&p from Talk: DisplayUtils:227 - * - * @return Spannable - */ - private Spanned searchAndReplaceWithMentionSpan( - String key, - String text, - String id, - String label, - @XmlRes int chipXmlRes) { - 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 view) { - //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 = getDrawableForMentionChipSpan( - chipXmlRes, - label - ); - - - 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" == type && conversationUser.userId != id && !isFederated) { -// spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); -// } - } - return spannableString; - } - - private Drawable getDrawableForMentionChipSpan(int chipResource, String text) { +// +// private void downloadIcon(Activity activity, ImageView itemViewType) { +// GenericRequestBuilder requestBuilder = Glide.with(context) +// .using(Glide.buildStreamModelLoader(Uri.class, context), InputStream.class) +// .from(Uri.class) +// .as(SVG.class) +// .transcode(new SvgBitmapTranscoder(128, 128), Bitmap.class) +// .sourceEncoder(new StreamEncoder()) +// .cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder())) +// .decoder(new SvgDecoder()) +// .placeholder(R.drawable.ic_activity) +// .error(R.drawable.ic_activity) +// .animate(android.R.anim.fade_in); +// +// Uri uri = Uri.parse(activity.getIcon()); +// requestBuilder +// .diskCacheStrategy(DiskCacheStrategy.SOURCE) +// .load(uri) +// .into(itemViewType); +// } + + private ChipDrawable getDrawableForMentionChipSpan(int chipResource, String text) { ChipDrawable chip = ChipDrawable.createFromResource(context, chipResource); chip.setEllipsize(TextUtils.TruncateAt.MIDDLE); + chip.setLayoutDirection(context.getResources().getConfiguration().getLayoutDirection()); chip.setText(text); chip.setChipIconResource(R.drawable.accent_circle); chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight()); @@ -396,28 +331,65 @@ private SpannableStringBuilder addClickablePart(RichElement richElement) { final String clickString = text.substring(idx1 + 1, idx2 - 1); final RichObject richObject = searchObjectByName(richElement.getRichObjectList(), clickString); if (richObject != null) { - String name = richObject.getName(); - ssb.replace(idx1, idx2, name); - text = ssb.toString(); - idx2 = idx1 + name.length(); - ssb.setSpan(new ClickableSpan() { - @Override - public void onClick(@NonNull View widget) { - activityListInterface.onActivityClicked(richObject); - } + if ("user".equals(richObject.getType())) { + String name = richObject.getName(); + + ChipDrawable drawableForChip = getDrawableForMentionChipSpan(R.xml.chip_others, name); + + Spans.MentionChipSpan mentionChipSpan = new Spans.MentionChipSpan(drawableForChip, + BetterImageSpan.ALIGN_CENTER, + richObject.getId(), + name + ); + + DisplayUtils.setAvatar( + currentAccountProvider.getUser(), + richObject.getId(), + name, + this, + context.getResources().getDimension(R.dimen.standard_padding), + context.getResources(), + drawableForChip, + context + ); + + ssb.setSpan(mentionChipSpan, idx1, idx2, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - @Override - public void updateDrawState(@NonNull TextPaint ds) { - ds.setUnderlineText(false); - } - }, idx1, idx2, 0); - ssb.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), idx1, idx2, 0); - ssb.setSpan( - new ForegroundColorSpan(context.getResources().getColor(R.color.text_color)), - idx1, - idx2, - 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 && !isFederated) { +// spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); +// } + } else { + String name = richObject.getName(); + ssb.replace(idx1, idx2, name); + text = ssb.toString(); + idx2 = idx1 + name.length(); + ssb.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View widget) { + activityListInterface.onActivityClicked(richObject); + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + ds.setUnderlineText(false); + } + }, idx1, idx2, 0); + ssb.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), idx1, idx2, 0); + ssb.setSpan( + new ForegroundColorSpan(context.getResources().getColor(R.color.log_level_error)), + idx1, + idx2, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } } idx1 = text.indexOf('{', idx2); } @@ -506,6 +478,16 @@ public boolean isHeader(int itemPosition) { return this.getItemViewType(itemPosition) == HEADER_TYPE; } + @Override + public void avatarGenerated(Drawable avatarDrawable, Object callContext) { + ((ChipDrawable) callContext).setChipIcon(avatarDrawable); + } + + @Override + public boolean shouldCallGeneratedCallback(String tag, Object callContext) { + return true; + } + protected class ActivityViewHolder extends RecyclerView.ViewHolder { ActivityListItemBinding binding; diff --git a/app/src/main/res/xml/chip_others.xml b/app/src/main/res/xml/chip_others.xml index e71cc44cee5a..9f878b3edb22 100644 --- a/app/src/main/res/xml/chip_others.xml +++ b/app/src/main/res/xml/chip_others.xml @@ -7,7 +7,7 @@ From 3f34b747efba33b84b9246ba89aee309e90b3492 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 5 Mar 2025 09:14:07 +0100 Subject: [PATCH 4/9] wip Signed-off-by: tobiasKaminsky --- .../ui/adapter/ActivityListAdapter.java | 86 +++++++++++++++++-- .../main/res/layout/activity_list_item.xml | 6 ++ 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java index 28e1e594d2d0..f6aa8639bd28 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java @@ -17,9 +17,12 @@ import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.text.Spannable; +import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextPaint; @@ -37,12 +40,20 @@ import android.widget.LinearLayout; import android.widget.TextView; +import com.google.android.material.chip.ChipDrawable; +import com.bumptech.glide.GenericRequestBuilder; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.model.StreamEncoder; +import com.bumptech.glide.load.resource.file.FileToStreamDecoder; +import com.caverock.androidsvg.SVG; import com.google.android.material.chip.ChipDrawable; import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.network.ClientFactory; import com.nextcloud.common.NextcloudClient; import com.nextcloud.utils.GlideHelper; import com.nextcloud.utils.text.Spans; +import com.nextcloud.utils.text.Spans; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.databinding.ActivityListItemBinding; @@ -61,9 +72,12 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.Optional; import androidx.annotation.NonNull; +import androidx.annotation.XmlRes; import androidx.recyclerview.widget.RecyclerView; import third_parties.fresco.BetterImageSpan; @@ -158,12 +172,15 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi activityViewHolder.binding.subject.setMovementMethod(LinkMovementMethod.getInstance()); -// activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()), TextView.BufferType.SPANNABLE); - - Spanned old = addClickablePart(activity.getRichSubjectElement()); + activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()), + TextView.BufferType.SPANNABLE); + + activityViewHolder.binding.subject.setText(searchAndReplaceWithMentionSpan("actor", + activity.getRichSubjectElement().getRichSubject(), + "1", + "label", + R.xml.chip_others)); - activityViewHolder.binding.subject.setText(old); - activityViewHolder.binding.subject.setVisibility(View.VISIBLE); } else if (!TextUtils.isEmpty(activity.getSubject())) { activityViewHolder.binding.subject.setVisibility(View.VISIBLE); @@ -320,6 +337,65 @@ private ChipDrawable getDrawableForMentionChipSpan(int chipResource, String text return chip; } + /** + * c&p from Talk: DisplayUtils:227 + * + * @return Spannable + */ + private Spannable searchAndReplaceWithMentionSpan( + String key, + String text, + String id, + String label, + @XmlRes int chipXmlRes) { + 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 view) { + //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 = getDrawableForMentionChipSpan( + chipXmlRes + ); + 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" == type && conversationUser.userId != id && !isFederated) { +// spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); +// } + } + return spannableString; + } + + private Drawable getDrawableForMentionChipSpan(int chipResource) { + return ChipDrawable.createFromResource(context, chipResource); + } + private SpannableStringBuilder addClickablePart(RichElement richElement) { String text = richElement.getRichSubject(); SpannableStringBuilder ssb = new SpannableStringBuilder(text); diff --git a/app/src/main/res/layout/activity_list_item.xml b/app/src/main/res/layout/activity_list_item.xml index c4601ca2b5b0..970ca3d18c72 100644 --- a/app/src/main/res/layout/activity_list_item.xml +++ b/app/src/main/res/layout/activity_list_item.xml @@ -36,6 +36,12 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + Date: Wed, 16 Apr 2025 10:43:05 +0200 Subject: [PATCH 5/9] wip Signed-off-by: tobiasKaminsky --- .../ui/adapter/ActivityListAdapter.java | 28 +++++++++++-------- .../main/res/layout/activity_list_item.xml | 6 ---- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java index f6aa8639bd28..25af602f9f4a 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java @@ -29,7 +29,6 @@ import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; -import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; @@ -167,13 +166,9 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi if (!TextUtils.isEmpty(activity.getRichSubjectElement().getRichSubject())) { activityViewHolder.binding.subject.setVisibility(View.VISIBLE); - - - - - activityViewHolder.binding.subject.setMovementMethod(LinkMovementMethod.getInstance()); - activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()), - TextView.BufferType.SPANNABLE); + // activityViewHolder.binding.subject.setMovementMethod(LinkMovementMethod.getInstance()); +// activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()), +// TextView.BufferType.SPANNABLE); activityViewHolder.binding.subject.setText(searchAndReplaceWithMentionSpan("actor", activity.getRichSubjectElement().getRichSubject(), @@ -342,7 +337,7 @@ private ChipDrawable getDrawableForMentionChipSpan(int chipResource, String text * * @return Spannable */ - private Spannable searchAndReplaceWithMentionSpan( + private Spanned searchAndReplaceWithMentionSpan( String key, String text, String id, @@ -368,8 +363,11 @@ public void onClick(@NonNull View view) { int end = start + m.group().length(); lastStartIndex = end; Drawable drawableForChip = getDrawableForMentionChipSpan( - chipXmlRes + chipXmlRes, + label ); + + mentionChipSpan = new Spans.MentionChipSpan( drawableForChip, BetterImageSpan.ALIGN_CENTER, @@ -392,8 +390,14 @@ public void onClick(@NonNull View view) { return spannableString; } - private Drawable getDrawableForMentionChipSpan(int chipResource) { - return ChipDrawable.createFromResource(context, chipResource); + private Drawable getDrawableForMentionChipSpan(int chipResource, String text) { + ChipDrawable chip = ChipDrawable.createFromResource(context, chipResource); + chip.setEllipsize(TextUtils.TruncateAt.MIDDLE); + chip.setText(text); + chip.setChipIconResource(R.drawable.accent_circle); + chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight()); + + return chip; } private SpannableStringBuilder addClickablePart(RichElement richElement) { diff --git a/app/src/main/res/layout/activity_list_item.xml b/app/src/main/res/layout/activity_list_item.xml index 970ca3d18c72..c4601ca2b5b0 100644 --- a/app/src/main/res/layout/activity_list_item.xml +++ b/app/src/main/res/layout/activity_list_item.xml @@ -36,12 +36,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - Date: Wed, 19 Nov 2025 11:07:26 +0100 Subject: [PATCH 6/9] wip Signed-off-by: tobiasKaminsky --- app/src/main/java/com/nextcloud/utils/text/Spans.java | 2 +- .../com/owncloud/android/ui/adapter/ActivityListAdapter.java | 4 +++- app/src/main/java/third_parties/fresco/BetterImageSpan.kt | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/text/Spans.java b/app/src/main/java/com/nextcloud/utils/text/Spans.java index 0834c6520cbe..098366292ccb 100644 --- a/app/src/main/java/com/nextcloud/utils/text/Spans.java +++ b/app/src/main/java/com/nextcloud/utils/text/Spans.java @@ -10,7 +10,7 @@ import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; -import third_parties.fresco.BetterImageSpan; +import thirdparties.fresco.BetterImageSpan; public class Spans { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java index 25af602f9f4a..0eff361c9d13 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java @@ -29,6 +29,7 @@ import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; +import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; @@ -51,6 +52,7 @@ import com.nextcloud.client.network.ClientFactory; import com.nextcloud.common.NextcloudClient; import com.nextcloud.utils.GlideHelper; +import com.nextcloud.utils.GlideHelper; import com.nextcloud.utils.text.Spans; import com.nextcloud.utils.text.Spans; import com.owncloud.android.MainApp; @@ -78,7 +80,7 @@ import androidx.annotation.NonNull; import androidx.annotation.XmlRes; import androidx.recyclerview.widget.RecyclerView; -import third_parties.fresco.BetterImageSpan; +import thirdparties.fresco.BetterImageSpan; /** * Adapter for the activity view. diff --git a/app/src/main/java/third_parties/fresco/BetterImageSpan.kt b/app/src/main/java/third_parties/fresco/BetterImageSpan.kt index 7de8994aaa5e..1c9a5b7e213c 100644 --- a/app/src/main/java/third_parties/fresco/BetterImageSpan.kt +++ b/app/src/main/java/third_parties/fresco/BetterImageSpan.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package third_parties.fresco +package thirdparties.fresco import android.graphics.Canvas import android.graphics.Paint From 170b0afa63a10fb2facc503c3bd8f4263a08b461 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 19 Nov 2025 11:21:24 +0100 Subject: [PATCH 7/9] wip Signed-off-by: tobiasKaminsky --- .../ui/adapter/ActivityListAdapter.java | 44 +------------------ .../third_parties/fresco/BetterImageSpan.kt | 20 ++++----- 2 files changed, 10 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java index 0eff361c9d13..4ec28aa2f601 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java @@ -17,8 +17,6 @@ import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.text.Spannable; @@ -29,7 +27,6 @@ import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; -import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; @@ -40,20 +37,11 @@ import android.widget.LinearLayout; import android.widget.TextView; -import com.google.android.material.chip.ChipDrawable; -import com.bumptech.glide.GenericRequestBuilder; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.model.StreamEncoder; -import com.bumptech.glide.load.resource.file.FileToStreamDecoder; -import com.caverock.androidsvg.SVG; import com.google.android.material.chip.ChipDrawable; import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.network.ClientFactory; import com.nextcloud.common.NextcloudClient; import com.nextcloud.utils.GlideHelper; -import com.nextcloud.utils.GlideHelper; -import com.nextcloud.utils.text.Spans; import com.nextcloud.utils.text.Spans; import com.owncloud.android.MainApp; import com.owncloud.android.R; @@ -168,15 +156,7 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi if (!TextUtils.isEmpty(activity.getRichSubjectElement().getRichSubject())) { activityViewHolder.binding.subject.setVisibility(View.VISIBLE); - // activityViewHolder.binding.subject.setMovementMethod(LinkMovementMethod.getInstance()); -// activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()), -// TextView.BufferType.SPANNABLE); - - activityViewHolder.binding.subject.setText(searchAndReplaceWithMentionSpan("actor", - activity.getRichSubjectElement().getRichSubject(), - "1", - "label", - R.xml.chip_others)); + activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement())); activityViewHolder.binding.subject.setVisibility(View.VISIBLE); } else if (!TextUtils.isEmpty(activity.getSubject())) { @@ -392,16 +372,6 @@ public void onClick(@NonNull View view) { return spannableString; } - private Drawable getDrawableForMentionChipSpan(int chipResource, String text) { - ChipDrawable chip = ChipDrawable.createFromResource(context, chipResource); - chip.setEllipsize(TextUtils.TruncateAt.MIDDLE); - chip.setText(text); - chip.setChipIconResource(R.drawable.accent_circle); - chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight()); - - return chip; - } - private SpannableStringBuilder addClickablePart(RichElement richElement) { String text = richElement.getRichSubject(); SpannableStringBuilder ssb = new SpannableStringBuilder(text); @@ -436,18 +406,6 @@ private SpannableStringBuilder addClickablePart(RichElement richElement) { ); ssb.setSpan(mentionChipSpan, idx1, idx2, Spannable.SPAN_INCLUSIVE_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 && !isFederated) { -// spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); -// } } else { String name = richObject.getName(); ssb.replace(idx1, idx2, name); diff --git a/app/src/main/java/third_parties/fresco/BetterImageSpan.kt b/app/src/main/java/third_parties/fresco/BetterImageSpan.kt index 1c9a5b7e213c..a2f34bab63cf 100644 --- a/app/src/main/java/third_parties/fresco/BetterImageSpan.kt +++ b/app/src/main/java/third_parties/fresco/BetterImageSpan.kt @@ -98,18 +98,16 @@ open class BetterImageSpan @JvmOverloads constructor( mHeight = mBounds!!.height() } - private fun getOffsetAboveBaseline(fm: Paint.FontMetricsInt): Int { - return when (mAlignment) { - ALIGN_BOTTOM -> fm.descent - mHeight - ALIGN_CENTER -> { - val textHeight = fm.descent - fm.ascent - val offset = (textHeight - mHeight) / 2 - fm.ascent + offset - } - - ALIGN_BASELINE -> -mHeight - else -> -mHeight + private fun getOffsetAboveBaseline(fm: Paint.FontMetricsInt): Int = when (mAlignment) { + ALIGN_BOTTOM -> fm.descent - mHeight + ALIGN_CENTER -> { + val textHeight = fm.descent - fm.ascent + val offset = (textHeight - mHeight) / 2 + fm.ascent + offset } + + ALIGN_BASELINE -> -mHeight + else -> -mHeight } companion object { From 64903891196a3998eae3affc22d4946ca0af043b Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 19 Nov 2025 11:27:07 +0100 Subject: [PATCH 8/9] wip Signed-off-by: tobiasKaminsky --- .../ui/adapter/ActivityListAdapter.java | 83 ------------------- 1 file changed, 83 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java index 4ec28aa2f601..678be02bcbac 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java @@ -20,9 +20,7 @@ import android.os.Handler; import android.os.Looper; import android.text.Spannable; -import android.text.SpannableString; import android.text.SpannableStringBuilder; -import android.text.Spanned; import android.text.TextPaint; import android.text.TextUtils; import android.text.format.DateFormat; @@ -61,12 +59,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.Optional; import androidx.annotation.NonNull; -import androidx.annotation.XmlRes; import androidx.recyclerview.widget.RecyclerView; import thirdparties.fresco.BetterImageSpan; @@ -282,26 +277,6 @@ private ImageView createThumbnailNew(PreviewObject previewObject, List requestBuilder = Glide.with(context) -// .using(Glide.buildStreamModelLoader(Uri.class, context), InputStream.class) -// .from(Uri.class) -// .as(SVG.class) -// .transcode(new SvgBitmapTranscoder(128, 128), Bitmap.class) -// .sourceEncoder(new StreamEncoder()) -// .cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder())) -// .decoder(new SvgDecoder()) -// .placeholder(R.drawable.ic_activity) -// .error(R.drawable.ic_activity) -// .animate(android.R.anim.fade_in); -// -// Uri uri = Uri.parse(activity.getIcon()); -// requestBuilder -// .diskCacheStrategy(DiskCacheStrategy.SOURCE) -// .load(uri) -// .into(itemViewType); -// } private ChipDrawable getDrawableForMentionChipSpan(int chipResource, String text) { ChipDrawable chip = ChipDrawable.createFromResource(context, chipResource); @@ -314,64 +289,6 @@ private ChipDrawable getDrawableForMentionChipSpan(int chipResource, String text return chip; } - /** - * c&p from Talk: DisplayUtils:227 - * - * @return Spannable - */ - private Spanned searchAndReplaceWithMentionSpan( - String key, - String text, - String id, - String label, - @XmlRes int chipXmlRes) { - 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 view) { - //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 = getDrawableForMentionChipSpan( - chipXmlRes, - label - ); - - - 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" == type && conversationUser.userId != id && !isFederated) { -// spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); -// } - } - return spannableString; - } - private SpannableStringBuilder addClickablePart(RichElement richElement) { String text = richElement.getRichSubject(); SpannableStringBuilder ssb = new SpannableStringBuilder(text); From cb96ecd30ddcfe6f5af8d125616e17ae4ef7fec4 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Fri, 21 Nov 2025 07:35:55 +0100 Subject: [PATCH 9/9] Add chip/avatars to Notification Signed-off-by: tobiasKaminsky --- .../ui/activity/NotificationsActivity.kt | 2 +- .../ui/adapter/NotificationListAdapter.java | 85 ++++++++++++++++--- .../res/layout/notification_list_item.xml | 3 +- 3 files changed, 74 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt index dc24fb140963..c00bab07244f 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt @@ -295,7 +295,7 @@ class NotificationsActivity : private fun initializeAdapter() { initializeClient() if (adapter == null) { - adapter = NotificationListAdapter(client, this, viewThemeUtils) + adapter = NotificationListAdapter(client, this, viewThemeUtils, accountManager) binding.list.adapter = adapter } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java index 67ca8c616846..739d38f1da1e 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java @@ -11,9 +11,11 @@ package com.owncloud.android.ui.adapter; import android.annotation.SuppressLint; +import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Typeface; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.text.Spannable; import android.text.SpannableStringBuilder; @@ -27,9 +29,12 @@ import android.widget.LinearLayout; import com.google.android.material.button.MaterialButton; +import com.google.android.material.chip.ChipDrawable; import com.nextcloud.android.common.ui.theme.utils.ColorRole; +import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.common.NextcloudClient; import com.nextcloud.utils.GlideHelper; +import com.nextcloud.utils.text.Spans; import com.owncloud.android.R; import com.owncloud.android.databinding.NotificationListItemBinding; import com.owncloud.android.lib.common.utils.Log_OC; @@ -50,11 +55,13 @@ import androidx.appcompat.widget.PopupMenu; import androidx.core.content.res.ResourcesCompat; import androidx.recyclerview.widget.RecyclerView; +import thirdparties.fresco.BetterImageSpan; /** * This Adapter populates a RecyclerView with all notifications for an account within the app. */ -public class NotificationListAdapter extends RecyclerView.Adapter { +public class NotificationListAdapter extends RecyclerView.Adapter + implements DisplayUtils.AvatarGenerationListener { private static final String FILE = "file"; private static final String ACTION_TYPE_WEB = "WEB"; private final StyleSpan styleSpanBold = new StyleSpan(Typeface.BOLD); @@ -64,14 +71,17 @@ public class NotificationListAdapter extends RecyclerView.Adapter(); this.client = client; this.notificationsActivity = notificationsActivity; this.viewThemeUtils = viewThemeUtils; + this.currentAccountProvider = currentAccountProvider; foregroundColorSpanBlack = new ForegroundColorSpan( notificationsActivity.getResources().getColor(R.color.text_color)); } @@ -107,12 +117,6 @@ public void onBindViewHolder(@NonNull NotificationViewHolder holder, int positio notification.getLink())); holder.binding.subject.setText(subject); } else { - if (!TextUtils.isEmpty(notification.subjectRich)) { - holder.binding.subject.setText(makeSpecialPartsBold(notification)); - } else { - holder.binding.subject.setText(subject); - } - if (file != null && !TextUtils.isEmpty(file.id)) { holder.binding.subject.setOnClickListener(v -> { Intent intent = new Intent(notificationsActivity, FileDisplayActivity.class); @@ -124,6 +128,12 @@ public void onBindViewHolder(@NonNull NotificationViewHolder holder, int positio } } + if (TextUtils.isEmpty(notification.subjectRich)) { + holder.binding.subject.setText(subject); + } else { + holder.binding.subject.setText(makeSpecialPartsBold(notification)); + } + if (notification.getMessage() != null && !notification.getMessage().isEmpty()) { holder.binding.message.setText(notification.getMessage()); holder.binding.message.setVisibility(View.VISIBLE); @@ -308,6 +318,7 @@ public void setButtons(NotificationViewHolder holder, Notification notification) private SpannableStringBuilder makeSpecialPartsBold(Notification notification) { String text = notification.getSubjectRich(); SpannableStringBuilder ssb = new SpannableStringBuilder(text); + Context context = notificationsActivity; int openingBrace = text.indexOf('{'); int closingBrace; @@ -318,13 +329,38 @@ private SpannableStringBuilder makeSpecialPartsBold(Notification notification) { RichObject richObject = notification.subjectRichParameters.get(replaceablePart); if (richObject != null) { - String name = richObject.getName(); - ssb.replace(openingBrace, closingBrace, name); - text = ssb.toString(); - closingBrace = openingBrace + name.length(); + if ("user".equals(richObject.getType())) { + String name = richObject.getName(); + + ChipDrawable drawableForChip = getDrawableForMentionChipSpan(R.xml.chip_others, name); + + Spans.MentionChipSpan mentionChipSpan = new Spans.MentionChipSpan(drawableForChip, + BetterImageSpan.ALIGN_CENTER, + richObject.id, + name + ); + + DisplayUtils.setAvatar( + currentAccountProvider.getUser(), + richObject.id, + name, + this, + context.getResources().getDimension(R.dimen.standard_padding), + context.getResources(), + drawableForChip, + context + ); + + ssb.setSpan(mentionChipSpan, openingBrace, closingBrace, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + } else { + String name = richObject.getName(); + ssb.replace(openingBrace, closingBrace, name); + text = ssb.toString(); + closingBrace = openingBrace + name.length(); - ssb.setSpan(styleSpanBold, openingBrace, closingBrace, 0); - ssb.setSpan(foregroundColorSpanBlack, openingBrace, closingBrace, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.setSpan(styleSpanBold, openingBrace, closingBrace, 0); + ssb.setSpan(foregroundColorSpanBlack, openingBrace, closingBrace, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } } openingBrace = text.indexOf('{', closingBrace); } @@ -332,6 +368,17 @@ private SpannableStringBuilder makeSpecialPartsBold(Notification notification) { return ssb; } + private ChipDrawable getDrawableForMentionChipSpan(int chipResource, String text) { + ChipDrawable chip = ChipDrawable.createFromResource(notificationsActivity, chipResource); + chip.setEllipsize(TextUtils.TruncateAt.MIDDLE); + chip.setLayoutDirection(notificationsActivity.getResources().getConfiguration().getLayoutDirection()); + chip.setText(text); + chip.setChipIconResource(R.drawable.accent_circle); + chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight()); + + return chip; + } + public void removeNotification(NotificationViewHolder holder) { int position = holder.getAdapterPosition(); @@ -360,6 +407,16 @@ public int getItemCount() { return notificationsList.size(); } + @Override + public void avatarGenerated(Drawable avatarDrawable, Object callContext) { + ((ChipDrawable) callContext).setChipIcon(avatarDrawable); + } + + @Override + public boolean shouldCallGeneratedCallback(String tag, Object callContext) { + return true; + } + public static class NotificationViewHolder extends RecyclerView.ViewHolder { NotificationListItemBinding binding; diff --git a/app/src/main/res/layout/notification_list_item.xml b/app/src/main/res/layout/notification_list_item.xml index 1419aafd5b92..ff0150aaa522 100644 --- a/app/src/main/res/layout/notification_list_item.xml +++ b/app/src/main/res/layout/notification_list_item.xml @@ -37,9 +37,10 @@