Display failure state in story info and other places.

fork-5.53.8
Alex Hart 2022-09-21 16:57:20 -03:00 zatwierdzone przez Cody Henthorne
rodzic 25c0dc801f
commit ea3fb774f8
13 zmienionych plików z 233 dodań i 40 usunięć

Wyświetl plik

@ -10,13 +10,14 @@ import org.thoughtcrime.securesms.R
object StoryDialogs { object StoryDialogs {
fun resendStory(context: Context, resend: () -> Unit) { fun resendStory(context: Context, onDismiss: () -> Unit = {}, resend: () -> Unit) {
MaterialAlertDialogBuilder(context) MaterialAlertDialogBuilder(context)
.setMessage(R.string.StoryDialogs__story_could_not_be_sent) .setMessage(R.string.StoryDialogs__story_could_not_be_sent)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.StoryDialogs__send) { _, _ -> resend() } .setPositiveButton(R.string.StoryDialogs__send) { _, _ -> resend() }
.setOnDismissListener { onDismiss() }
.show() .show()
} }
fun displayStoryOrProfileImage( fun displayStoryOrProfileImage(
context: Context, context: Context,

Wyświetl plik

@ -72,9 +72,20 @@ object MyStoriesItem {
val oldRecord = distributionStory.messageRecord val oldRecord = distributionStory.messageRecord
val newRecord = newItem.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 && return oldRecord.isOutgoing &&
newRecord.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 date.visible = true
viewCount.setText(R.string.StoriesLandingItem__send_failed) viewCount.setText(R.string.StoriesLandingItem__send_failed)
date.setText(R.string.StoriesLandingItem__tap_to_retry) 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 { } else {
errorIndicator.visible = false errorIndicator.visible = false
date.visible = true date.visible = true

Wyświetl plik

@ -58,19 +58,25 @@ class StoryInfoBottomSheetDialogFragment : DSLSettingsBottomSheetFragment() {
) )
) )
state.sections.map { (section, recipients) ->
renderSection(section, recipients)
}
}
}
private fun DSLConfiguration.renderSection(sectionKey: StoryInfoState.SectionKey, recipients: List<StoryInfoRecipientRow.Model>) {
sectionHeaderPref( sectionHeaderPref(
title = if (state.isOutgoing) { title = when (sectionKey) {
R.string.StoryInfoBottomSheetDialogFragment__sent_to StoryInfoState.SectionKey.FAILED -> R.string.StoryInfoBottomSheetDialogFragment__failed
} else { StoryInfoState.SectionKey.SENT_TO -> R.string.StoryInfoBottomSheetDialogFragment__sent_to
R.string.StoryInfoBottomSheetDialogFragment__sent_from StoryInfoState.SectionKey.SENT_FROM -> R.string.StoryInfoBottomSheetDialogFragment__sent_from
} }
) )
state.recipients.forEach { recipients.forEach {
customPref(it) customPref(it)
} }
} }
}
override fun onDismiss(dialog: DialogInterface) { override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog) super.onDismiss(dialog)

Wyświetl plik

@ -23,7 +23,8 @@ object StoryInfoRecipientRow {
class Model( class Model(
val recipient: Recipient, val recipient: Recipient,
val date: Long, val date: Long,
val status: Int val status: Int,
val isFailed: Boolean
) : MappingModel<Model> { ) : MappingModel<Model> {
override fun areItemsTheSame(newItem: Model): Boolean { override fun areItemsTheSame(newItem: Model): Boolean {
return recipient.id == newItem.recipient.id return recipient.id == newItem.recipient.id

Wyświetl plik

@ -8,6 +8,12 @@ data class StoryInfoState(
val receivedMillis: Long = -1L, val receivedMillis: Long = -1L,
val size: Long = -1L, val size: Long = -1L,
val isOutgoing: Boolean = false, val isOutgoing: Boolean = false,
val recipients: List<StoryInfoRecipientRow.Model> = emptyList(), val sections: Map<SectionKey, List<StoryInfoRecipientRow.Model>> = emptyMap(),
val isLoaded: Boolean = false val isLoaded: Boolean = false
) ) {
enum class SectionKey {
FAILED,
SENT_TO,
SENT_FROM
}
}

Wyświetl plik

@ -7,8 +7,11 @@ import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.rx.RxStore import org.thoughtcrime.securesms.util.rx.RxStore
/** /**
@ -29,31 +32,47 @@ class StoryInfoViewModel(storyId: Long, repository: StoryInfoRepository = StoryI
receivedMillis = storyInfo.messageRecord.dateReceived, receivedMillis = storyInfo.messageRecord.dateReceived,
size = (storyInfo.messageRecord as? MmsMessageRecord)?.let { it.slideDeck.firstSlide?.fileSize } ?: -1L, size = (storyInfo.messageRecord as? MmsMessageRecord)?.let { it.slideDeck.firstSlide?.fileSize } ?: -1L,
isOutgoing = storyInfo.messageRecord.isOutgoing, isOutgoing = storyInfo.messageRecord.isOutgoing,
recipients = buildRecipients(storyInfo) sections = buildSections(storyInfo)
) )
} }
} }
private fun buildRecipients(storyInfo: StoryInfoRepository.StoryInfo): List<StoryInfoRecipientRow.Model> { private fun buildSections(storyInfo: StoryInfoRepository.StoryInfo): Map<StoryInfoState.SectionKey, List<StoryInfoRecipientRow.Model>> {
return if (storyInfo.messageRecord.isOutgoing) { return if (storyInfo.messageRecord.isOutgoing) {
storyInfo.receiptInfo.map { storyInfo.receiptInfo.map { groupReceiptInfo ->
StoryInfoRecipientRow.Model( StoryInfoRecipientRow.Model(
recipient = Recipient.resolved(it.recipientId), recipient = Recipient.resolved(groupReceiptInfo.recipientId),
date = it.timestamp, date = groupReceiptInfo.timestamp,
status = it.status status = groupReceiptInfo.status,
isFailed = hasFailure(storyInfo.messageRecord, groupReceiptInfo.recipientId)
) )
}.groupBy {
when {
it.isFailed -> StoryInfoState.SectionKey.FAILED
else -> StoryInfoState.SectionKey.SENT_TO
}
} }
} else { } else {
listOf( mapOf(
StoryInfoState.SectionKey.SENT_FROM to listOf(
StoryInfoRecipientRow.Model( StoryInfoRecipientRow.Model(
recipient = storyInfo.messageRecord.individualRecipient, recipient = storyInfo.messageRecord.individualRecipient,
date = storyInfo.messageRecord.dateSent, date = storyInfo.messageRecord.dateSent,
status = -1 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() { override fun onCleared() {
disposables.clear() disposables.clear()
} }

Wyświetl plik

@ -5,6 +5,7 @@ import android.animation.AnimatorSet
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.graphics.RenderEffect import android.graphics.RenderEffect
import android.graphics.Shader import android.graphics.Shader
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
@ -14,7 +15,6 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.method.ScrollingMovementMethod import android.text.method.ScrollingMovementMethod
import android.view.GestureDetector import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ScaleGestureDetector import android.view.ScaleGestureDetector
import android.view.View import android.view.View
@ -24,6 +24,7 @@ import android.widget.TextView
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.GestureDetectorCompat import androidx.core.view.GestureDetectorCompat
import androidx.core.view.animation.PathInterpolatorCompat import androidx.core.view.animation.PathInterpolatorCompat
@ -32,9 +33,12 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import com.google.android.material.button.MaterialButton 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.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
import org.signal.core.util.DimensionUnit import org.signal.core.util.DimensionUnit
import org.signal.core.util.dp
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.animation.AnimationCompleteListener 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.FallbackContactPhoto
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto20dp import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto20dp
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto 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.ConversationIntents
import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardBottomSheet 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.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
import org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView import org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView
import org.thoughtcrime.securesms.stories.StorySlateView import org.thoughtcrime.securesms.stories.StorySlateView
import org.thoughtcrime.securesms.stories.StoryVolumeOverlayView import org.thoughtcrime.securesms.stories.StoryVolumeOverlayView
@ -94,7 +100,8 @@ class StoryViewerPageFragment :
StorySlateView.Callback, StorySlateView.Callback,
StoryTextPostPreviewFragment.Callback, StoryTextPostPreviewFragment.Callback,
StoryFirstTimeNavigationView.Callback, StoryFirstTimeNavigationView.Callback,
StoryInfoBottomSheetDialogFragment.OnInfoSheetDismissedListener { StoryInfoBottomSheetDialogFragment.OnInfoSheetDismissedListener,
SafetyNumberBottomSheet.Callbacks {
private val storyVolumeViewModel: StoryVolumeViewModel by viewModels(ownerProducer = { requireActivity() }) private val storyVolumeViewModel: StoryVolumeViewModel by viewModels(ownerProducer = { requireActivity() })
@ -140,6 +147,8 @@ class StoryViewerPageFragment :
private val lifecycleDisposable = LifecycleDisposable() private val lifecycleDisposable = LifecycleDisposable()
private val timeoutDisposable = LifecycleDisposable() private val timeoutDisposable = LifecycleDisposable()
private var sendingProgressDrawable: IndeterminateDrawable<CircularProgressIndicatorSpec>? = null
private val storyRecipientId: RecipientId private val storyRecipientId: RecipientId
get() = requireArguments().getParcelable(ARG_STORY_RECIPIENT_ID)!! get() = requireArguments().getParcelable(ARG_STORY_RECIPIENT_ID)!!
@ -374,7 +383,7 @@ class StoryViewerPageFragment :
if (state.posts.isNotEmpty() && state.selectedPostIndex in state.posts.indices) { if (state.posts.isNotEmpty() && state.selectedPostIndex in state.posts.indices) {
val post = state.posts[state.selectedPostIndex] val post = state.posts[state.selectedPostIndex]
presentViewsAndReplies(post, state.replyState, state.isReceiptsEnabled) presentBottomBar(post, state.replyState, state.isReceiptsEnabled)
presentSenderAvatar(senderAvatar, post) presentSenderAvatar(senderAvatar, post)
presentGroupAvatar(groupAvatar, post) presentGroupAvatar(groupAvatar, post)
presentFrom(from, post) presentFrom(from, post)
@ -649,6 +658,15 @@ class StoryViewerPageFragment :
isFromNotification, isFromNotification,
groupReplyStartPosition 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) { if (viewModel.getSwipeToReplyState() == StoryViewerPageState.ReplyState.PRIVATE) {
@ -660,6 +678,19 @@ class StoryViewerPageFragment :
replyFragment.showNow(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) 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) { private fun showInfo(storyPost: StoryPost) {
viewModel.setIsDisplayingInfoDialog(true) viewModel.setIsDisplayingInfoDialog(true)
StoryInfoBottomSheetDialogFragment.create(storyPost.id).show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) 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) { if (replyState == StoryViewerPageState.ReplyState.NONE) {
viewsAndReplies.visible = false viewsAndReplies.visible = false
return return
@ -897,6 +928,51 @@ class StoryViewerPageFragment :
viewsAndReplies.visible = true 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 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) val replies = resources.getQuantityString(R.plurals.StoryViewerFragment__d_replies, post.replyCount, post.replyCount)
@ -1280,4 +1356,16 @@ class StoryViewerPageFragment :
override fun onInfoSheetDismissed() { override fun onInfoSheetDismissed() {
viewModel.setIsDisplayingInfoDialog(false) viewModel.setIsDisplayingInfoDialog(false)
} }
override fun sendAnywayAfterSafetyNumberChangedInBottomSheet(destinations: List<ContactSearchKey.RecipientSearchKey>) {
error("Not supported, we handed a message record to the bottom sheet.")
}
override fun onMessageResentAfterSafetyNumberChangeInBottomSheet() {
viewModel.setIsDisplayingPartialSendDialog(false)
}
override fun onCanceled() {
viewModel.setIsDisplayingPartialSendDialog(false)
}
} }

Wyświetl plik

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.stories.viewer.page
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.annotation.CheckResult
import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.schedulers.Schedulers 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.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.TextSecurePreferences 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 { private fun getContent(record: MmsMessageRecord): StoryPost.Content {
return if (record.storyType.isTextStory || record.slideDeck.asAttachments().isEmpty()) { return if (record.storyType.isTextStory || record.slideDeck.asAttachments().isEmpty()) {
StoryPost.Content.TextContent( StoryPost.Content.TextContent(

Wyświetl plik

@ -36,7 +36,22 @@ data class StoryViewerPageState(
/** /**
* Story is from self and in a group * 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 { companion object {
fun resolve(isFromSelf: Boolean, isToGroup: Boolean): ReplyState { fun resolve(isFromSelf: Boolean, isToGroup: Boolean): ReplyState {

Wyświetl plik

@ -1,8 +1,10 @@
package org.thoughtcrime.securesms.stories.viewer.page package org.thoughtcrime.securesms.stories.viewer.page
import androidx.annotation.CheckResult
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
@ -266,16 +268,27 @@ class StoryViewerPageViewModel(
storyViewerPlaybackStore.update { it.copy(isUserScaling = isUserScaling) } 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 { private fun resolveSwipeToReplyState(state: StoryViewerPageState, index: Int): StoryViewerPageState.ReplyState {
if (index !in state.posts.indices) { if (index !in state.posts.indices) {
return StoryViewerPageState.ReplyState.NONE return StoryViewerPageState.ReplyState.NONE
} }
val post = state.posts[index] val post = state.posts[index]
val message = post.conversationMessage.messageRecord
val isFromSelf = post.sender.isSelf val isFromSelf = post.sender.isSelf
val isToGroup = post.group != null val isToGroup = post.group != null
val isFailed = message.isFailed
val isPartialSend = message.isIdentityMismatchFailure
val isInProgress = !post.conversationMessage.messageRecord.isSent
return when { 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) post.allowsReplies -> StoryViewerPageState.ReplyState.resolve(isFromSelf, isToGroup)
isFromSelf -> StoryViewerPageState.ReplyState.SELF isFromSelf -> StoryViewerPageState.ReplyState.SELF
else -> StoryViewerPageState.ReplyState.NONE else -> StoryViewerPageState.ReplyState.NONE
@ -290,6 +303,13 @@ class StoryViewerPageViewModel(
return store.state.posts.getOrNull(index) return store.state.posts.getOrNull(index)
} }
@CheckResult
fun resend(storyPost: StoryPost): Completable {
return repository
.resend(storyPost.conversationMessage.messageRecord)
.observeOn(AndroidSchedulers.mainThread())
}
class Factory( class Factory(
private val recipientId: RecipientId, private val recipientId: RecipientId,
private val initialStoryId: Long, private val initialStoryId: Long,

Wyświetl plik

@ -21,7 +21,8 @@ data class StoryViewerPlaybackState(
val isDisplayingInfoDialog: Boolean = false, val isDisplayingInfoDialog: Boolean = false,
val isUserLongTouching: Boolean = false, val isUserLongTouching: Boolean = false,
val isUserScrollingChild: Boolean = false, val isUserScrollingChild: Boolean = false,
val isUserScaling: Boolean = false val isUserScaling: Boolean = false,
val isDisplayingPartialSendDialog: Boolean = false
) { ) {
val hideChromeImmediate: Boolean = isRunningSharedElementAnimation val hideChromeImmediate: Boolean = isRunningSharedElementAnimation
@ -49,5 +50,6 @@ data class StoryViewerPlaybackState(
isDisplayingFirstTimeNavigation || isDisplayingFirstTimeNavigation ||
isDisplayingInfoDialog || isDisplayingInfoDialog ||
isUserScaling || isUserScaling ||
isDisplayingHideDialog isDisplayingHideDialog ||
isDisplayingPartialSendDialog
} }

Wyświetl plik

@ -4756,6 +4756,10 @@
<string name="StoryViewerPageFragment__s_to_s">%1$s to %2$s</string> <string name="StoryViewerPageFragment__s_to_s">%1$s to %2$s</string>
<!-- Displayed when viewing a post from another user with no replies --> <!-- Displayed when viewing a post from another user with no replies -->
<string name="StoryViewerPageFragment__reply">Reply</string> <string name="StoryViewerPageFragment__reply">Reply</string>
<!-- Displayed when viewing a post that has failed to send to some users -->
<string name="StoryViewerPageFragment__partially_sent">Partially sent. Tap for details</string>
<!-- Displayed when viewing a post that has failed to send -->
<string name="StoryViewerPageFragment__send_failed">Send failed. Tap to retry</string>
<!-- Label for the reply button in story viewer, which will launch the group story replies bottom sheet. --> <!-- Label for the reply button in story viewer, which will launch the group story replies bottom sheet. -->
<string name="StoryViewerPageFragment__reply_to_group">Reply to group</string> <string name="StoryViewerPageFragment__reply_to_group">Reply to group</string>
<!-- Displayed when a story has no views --> <!-- Displayed when a story has no views -->
@ -5162,6 +5166,8 @@
<string name="StoryInfoBottomSheetDialogFragment__sent_to">Sent to</string> <string name="StoryInfoBottomSheetDialogFragment__sent_to">Sent to</string>
<!-- Story info "Sent from" header --> <!-- Story info "Sent from" header -->
<string name="StoryInfoBottomSheetDialogFragment__sent_from">Sent from</string> <string name="StoryInfoBottomSheetDialogFragment__sent_from">Sent from</string>
<!-- Story info "Failed" header -->
<string name="StoryInfoBottomSheetDialogFragment__failed">Failed</string>
<!-- Story Info context menu label --> <!-- Story Info context menu label -->
<string name="StoryInfoBottomSheetDialogFragment__info">Info</string> <string name="StoryInfoBottomSheetDialogFragment__info">Info</string>

Wyświetl plik

@ -14,6 +14,7 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.database.FakeMessageRecords
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
@ -186,7 +187,10 @@ class StoryViewerPageViewModelTest {
conversationMessage = mock(), conversationMessage = mock(),
allowsReplies = true, allowsReplies = true,
hasSelfViewed = isViewed(it) hasSelfViewed = isViewed(it)
) ).apply {
val messageRecord = FakeMessageRecords.buildMediaMmsMessageRecord()
whenever(conversationMessage.messageRecord).thenReturn(messageRecord)
}
} }
} }
} }