From 1f82ceecc615d46cadfe58497d8e8a698fb74dba Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 10 Mar 2022 12:13:36 -0400 Subject: [PATCH] Story Status for landing page and my stories. --- .../mediasend/v2/MediaSelectionRepository.kt | 2 +- .../mms/OutgoingSecureMediaMessage.java | 16 +++++ .../stories/landing/StoriesLandingItem.kt | 71 ++++++++++++++++--- .../securesms/stories/my/MyStoriesItem.kt | 57 ++++++++++++++- .../main/res/layout/stories_landing_item.xml | 19 ++++- .../res/layout/stories_my_stories_item.xml | 23 ++++-- app/src/main/res/values/strings.xml | 4 ++ 7 files changed, 172 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt index 06d5bebd8..86619276b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt @@ -236,7 +236,7 @@ class MediaSelectionRepository(context: Context) { if (isStory && preUploadResults.size > 1) { preUploadResults.forEach { val list = storyMessages[it] ?: mutableListOf() - list.add(OutgoingSecureMediaMessage(message)) + list.add(OutgoingSecureMediaMessage(message).withSentTimestamp(System.currentTimeMillis())) storyMessages[it] = list // XXX We must do this to avoid sending out messages to the same recipient with the same diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java index a5d994b15..76dab35fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java @@ -58,4 +58,20 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage { getLinkPreviews(), getMentions()); } + + public @NonNull OutgoingSecureMediaMessage withSentTimestamp(long sentTimestamp) { + return new OutgoingSecureMediaMessage(getRecipient(), + getBody(), + getAttachments(), + sentTimestamp, + getDistributionType(), + getExpiresIn(), + isViewOnce(), + getStoryType(), + getParentStoryId(), + getOutgoingQuote(), + getSharedContacts(), + getLinkPreviews(), + getMentions()); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingItem.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingItem.kt index 7da195351..cd2ecb335 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingItem.kt @@ -1,10 +1,9 @@ package org.thoughtcrime.securesms.stories.landing -import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.view.View import android.widget.ImageView import android.widget.TextView +import androidx.core.content.ContextCompat import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.avatar.view.AvatarView import org.thoughtcrime.securesms.components.ThumbnailView @@ -17,6 +16,7 @@ import org.thoughtcrime.securesms.stories.StoryTextPostModel import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.DateUtils +import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder @@ -27,6 +27,9 @@ import java.util.Locale * Items displaying a preview and metadata for a story from a user, allowing them to launch into the story viewer. */ object StoriesLandingItem { + + private const val STATUS_CHANGE = 0 + fun register(mappingAdapter: MappingAdapter) { mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.stories_landing_item)) } @@ -48,8 +51,30 @@ object StoriesLandingItem { override fun areContentsTheSame(newItem: Model): Boolean { return data.storyRecipient.hasSameContent(newItem.data.storyRecipient) && data == newItem.data && + !hasStatusChange(newItem) && super.areContentsTheSame(newItem) } + + override fun getChangePayload(newItem: Model): Any? { + return if (isSameRecord(newItem) && hasStatusChange(newItem)) { + STATUS_CHANGE + } else { + null + } + } + + private fun isSameRecord(newItem: Model): Boolean { + return data.primaryStory.messageRecord.id == newItem.data.primaryStory.messageRecord.id + } + + private fun hasStatusChange(newItem: Model): Boolean { + val oldRecord = data.primaryStory.messageRecord + val newRecord = newItem.data.primaryStory.messageRecord + + return oldRecord.isOutgoing && + newRecord.isOutgoing && + (oldRecord.isPending != newRecord.isPending || oldRecord.isSent != newRecord.isSent || oldRecord.isFailed != newRecord.isFailed) + } } private class ViewHolder(itemView: View) : MappingViewHolder(itemView) { @@ -64,19 +89,20 @@ object StoriesLandingItem { private val sender: TextView = itemView.findViewById(R.id.sender) private val date: TextView = itemView.findViewById(R.id.date) private val icon: ImageView = itemView.findViewById(R.id.icon) + private val errorIndicator: View = itemView.findViewById(R.id.error_indicator) override fun bind(model: Model) { - itemView.setOnClickListener { model.onRowClick(model) } + + presentDateOrStatus(model) + setUpClickListeners(model) + + if (payload.contains(STATUS_CHANGE)) { + return + } if (model.data.storyRecipient.isMyStory) { - itemView.setOnLongClickListener(null) avatarView.displayProfileAvatar(Recipient.self()) } else { - itemView.setOnLongClickListener { - displayContext(model) - true - } - avatarView.displayProfileAvatar(model.data.storyRecipient) } @@ -111,7 +137,6 @@ object StoriesLandingItem { else -> model.data.storyRecipient.getDisplayName(context) } - date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.data.dateInMilliseconds) icon.visible = model.data.hasReplies || model.data.hasRepliesFromSelf // TODO [stories] -- Set actual image resource icon.setImageDrawable(ColorDrawable(Color.RED)) @@ -121,6 +146,32 @@ object StoriesLandingItem { } } + private fun presentDateOrStatus(model: Model) { + if (model.data.primaryStory.messageRecord.isOutgoing && (model.data.primaryStory.messageRecord.isPending || model.data.primaryStory.messageRecord.isMediaPending)) { + errorIndicator.visible = false + date.setText(R.string.StoriesLandingItem__sending) + } else if (model.data.primaryStory.messageRecord.isOutgoing && model.data.primaryStory.messageRecord.isFailed) { + errorIndicator.visible = true + date.text = SpanUtil.color(ContextCompat.getColor(context, R.color.signal_alert_primary), context.getString(R.string.StoriesLandingItem__couldnt_send)) + } else { + errorIndicator.visible = false + date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.data.dateInMilliseconds) + } + } + + private fun setUpClickListeners(model: Model) { + itemView.setOnClickListener { model.onRowClick(model) } + + if (model.data.storyRecipient.isMyStory) { + itemView.setOnLongClickListener(null) + } else { + itemView.setOnLongClickListener { + displayContext(model) + true + } + } + } + private fun getGroupPresentation(model: Model): String { return context.getString( R.string.StoryViewerPageFragment__s_to_s, diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt index 7ea00fde6..bfdacf485 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.stories.my import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.core.content.ContextCompat import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ThumbnailView import org.thoughtcrime.securesms.components.menu.ActionItem @@ -16,13 +17,17 @@ import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.stories.StoryTextPostModel import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.DateUtils +import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder +import org.thoughtcrime.securesms.util.visible import java.util.Locale object MyStoriesItem { + private const val STATUS_CHANGE = 0 + fun register(mappingAdapter: MappingAdapter) { mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.stories_my_stories_item)) } @@ -40,7 +45,30 @@ object MyStoriesItem { } override fun areContentsTheSame(newItem: Model): Boolean { - return distributionStory == newItem.distributionStory && super.areContentsTheSame(newItem) + return distributionStory == newItem.distributionStory && + !hasStatusChange(newItem) && + super.areContentsTheSame(newItem) + } + + override fun getChangePayload(newItem: Model): Any? { + return if (isSameRecord(newItem) && hasStatusChange(newItem)) { + STATUS_CHANGE + } else { + null + } + } + + private fun isSameRecord(newItem: Model): Boolean { + return distributionStory.messageRecord.id == newItem.distributionStory.messageRecord.id + } + + private fun hasStatusChange(newItem: Model): Boolean { + val oldRecord = distributionStory.messageRecord + val newRecord = newItem.distributionStory.messageRecord + + return oldRecord.isOutgoing && + newRecord.isOutgoing && + (oldRecord.isPending != newRecord.isPending || oldRecord.isSent != newRecord.isSent || oldRecord.isFailed != newRecord.isFailed) } } @@ -51,13 +79,23 @@ object MyStoriesItem { private val storyPreview: ThumbnailView = itemView.findViewById(R.id.story) private val viewCount: TextView = itemView.findViewById(R.id.view_count) private val date: TextView = itemView.findViewById(R.id.date) + private val errorIndicator: View = itemView.findViewById(R.id.error_indicator) override fun bind(model: Model) { itemView.setOnClickListener { model.onClick(model) } downloadTarget.setOnClickListener { model.onSaveClick(model) } moreTarget.setOnClickListener { showContextMenu(model) } - viewCount.text = context.resources.getQuantityString(R.plurals.MyStories__d_views, model.distributionStory.messageRecord.readReceiptCount, model.distributionStory.messageRecord.readReceiptCount) - date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.distributionStory.messageRecord.dateSent) + presentDateOrStatus(model) + + viewCount.text = context.resources.getQuantityString( + R.plurals.MyStories__d_views, + model.distributionStory.messageRecord.readReceiptCount, + model.distributionStory.messageRecord.readReceiptCount + ) + + if (STATUS_CHANGE in payload) { + return + } val record: MmsMessageRecord = model.distributionStory.messageRecord as MmsMessageRecord val thumbnail: Slide? = record.slideDeck.thumbnailSlide @@ -72,6 +110,19 @@ object MyStoriesItem { } } + private fun presentDateOrStatus(model: Model) { + if (model.distributionStory.messageRecord.isPending || model.distributionStory.messageRecord.isMediaPending) { + errorIndicator.visible = false + date.setText(R.string.StoriesLandingItem__sending) + } else if (model.distributionStory.messageRecord.isFailed) { + errorIndicator.visible = true + date.text = SpanUtil.color(ContextCompat.getColor(context, R.color.signal_alert_primary), context.getString(R.string.StoriesLandingItem__couldnt_send)) + } else { + errorIndicator.visible = false + date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.distributionStory.messageRecord.dateSent) + } + } + private fun showContextMenu(model: Model) { SignalContextMenu.Builder(itemView, itemView.rootView as ViewGroup) .preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.END) diff --git a/app/src/main/res/layout/stories_landing_item.xml b/app/src/main/res/layout/stories_landing_item.xml index 40c7d92c1..781e935f3 100644 --- a/app/src/main/res/layout/stories_landing_item.xml +++ b/app/src/main/res/layout/stories_landing_item.xml @@ -40,18 +40,33 @@ app:layout_constraintVertical_chainStyle="packed" tools:text="My Stories" /> + + + android:paddingEnd="@dimen/dsl_settings_gutter"> + + Share… Go to chat + + Sending… + + Couldn\'t send Hide story?