Update conversations list UI.

This commit is contained in:
Lucio Maciel
2021-09-03 17:38:20 -03:00
committed by Cody Henthorne
parent c84de8fa60
commit e09d162c1e
20 changed files with 330 additions and 211 deletions

View File

@@ -5,10 +5,14 @@ import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.style.CharacterStyle;
import android.text.style.MetricAffectingSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.util.AttributeSet;
import androidx.annotation.Nullable;
@@ -17,15 +21,20 @@ import androidx.core.content.ContextCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Objects;
public class FromTextView extends EmojiTextView {
public class FromTextView extends SimpleEmojiTextView {
private static final String TAG = Log.tag(FromTextView.class);
private final static Typeface BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL);
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
public FromTextView(Context context) {
super(context);
}
@@ -45,20 +54,9 @@ public class FromTextView extends EmojiTextView {
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
String fromString = recipient.getDisplayName(getContext());
int typeface;
if (!read) {
typeface = Typeface.BOLD;
} else {
typeface = Typeface.NORMAL;
}
SpannableStringBuilder builder = new SpannableStringBuilder();
SpannableString fromSpan = new SpannableString(fromString);
fromSpan.setSpan(new StyleSpan(typeface), 0, builder.length(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
SpannableStringBuilder builder = new SpannableStringBuilder();
SpannableString fromSpan = new SpannableString(fromString);
fromSpan.setSpan(getFontSpan(!read), 0, fromSpan.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
if (recipient.isSelf()) {
builder.append(getContext().getString(R.string.note_to_self));
@@ -85,4 +83,8 @@ public class FromTextView extends EmojiTextView {
return mutedDrawable;
}
private CharacterStyle getFontSpan(boolean isBold) {
return isBold ? SpanUtil.getBoldSpan() : SpanUtil.getNormalSpan();
}
}

View File

@@ -7,7 +7,7 @@ import androidx.appcompat.widget.AppCompatTextView
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.libsignal.util.guava.Optional
open class SingleLineEmojiTextView @JvmOverloads constructor(
open class SimpleEmojiTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
@@ -15,20 +15,16 @@ open class SingleLineEmojiTextView @JvmOverloads constructor(
private var bufferType: BufferType? = null
init {
maxLines = 1
}
override fun setText(text: CharSequence?, type: BufferType?) {
bufferType = type
val candidates = if (isInEditMode) null else EmojiProvider.getCandidates(text)
if (SignalStore.settings().isPreferSystemEmoji || candidates == null || candidates.size() == 0) {
super.setText(Optional.fromNullable(text).or(""), BufferType.NORMAL)
} else {
val newContent = if (width == 0) {
val newContent = if (width == 0 || maxLines == -1) {
text
} else {
TextUtils.ellipsize(text, paint, width.toFloat(), TextUtils.TruncateAt.END, false, null)
TextUtils.ellipsize(text, paint, (width * maxLines).toFloat(), TextUtils.TruncateAt.END, false, null)
}
val newCandidates = if (isInEditMode) null else EmojiProvider.getCandidates(newContent)
@@ -47,9 +43,4 @@ open class SingleLineEmojiTextView @JvmOverloads constructor(
setText(text, bufferType ?: BufferType.NORMAL)
}
}
override fun setMaxLines(maxLines: Int) {
check(maxLines == 1) { "setMaxLines: $maxLines != 1" }
super.setMaxLines(maxLines)
}
}

View File

@@ -836,8 +836,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (messageRequestAccepted) {
linkifyMessageBody(styledText, batchSelected.isEmpty());
}
styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery);
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery);
styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery, SearchUtil.STRICT);
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery, SearchUtil.STRICT);
if (hasExtraText(messageRecord)) {
bodyText.setOverflowText(getLongMessageSpan(messageRecord));

View File

@@ -101,7 +101,7 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
v.setLayoutParams(new FrameLayout.LayoutParams(1, ViewUtil.dpToPx(100)));
return new PlaceholderViewHolder(v);
} else if (viewType == TYPE_HEADER) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_list_item_header, parent, false);
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.dsl_section_header, parent, false);
return new HeaderViewHolder(v);
} else {
throw new IllegalStateException("Unknown type! " + viewType);
@@ -297,7 +297,7 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
public HeaderViewHolder(@NonNull View itemView) {
super(itemView);
headerText = (TextView) itemView;
headerText = itemView.findViewById(R.id.section_header);
}
}

View File

@@ -24,7 +24,6 @@ import android.os.Build;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.StyleSpan;
import android.text.style.TextAppearanceSpan;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
@@ -66,6 +65,7 @@ import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.Collections;
@@ -102,6 +102,8 @@ public final class ConversationListItem extends ConstraintLayout
private long lastSeen;
private ThreadRecord thread;
private boolean batchMode;
private Locale locale;
private String highlightSubstring;
private int unreadCount;
private AvatarImageView contactPhotoImage;
@@ -158,17 +160,19 @@ public final class ConversationListItem extends ConstraintLayout
observeDisplayBody(null);
setSubjectViewText(null);
this.selectedThreads = selectedThreads;
this.threadId = thread.getThreadId();
this.glideRequests = glideRequests;
this.unreadCount = thread.getUnreadCount();
this.lastSeen = thread.getLastSeen();
this.thread = thread;
this.selectedThreads = selectedThreads;
this.threadId = thread.getThreadId();
this.glideRequests = glideRequests;
this.unreadCount = thread.getUnreadCount();
this.lastSeen = thread.getLastSeen();
this.thread = thread;
this.locale = locale;
this.highlightSubstring = highlightSubstring;
if (highlightSubstring != null) {
String name = recipient.get().isSelf() ? getContext().getString(R.string.note_to_self) : recipient.get().getDisplayName(getContext());
this.fromView.setText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), name, highlightSubstring));
this.fromView.setText(SearchUtil.getHighlightedSpan(locale, SpanUtil::getBoldSpan, name, highlightSubstring, SearchUtil.MATCH_ALL));
} else {
this.fromView.setText(recipient.get(), thread.isRead());
}
@@ -178,10 +182,6 @@ public final class ConversationListItem extends ConstraintLayout
observeDisplayBody(getThreadDisplayBody(getContext(), thread));
this.subjectView.setTypeface(thread.isRead() ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
this.subjectView.setTextColor(thread.isRead() ? ContextCompat.getColor(getContext(), R.color.signal_text_secondary)
: ContextCompat.getColor(getContext(), R.color.signal_text_primary));
if (thread.getDate() > 0) {
CharSequence date = DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, thread.getDate());
dateView.setText(date);
@@ -213,13 +213,13 @@ public final class ConversationListItem extends ConstraintLayout
observeDisplayBody(null);
setSubjectViewText(null);
this.selectedThreads = Collections.emptySet();
this.glideRequests = glideRequests;
this.selectedThreads = Collections.emptySet();
this.glideRequests = glideRequests;
this.locale = locale;
this.highlightSubstring = highlightSubstring;
fromView.setText(contact);
fromView.setText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), new SpannableString(fromView.getText()), highlightSubstring));
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), contact.getE164().or(""), highlightSubstring));
fromView.setText(SearchUtil.getHighlightedSpan(locale, SpanUtil::getBoldSpan, new SpannableString(contact.getDisplayName(getContext())), highlightSubstring, SearchUtil.MATCH_ALL));
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, SpanUtil::getBoldSpan, contact.getE164().or(""), highlightSubstring, SearchUtil.MATCH_ALL));
dateView.setText("");
archivedView.setVisibility(GONE);
unreadIndicator.setVisibility(GONE);
@@ -241,11 +241,13 @@ public final class ConversationListItem extends ConstraintLayout
observeDisplayBody(null);
setSubjectViewText(null);
this.selectedThreads = Collections.emptySet();
this.glideRequests = glideRequests;
this.selectedThreads = Collections.emptySet();
this.glideRequests = glideRequests;
this.locale = locale;
this.highlightSubstring = highlightSubstring;
fromView.setText(recipient.get(), true);
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), messageResult.getBodySnippet(), highlightSubstring));
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, SpanUtil::getBoldSpan, messageResult.getBodySnippet(), highlightSubstring, SearchUtil.MATCH_ALL));
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, messageResult.getReceivedTimestampMs()));
archivedView.setVisibility(GONE);
unreadIndicator.setVisibility(GONE);
@@ -346,10 +348,10 @@ public final class ConversationListItem extends ConstraintLayout
private void setThumbnailSnippet(ThreadRecord thread) {
if (thread.getSnippetUri() != null) {
this.thumbnailView.setVisibility(View.VISIBLE);
this.thumbnailView.setVisibility(VISIBLE);
this.thumbnailView.setImageResource(glideRequests, thread.getSnippetUri());
} else {
this.thumbnailView.setVisibility(View.GONE);
this.thumbnailView.setVisibility(GONE);
}
}
@@ -401,11 +403,12 @@ public final class ConversationListItem extends ConstraintLayout
}
private void setUnreadIndicator(ThreadRecord thread) {
if ((thread.isOutgoing() && !thread.isForcedUnread()) || thread.isRead()) {
if ((thread.isOutgoing() && !thread.isForcedUnread()) || thread.isRead() || unreadCount == 0) {
unreadIndicator.setVisibility(View.GONE);
return;
}
String count = unreadCount > 100 ? String.valueOf(unreadCount) : "+99";
unreadIndicator.setText(unreadCount > 0 ? String.valueOf(unreadCount) : " ");
unreadIndicator.setVisibility(View.VISIBLE);
}
@@ -417,14 +420,18 @@ public final class ConversationListItem extends ConstraintLayout
return;
}
fromView.setText(recipient, unreadCount == 0);
if (highlightSubstring != null) {
String name = recipient.isSelf() ? getContext().getString(R.string.note_to_self) : recipient.getDisplayName(getContext());
fromView.setText(SearchUtil.getHighlightedSpan(locale, SpanUtil::getBoldSpan, new SpannableString(name), highlightSubstring, SearchUtil.MATCH_ALL));
} else {
fromView.setText(recipient, unreadCount == 0);
}
contactPhotoImage.setAvatar(glideRequests, recipient, !batchMode);
setRippleColor(recipient);
}
private static @NonNull LiveData<SpannableString> getThreadDisplayBody(@NonNull Context context, @NonNull ThreadRecord thread) {
int defaultTint = thread.isRead() ? ContextCompat.getColor(context, R.color.signal_text_secondary)
: ContextCompat.getColor(context, R.color.signal_text_primary);
int defaultTint = ContextCompat.getColor(context, R.color.signal_text_secondary);
if (!thread.isMessageRequestAccepted()) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_request), defaultTint);
@@ -520,8 +527,7 @@ public final class ConversationListItem extends ConstraintLayout
: recipient.getShortDisplayName(context)) + ": ";
SpannableString spannable = new SpannableString(sender + body);
spannable.setSpan(new TextAppearanceSpan(context, read ? R.style.Signal_Text_Preview_Medium_Secondary
: R.style.Signal_Text_Preview_Medium_Primary),
spannable.setSpan(SpanUtil.getBoldSpan(),
0,
sender.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

View File

@@ -96,7 +96,7 @@ class ConversationListSearchAdapter extends RecyclerView.Adapter<Conversation
@Override
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position, int type) {
return new HeaderViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.search_result_list_divider, parent, false));
.inflate(R.layout.dsl_section_header, parent, false));
}
@Override
@@ -198,7 +198,7 @@ class ConversationListSearchAdapter extends RecyclerView.Adapter<Conversation
public HeaderViewHolder(View itemView) {
super(itemView);
titleView = itemView.findViewById(R.id.label);
titleView = itemView.findViewById(R.id.section_header);
}
public void bind(int headerType) {

View File

@@ -12,6 +12,7 @@ import com.annimon.stream.Stream;
import org.whispersystems.libsignal.util.Pair;
import java.security.InvalidParameterException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -19,10 +20,14 @@ import java.util.Locale;
public class SearchUtil {
public static final int STRICT = 0;
public static final int MATCH_ALL = 1;
public static Spannable getHighlightedSpan(@NonNull Locale locale,
@NonNull StyleFactory styleFactory,
@Nullable String text,
@Nullable String highlight)
@Nullable String highlight,
int matchMode)
{
if (TextUtils.isEmpty(text)) {
return new SpannableString("");
@@ -30,13 +35,14 @@ public class SearchUtil {
text = text.replaceAll("\n", " ");
return getHighlightedSpan(locale, styleFactory, new SpannableString(text), highlight);
return getHighlightedSpan(locale, styleFactory, new SpannableString(text), highlight, matchMode);
}
public static Spannable getHighlightedSpan(@NonNull Locale locale,
@NonNull StyleFactory styleFactory,
@Nullable Spannable text,
@Nullable String highlight)
@Nullable String highlight,
int matchMode)
{
if (TextUtils.isEmpty(text)) {
return new SpannableString("");
@@ -47,8 +53,24 @@ public class SearchUtil {
return text;
}
List<Pair<Integer, Integer>> ranges = getHighlightRanges(locale, text.toString(), highlight);
SpannableString spanned = new SpannableString(text);
List<Pair<Integer, Integer>> ranges;
switch (matchMode) {
case STRICT:
ranges = getStrictHighlightRanges(locale, text.toString(), highlight);
break;
case MATCH_ALL:
ranges = getHighlightRanges(locale, text.toString(), highlight);
break;
default:
throw new InvalidParameterException("match mode must be STRICT or MATCH_ALL: " + matchMode);
}
if (matchMode == STRICT) {
ranges = getStrictHighlightRanges(locale, text.toString(), highlight);
} else {
ranges = getHighlightRanges(locale, text.toString(), highlight);
}
for (Pair<Integer, Integer> range : ranges) {
spanned.setSpan(styleFactory.create(), range.first(), range.second(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
@@ -57,9 +79,9 @@ public class SearchUtil {
return spanned;
}
static List<Pair<Integer, Integer>> getHighlightRanges(@NonNull Locale locale,
@NonNull String text,
@NonNull String highlight)
static List<Pair<Integer, Integer>> getStrictHighlightRanges(@NonNull Locale locale,
@NonNull String text,
@NonNull String highlight)
{
if (text.length() == 0) {
return Collections.emptyList();
@@ -97,6 +119,39 @@ public class SearchUtil {
return ranges;
}
static List<Pair<Integer, Integer>> getHighlightRanges(@NonNull Locale locale,
@NonNull String text,
@NonNull String highlight)
{
if (text.length() == 0) {
return Collections.emptyList();
}
String normalizedText = text.toLowerCase(locale);
String normalizedHighlight = highlight.toLowerCase(locale);
List<String> highlightTokens = Stream.of(normalizedHighlight.split("\\s")).filter(s -> s.trim().length() > 0).toList();
List<Pair<Integer, Integer>> ranges = new LinkedList<>();
int lastHighlightEndIndex = 0;
for (String highlightToken : highlightTokens) {
int index = 0;
lastHighlightEndIndex = 0;
while (index != -1) {
index = normalizedText.indexOf(highlightToken, lastHighlightEndIndex);
if (index != -1) {
lastHighlightEndIndex = index + highlightToken.length();
ranges.add(new Pair<>(index, lastHighlightEndIndex));
index = lastHighlightEndIndex;
}
}
}
return ranges;
}
public interface StyleFactory {
CharacterStyle create();
}

View File

@@ -13,12 +13,15 @@ import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.BulletSpan;
import android.text.style.CharacterStyle;
import android.text.style.ClickableSpan;
import android.text.style.DynamicDrawableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.text.style.MetricAffectingSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.view.View;
import androidx.annotation.ColorInt;
@@ -34,6 +37,9 @@ public final class SpanUtil {
public static final String SPAN_PLACE_HOLDER = "<<<SPAN>>>";
private final static Typeface BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL);
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
public static CharSequence italic(CharSequence sequence) {
return italic(sequence, sequence.length());
}
@@ -205,4 +211,20 @@ public final class SpanUtil {
builder.replace(index, index + SpanUtil.SPAN_PLACE_HOLDER.length(), span);
return builder;
}
public static CharacterStyle getBoldSpan() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return new TypefaceSpan(BOLD_TYPEFACE);
} else {
return new StyleSpan(Typeface.BOLD);
}
}
public static CharacterStyle getNormalSpan() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return new TypefaceSpan(LIGHT_TYPEFACE);
} else {
return new StyleSpan(Typeface.NORMAL);
}
}
}