Add story send multi-send, error, and improved SNC states.

This commit is contained in:
Cody Henthorne 2022-04-11 12:55:08 -04:00 committed by Greyson Parrelli
parent 7f2f5a182f
commit 2f0f26c328
13 changed files with 117 additions and 35 deletions

View file

@ -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);

View file

@ -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

View file

@ -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()
}
}

View file

@ -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) ?: "")

View file

@ -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)

View file

@ -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<StoriesLandingItemData> {
override fun compareTo(other: StoriesLandingItemData): Int {
return if (storyRecipient.isMyStory && !other.storyRecipient.isMyStory) {

View file

@ -27,6 +27,7 @@ class StoriesLandingRepository(context: Context) {
}.subscribeOn(Schedulers.io())
}
@Suppress("UsePropertyAccessSyntax")
fun getStories(): Observable<List<StoriesLandingItemData>> {
val storyRecipients: Observable<Map<Recipient, List<StoryResult>>> = 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<MessageRecord>): Observable<StoriesLandingItemData> {
private fun createStoriesLandingItemData(sender: Recipient, messageRecords: List<MessageRecord>, sendingCount: Long, failureCount: Long): Observable<StoriesLandingItemData> {
val itemDataObservable = Observable.create<StoriesLandingItemData> { 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)

View file

@ -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) {

View file

@ -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)
}
}

View file

@ -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<Mention> = 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<Mention> = 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<RecipientId>) {
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 {

View file

@ -51,7 +51,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/safety_number_change_recipient_view"
style="@style/Signal.Widget.Button.Large.Secondary"
style="@style/Signal.Widget.Button.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"

View file

@ -27,13 +27,13 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
app:layout_constraintBottom_toTopOf="@id/date"
app:layout_constraintStart_toEndOf="@id/story"
app:layout_constraintStart_toEndOf="@+id/error_indicator"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:layout_goneMarginStart="20dp"
tools:text="12 views" />
<androidx.appcompat.widget.AppCompatImageView
@ -45,7 +45,7 @@
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/date"
app:layout_constraintStart_toEndOf="@id/story"
app:layout_constraintTop_toTopOf="@id/date"
app:layout_constraintTop_toTopOf="@+id/view_count"
app:srcCompat="@drawable/ic_error_outline_24"
app:tint="@color/signal_alert_primary"
tools:visibility="visible" />
@ -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" />

View file

@ -4430,8 +4430,12 @@
<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 multiple stories are pending sending -->
<string name="StoriesLandingItem__sending_d">Sending %1$d…</string>
<!-- Label when a story fails to send -->
<string name="StoriesLandingItem__couldnt_send">Couldn\'t send</string>
<string name="StoriesLandingItem__send_failed">Send failed</string>
<!-- Status label when a story fails to send indicating user action to retry -->
<string name="StoriesLandingItem__tap_to_retry">Tap to retry</string>
<!-- Title of dialog confirming decision to hide a story -->
<string name="StoriesLandingFragment__hide_story">Hide story?</string>
<!-- Message of dialog confirming decision to hide a story -->
@ -4580,6 +4584,10 @@
<string name="StoryDialogs__add_to_story">Add to story</string>
<!-- First time share to story dialog: Neutral action to edit who can view "My Story" -->
<string name="StoryDialogs__edit_viewers">Edit viewers</string>
<!-- Error message shown when a failure occurs during story send -->
<string name="StoryDialogs__story_could_not_be_sent">Story could not be sent. Check your connection and try again.</string>
<!-- Error message dialog button to resend a previously failed story send -->
<string name="StoryDialogs__send">Send</string>
<!-- Privacy Settings toggle title for stories -->
<string name="PrivacySettingsFragment__share_and_view_stories">Share &amp; View Stories</string>
<!-- Privacy Settings toggle summary for stories -->