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 511a3df05..41ec96683 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -404,13 +404,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } } - protected void setIsReply(boolean isReply) { + protected void setReplyButtonImage(boolean isReply) { if (isReply) { replyButton.setImageResource(R.drawable.ic_reply_all_24dp); } else { replyButton.setImageResource(R.drawable.ic_reply_24dp); } - } protected void setReplyCount(int repliesCount, boolean fullStats) { @@ -771,21 +770,17 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { popup.show(); } - public void setupWithStatus(@NonNull StatusViewData.Concrete status, final @NonNull StatusActionListener listener, - @NonNull StatusDisplayOptions statusDisplayOptions) { - this.setupWithStatus(status, listener, statusDisplayOptions, null); - } - public void setupWithStatus(@NonNull StatusViewData.Concrete status, @NonNull final StatusActionListener listener, @NonNull StatusDisplayOptions statusDisplayOptions, - @Nullable Object payloads) { + @Nullable Object payloads, + final boolean showStatusInfo) { if (payloads == null) { Status actionable = status.getActionable(); setDisplayName(actionable.getAccount().getName(), actionable.getAccount().getEmojis(), statusDisplayOptions); setUsername(actionable.getAccount().getUsername()); setMetaData(status, statusDisplayOptions, listener); - setIsReply(actionable.getInReplyToId() != null); + setReplyButtonImage(actionable.isReply()); setReplyCount(actionable.getRepliesCount(), statusDisplayOptions.showStatsInline()); setAvatar(actionable.getAccount().getAvatar(), status.getRebloggedAvatar(), actionable.getAccount().getBot(), statusDisplayOptions); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index d8921c28e..31846a7f5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -140,13 +140,14 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder { public void setupWithStatus(@NonNull final StatusViewData.Concrete status, @NonNull final StatusActionListener listener, @NonNull StatusDisplayOptions statusDisplayOptions, - @Nullable Object payloads) { + @Nullable Object payloads, + final boolean showStatusInfo) { // We never collapse statuses in the detail view StatusViewData.Concrete uncollapsedStatus = (status.isCollapsible() && status.isCollapsed()) ? status.copyWithCollapsed(false) : status; - super.setupWithStatus(uncollapsedStatus, listener, statusDisplayOptions, payloads); + super.setupWithStatus(uncollapsedStatus, listener, statusDisplayOptions, payloads, showStatusInfo); setupCard(uncollapsedStatus, status.isExpanded(), CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status if (payloads == null) { Status actionable = uncollapsedStatus.getActionable(); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index 327f7cfbb..9666af7a0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -27,9 +27,9 @@ import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Filter; import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.TimelineAccount; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.NumberUtils; @@ -38,7 +38,8 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.viewdata.StatusViewData; -import java.util.List; +import java.util.Collections; +import java.util.Objects; import at.connyduck.sparkbutton.helpers.Utils; @@ -63,24 +64,37 @@ public class StatusViewHolder extends StatusBaseViewHolder { public void setupWithStatus(@NonNull StatusViewData.Concrete status, @NonNull final StatusActionListener listener, @NonNull StatusDisplayOptions statusDisplayOptions, - @Nullable Object payloads) { + @Nullable Object payloads, + final boolean showStatusInfo) { if (payloads == null) { - boolean sensitive = !TextUtils.isEmpty(status.getActionable().getSpoilerText()); boolean expanded = status.isExpanded(); setupCollapsedState(sensitive, expanded, status, listener); - Status reblogging = status.getRebloggingStatus(); - if (reblogging == null || status.getFilterAction() == Filter.Action.WARN) { + if (!showStatusInfo || status.getFilterAction() == Filter.Action.WARN) { hideStatusInfo(); } else { - String rebloggedByDisplayName = reblogging.getAccount().getName(); - setRebloggedByDisplayName(rebloggedByDisplayName, - reblogging.getAccount().getEmojis(), statusDisplayOptions); - statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition())); - } + Status rebloggingStatus = status.getRebloggingStatus(); + boolean isReplyOnly = rebloggingStatus == null && status.isReply(); + boolean isReplySelf = isReplyOnly && status.isSelfReply(); + boolean hasStatusInfo = rebloggingStatus != null | isReplyOnly; + + TimelineAccount statusInfoAccount = rebloggingStatus != null ? rebloggingStatus.getAccount() : status.getRepliedToAccount(); + + if (!hasStatusInfo) { + hideStatusInfo(); + } else { + setStatusInfoContent(statusInfoAccount, isReplyOnly, isReplySelf, statusDisplayOptions); + } + + if (isReplyOnly) { + statusInfo.setOnClickListener(null); + } else { + statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition())); + } + } } reblogsCountLabel.setVisibility(statusDisplayOptions.showStatsInline() ? View.VISIBLE : View.INVISIBLE); @@ -88,23 +102,42 @@ public class StatusViewHolder extends StatusBaseViewHolder { setFavouritedCount(status.getActionable().getFavouritesCount()); setReblogsCount(status.getActionable().getReblogsCount()); - super.setupWithStatus(status, listener, statusDisplayOptions, payloads); + super.setupWithStatus(status, listener, statusDisplayOptions, payloads, showStatusInfo); } - private void setRebloggedByDisplayName(final CharSequence name, - final List accountEmoji, - final StatusDisplayOptions statusDisplayOptions) { + private void setStatusInfoContent(final TimelineAccount account, + final boolean isReply, + final boolean isSelfReply, + final StatusDisplayOptions statusDisplayOptions) { Context context = statusInfo.getContext(); - CharSequence wrappedName = StringUtils.unicodeWrap(name); - CharSequence boostedText = context.getString(R.string.post_boosted_format, wrappedName); + CharSequence accountName = account != null ? account.getName() : ""; + CharSequence wrappedName = StringUtils.unicodeWrap(accountName); + CharSequence translatedText = ""; + + if (!isReply) { + translatedText = context.getString(R.string.post_boosted_format, wrappedName); + } else if (isSelfReply) { + translatedText = context.getString(R.string.post_replied_self); + } else { + if (account != null && accountName.length() > 0) { + translatedText = context.getString(R.string.post_replied_format, wrappedName); + } else { + translatedText = context.getString(R.string.post_replied); + } + } + CharSequence emojifiedText = CustomEmojiHelper.emojify( - boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis() + translatedText, + account != null ? account.getEmojis() : Collections.emptyList(), + statusInfo, + statusDisplayOptions.animateEmojis() ); statusInfo.setText(emojifiedText); + statusInfo.setCompoundDrawablesWithIntrinsicBounds(isReply ? R.drawable.ic_reply_all_18dp : R.drawable.ic_reblog_18dp, 0, 0, 0); statusInfo.setVisibility(View.VISIBLE); } - // don't use this on the same ViewHolder as setRebloggedByDisplayName, will cause recycling issues as paddings are changed + // don't use this on the same ViewHolder as setStatusInfoContent, will cause recycling issues as paddings are changed protected void setPollInfo(final boolean ownPoll) { statusInfo.setText(ownPoll ? R.string.poll_ended_created : R.string.poll_ended_voted); statusInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_poll_24dp, 0, 0, 0); diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java index f73fe854a..989a2914e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java @@ -87,7 +87,6 @@ public class ConversationViewHolder extends StatusBaseViewHolder { setDisplayName(displayName, account.getEmojis(), statusDisplayOptions); setUsername(account.getUsername()); setMetaData(statusViewData, statusDisplayOptions, listener); - setIsReply(status.getInReplyToId() != null); setFavourited(status.getFavourited()); setBookmarked(status.getBookmarked()); List attachments = status.getAttachments(); diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusNotificationViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusNotificationViewHolder.kt index 5e5036a20..d18dcc706 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusNotificationViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusNotificationViewHolder.kt @@ -79,7 +79,8 @@ internal class StatusNotificationViewHolder( showNotificationContent(false) } else { showNotificationContent(true) - val (_, _, account, _, _, _, _, createdAt) = statusViewData.actionable + val account = statusViewData.actionable.account + val createdAt = statusViewData.actionable.createdAt setDisplayName(account.name, account.emojis, statusDisplayOptions.animateEmojis) setUsername(account.username) setCreatedAt(createdAt, statusDisplayOptions.useAbsoluteTime) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusViewHolder.kt index 19ec41608..cec1c6828 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusViewHolder.kt @@ -47,7 +47,8 @@ internal class StatusViewHolder( statusViewData, statusActionListener, statusDisplayOptions, - payloads.firstOrNull() + payloads.firstOrNull(), + false ) } if (viewData.type == Notification.Type.POLL) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt index 1d3cabe21..6bbb17999 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt @@ -38,7 +38,7 @@ class SearchStatusesAdapter( override fun onBindViewHolder(holder: StatusViewHolder, position: Int) { getItem(position)?.let { item -> - holder.setupWithStatus(item, statusListener, statusDisplayOptions) + holder.setupWithStatus(item, statusListener, statusDisplayOptions, null, true) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt index 511c70d11..461e7095e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt @@ -101,7 +101,8 @@ class TimelinePagingAdapter( viewData, statusListener, statusDisplayOptions, - if (payloads != null && payloads.isNotEmpty()) payloads[0] else null + if (payloads != null && payloads.isNotEmpty()) payloads[0] else null, + true ) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt index b3f0de1ee..e3c5fcefc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt @@ -109,7 +109,7 @@ fun Status.toEntity( ) fun TimelineStatusEntity.toStatus( - account: TimelineAccountEntity + account: TimelineAccountEntity, ) = Status( id = serverId, url = url, @@ -192,6 +192,7 @@ fun HomeTimelineData.toViewData(isDetailed: Boolean = false, translation: Transl isShowingContent = this.status.contentShowing, isCollapsed = this.status.contentCollapsed, isDetailed = isDetailed, + repliedToAccount = repliedToAccount?.toAccount(), translation = translation, ) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt index 823fb7cd4..9fa1f5838 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt @@ -59,7 +59,7 @@ class ThreadAdapter( if (viewHolder is FilteredStatusViewHolder) { viewHolder.bind(status) } else if (viewHolder is StatusBaseViewHolder) { - viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions) + viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions, null, false) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt index 13d1afa8e..f32f2c3a2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt @@ -117,6 +117,7 @@ class ViewThreadViewModel @Inject constructor( isShowingContent = statusAndAccount.first.contentShowing, isCollapsed = statusAndAccount.first.contentCollapsed, isDetailed = true, + // NOTE repliedToAccount is null here: this avoids showing "in reply to" over every post translation = null ) } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineDao.kt index 7ae0aac4d..50c1a317c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineDao.kt @@ -44,11 +44,16 @@ rb.serverId as 'rb_serverId', rb.tuskyAccountId 'rb_tuskyAccountId', rb.localUsername as 'rb_localUsername', rb.username as 'rb_username', rb.displayName as 'rb_displayName', rb.url as 'rb_url', rb.avatar as 'rb_avatar', rb.emojis as 'rb_emojis', rb.bot as 'rb_bot', +replied.serverId as 'replied_serverId', replied.tuskyAccountId 'replied_tuskyAccountId', +replied.localUsername as 'replied_localUsername', replied.username as 'replied_username', +replied.displayName as 'replied_displayName', replied.url as 'replied_url', replied.avatar as 'replied_avatar', +replied.emojis as 'replied_emojis', replied.bot as 'replied_bot', h.loading FROM HomeTimelineEntity h LEFT JOIN TimelineStatusEntity s ON (h.statusId = s.serverId AND s.tuskyAccountId = :tuskyAccountId) LEFT JOIN TimelineAccountEntity a ON (s.authorServerId = a.serverId AND a.tuskyAccountId = :tuskyAccountId) LEFT JOIN TimelineAccountEntity rb ON (h.reblogAccountId = rb.serverId AND rb.tuskyAccountId = :tuskyAccountId) +LEFT JOIN TimelineAccountEntity replied ON (s.inReplyToAccountId = replied.serverId) WHERE h.tuskyAccountId = :tuskyAccountId ORDER BY LENGTH(h.id) DESC, h.id DESC""" ) diff --git a/app/src/main/java/com/keylesspalace/tusky/db/entity/HomeTimelineEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/entity/HomeTimelineEntity.kt index ccbdb44f7..35e449332 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/entity/HomeTimelineEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/entity/HomeTimelineEntity.kt @@ -64,5 +64,6 @@ data class HomeTimelineData( @Embedded val status: TimelineStatusEntity?, @Embedded(prefix = "a_") val account: TimelineAccountEntity?, @Embedded(prefix = "rb_") val reblogAccount: TimelineAccountEntity?, + @Embedded(prefix = "replied_") val repliedToAccount: TimelineAccountEntity?, val loading: Boolean ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt index 63e165934..4812eefe7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -67,6 +67,9 @@ data class Status( val actionableStatus: Status get() = reblog ?: this + val isReply: Boolean + get() = inReplyToId != null + @JsonClass(generateAdapter = false) enum class Visibility(val int: Int) { UNKNOWN(0), diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt index 485467909..597832003 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -18,6 +18,7 @@ import android.text.Spanned import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.entity.Translation import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.shouldTrimStatus @@ -55,6 +56,7 @@ sealed class StatusViewData { */ val isCollapsed: Boolean, val isDetailed: Boolean = false, + val repliedToAccount: TimelineAccount? = null, val translation: TranslationViewData? = null, ) : StatusViewData() { override val id: String @@ -105,6 +107,12 @@ sealed class StatusViewData { val rebloggingStatus: Status? get() = if (status.reblog != null) status else null + val isReply: Boolean + get() = status.inReplyToAccountId != null + + val isSelfReply: Boolean + get() = status.inReplyToAccountId == status.account.id + /** Helper for Java */ fun copyWithCollapsed(isCollapsed: Boolean): Concrete { return copy(isCollapsed = isCollapsed) diff --git a/app/src/main/res/drawable/ic_reply_all_18dp.xml b/app/src/main/res/drawable/ic_reply_all_18dp.xml new file mode 100644 index 000000000..4b61a8d21 --- /dev/null +++ b/app/src/main/res/drawable/ic_reply_all_18dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_reply_all_24dp.xml b/app/src/main/res/drawable/ic_reply_all_24dp.xml index 9da31f037..7da7c05fe 100644 --- a/app/src/main/res/drawable/ic_reply_all_24dp.xml +++ b/app/src/main/res/drawable/ic_reply_all_24dp.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index 906257424..ab8a3dbed 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -24,6 +24,7 @@ android:textColor="?android:textColorTertiary" android:textSize="?attr/status_text_medium" app:drawableStartCompat="@drawable/ic_reblog_18dp" + app:drawableTint="?android:textColorTertiary" app:layout_constraintLeft_toRightOf="parent" app:layout_constraintRight_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cdec38c39..fc3702f05 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -86,6 +86,9 @@ \@%1$s %1$s boosted + Replied + In reply to %1$s + Continued thread Sensitive content Media hidden ALT diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineFaker.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineFaker.kt index 80fc0c25d..244698a59 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineFaker.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineFaker.kt @@ -133,6 +133,7 @@ fun fakeHomeTimelineData( tuskyAccountId = tuskyAccountId, ) }, + repliedToAccount = null, loading = false ) } @@ -144,6 +145,7 @@ fun fakePlaceholderHomeTimelineData( account = null, status = null, reblogAccount = null, + repliedToAccount = null, loading = false )