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 49101b46..78edbd7d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -66,6 +66,7 @@ import com.keylesspalace.tusky.util.TimestampUtils; import com.keylesspalace.tusky.util.TouchDelegateHelper; import com.keylesspalace.tusky.view.MediaPreviewImageView; import com.keylesspalace.tusky.view.MediaPreviewLayout; +import com.keylesspalace.tusky.view.StatusQuoteView; import com.keylesspalace.tusky.viewdata.PollOptionViewData; import com.keylesspalace.tusky.viewdata.PollViewData; import com.keylesspalace.tusky.viewdata.PollViewDataKt; @@ -135,6 +136,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private final Drawable mediaPreviewUnloaded; private ChipGroup emojiReactionsView; + private StatusQuoteView quoteView; protected StatusBaseViewHolder(View itemView) { super(itemView); @@ -151,6 +153,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { bookmarkButton = itemView.findViewById(R.id.status_bookmark); moreButton = itemView.findViewById(R.id.status_more); emojiReactionsView = itemView.findViewById(R.id.status_emoji_reactions); + quoteView = itemView.findViewById(R.id.status_quote_view); mediaContainer = itemView.findViewById(R.id.status_media_preview_container); mediaContainer.setClipToOutline(true); @@ -475,6 +478,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { bookmarkButton.setChecked(bookmarked); } + protected void setQuote(StatusViewData.Concrete status, final StatusActionListener listener) { + Status quotedStatus = status.getActionable().getQuote(); + if (quotedStatus == null) { + quoteView.setVisibility(View.GONE); + return; + } + + StatusViewData.Concrete statusViewData = status.copyWithStatus(quotedStatus); + quoteView.setupWithStatus(statusViewData, listener, getBindingAdapterPosition()); + quoteView.setVisibility(View.VISIBLE); + } + private BitmapDrawable decodeBlurHash(String blurhash) { return ImageLoadingHelper.decodeBlurHash(this.avatar.getContext(), blurhash); } @@ -820,6 +835,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setReblogged(actionable.getReblogged()); setFavourited(actionable.getFavourited()); setBookmarked(actionable.getBookmarked()); + setQuote(status, listener); List attachments = actionable.getAttachments(); boolean sensitive = actionable.getSensitive(); if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { @@ -1277,10 +1293,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { pollOptions.setVisibility(visibility); pollButton.setVisibility(visibility); pollDescription.setVisibility(visibility); + showStatusButtons(show); + } + + public void showStatusButtons(boolean show) { + int visibility = show ? View.VISIBLE : View.GONE; + emojiReactionsView.setVisibility(visibility); replyButton.setVisibility(visibility); + replyCountLabel.setVisibility(visibility); reblogButton.setVisibility(visibility); favouriteButton.setVisibility(visibility); bookmarkButton.setVisibility(visibility); + reactButton.setVisibility(visibility); moreButton.setVisibility(visibility); } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index 773cee49..395b38c0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -133,6 +133,7 @@ data class ConversationStatusEntity( language = language, filtered = null, pleroma = null, // FIXME + quote = null, // FIXME?? ), isExpanded = expanded, isShowingContent = showingHiddenContent, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt index 7af3358b..5b6c6a7e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt @@ -497,6 +497,11 @@ class TimelineFragment : super.viewReplyTo(status.actionable.inReplyToId); } + override fun onViewQuote(position: Int) { + val status = adapter.peek(position)?.asStatusOrNull()?.actionable?.quote ?: return + super.viewThread(status.id, status.url) + } + override fun onViewTag(tag: String) { if (viewModel.kind == TimelineViewModel.Kind.TAG && viewModel.tags.size == 1 && viewModel.tags.contains(tag) 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 5aa96d3f..9700af6e 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 @@ -204,6 +204,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false language = status.language, filtered = status.filtered, pleroma = pleroma, + quote = null, // FIXME: cache this ) } val status = if (reblog != null) { @@ -238,6 +239,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false language = status.language, filtered = status.filtered, pleroma = pleroma, + quote = null, // FIXME: cache this ) } else { Status( @@ -271,6 +273,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false language = status.language, filtered = status.filtered, pleroma = pleroma, + quote = null, // FIXME: cache this ) } return StatusViewData.Concrete( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt index b1b98e61..51b9308c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt @@ -366,6 +366,15 @@ class ViewThreadFragment : super.viewReplyTo(id) } + override fun onViewQuote(position: Int) { + val status = adapter.currentList[position].actionable.quote ?: return + if (thisThreadsStatusId == status.id) { + // If already viewing this thread, don't reopen it. + return + } + super.viewThread(status.id, status.url) + } + override fun onViewUrl(url: String) { val status: StatusViewData.Concrete? = viewModel.detailedStatus() if (status != null && status.status.url == url) { 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 645c5c92..fb347c2a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -52,6 +52,7 @@ data class Status( val language: String?, val filtered: List?, val pleroma: PleromaStatus?, + val quote: Status?, ) { val actionableId: String diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java index 97dc992d..b6b654fa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java @@ -33,6 +33,7 @@ public interface StatusActionListener extends LinkListener { void onViewMedia(int position, int attachmentIndex, @Nullable View view); void onViewThread(int position); void onViewReplyTo(int position); + default void onViewQuote(int position) {} /** * Open reblog author for the status. diff --git a/app/src/main/java/com/keylesspalace/tusky/view/StatusQuoteView.kt b/app/src/main/java/com/keylesspalace/tusky/view/StatusQuoteView.kt new file mode 100644 index 00000000..7be00394 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/StatusQuoteView.kt @@ -0,0 +1,91 @@ +package com.keylesspalace.tusky.view + +import android.content.* +import android.util.* +import android.view.* +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.preference.PreferenceManager +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.StatusViewData + +class StatusQuoteView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0) + : ConstraintLayout(context, attrs, defStyleAttr) { + + private lateinit var viewHolder : StatusViewHolder + private var statusDisplayOptions : StatusDisplayOptions + init { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + mediaPreviewEnabled = true, // FIXME: get value from AccountManager + useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), + showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true), + useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = false, + confirmFavourites = false, + hideStats = false, + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), + showStatsInline = false, + showSensitiveMedia = false, // FIXME: get value from AccountManager + openSpoiler = false, // FIXME: get value from AccountManager + bigEmojis = preferences.getBoolean(PrefKeys.BIG_EMOJIS, true), + ) + } + + fun setupWithStatus(status: StatusViewData.Concrete, statusActionListener: StatusActionListener, position: Int) { + removeAllViews() + View.inflate(context, R.layout.item_status, this) + viewHolder = StatusViewHolder(this) + viewHolder.showStatusButtons(false) + viewHolder.setupWithStatus(status, ProxyQuoteStatusActionListener(statusActionListener, position), statusDisplayOptions) + } + + class ProxyQuoteStatusActionListener constructor( + private val higherLevelStatusListener: StatusActionListener, + val position: Int + ): StatusActionListener { + + override fun onReply(position: Int) { } + override fun onReblog(reblog: Boolean, position: Int) { } + override fun onFavourite(favourite: Boolean, position: Int) { } + override fun onBookmark(bookmark: Boolean, position: Int) { } + override fun onMore(view: View, position: Int) { } + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { } + + override fun onViewThread(position: Int) { + higherLevelStatusListener.onViewQuote(this.position) + } + + override fun onViewReplyTo(position: Int) { } + override fun onOpenReblog(position: Int) { } + override fun onExpandedChange(expanded: Boolean, position: Int) { } + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { } + override fun onLoadMore(position: Int) { } + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { } + override fun onVoteInPoll(position: Int, choices: MutableList) { } + + override fun onViewAccount(id: String) { + higherLevelStatusListener.onViewAccount(id) + } + + override fun onViewTag(id: String) { + higherLevelStatusListener.onViewTag(id) + } + + override fun onViewUrl(id: String) { + higherLevelStatusListener.onViewUrl(id) + } + + override fun clearWarningAction(position: Int) { } + } +} 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 dcf2ec83..2e98acbf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -103,6 +103,11 @@ sealed class StatusViewData { this.isCollapsible = shouldTrimStatus(this.content) } + /** Helper for Java */ + fun copyWithStatus(status: Status): Concrete { + return copy(status = status) + } + /** Helper for Java */ fun copyWithCollapsed(isCollapsed: Boolean): Concrete { return copy(isCollapsed = isCollapsed) diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index 3e03eb6e..d5872bf1 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -326,6 +326,18 @@ app:layout_constraintTop_toBottomOf="@id/status_poll_button" tools:text="7 votes • 7 hours remaining" /> + + + + + app:layout_constraintTop_toBottomOf="@id/status_quote_view" />