From 20417565139583a22aa59d525fd300ee100374ef Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 15:44:48 -0300 Subject: [PATCH] Story info page should mirror message details. --- .../messagedetails/MessageDetails.java | 18 ++--- .../MessageDetailsRepository.java | 31 +++++++- .../RecipientDeliveryStatus.java | 16 ++-- .../StoryInfoBottomSheetDialogFragment.kt | 64 +++++++++++++--- .../viewer/info/StoryInfoRecipientRow.kt | 18 ++--- .../viewer/info/StoryInfoRepository.kt | 73 ------------------- .../stories/viewer/info/StoryInfoState.kt | 22 +++--- .../stories/viewer/info/StoryInfoViewModel.kt | 54 ++------------ 8 files changed, 122 insertions(+), 174 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRepository.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java index cb8b30bee..03d141fac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java @@ -12,7 +12,7 @@ import java.util.Comparator; import java.util.List; import java.util.TreeSet; -final class MessageDetails { +public final class MessageDetails { private static final Comparator HAS_DISPLAY_NAME = (r1, r2) -> Boolean.compare(r2.getRecipient().hasAUserSetDisplayName(ApplicationDependencies.getApplication()), r1.getRecipient().hasAUserSetDisplayName(ApplicationDependencies.getApplication())); private static final Comparator ALPHABETICAL = (r1, r2) -> r1.getRecipient().getDisplayName(ApplicationDependencies.getApplication()).compareToIgnoreCase(r2.getRecipient().getDisplayName(ApplicationDependencies.getApplication())); private static final Comparator RECIPIENT_COMPARATOR = ComparatorCompat.chain(HAS_DISPLAY_NAME).thenComparing(ALPHABETICAL); @@ -71,33 +71,33 @@ final class MessageDetails { } } - @NonNull ConversationMessage getConversationMessage() { + public @NonNull ConversationMessage getConversationMessage() { return conversationMessage; } - @NonNull Collection getPending() { + public @NonNull Collection getPending() { return pending; } - @NonNull Collection getSent() { + public @NonNull Collection getSent() { return sent; } - @NonNull Collection getSkipped() {return skipped;} + public @NonNull Collection getSkipped() {return skipped;} - @NonNull Collection getDelivered() { + public @NonNull Collection getDelivered() { return delivered; } - @NonNull Collection getRead() { + public @NonNull Collection getRead() { return read; } - @NonNull Collection getNotSent() { + public @NonNull Collection getNotSent() { return notSent; } - @NonNull Collection getViewed() { + public @NonNull Collection getViewed() { return viewed; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java index 8d92baf1c..60efc0c5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java @@ -10,9 +10,11 @@ import androidx.lifecycle.MutableLiveData; import org.signal.core.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory; +import org.thoughtcrime.securesms.database.DatabaseObserver; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupReceiptDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase; +import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.NetworkFailure; @@ -24,7 +26,10 @@ import org.thoughtcrime.securesms.recipients.Recipient; import java.util.LinkedList; import java.util.List; -final class MessageDetailsRepository { +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public final class MessageDetailsRepository { private final Context context = ApplicationDependencies.getApplication(); @@ -44,11 +49,33 @@ final class MessageDetailsRepository { return liveData; } + public @NonNull Observable getMessageDetails(@NonNull MessageId messageId) { + return Observable.create(emitter -> { + DatabaseObserver.MessageObserver messageObserver = mId -> { + try { + MessageRecord messageRecord = messageId.isMms() ? SignalDatabase.mms().getMessageRecord(messageId.getId()) + : SignalDatabase.sms().getMessageRecord(messageId.getId()); + + MessageDetails messageDetails = getRecipientDeliveryStatusesInternal(messageRecord); + + emitter.onNext(messageDetails); + } catch (NoSuchMessageException e) { + emitter.onError(e); + } + }; + + ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageObserver); + emitter.setCancellable(() -> ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver)); + + messageObserver.onMessageChanged(messageId); + }).observeOn(Schedulers.io()); + } + @WorkerThread private @NonNull MessageDetails getRecipientDeliveryStatusesInternal(@NonNull MessageRecord messageRecord) { List recipients = new LinkedList<>(); - if (!messageRecord.getRecipient().isGroup()) { + if (!messageRecord.getRecipient().isGroup() && !messageRecord.getRecipient().isDistributionList()) { recipients.add(new RecipientDeliveryStatus(messageRecord, messageRecord.getRecipient(), getStatusFor(messageRecord), diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientDeliveryStatus.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientDeliveryStatus.java index 1f2d9e247..baee7b158 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientDeliveryStatus.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientDeliveryStatus.java @@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.recipients.Recipient; -final class RecipientDeliveryStatus { +public final class RecipientDeliveryStatus { enum Status { UNKNOWN, PENDING, SENT, DELIVERED, READ, VIEWED, SKIPPED, @@ -32,31 +32,31 @@ final class RecipientDeliveryStatus { this.keyMismatchFailure = keyMismatchFailure; } - @NonNull MessageRecord getMessageRecord() { + public @NonNull MessageRecord getMessageRecord() { return messageRecord; } - @NonNull Status getDeliveryStatus() { + public @NonNull Status getDeliveryStatus() { return deliveryStatus; } - boolean isUnidentified() { + public boolean isUnidentified() { return isUnidentified; } - long getTimestamp() { + public long getTimestamp() { return timestamp; } - @NonNull Recipient getRecipient() { + public @NonNull Recipient getRecipient() { return recipient; } - @Nullable NetworkFailure getNetworkFailure() { + public @Nullable NetworkFailure getNetworkFailure() { return networkFailure; } - @Nullable IdentityKeyMismatch getKeyMismatchFailure() { + public @Nullable IdentityKeyMismatch getKeyMismatchFailure() { return keyMismatchFailure; } } 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 3b9b276a2..1f26a14a3 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 @@ -1,12 +1,14 @@ package org.thoughtcrime.securesms.stories.viewer.info import android.content.DialogInterface +import androidx.annotation.StringRes import androidx.core.os.bundleOf import androidx.fragment.app.viewModels import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment +import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.fragments.findListener @@ -58,23 +60,61 @@ class StoryInfoBottomSheetDialogFragment : DSLSettingsBottomSheetFragment() { ) ) - state.sections.map { (section, recipients) -> - renderSection(section, recipients) + val details = state.messageDetails!! + + if (state.isOutgoing) { + renderSection( + title = R.string.message_details_recipient_header__not_sent, + recipients = details.notSent.map { StoryInfoRecipientRow.Model(it) } + ) + + renderSection( + title = R.string.message_details_recipient_header__viewed, + recipients = details.viewed.map { StoryInfoRecipientRow.Model(it) } + ) + + renderSection( + title = R.string.message_details_recipient_header__read_by, + recipients = details.read.map { StoryInfoRecipientRow.Model(it) } + ) + + renderSection( + title = R.string.message_details_recipient_header__delivered_to, + recipients = details.delivered.map { StoryInfoRecipientRow.Model(it) } + ) + + renderSection( + title = R.string.message_details_recipient_header__sent_to, + recipients = details.sent.map { StoryInfoRecipientRow.Model(it) } + ) + + renderSection( + title = R.string.message_details_recipient_header__pending_send, + recipients = details.pending.map { StoryInfoRecipientRow.Model(it) } + ) + + renderSection( + title = R.string.message_details_recipient_header__skipped, + recipients = details.skipped.map { StoryInfoRecipientRow.Model(it) } + ) + } else { + renderSection( + title = R.string.message_details_recipient_header__sent_from, + recipients = details.sent.map { StoryInfoRecipientRow.Model(it) } + ) } } } - 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 - } - ) + private fun DSLConfiguration.renderSection(@StringRes title: Int, recipients: List) { + if (recipients.isNotEmpty()) { + sectionHeaderPref( + title = DSLSettingsText.from(title) + ) - recipients.forEach { - customPref(it) + recipients.forEach { + customPref(it) + } } } 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 00950398c..0ca8615c0 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 @@ -4,7 +4,7 @@ import android.view.View import android.widget.TextView import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.AvatarImageView -import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.messagedetails.RecipientDeliveryStatus import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter @@ -21,17 +21,15 @@ object StoryInfoRecipientRow { } class Model( - val recipient: Recipient, - val date: Long, - val status: Int, - val isFailed: Boolean + val recipientDeliveryStatus: RecipientDeliveryStatus ) : MappingModel { override fun areItemsTheSame(newItem: Model): Boolean { - return recipient.id == newItem.recipient.id + return recipientDeliveryStatus.recipient.id == newItem.recipientDeliveryStatus.recipient.id } override fun areContentsTheSame(newItem: Model): Boolean { - return recipient.hasSameContent(newItem.recipient) && date == newItem.date + return recipientDeliveryStatus.recipient.hasSameContent(newItem.recipientDeliveryStatus.recipient) && + recipientDeliveryStatus.timestamp == newItem.recipientDeliveryStatus.timestamp } } @@ -42,9 +40,9 @@ object StoryInfoRecipientRow { private val timestampView: TextView = itemView.findViewById(R.id.story_info_timestamp) override fun bind(model: Model) { - avatarView.setRecipient(model.recipient) - nameView.text = model.recipient.getDisplayName(context) - timestampView.text = DateUtils.getTimeString(context, Locale.getDefault(), model.date) + avatarView.setRecipient(model.recipientDeliveryStatus.recipient) + nameView.text = model.recipientDeliveryStatus.recipient.getDisplayName(context) + timestampView.text = DateUtils.getTimeString(context, Locale.getDefault(), model.recipientDeliveryStatus.timestamp) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRepository.kt deleted file mode 100644 index 2ad567eac..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRepository.kt +++ /dev/null @@ -1,73 +0,0 @@ -package org.thoughtcrime.securesms.stories.viewer.info - -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.database.DatabaseObserver -import org.thoughtcrime.securesms.database.GroupReceiptDatabase -import org.thoughtcrime.securesms.database.NoSuchMessageException -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.model.MessageRecord -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies - -/** - * Gathers necessary message record and receipt data for a given story id. - */ -class StoryInfoRepository { - - companion object { - private val TAG = Log.tag(StoryInfoRepository::class.java) - } - - /** - * Retrieves the StoryInfo for a given ID and emits a new item whenever the underlying - * message record changes. - */ - fun getStoryInfo(storyId: Long): Observable { - return observeMessageRecord(storyId) - .switchMap { record -> - getReceiptInfo(storyId).map { receiptInfo -> - StoryInfo(record, receiptInfo) - }.toObservable() - } - .subscribeOn(Schedulers.io()) - } - - private fun observeMessageRecord(storyId: Long): Observable { - return Observable.create { emitter -> - fun refresh() { - try { - emitter.onNext(SignalDatabase.mms.getMessageRecord(storyId)) - } catch (e: NoSuchMessageException) { - Log.w(TAG, "The story message disappeared. Terminating emission.") - emitter.onComplete() - } - } - - val observer = DatabaseObserver.MessageObserver { - if (it.mms && it.id == storyId) { - refresh() - } - } - - ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(observer) - emitter.setCancellable { - ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer) - } - - refresh() - } - } - - private fun getReceiptInfo(storyId: Long): Single> { - return Single.fromCallable { - SignalDatabase.groupReceipts.getGroupReceiptInfo(storyId) - } - } - - /** - * The message record and receipt info for a given story id. - */ - data class StoryInfo(val messageRecord: MessageRecord, val receiptInfo: List) -} 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 1d96a19dd..20cadbd71 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 @@ -1,19 +1,19 @@ package org.thoughtcrime.securesms.stories.viewer.info +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord +import org.thoughtcrime.securesms.messagedetails.MessageDetails + /** * Contains the needed information to render the story info sheet. */ data class StoryInfoState( - val sentMillis: Long = -1L, - val receivedMillis: Long = -1L, - val size: Long = -1L, - val isOutgoing: Boolean = false, - val sections: Map> = emptyMap(), - val isLoaded: Boolean = false + val messageDetails: MessageDetails? = null ) { - enum class SectionKey { - FAILED, - SENT_TO, - SENT_FROM - } + private val mediaMessage = messageDetails?.conversationMessage?.messageRecord as? MediaMmsMessageRecord + + val sentMillis: Long = mediaMessage?.dateSent ?: -1L + val receivedMillis: Long = mediaMessage?.dateReceived ?: -1L + val size: Long = mediaMessage?.slideDeck?.thumbnailSlide?.fileSize ?: 0 + val isOutgoing: Boolean = mediaMessage?.isOutgoing ?: false + val isLoaded: Boolean = mediaMessage != null } 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 1ed2e043a..22ccc80a1 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,17 +7,14 @@ 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.database.model.MessageId +import org.thoughtcrime.securesms.messagedetails.MessageDetailsRepository import org.thoughtcrime.securesms.util.rx.RxStore /** * Gathers and stores the StoryInfoState which is used to render the story info sheet. */ -class StoryInfoViewModel(storyId: Long, repository: StoryInfoRepository = StoryInfoRepository()) : ViewModel() { +class StoryInfoViewModel(storyId: Long, repository: MessageDetailsRepository = MessageDetailsRepository()) : ViewModel() { private val store = RxStore(StoryInfoState()) private val disposables = CompositeDisposable() @@ -25,54 +22,13 @@ class StoryInfoViewModel(storyId: Long, repository: StoryInfoRepository = StoryI val state: Flowable = store.stateFlowable.observeOn(AndroidSchedulers.mainThread()) init { - disposables += store.update(repository.getStoryInfo(storyId).toFlowable(BackpressureStrategy.LATEST)) { storyInfo, storyInfoState -> + disposables += store.update(repository.getMessageDetails(MessageId(storyId, true)).toFlowable(BackpressureStrategy.LATEST)) { messageDetails, storyInfoState -> storyInfoState.copy( - isLoaded = true, - sentMillis = storyInfo.messageRecord.dateSent, - receivedMillis = storyInfo.messageRecord.dateReceived, - size = (storyInfo.messageRecord as? MmsMessageRecord)?.let { it.slideDeck.firstSlide?.fileSize } ?: -1L, - isOutgoing = storyInfo.messageRecord.isOutgoing, - sections = buildSections(storyInfo) + messageDetails = messageDetails ) } } - private fun buildSections(storyInfo: StoryInfoRepository.StoryInfo): Map> { - return if (storyInfo.messageRecord.isOutgoing) { - storyInfo.receiptInfo.map { groupReceiptInfo -> - StoryInfoRecipientRow.Model( - 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 { - 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() store.dispose()