Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
tobiasKaminsky
2025-03-05 09:14:07 +01:00
parent d94fbcc61b
commit 1791ce9f44
6 changed files with 316 additions and 11 deletions

View File

@ -0,0 +1,82 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
* 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() + ")";
}
}
}

View File

@ -16,8 +16,10 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextPaint;
import android.text.TextUtils;
@ -40,9 +42,11 @@ 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.owncloud.android.MainApp;
import com.owncloud.android.R;
import com.owncloud.android.databinding.ActivityListItemBinding;
@ -64,9 +68,13 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.annotation.NonNull;
import androidx.annotation.XmlRes;
import androidx.recyclerview.widget.RecyclerView;
import third_parties.fresco.BetterImageSpan;
/**
* Adapter for the activity view.
@ -154,6 +162,13 @@ public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
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);
@ -288,6 +303,65 @@ public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
.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);

View File

@ -0,0 +1,120 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
* 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
}
}

View File

@ -32,17 +32,29 @@
android:layout_toEndOf="@id/icon"
android:orientation="vertical">
<TextView
android:id="@+id/subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingStart="@dimen/activity_icon_layout_right_end_margin"
android:paddingTop="@dimen/standard_padding"
android:paddingEnd="@dimen/zero"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="@dimen/two_line_primary_text_size"
tools:text="@string/placeholder_filename" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.chip.Chip
android:id="@+id/chip"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="username" />
<TextView
android:id="@+id/subject"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:ellipsize="end"
android:paddingStart="@dimen/activity_icon_layout_right_end_margin"
android:paddingTop="@dimen/standard_padding"
android:paddingEnd="@dimen/zero"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="@dimen/two_line_primary_text_size"
tools:text="@string/placeholder_filename" />
</LinearLayout>
<TextView
android:id="@+id/message"

View File

@ -461,4 +461,8 @@
<item name="postSplashScreenTheme">@style/Theme.ownCloud.Launcher</item>
</style>
<style name="ChipIncomingTextAppearance" parent="TextAppearance.MaterialComponents.Chip">
<item name="android:textColor">#de000000</item>
</style>
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<chip xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:textAppearance="@style/ChipIncomingTextAppearance"
app:chipBackgroundColor="#deffffff"
app:chipCornerRadius="@dimen/standard_padding"
app:chipStrokeWidth="@dimen/zero"
app:closeIconEnabled="false" />