3771: Add "display replied to" functionality (#4834)
Earlier PR: https://github.com/tuskyapp/Tusky/pull/3778 Fixes: https://github.com/tuskyapp/Tusky/issues/3771
This commit is contained in:
parent
c630c47744
commit
d6b276d8df
20 changed files with 104 additions and 38 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<Emoji> 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);
|
||||
|
|
|
|||
|
|
@ -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<Attachment> attachments = status.getAttachments();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ internal class StatusViewHolder(
|
|||
statusViewData,
|
||||
statusActionListener,
|
||||
statusDisplayOptions,
|
||||
payloads.firstOrNull()
|
||||
payloads.firstOrNull(),
|
||||
false
|
||||
)
|
||||
}
|
||||
if (viewData.type == Notification.Type.POLL) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
10
app/src/main/res/drawable/ic_reply_all_18dp.xml
Normal file
10
app/src/main/res/drawable/ic_reply_all_18dp.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="18dp"
|
||||
android:height="18dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorTertiary"
|
||||
android:pathData="M7,8L7,5l-7,7 7,7v-3l-4,-4 4,-4zM13,9L13,5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z" />
|
||||
</vector>
|
||||
|
|
@ -5,6 +5,6 @@
|
|||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:fillColor="?android:attr/textColorTertiary"
|
||||
android:pathData="M7,8L7,5l-7,7 7,7v-3l-4,-4 4,-4zM13,9L13,5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z" />
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@
|
|||
|
||||
<string name="post_username_format">\@%1$s</string>
|
||||
<string name="post_boosted_format">%1$s boosted</string>
|
||||
<string name="post_replied">Replied</string>
|
||||
<string name="post_replied_format">In reply to %1$s</string>
|
||||
<string name="post_replied_self">Continued thread</string>
|
||||
<string name="post_sensitive_media_title">Sensitive content</string>
|
||||
<string name="post_media_hidden_title">Media hidden</string>
|
||||
<string name="post_media_alt">ALT</string>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue