From 4bf8e2c48803c7b37817de76fc703b61750cde65 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Mon, 18 Sep 2023 08:47:09 -0400 Subject: [PATCH] Fix auto-update timestamps. --- .../conversation/ConversationMessage.java | 51 +++++++++++++------ .../conversation/v2/ConversationFragment.kt | 10 +++- .../ConversationMessageComputeWorkers.kt | 40 +++++++++++++++ .../conversation/v2/computed/FormattedDate.kt | 11 ++++ .../V2ConversationItemTextOnlyViewHolder.kt | 11 ++-- .../thoughtcrime/securesms/util/DateUtils.kt | 12 +++-- 6 files changed, 113 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/computed/ConversationMessageComputeWorkers.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/computed/FormattedDate.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java index 4228f037e3..1d3595c991 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java @@ -12,6 +12,7 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.components.mention.MentionAnnotation; import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect; import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection; +import org.thoughtcrime.securesms.conversation.v2.computed.FormattedDate; import org.thoughtcrime.securesms.database.BodyRangeUtil; import org.thoughtcrime.securesms.database.MentionUtil; import org.thoughtcrime.securesms.database.NoSuchMessageException; @@ -45,7 +46,7 @@ public class ConversationMessage { @NonNull private final Recipient threadRecipient; private final boolean hasBeenQuoted; @Nullable private final MessageRecord originalMessage; - @NonNull private final String formattedDate; + @NonNull private final ComputedProperties computedProperties; private ConversationMessage(@NonNull MessageRecord messageRecord, @Nullable CharSequence body, @@ -54,15 +55,15 @@ public class ConversationMessage { @Nullable MessageStyler.Result styleResult, @NonNull Recipient threadRecipient, @Nullable MessageRecord originalMessage, - @NonNull String formattedDate) + @NonNull ComputedProperties computedProperties) { - this.messageRecord = messageRecord; - this.hasBeenQuoted = hasBeenQuoted; - this.mentions = mentions != null ? mentions : Collections.emptyList(); - this.styleResult = styleResult != null ? styleResult : MessageStyler.Result.none(); - this.threadRecipient = threadRecipient; - this.originalMessage = originalMessage; - this.formattedDate = formattedDate; + this.messageRecord = messageRecord; + this.hasBeenQuoted = hasBeenQuoted; + this.mentions = mentions != null ? mentions : Collections.emptyList(); + this.styleResult = styleResult != null ? styleResult : MessageStyler.Result.none(); + this.threadRecipient = threadRecipient; + this.originalMessage = originalMessage; + this.computedProperties = computedProperties; if (body != null) { this.body = SpannableString.valueOf(body); @@ -95,9 +96,8 @@ public class ConversationMessage { return hasBeenQuoted; } - @NonNull - public String getFormattedDate() { - return formattedDate; + public @NonNull ComputedProperties getComputedProperties() { + return computedProperties; } @Override @@ -160,6 +160,27 @@ public class ConversationMessage { return threadRecipient; } + public static @NonNull FormattedDate getFormattedDate(@NonNull Context context, @NonNull MessageRecord messageRecord) { + return MessageRecordUtil.isScheduled(messageRecord) ? new FormattedDate(false, DateUtils.getOnlyTimeString(context, Locale.getDefault(), ((MediaMmsMessageRecord) messageRecord).getScheduledDate())) + : DateUtils.getDatelessRelativeTimeSpanFormattedDate(context, Locale.getDefault(), messageRecord.getTimestamp()); + } + + public static class ComputedProperties { + private @NonNull FormattedDate formattedDate; + + ComputedProperties(@NonNull FormattedDate formattedDate) { + this.formattedDate = formattedDate; + } + + public synchronized FormattedDate getFormattedDate() { + return formattedDate; + } + + public synchronized void setFormattedDate(@NonNull FormattedDate formattedDate) { + this.formattedDate = formattedDate; + } + } + /** * Factory providing multiple ways of creating {@link ConversationMessage}s. */ @@ -204,8 +225,8 @@ public class ConversationMessage { } } - String formattedDate = MessageRecordUtil.isScheduled(messageRecord) ? DateUtils.getOnlyTimeString(context, Locale.getDefault(), ((MediaMmsMessageRecord) messageRecord).getScheduledDate()) - : DateUtils.getDatelessRelativeTimeSpanString(context, Locale.getDefault(), messageRecord.getTimestamp()); + FormattedDate formattedDate = getFormattedDate(context, messageRecord); + return new ConversationMessage(messageRecord, styledAndMentionBody != null ? styledAndMentionBody : mentionsUpdate != null ? mentionsUpdate.getBody() : body, mentionsUpdate != null ? mentionsUpdate.getMentions() : null, @@ -213,7 +234,7 @@ public class ConversationMessage { styleResult, threadRecipient, originalMessage, - formattedDate); + new ComputedProperties(formattedDate)); } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index a232fbcd22..30bde47e1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -183,6 +183,8 @@ import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryChanged import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryReplacement import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsControllerV2 import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModelV2 +import org.thoughtcrime.securesms.conversation.v2.computed.ConversationMessageComputeWorkers +import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupCallViewModel import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupViewModel import org.thoughtcrime.securesms.conversation.v2.items.ChatColorsDrawable @@ -1040,7 +1042,13 @@ class ConversationFragment : getVoiceNoteMediaController().voiceNotePlaybackState.observe(viewLifecycleOwner, inputPanel.playbackStateObserver) - val conversationUpdateTick = ConversationUpdateTick { adapter.updateTimestamps() } + val conversationUpdateTick = ConversationUpdateTick { + disposables += ConversationMessageComputeWorkers.recomputeFormattedDate( + requireContext(), + adapter.currentList.filterIsInstance() + ).observeOn(AndroidSchedulers.mainThread()).subscribeBy { adapter.updateTimestamps() } + } + viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick) if (args.conversationScreenType.isInPopup) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/computed/ConversationMessageComputeWorkers.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/computed/ConversationMessageComputeWorkers.kt new file mode 100644 index 0000000000..5c38bb2941 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/computed/ConversationMessageComputeWorkers.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.thoughtcrime.securesms.conversation.v2.computed + +import android.content.Context +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import org.signal.core.util.ThreadUtil +import org.signal.core.util.concurrent.SignalExecutors +import org.thoughtcrime.securesms.conversation.ConversationMessage +import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement + +/** + * Collection of workers to recompute computed ConversationMessage fields. + */ +object ConversationMessageComputeWorkers { + + private val executor = SignalExecutors.newCachedSingleThreadExecutor("conversation-message-compute", ThreadUtil.PRIORITY_IMPORTANT_BACKGROUND_THREAD) + + fun recomputeFormattedDate( + context: Context, + items: List + ): Single { + return Single.fromCallable { + var hasUpdatedProperties = false + for (item in items) { + val oldDate = item.conversationMessage.computedProperties.formattedDate + if (oldDate.isRelative) { + val newDate = ConversationMessage.getFormattedDate(context, item.conversationMessage.messageRecord) + item.conversationMessage.computedProperties.formattedDate = newDate + hasUpdatedProperties = true + } + } + + hasUpdatedProperties + }.subscribeOn(Schedulers.from(executor)) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/computed/FormattedDate.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/computed/FormattedDate.kt new file mode 100644 index 0000000000..52176d9142 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/computed/FormattedDate.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation.v2.computed + +data class FormattedDate( + val isRelative: Boolean, + val value: String +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt index f6c6e97884..d195a37382 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt @@ -98,6 +98,7 @@ open class V2ConversationItemTextOnlyViewHolder>( override val badgeImageView: View? = binding.senderBadge private var reactionMeasureListener: ReactionMeasureListener = ReactionMeasureListener() + private var dateString: String = "" private val bodyBubbleDrawable = ChatColorsDrawable() private val footerDrawable = ChatColorsDrawable() @@ -196,7 +197,9 @@ open class V2ConversationItemTextOnlyViewHolder>( ) if (ConversationAdapterBridge.PAYLOAD_TIMESTAMP in payload) { - presentDate() + if (conversationMessage.computedProperties.formattedDate.value != dateString) { + presentDate() + } hasProcessedSupportedPayload = true } @@ -605,6 +608,8 @@ open class V2ConversationItemTextOnlyViewHolder>( return } + dateString = conversationMessage.computedProperties.formattedDate.value + binding.footerDate.setOnClickListener(null) binding.footerDate.visible = true binding.footerDate.setTextColor(themeDelegate.getFooterTextColor(conversationMessage)) @@ -623,9 +628,9 @@ open class V2ConversationItemTextOnlyViewHolder>( } else if (record.isRateLimited) { binding.footerDate.setText(R.string.ConversationItem_send_paused) } else if (record.isScheduled()) { - binding.footerDate.text = conversationMessage.formattedDate + binding.footerDate.text = conversationMessage.computedProperties.formattedDate.value } else { - var date = conversationMessage.formattedDate + var date = dateString if (conversationContext.displayMode != ConversationItemDisplayMode.Detailed && record is MediaMmsMessageRecord && record.isEditMessage()) { date = getContext().getString(R.string.ConversationItem_edited_timestamp_footer, date) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt index 7be0c4b33d..042915cc9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt @@ -22,6 +22,7 @@ import android.os.Build import android.text.format.DateFormat import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.conversation.v2.computed.FormattedDate import java.text.DateFormatSymbols import java.text.ParseException import java.text.SimpleDateFormat @@ -124,16 +125,21 @@ object DateUtils : android.text.format.DateUtils() { */ @JvmStatic fun getDatelessRelativeTimeSpanString(context: Context, locale: Locale, timestamp: Long): String { + return getDatelessRelativeTimeSpanFormattedDate(context, locale, timestamp).value + } + + @JvmStatic + fun getDatelessRelativeTimeSpanFormattedDate(context: Context, locale: Locale, timestamp: Long): FormattedDate { return when { timestamp.isWithin(1.minutes) -> { - context.getString(R.string.DateUtils_just_now) + FormattedDate(true, context.getString(R.string.DateUtils_just_now)) } timestamp.isWithin(1.hours) -> { val minutes = timestamp.convertDeltaTo(DurationUnit.MINUTES) - context.resources.getString(R.string.DateUtils_minutes_ago, minutes) + FormattedDate(true, context.resources.getString(R.string.DateUtils_minutes_ago, minutes)) } else -> { - getOnlyTimeString(context, locale, timestamp) + FormattedDate(false, getOnlyTimeString(context, locale, timestamp)) } } }