Add story send multi-send, error, and improved SNC states.
This commit is contained in:
parent
7f2f5a182f
commit
2f0f26c328
13 changed files with 117 additions and 35 deletions
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) ?: "")
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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 & View Stories</string>
|
||||
<!-- Privacy Settings toggle summary for stories -->
|
||||
|
|
Loading…
Add table
Reference in a new issue