diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index 16e7449d..264fe35b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -36,6 +36,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Locale; +import java.lang.CharSequence; import at.connyduck.sparkbutton.SparkButton; import at.connyduck.sparkbutton.SparkEventListener; @@ -120,11 +121,46 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { username.setText(usernameText); } - private void setContent(Spanned content, Status.Mention[] mentions, List emojis, - StatusActionListener listener) { - Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, this.content); + private void setSpoilerAndContent(StatusViewData.Concrete status, + final StatusActionListener listener) { + if (status.getSpoilerText() == null || status.getSpoilerText().isEmpty()) { + contentWarningDescription.setVisibility(View.GONE); + contentWarningButton.setVisibility(View.GONE); + this.setTextVisible(true, status, listener); + } else { + boolean expanded = status.isExpanded(); + CharSequence emojiSpoiler = CustomEmojiHelper.emojifyString( + status.getSpoilerText(), status.getStatusEmojis(), contentWarningDescription); + contentWarningDescription.setText(emojiSpoiler); + contentWarningDescription.setVisibility(View.VISIBLE); + contentWarningButton.setVisibility(View.VISIBLE); + contentWarningButton.setChecked(expanded); + contentWarningButton.setOnCheckedChangeListener((buttonView, isChecked) -> { + contentWarningDescription.invalidate(); + if (getAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onExpandedChange(isChecked, getAdapterPosition()); + } + this.setTextVisible(isChecked, status, listener); + }); + this.setTextVisible(expanded, status, listener); + } + } - LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener); + private void setTextVisible(boolean visible, StatusViewData.Concrete status, + final StatusActionListener listener) { + Status.Mention[] mentions = status.getMentions(); + if (visible) { + Spanned emojifiedText = CustomEmojiHelper.emojifyText( + status.getContent(), status.getStatusEmojis(), this.content); + LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener); + this.content.setVisibility(View.VISIBLE); + } else { + if (mentions == null || mentions.length == 0) { + this.content.setVisibility(View.GONE); + } else { + LinkHelper.setClickableMentions(this.content, mentions, listener); + } + } } void setAvatar(String url, @Nullable String rebloggedUrl) { @@ -386,32 +422,6 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { sensitiveMediaShow.setVisibility(View.GONE); } - private void setSpoilerText(String spoilerText, List emojis, - final boolean expanded, final StatusActionListener listener) { - CharSequence emojiSpoiler = - CustomEmojiHelper.emojifyString(spoilerText, emojis, contentWarningDescription); - contentWarningDescription.setText(emojiSpoiler); - contentWarningDescription.setVisibility(View.VISIBLE); - contentWarningButton.setVisibility(View.VISIBLE); - contentWarningButton.setChecked(expanded); - contentWarningButton.setOnCheckedChangeListener((buttonView, isChecked) -> { - contentWarningDescription.invalidate(); - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onExpandedChange(isChecked, getAdapterPosition()); - } - content.setVisibility(isChecked ? View.VISIBLE : View.GONE); - - }); - content.setVisibility(expanded ? View.VISIBLE : View.GONE); - - } - - private void hideSpoilerText() { - contentWarningDescription.setVisibility(View.GONE); - contentWarningButton.setVisibility(View.GONE); - content.setVisibility(View.VISIBLE); - } - private void setupButtons(final StatusActionListener listener, final String accountId) { /* Originally position was passed through to all these listeners, but it caused several * bugs where other statuses in the list would be removed or added and cause the position @@ -509,11 +519,8 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setupButtons(listener, status.getSenderId()); setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility()); - if (status.getSpoilerText() == null || status.getSpoilerText().isEmpty()) { - hideSpoilerText(); - } else { - setSpoilerText(status.getSpoilerText(), status.getStatusEmojis(), status.isExpanded(), listener); - } + + setSpoilerAndContent(status, listener); // When viewing threads this ViewHolder is used and the main post does not have a collapse // button by design so avoid crashing the app when that happens @@ -538,7 +545,5 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { content.setFilters(NO_INPUT_FILTER); } } - - setContent(status.getContent(), status.getMentions(), status.getStatusEmojis(), listener); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java index 2d3df52b..4c68adc6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java @@ -34,6 +34,7 @@ import android.widget.TextView; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.LinkListener; +import java.lang.CharSequence; import java.net.URI; import java.net.URISyntaxException; @@ -64,7 +65,6 @@ public class LinkHelper { */ public static void setClickableText(TextView view, Spanned content, @Nullable Status.Mention[] mentions, final LinkListener listener) { - SpannableStringBuilder builder = new SpannableStringBuilder(content); URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class); for (URLSpan span : urlSpans) { @@ -117,20 +117,63 @@ public class LinkHelper { /* Add zero-width space after links in end of line to fix its too large hitbox. * See also : https://github.com/tuskyapp/Tusky/issues/846 * https://github.com/tuskyapp/Tusky/pull/916 */ - if(end >= builder.length()){ + if (end >= builder.length() || + builder.subSequence(end, end + 1).toString().equals("\n")){ builder.insert(end, "\u200B"); - } else { - if(builder.subSequence(end, end + 1).toString().equals("\n")){ - builder.insert(end, "\u200B"); - } } - } + view.setText(builder); view.setLinksClickable(true); view.setMovementMethod(LinkMovementMethod.getInstance()); } + /** + * Put mentions in a piece of text and makes them clickable, associating them with callbacks to + * notify when they're clicked. + * + * @param view the returned text will be put in + * @param mentions any '@' mentions which are known to be in the content + * @param listener to notify about particular spans that are clicked + */ + public static void setClickableMentions( + TextView view, @Nullable Status.Mention[] mentions, final LinkListener listener) { + if (mentions == null || mentions.length == 0) { + view.setText(null); + return; + } + SpannableStringBuilder builder = new SpannableStringBuilder(); + int start = 0; + int end = 0; + int flags; + boolean firstMention = true; + for (Status.Mention mention : mentions) { + String accountUsername = mention.getLocalUsername(); + final String accountId = mention.getId(); + ClickableSpan customSpan = new ClickableSpanNoUnderline() { + @Override + public void onClick(View widget) { listener.onViewAccount(accountId); } + }; + + end += 1 + accountUsername.length(); // length of @ + username + flags = builder.getSpanFlags(customSpan); + if (firstMention) { + firstMention = false; + } else { + builder.append(" "); + start += 1; + end += 1; + } + builder.append("@"); + builder.append(accountUsername); + builder.setSpan(customSpan, start, end, flags); + builder.append("\u200B"); // same reasonning than in setClickableText + end += 1; // shift position to take the previous character into account + start = end; + } + view.setText(builder); + } + /** * Opens a link, depending on the settings, either in the browser or in a custom tab *