mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-25 20:23:19 +00:00
Add story send multi-send, error, and improved SNC states.
This commit is contained in:
committed by
Greyson Parrelli
parent
7f2f5a182f
commit
2f0f26c328
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user