From 2f0f26c328f883816a5bded0e310bc001f7fd278 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Mon, 11 Apr 2022 12:55:08 -0400 Subject: [PATCH] Add story send multi-send, error, and improved SNC states. --- .../ui/error/SafetyNumberChangeDialog.java | 3 +- .../securesms/database/MmsDatabase.java | 1 + .../securesms/stories/dialogs/StoryDialogs.kt | 8 +++++ .../stories/landing/StoriesLandingFragment.kt | 11 +++++-- .../stories/landing/StoriesLandingItem.kt | 15 ++++++--- .../stories/landing/StoriesLandingItemData.kt | 4 ++- .../landing/StoriesLandingRepository.kt | 27 ++++++++++++++-- .../securesms/stories/my/MyStoriesFragment.kt | 6 ++-- .../securesms/stories/my/MyStoriesItem.kt | 22 +++++++------ .../reply/group/StoryGroupReplyFragment.kt | 32 ++++++++++++++++--- .../layout/safety_number_change_recipient.xml | 2 +- .../res/layout/stories_my_stories_item.xml | 11 +++---- app/src/main/res/values/strings.xml | 10 +++++- 13 files changed, 117 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java index 5a1130cdf..e2a01a41f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java @@ -23,6 +23,7 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.annimon.stream.Stream; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; @@ -165,7 +166,7 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa dialogView = LayoutInflater.from(requireActivity()).inflate(R.layout.safety_number_change_dialog, null); - AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity(), getTheme()); + AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity()); configureView(dialogView); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index b44c2777e..dd489ee80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -1146,6 +1146,7 @@ public class MmsDatabase extends MessageDatabase { long threadId = getThreadIdForMessage(messageId); updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE, Optional.of(threadId)); ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); + ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners(); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt index 51fb173fc..4cfbb3e20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt @@ -43,4 +43,12 @@ object StoryDialogs { return shareContacts.any { it is ContactSearchKey.Story && Recipient.resolved(it.recipientId).isMyStory } } + + fun resendStory(context: Context, resend: () -> Unit) { + MaterialAlertDialogBuilder(context) + .setMessage(R.string.StoryDialogs__story_could_not_be_sent) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.StoryDialogs__send) { _, _ -> resend() } + .show() + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt index c219f55fe..165a87557 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt @@ -31,12 +31,14 @@ import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.conversation.ConversationIntents import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs +import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.stories.StoryTextPostModel import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu +import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs import org.thoughtcrime.securesms.stories.my.MyStoriesActivity import org.thoughtcrime.securesms.stories.settings.StorySettingsActivity import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel @@ -178,8 +180,13 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l if (model.data.storyRecipient.isMyStory) { startActivity(Intent(requireContext(), MyStoriesActivity::class.java)) } else if (model.data.primaryStory.messageRecord.isOutgoing && model.data.primaryStory.messageRecord.isFailed) { - lifecycleDisposable += viewModel.resend(model.data.primaryStory.messageRecord).subscribe() - Toast.makeText(requireContext(), R.string.message_recipients_list_item__resend, Toast.LENGTH_SHORT).show() + if (model.data.primaryStory.messageRecord.isIdentityMismatchFailure) { + SafetyNumberChangeDialog.show(requireContext(), childFragmentManager, model.data.primaryStory.messageRecord) + } else { + StoryDialogs.resendStory(requireContext()) { + lifecycleDisposable += viewModel.resend(model.data.primaryStory.messageRecord).subscribe() + } + } } else { val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), preview, ViewCompat.getTransitionName(preview) ?: "") 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 a4841b47a..cc8763195 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 @@ -57,6 +57,7 @@ object StoriesLandingItem { return data.storyRecipient.hasSameContent(newItem.data.storyRecipient) && data == newItem.data && !hasStatusChange(newItem) && + (data.sendingCount == newItem.data.sendingCount && data.failureCount == newItem.data.failureCount) && super.areContentsTheSame(newItem) } @@ -205,12 +206,16 @@ 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) { + if (model.data.sendingCount > 0 || (model.data.primaryStory.messageRecord.isOutgoing && (model.data.primaryStory.messageRecord.isPending || model.data.primaryStory.messageRecord.isMediaPending))) { + errorIndicator.visible = model.data.failureCount > 0L + if (model.data.sendingCount > 1) { + date.text = context.getString(R.string.StoriesLandingItem__sending_d, model.data.sendingCount) + } else { + date.setText(R.string.StoriesLandingItem__sending) + } + } else if (model.data.failureCount > 0 || (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)) + date.text = SpanUtil.color(ContextCompat.getColor(context, R.color.signal_alert_primary), context.getString(R.string.StoriesLandingItem__send_failed)) } else { errorIndicator.visible = false date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.data.dateInMilliseconds) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingItemData.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingItemData.kt index 4774f5c81..1edf8b6c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingItemData.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingItemData.kt @@ -16,7 +16,9 @@ data class StoriesLandingItemData( val secondaryStory: ConversationMessage?, val storyRecipient: Recipient, val individualRecipient: Recipient = primaryStory.messageRecord.individualRecipient, - val dateInMilliseconds: Long = primaryStory.messageRecord.dateSent + val dateInMilliseconds: Long = primaryStory.messageRecord.dateSent, + val sendingCount: Long = 0, + val failureCount: Long = 0 ) : Comparable { override fun compareTo(other: StoriesLandingItemData): Int { return if (storyRecipient.isMyStory && !other.storyRecipient.isMyStory) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt index bdd050c58..7a8199c74 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt @@ -27,6 +27,7 @@ class StoriesLandingRepository(context: Context) { }.subscribeOn(Schedulers.io()) } + @Suppress("UsePropertyAccessSyntax") fun getStories(): Observable> { val storyRecipients: Observable>> = Observable.create { emitter -> fun refresh() { @@ -67,7 +68,25 @@ class StoriesLandingRepository(context: Context) { SignalDatabase.mms.getMessageRecord(it.messageId) } - createStoriesLandingItemData(recipient, messages) + var sendingCount: Long = 0 + var failureCount: Long = 0 + + if (recipient.isMyStory) { + SignalDatabase.mms.getMessages(results.map { it.messageId }).use { reader -> + var messageRecord: MessageRecord? = reader.getNext() + while (messageRecord != null) { + if (messageRecord.isOutgoing && (messageRecord.isPending || messageRecord.isMediaPending)) { + sendingCount++ + } else if (messageRecord.isFailed) { + failureCount++ + } + + messageRecord = reader.getNext() + } + } + } + + createStoriesLandingItemData(recipient, messages, sendingCount, failureCount) } if (observables.isEmpty()) { @@ -80,7 +99,7 @@ class StoriesLandingRepository(context: Context) { }.subscribeOn(Schedulers.io()) } - private fun createStoriesLandingItemData(sender: Recipient, messageRecords: List): Observable { + private fun createStoriesLandingItemData(sender: Recipient, messageRecords: List, sendingCount: Long, failureCount: Long): Observable { val itemDataObservable = Observable.create { emitter -> fun refresh(sender: Recipient) { val primaryIndex = messageRecords.indexOfFirst { !it.isOutgoing && it.viewedReceiptCount == 0 }.takeIf { it > -1 } ?: 0 @@ -93,7 +112,9 @@ class StoriesLandingRepository(context: Context) { primaryStory = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, messageRecords[primaryIndex]), secondaryStory = if (sender.isMyStory) messageRecords.drop(1).firstOrNull()?.let { ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, it) - } else null + } else null, + sendingCount = sendingCount, + failureCount = failureCount ) emitter.onNext(itemData) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt index bb68ba325..6d345e16d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt @@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.stories.StoryTextPostModel import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu +import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.Util @@ -80,8 +81,9 @@ class MyStoriesFragment : DSLSettingsFragment( if (it.distributionStory.messageRecord.isIdentityMismatchFailure) { SafetyNumberChangeDialog.show(requireContext(), childFragmentManager, it.distributionStory.messageRecord) } else { - lifecycleDisposable += viewModel.resend(it.distributionStory.messageRecord).subscribe() - Toast.makeText(requireContext(), R.string.message_recipients_list_item__resend, Toast.LENGTH_SHORT).show() + StoryDialogs.resendStory(requireContext()) { + lifecycleDisposable += viewModel.resend(it.distributionStory.messageRecord).subscribe() + } } } else { val recipient = if (it.distributionStory.messageRecord.recipient.isGroup) { 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 04b73a33b..89086c142 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,7 +3,6 @@ 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.signal.core.util.DimensionUnit import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ThumbnailView @@ -16,7 +15,6 @@ import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.stories.StoryTextPostModel 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 @@ -90,11 +88,13 @@ object MyStoriesItem { moreTarget.setOnClickListener { showContextMenu(model) } presentDateOrStatus(model) - viewCount.text = context.resources.getQuantityString( - R.plurals.MyStories__d_views, - model.distributionStory.messageRecord.viewedReceiptCount, - model.distributionStory.messageRecord.viewedReceiptCount - ) + if (model.distributionStory.messageRecord.isSent) { + viewCount.text = context.resources.getQuantityString( + R.plurals.MyStories__d_views, + model.distributionStory.messageRecord.viewedReceiptCount, + model.distributionStory.messageRecord.viewedReceiptCount + ) + } if (STATUS_CHANGE in payload) { return @@ -116,12 +116,16 @@ 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) + date.visible = false + viewCount.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)) + date.visible = true + viewCount.setText(R.string.StoriesLandingItem__send_failed) + date.setText(R.string.StoriesLandingItem__tap_to_retry) } else { errorIndicator.visible = false + date.visible = true date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.distributionStory.messageRecord.dateSent) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt index befe66157..5997ccb55 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt @@ -103,6 +103,10 @@ class StoryGroupReplyFragment : private lateinit var composer: StoryReplyComposer private var currentChild: StoryViewsAndRepliesPagerParent.Child? = null + private var resendBody: CharSequence? = null + private var resendMentions: List = emptyList() + private var resendReaction: String? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { SignalExecutors.BOUNDED.execute { RetrieveProfileJob.enqueue(groupRecipientId) @@ -226,9 +230,6 @@ class StoryGroupReplyFragment : recyclerView.isNestedScrollingEnabled = currentChild == StoryViewsAndRepliesPagerParent.Child.REPLIES && !(mentionsViewModel.isShowing.value ?: false) } - private var resendBody: CharSequence? = null - private var resendMentions: List = emptyList() - override fun onSendActionClicked() { val (body, mentions) = composer.consumeInput() performSend(body, mentions) @@ -262,7 +263,26 @@ class StoryGroupReplyFragment : } private fun sendReaction(emoji: String) { - lifecycleDisposable += StoryGroupReplySender.sendReaction(requireContext(), storyId, emoji).subscribe() + lifecycleDisposable += StoryGroupReplySender.sendReaction(requireContext(), storyId, emoji) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeBy( + onError = { error -> + if (error is UntrustedRecords.UntrustedRecordsException) { + resendReaction = emoji + + SafetyNumberChangeDialog.show(childFragmentManager, error.untrustedRecords) + } else { + Log.w(TAG, "Failed to send reply", error) + val context = context + if (context != null) { + Toast.makeText(context, R.string.message_details_recipient__failed_to_send, Toast.LENGTH_SHORT).show() + } + } + }, + onComplete = { + snapToTopDataObserver.requestScrollPosition(0) + } + ) } override fun onKeyEvent(keyEvent: KeyEvent?) = Unit @@ -385,8 +405,11 @@ class StoryGroupReplyFragment : override fun onSendAnywayAfterSafetyNumberChange(changedRecipients: MutableList) { val resendBody = resendBody + val resendReaction = resendReaction if (resendBody != null) { performSend(resendBody, resendMentions) + } else if (resendReaction != null) { + sendReaction(resendReaction) } } @@ -397,6 +420,7 @@ class StoryGroupReplyFragment : override fun onCanceled() { resendBody = null resendMentions = emptyList() + resendReaction = null } interface Callback { diff --git a/app/src/main/res/layout/safety_number_change_recipient.xml b/app/src/main/res/layout/safety_number_change_recipient.xml index 48273e958..54245224d 100644 --- a/app/src/main/res/layout/safety_number_change_recipient.xml +++ b/app/src/main/res/layout/safety_number_change_recipient.xml @@ -51,7 +51,7 @@ @@ -54,11 +54,10 @@ android:id="@+id/date" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="7dp" - android:layout_marginEnd="16dp" android:textAppearance="@style/TextAppearance.Signal.Body2" android:textColor="@color/signal_text_secondary" - app:layout_constraintStart_toEndOf="@id/error_indicator" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="@+id/view_count" app:layout_constraintTop_toBottomOf="@id/view_count" app:layout_goneMarginStart="16dp" tools:text="10m" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 500e02ad4..b0a2a4373 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4430,8 +4430,12 @@ Go to chat Sending… + + Sending %1$d… - Couldn\'t send + Send failed + + Tap to retry Hide story? @@ -4580,6 +4584,10 @@ Add to story Edit viewers + + Story could not be sent. Check your connection and try again. + + Send Share & View Stories