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 99b0620b08..db1c781a55 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 @@ -10,13 +10,14 @@ import org.thoughtcrime.securesms.R object StoryDialogs { - 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() - } + fun resendStory(context: Context, onDismiss: () -> Unit = {}, 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() } + .setOnDismissListener { onDismiss() } + .show() +} fun displayStoryOrProfileImage( context: Context, 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 18c9a23b13..0e6d7240fd 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 @@ -72,9 +72,20 @@ object MyStoriesItem { val oldRecord = distributionStory.messageRecord val newRecord = newItem.distributionStory.messageRecord + val oldRecordHasIdentityMismatch = distributionStory.messageRecord.identityKeyMismatches.isNotEmpty() + val newRecordHasIdentityMismatch = newItem.distributionStory.messageRecord.identityKeyMismatches.isNotEmpty() + val oldRecordHasNetworkFailures = distributionStory.messageRecord.hasNetworkFailures() + val newRecordHasNetworkFailures = newItem.distributionStory.messageRecord.hasNetworkFailures() + return oldRecord.isOutgoing && newRecord.isOutgoing && - (oldRecord.isPending != newRecord.isPending || oldRecord.isSent != newRecord.isSent || oldRecord.isFailed != newRecord.isFailed) + ( + oldRecord.isPending != newRecord.isPending || + oldRecord.isSent != newRecord.isSent || + oldRecord.isFailed != newRecord.isFailed || + oldRecordHasIdentityMismatch != newRecordHasIdentityMismatch || + oldRecordHasNetworkFailures != newRecordHasNetworkFailures + ) } } @@ -157,6 +168,11 @@ object MyStoriesItem { date.visible = true viewCount.setText(R.string.StoriesLandingItem__send_failed) date.setText(R.string.StoriesLandingItem__tap_to_retry) + } else if (model.distributionStory.messageRecord.isIdentityMismatchFailure) { + errorIndicator.visible = true + date.visible = true + viewCount.setText(R.string.StoriesLandingItem__partially_sent) + date.setText(R.string.StoriesLandingItem__tap_to_retry) } else { errorIndicator.visible = false date.visible = true diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoBottomSheetDialogFragment.kt index 59c5de075b..3b9b276a2f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoBottomSheetDialogFragment.kt @@ -58,20 +58,26 @@ class StoryInfoBottomSheetDialogFragment : DSLSettingsBottomSheetFragment() { ) ) - sectionHeaderPref( - title = if (state.isOutgoing) { - R.string.StoryInfoBottomSheetDialogFragment__sent_to - } else { - R.string.StoryInfoBottomSheetDialogFragment__sent_from - } - ) - - state.recipients.forEach { - customPref(it) + state.sections.map { (section, recipients) -> + renderSection(section, recipients) } } } + private fun DSLConfiguration.renderSection(sectionKey: StoryInfoState.SectionKey, recipients: List) { + sectionHeaderPref( + title = when (sectionKey) { + StoryInfoState.SectionKey.FAILED -> R.string.StoryInfoBottomSheetDialogFragment__failed + StoryInfoState.SectionKey.SENT_TO -> R.string.StoryInfoBottomSheetDialogFragment__sent_to + StoryInfoState.SectionKey.SENT_FROM -> R.string.StoryInfoBottomSheetDialogFragment__sent_from + } + ) + + recipients.forEach { + customPref(it) + } + } + override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) findListener()?.onInfoSheetDismissed() diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRecipientRow.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRecipientRow.kt index 19c5435a79..00950398c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRecipientRow.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRecipientRow.kt @@ -23,7 +23,8 @@ object StoryInfoRecipientRow { class Model( val recipient: Recipient, val date: Long, - val status: Int + val status: Int, + val isFailed: Boolean ) : MappingModel { override fun areItemsTheSame(newItem: Model): Boolean { return recipient.id == newItem.recipient.id diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoState.kt index ebfbc731dc..1d96a19dd8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoState.kt @@ -8,6 +8,12 @@ data class StoryInfoState( val receivedMillis: Long = -1L, val size: Long = -1L, val isOutgoing: Boolean = false, - val recipients: List = emptyList(), + val sections: Map> = emptyMap(), val isLoaded: Boolean = false -) +) { + enum class SectionKey { + FAILED, + SENT_TO, + SENT_FROM + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt index 039f39e33e..c2d9c642a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt @@ -7,8 +7,11 @@ import io.reactivex.rxjava3.core.BackpressureStrategy import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign +import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.rx.RxStore /** @@ -29,31 +32,47 @@ class StoryInfoViewModel(storyId: Long, repository: StoryInfoRepository = StoryI receivedMillis = storyInfo.messageRecord.dateReceived, size = (storyInfo.messageRecord as? MmsMessageRecord)?.let { it.slideDeck.firstSlide?.fileSize } ?: -1L, isOutgoing = storyInfo.messageRecord.isOutgoing, - recipients = buildRecipients(storyInfo) + sections = buildSections(storyInfo) ) } } - private fun buildRecipients(storyInfo: StoryInfoRepository.StoryInfo): List { + private fun buildSections(storyInfo: StoryInfoRepository.StoryInfo): Map> { return if (storyInfo.messageRecord.isOutgoing) { - storyInfo.receiptInfo.map { + storyInfo.receiptInfo.map { groupReceiptInfo -> StoryInfoRecipientRow.Model( - recipient = Recipient.resolved(it.recipientId), - date = it.timestamp, - status = it.status + recipient = Recipient.resolved(groupReceiptInfo.recipientId), + date = groupReceiptInfo.timestamp, + status = groupReceiptInfo.status, + isFailed = hasFailure(storyInfo.messageRecord, groupReceiptInfo.recipientId) ) + }.groupBy { + when { + it.isFailed -> StoryInfoState.SectionKey.FAILED + else -> StoryInfoState.SectionKey.SENT_TO + } } } else { - listOf( - StoryInfoRecipientRow.Model( - recipient = storyInfo.messageRecord.individualRecipient, - date = storyInfo.messageRecord.dateSent, - status = -1 + mapOf( + StoryInfoState.SectionKey.SENT_FROM to listOf( + StoryInfoRecipientRow.Model( + recipient = storyInfo.messageRecord.individualRecipient, + date = storyInfo.messageRecord.dateSent, + status = -1, + isFailed = false + ) ) ) } } + private fun hasFailure(messageRecord: MessageRecord, recipientId: RecipientId): Boolean { + val hasNetworkFailure = messageRecord.networkFailures.any { it.getRecipientId(ApplicationDependencies.getApplication()) == recipientId } + val hasIdentityFailure = messageRecord.identityKeyMismatches.any { it.getRecipientId(ApplicationDependencies.getApplication()) == recipientId } + + return hasNetworkFailure || hasIdentityFailure + } + override fun onCleared() { disposables.clear() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt index 1ba1eb4550..dab460d68c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt @@ -5,6 +5,7 @@ import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.Context +import android.content.res.ColorStateList import android.graphics.RenderEffect import android.graphics.Shader import android.graphics.drawable.Drawable @@ -14,7 +15,6 @@ import android.os.Build import android.os.Bundle import android.text.method.ScrollingMovementMethod import android.view.GestureDetector -import android.view.GestureDetector.SimpleOnGestureListener import android.view.MotionEvent import android.view.ScaleGestureDetector import android.view.View @@ -24,6 +24,7 @@ import android.widget.TextView import androidx.cardview.widget.CardView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.core.view.GestureDetectorCompat import androidx.core.view.animation.PathInterpolatorCompat @@ -32,9 +33,12 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.google.android.material.button.MaterialButton +import com.google.android.material.progressindicator.CircularProgressIndicatorSpec +import com.google.android.material.progressindicator.IndeterminateDrawable import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import org.signal.core.util.DimensionUnit +import org.signal.core.util.dp import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.animation.AnimationCompleteListener @@ -44,6 +48,7 @@ import org.thoughtcrime.securesms.components.segmentedprogressbar.SegmentedProgr import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto20dp import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto +import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey import org.thoughtcrime.securesms.conversation.ConversationIntents import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardBottomSheet @@ -57,6 +62,7 @@ import org.thoughtcrime.securesms.mediapreview.VideoControlsDelegate import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet import org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView import org.thoughtcrime.securesms.stories.StorySlateView import org.thoughtcrime.securesms.stories.StoryVolumeOverlayView @@ -94,7 +100,8 @@ class StoryViewerPageFragment : StorySlateView.Callback, StoryTextPostPreviewFragment.Callback, StoryFirstTimeNavigationView.Callback, - StoryInfoBottomSheetDialogFragment.OnInfoSheetDismissedListener { + StoryInfoBottomSheetDialogFragment.OnInfoSheetDismissedListener, + SafetyNumberBottomSheet.Callbacks { private val storyVolumeViewModel: StoryVolumeViewModel by viewModels(ownerProducer = { requireActivity() }) @@ -140,6 +147,8 @@ class StoryViewerPageFragment : private val lifecycleDisposable = LifecycleDisposable() private val timeoutDisposable = LifecycleDisposable() + private var sendingProgressDrawable: IndeterminateDrawable? = null + private val storyRecipientId: RecipientId get() = requireArguments().getParcelable(ARG_STORY_RECIPIENT_ID)!! @@ -374,7 +383,7 @@ class StoryViewerPageFragment : if (state.posts.isNotEmpty() && state.selectedPostIndex in state.posts.indices) { val post = state.posts[state.selectedPostIndex] - presentViewsAndReplies(post, state.replyState, state.isReceiptsEnabled) + presentBottomBar(post, state.replyState, state.isReceiptsEnabled) presentSenderAvatar(senderAvatar, post) presentGroupAvatar(groupAvatar, post) presentFrom(from, post) @@ -649,6 +658,15 @@ class StoryViewerPageFragment : isFromNotification, groupReplyStartPosition ) + StoryViewerPageState.ReplyState.PARTIAL_SEND -> { + handleResend(storyPost) + return + } + StoryViewerPageState.ReplyState.SEND_FAILURE -> { + handleResend(storyPost) + return + } + StoryViewerPageState.ReplyState.SENDING -> return } if (viewModel.getSwipeToReplyState() == StoryViewerPageState.ReplyState.PRIVATE) { @@ -660,6 +678,19 @@ class StoryViewerPageFragment : replyFragment.showNow(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) } + private fun handleResend(storyPost: StoryPost) { + viewModel.setIsDisplayingPartialSendDialog(true) + if (storyPost.conversationMessage.messageRecord.isIdentityMismatchFailure) { + SafetyNumberBottomSheet + .forMessageRecord(requireContext(), storyPost.conversationMessage.messageRecord) + .show(childFragmentManager) + } else { + StoryDialogs.resendStory(requireContext(), { viewModel.setIsDisplayingPartialSendDialog(false) }) { + lifecycleDisposable += viewModel.resend(storyPost).subscribe() + } + } + } + private fun showInfo(storyPost: StoryPost) { viewModel.setIsDisplayingInfoDialog(true) StoryInfoBottomSheetDialogFragment.create(storyPost.id).show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) @@ -889,7 +920,7 @@ class StoryViewerPageFragment : } } - private fun presentViewsAndReplies(post: StoryPost, replyState: StoryViewerPageState.ReplyState, isReceiptsEnabled: Boolean) { + private fun presentBottomBar(post: StoryPost, replyState: StoryViewerPageState.ReplyState, isReceiptsEnabled: Boolean) { if (replyState == StoryViewerPageState.ReplyState.NONE) { viewsAndReplies.visible = false return @@ -897,6 +928,51 @@ class StoryViewerPageFragment : viewsAndReplies.visible = true } + viewsAndReplies.iconTint = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurface)) + + when (replyState) { + StoryViewerPageState.ReplyState.SENDING -> presentSendingBottomBar() + StoryViewerPageState.ReplyState.PARTIAL_SEND -> presentPartialSendBottomBar() + StoryViewerPageState.ReplyState.SEND_FAILURE -> presentSendFailureBottomBar() + else -> presentViewsAndRepliesBottomBar(post, isReceiptsEnabled) + } + } + + private fun presentSendingBottomBar() { + if (sendingProgressDrawable == null) { + sendingProgressDrawable = IndeterminateDrawable.createCircularDrawable( + requireContext(), + CircularProgressIndicatorSpec(requireContext(), null).apply { + indicatorSize = 18.dp + indicatorInset = 2.dp + trackColor = ContextCompat.getColor(requireContext(), R.color.transparent_white_40) + indicatorColors = intArrayOf(ContextCompat.getColor(requireContext(), R.color.signal_dark_colorNeutralInverse)) + trackThickness = 2.dp + } + ) + } + + viewsAndReplies.icon = sendingProgressDrawable + viewsAndReplies.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START + viewsAndReplies.iconSize = 20.dp + viewsAndReplies.setText(R.string.StoriesLandingItem__sending) + } + + private fun presentPartialSendBottomBar() { + viewsAndReplies.setIconResource(R.drawable.ic_error_outline_24) + viewsAndReplies.iconTint = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_light_colorError)) + viewsAndReplies.iconSize = 20.dp + viewsAndReplies.setText(R.string.StoryViewerPageFragment__partially_sent) + } + + private fun presentSendFailureBottomBar() { + viewsAndReplies.setIconResource(R.drawable.ic_error_outline_24) + viewsAndReplies.iconTint = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_light_colorError)) + viewsAndReplies.iconSize = 20.dp + viewsAndReplies.setText(R.string.StoryViewerPageFragment__send_failed) + } + + private fun presentViewsAndRepliesBottomBar(post: StoryPost, isReceiptsEnabled: Boolean) { val views = resources.getQuantityString(R.plurals.StoryViewerFragment__d_views, post.viewCount, post.viewCount) val replies = resources.getQuantityString(R.plurals.StoryViewerFragment__d_replies, post.replyCount, post.replyCount) @@ -1280,4 +1356,16 @@ class StoryViewerPageFragment : override fun onInfoSheetDismissed() { viewModel.setIsDisplayingInfoDialog(false) } + + override fun sendAnywayAfterSafetyNumberChangedInBottomSheet(destinations: List) { + error("Not supported, we handed a message record to the bottom sheet.") + } + + override fun onMessageResentAfterSafetyNumberChangeInBottomSheet() { + viewModel.setIsDisplayingPartialSendDialog(false) + } + + override fun onCanceled() { + viewModel.setIsDisplayingPartialSendDialog(false) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt index 6934a8eb8a..e896b1cccd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.stories.viewer.page import android.content.Context import android.net.Uri +import androidx.annotation.CheckResult import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.schedulers.Schedulers @@ -22,6 +23,7 @@ import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.TextSecurePreferences @@ -194,6 +196,13 @@ open class StoryViewerPageRepository(context: Context) { } } + @CheckResult + fun resend(messageRecord: MessageRecord): Completable { + return Completable.fromAction { + MessageSender.resend(ApplicationDependencies.getApplication(), messageRecord) + }.subscribeOn(Schedulers.io()) + } + private fun getContent(record: MmsMessageRecord): StoryPost.Content { return if (record.storyType.isTextStory || record.slideDeck.asAttachments().isEmpty()) { StoryPost.Content.TextContent( diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageState.kt index a28a22bbdb..8c61ad11d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageState.kt @@ -36,7 +36,22 @@ data class StoryViewerPageState( /** * Story is from self and in a group */ - GROUP_SELF; + GROUP_SELF, + + /** + * Story was not sent to all recipients. + */ + PARTIAL_SEND, + + /** + * Story failed to send. + */ + SEND_FAILURE, + + /** + * Story is currently being sent. + */ + SENDING; companion object { fun resolve(isFromSelf: Boolean, isToGroup: Boolean): ReplyState { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt index 911fc0571c..2aed5a59ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt @@ -1,8 +1,10 @@ package org.thoughtcrime.securesms.stories.viewer.page +import androidx.annotation.CheckResult import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Observable @@ -266,16 +268,27 @@ class StoryViewerPageViewModel( storyViewerPlaybackStore.update { it.copy(isUserScaling = isUserScaling) } } + fun setIsDisplayingPartialSendDialog(isDisplayingPartialSendDialog: Boolean) { + storyViewerPlaybackStore.update { it.copy(isDisplayingPartialSendDialog = isDisplayingPartialSendDialog) } + } + private fun resolveSwipeToReplyState(state: StoryViewerPageState, index: Int): StoryViewerPageState.ReplyState { if (index !in state.posts.indices) { return StoryViewerPageState.ReplyState.NONE } val post = state.posts[index] + val message = post.conversationMessage.messageRecord val isFromSelf = post.sender.isSelf val isToGroup = post.group != null + val isFailed = message.isFailed + val isPartialSend = message.isIdentityMismatchFailure + val isInProgress = !post.conversationMessage.messageRecord.isSent return when { + isFromSelf && isPartialSend -> StoryViewerPageState.ReplyState.PARTIAL_SEND + isFromSelf && isFailed -> StoryViewerPageState.ReplyState.SEND_FAILURE + isFromSelf && isInProgress -> StoryViewerPageState.ReplyState.SENDING post.allowsReplies -> StoryViewerPageState.ReplyState.resolve(isFromSelf, isToGroup) isFromSelf -> StoryViewerPageState.ReplyState.SELF else -> StoryViewerPageState.ReplyState.NONE @@ -290,6 +303,13 @@ class StoryViewerPageViewModel( return store.state.posts.getOrNull(index) } + @CheckResult + fun resend(storyPost: StoryPost): Completable { + return repository + .resend(storyPost.conversationMessage.messageRecord) + .observeOn(AndroidSchedulers.mainThread()) + } + class Factory( private val recipientId: RecipientId, private val initialStoryId: Long, diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPlaybackState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPlaybackState.kt index 2bde478664..321367bcb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPlaybackState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPlaybackState.kt @@ -21,7 +21,8 @@ data class StoryViewerPlaybackState( val isDisplayingInfoDialog: Boolean = false, val isUserLongTouching: Boolean = false, val isUserScrollingChild: Boolean = false, - val isUserScaling: Boolean = false + val isUserScaling: Boolean = false, + val isDisplayingPartialSendDialog: Boolean = false ) { val hideChromeImmediate: Boolean = isRunningSharedElementAnimation @@ -49,5 +50,6 @@ data class StoryViewerPlaybackState( isDisplayingFirstTimeNavigation || isDisplayingInfoDialog || isUserScaling || - isDisplayingHideDialog + isDisplayingHideDialog || + isDisplayingPartialSendDialog } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 249b49a298..fcf4f9fa56 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4756,6 +4756,10 @@ %1$s to %2$s Reply + + Partially sent. Tap for details + + Send failed. Tap to retry Reply to group @@ -5162,6 +5166,8 @@ Sent to Sent from + + Failed Info diff --git a/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModelTest.kt index 60c25fb0d6..7b234e0474 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModelTest.kt @@ -14,6 +14,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import org.thoughtcrime.securesms.database.FakeMessageRecords import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId @@ -186,7 +187,10 @@ class StoryViewerPageViewModelTest { conversationMessage = mock(), allowsReplies = true, hasSelfViewed = isViewed(it) - ) + ).apply { + val messageRecord = FakeMessageRecords.buildMediaMmsMessageRecord() + whenever(conversationMessage.messageRecord).thenReturn(messageRecord) + } } } }