Story Status for landing page and my stories.

fork-5.53.8
Alex Hart 2022-03-10 12:13:36 -04:00 zatwierdzone przez Cody Henthorne
rodzic 1ac8701ada
commit 1f82ceecc6
7 zmienionych plików z 172 dodań i 20 usunięć

Wyświetl plik

@ -236,7 +236,7 @@ class MediaSelectionRepository(context: Context) {
if (isStory && preUploadResults.size > 1) { if (isStory && preUploadResults.size > 1) {
preUploadResults.forEach { preUploadResults.forEach {
val list = storyMessages[it] ?: mutableListOf() val list = storyMessages[it] ?: mutableListOf()
list.add(OutgoingSecureMediaMessage(message)) list.add(OutgoingSecureMediaMessage(message).withSentTimestamp(System.currentTimeMillis()))
storyMessages[it] = list storyMessages[it] = list
// XXX We must do this to avoid sending out messages to the same recipient with the same // XXX We must do this to avoid sending out messages to the same recipient with the same

Wyświetl plik

@ -58,4 +58,20 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
getLinkPreviews(), getLinkPreviews(),
getMentions()); getMentions());
} }
public @NonNull OutgoingSecureMediaMessage withSentTimestamp(long sentTimestamp) {
return new OutgoingSecureMediaMessage(getRecipient(),
getBody(),
getAttachments(),
sentTimestamp,
getDistributionType(),
getExpiresIn(),
isViewOnce(),
getStoryType(),
getParentStoryId(),
getOutgoingQuote(),
getSharedContacts(),
getLinkPreviews(),
getMentions());
}
} }

Wyświetl plik

@ -1,10 +1,9 @@
package org.thoughtcrime.securesms.stories.landing package org.thoughtcrime.securesms.stories.landing
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.view.AvatarView import org.thoughtcrime.securesms.avatar.view.AvatarView
import org.thoughtcrime.securesms.components.ThumbnailView 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.stories.dialogs.StoryContextMenu
import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.DateUtils 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.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder 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. * Items displaying a preview and metadata for a story from a user, allowing them to launch into the story viewer.
*/ */
object StoriesLandingItem { object StoriesLandingItem {
private const val STATUS_CHANGE = 0
fun register(mappingAdapter: MappingAdapter) { fun register(mappingAdapter: MappingAdapter) {
mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.stories_landing_item)) mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.stories_landing_item))
} }
@ -48,8 +51,30 @@ object StoriesLandingItem {
override fun areContentsTheSame(newItem: Model): Boolean { override fun areContentsTheSame(newItem: Model): Boolean {
return data.storyRecipient.hasSameContent(newItem.data.storyRecipient) && return data.storyRecipient.hasSameContent(newItem.data.storyRecipient) &&
data == newItem.data && data == newItem.data &&
!hasStatusChange(newItem) &&
super.areContentsTheSame(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<Model>(itemView) { private class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
@ -64,19 +89,20 @@ object StoriesLandingItem {
private val sender: TextView = itemView.findViewById(R.id.sender) private val sender: TextView = itemView.findViewById(R.id.sender)
private val date: TextView = itemView.findViewById(R.id.date) private val date: TextView = itemView.findViewById(R.id.date)
private val icon: ImageView = itemView.findViewById(R.id.icon) private val icon: ImageView = itemView.findViewById(R.id.icon)
private val errorIndicator: View = itemView.findViewById(R.id.error_indicator)
override fun bind(model: Model) { override fun bind(model: Model) {
itemView.setOnClickListener { model.onRowClick(model) }
if (model.data.storyRecipient.isMyStory) { presentDateOrStatus(model)
itemView.setOnLongClickListener(null) setUpClickListeners(model)
avatarView.displayProfileAvatar(Recipient.self())
} else { if (payload.contains(STATUS_CHANGE)) {
itemView.setOnLongClickListener { return
displayContext(model)
true
} }
if (model.data.storyRecipient.isMyStory) {
avatarView.displayProfileAvatar(Recipient.self())
} else {
avatarView.displayProfileAvatar(model.data.storyRecipient) avatarView.displayProfileAvatar(model.data.storyRecipient)
} }
@ -111,7 +137,6 @@ object StoriesLandingItem {
else -> model.data.storyRecipient.getDisplayName(context) else -> model.data.storyRecipient.getDisplayName(context)
} }
date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.data.dateInMilliseconds)
icon.visible = model.data.hasReplies || model.data.hasRepliesFromSelf icon.visible = model.data.hasReplies || model.data.hasRepliesFromSelf
// TODO [stories] -- Set actual image resource // TODO [stories] -- Set actual image resource
icon.setImageDrawable(ColorDrawable(Color.RED)) 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 { private fun getGroupPresentation(model: Model): String {
return context.getString( return context.getString(
R.string.StoryViewerPageFragment__s_to_s, R.string.StoryViewerPageFragment__s_to_s,

Wyświetl plik

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.stories.my
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ThumbnailView import org.thoughtcrime.securesms.components.ThumbnailView
import org.thoughtcrime.securesms.components.menu.ActionItem 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.stories.StoryTextPostModel
import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.DateUtils 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.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
import org.thoughtcrime.securesms.util.visible
import java.util.Locale import java.util.Locale
object MyStoriesItem { object MyStoriesItem {
private const val STATUS_CHANGE = 0
fun register(mappingAdapter: MappingAdapter) { fun register(mappingAdapter: MappingAdapter) {
mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.stories_my_stories_item)) 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 { 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 storyPreview: ThumbnailView = itemView.findViewById(R.id.story)
private val viewCount: TextView = itemView.findViewById(R.id.view_count) private val viewCount: TextView = itemView.findViewById(R.id.view_count)
private val date: TextView = itemView.findViewById(R.id.date) private val date: TextView = itemView.findViewById(R.id.date)
private val errorIndicator: View = itemView.findViewById(R.id.error_indicator)
override fun bind(model: Model) { override fun bind(model: Model) {
itemView.setOnClickListener { model.onClick(model) } itemView.setOnClickListener { model.onClick(model) }
downloadTarget.setOnClickListener { model.onSaveClick(model) } downloadTarget.setOnClickListener { model.onSaveClick(model) }
moreTarget.setOnClickListener { showContextMenu(model) } moreTarget.setOnClickListener { showContextMenu(model) }
viewCount.text = context.resources.getQuantityString(R.plurals.MyStories__d_views, model.distributionStory.messageRecord.readReceiptCount, model.distributionStory.messageRecord.readReceiptCount) presentDateOrStatus(model)
date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.distributionStory.messageRecord.dateSent)
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 record: MmsMessageRecord = model.distributionStory.messageRecord as MmsMessageRecord
val thumbnail: Slide? = record.slideDeck.thumbnailSlide 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) { private fun showContextMenu(model: Model) {
SignalContextMenu.Builder(itemView, itemView.rootView as ViewGroup) SignalContextMenu.Builder(itemView, itemView.rootView as ViewGroup)
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.END) .preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.END)

Wyświetl plik

@ -40,18 +40,33 @@
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed"
tools:text="My Stories" /> tools:text="My Stories" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/error_indicator"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_marginStart="16dp"
android:importantForAccessibility="no"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/date"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="@id/date"
app:srcCompat="@drawable/ic_error_outline_24"
app:tint="@color/signal_alert_primary"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/date" android:id="@+id/date"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="7dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:textAppearance="@style/TextAppearance.Signal.Body2" android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/signal_text_secondary" android:textColor="@color/signal_text_secondary"
app:layout_constraintBottom_toTopOf="@id/icon" app:layout_constraintBottom_toTopOf="@id/icon"
app:layout_constraintEnd_toStartOf="@id/story" app:layout_constraintEnd_toStartOf="@id/story"
app:layout_constraintStart_toEndOf="@id/avatar" app:layout_constraintStart_toEndOf="@id/error_indicator"
app:layout_constraintTop_toBottomOf="@id/sender" app:layout_constraintTop_toBottomOf="@id/sender"
app:layout_goneMarginStart="16dp"
tools:text="10m" /> tools:text="10m" />
<ImageView <ImageView

Wyświetl plik

@ -4,9 +4,9 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:paddingStart="@dimen/dsl_settings_gutter" android:paddingStart="@dimen/dsl_settings_gutter"
android:paddingEnd="@dimen/dsl_settings_gutter" android:paddingEnd="@dimen/dsl_settings_gutter">
android:background="?selectableItemBackground">
<org.thoughtcrime.securesms.components.OutlinedThumbnailView <org.thoughtcrime.securesms.components.OutlinedThumbnailView
android:id="@+id/story" android:id="@+id/story"
@ -35,16 +35,31 @@
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed"
tools:text="12 views" /> tools:text="12 views" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/error_indicator"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_marginStart="16dp"
android:importantForAccessibility="no"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/date"
app:layout_constraintStart_toEndOf="@id/story"
app:layout_constraintTop_toTopOf="@id/date"
app:srcCompat="@drawable/ic_error_outline_24"
app:tint="@color/signal_alert_primary"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/date" android:id="@+id/date"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="7dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:textAppearance="@style/TextAppearance.Signal.Body2" android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/signal_text_secondary" android:textColor="@color/signal_text_secondary"
app:layout_constraintStart_toEndOf="@id/story" app:layout_constraintStart_toEndOf="@id/error_indicator"
app:layout_constraintTop_toBottomOf="@id/view_count" app:layout_constraintTop_toBottomOf="@id/view_count"
app:layout_goneMarginStart="16dp"
tools:text="10m" /> tools:text="10m" />
<View <View

Wyświetl plik

@ -4381,6 +4381,10 @@
<string name="StoriesLandingItem__share">Share…</string> <string name="StoriesLandingItem__share">Share…</string>
<!-- Context menu option to go to story chat --> <!-- Context menu option to go to story chat -->
<string name="StoriesLandingItem__go_to_chat">Go to chat</string> <string name="StoriesLandingItem__go_to_chat">Go to chat</string>
<!-- Label when a story is pending sending -->
<string name="StoriesLandingItem__sending">Sending…</string>
<!-- Label when a story fails to send -->
<string name="StoriesLandingItem__couldnt_send">Couldn\'t send</string>
<!-- Title of dialog confirming decision to hide a story --> <!-- Title of dialog confirming decision to hide a story -->
<string name="StoriesLandingFragment__hide_story">Hide story?</string> <string name="StoriesLandingFragment__hide_story">Hide story?</string>
<!-- Message of dialog confirming decision to hide a story --> <!-- Message of dialog confirming decision to hide a story -->