From f90cea8fe2cc0f58e9ee5418142d6c167efece26 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 23 May 2024 18:59:09 +0930 Subject: [PATCH 1/5] Do not fetch quotes recursively --- .../v2/utilities/ResendMessageUtilities.kt | 18 +++++----- .../securesms/database/MmsDatabase.kt | 34 ++++++------------- .../securesms/database/MmsSmsDatabase.java | 18 +++++++--- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt index e01a75b30c..c1d6987904 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt @@ -40,18 +40,16 @@ object ResendMessageUtilities { message.recipient = messageRecord.recipient.address.serialize() } message.threadID = messageRecord.threadId - if (messageRecord.isMms) { - val mmsMessageRecord = messageRecord as MmsMessageRecord - if (mmsMessageRecord.linkPreviews.isNotEmpty()) { - message.linkPreview = LinkPreview.from(mmsMessageRecord.linkPreviews[0]) - } - if (mmsMessageRecord.quote != null) { - message.quote = Quote.from(mmsMessageRecord.quote!!.quoteModel) - if (userBlindedKey != null && messageRecord.quote!!.author.serialize() == TextSecurePreferences.getLocalNumber(context)) { - message.quote!!.publicKey = userBlindedKey + if (messageRecord.isMms && messageRecord is MmsMessageRecord) { + messageRecord.linkPreviews.firstOrNull()?.let { message.linkPreview = LinkPreview.from(it) } + messageRecord.quote?.quoteModel?.let { + message.quote = Quote.from(it)?.apply { + if (userBlindedKey != null && publicKey == TextSecurePreferences.getLocalNumber(context)) { + publicKey = userBlindedKey + } } } - message.addSignalAttachments(mmsMessageRecord.slideDeck.asAttachments()) + message.addSignalAttachments(messageRecord.slideDeck.asAttachments()) } val sentTimestamp = message.sentTimestamp val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index f2fcefd0aa..5648cdace1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -1147,13 +1147,9 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa } } - fun readerFor(cursor: Cursor?): Reader { - return Reader(cursor) - } + fun readerFor(cursor: Cursor?, getQuote: Boolean = true) = Reader(cursor, getQuote) - fun readerFor(message: OutgoingMediaMessage?, threadId: Long): OutgoingMessageReader { - return OutgoingMessageReader(message, threadId) - } + fun readerFor(message: OutgoingMediaMessage?, threadId: Long) = OutgoingMessageReader(message, threadId) fun setQuoteMissing(messageId: Long): Int { val contentValues = ContentValues() @@ -1217,7 +1213,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa } - inner class Reader(private val cursor: Cursor?) : Closeable { + inner class Reader(private val cursor: Cursor?, private val getQuote: Boolean = true) : Closeable { val next: MessageRecord? get() = if (cursor == null || !cursor.moveToNext()) null else current val current: MessageRecord @@ -1226,7 +1222,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa return if (mmsType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND.toLong()) { getNotificationMmsMessageRecord(cursor) } else { - getMediaMmsMessageRecord(cursor) + getMediaMmsMessageRecord(cursor, getQuote) } } @@ -1253,20 +1249,10 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa DELIVERY_RECEIPT_COUNT ) ) - var readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT)) - val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID)) + val readReceiptCount = if (isReadReceiptsEnabled(context)) cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT)) else 0 val hasMention = (cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1) - if (!isReadReceiptsEnabled(context)) { - readReceiptCount = 0 - } - var contentLocationBytes: ByteArray? = null - var transactionIdBytes: ByteArray? = null - if (!contentLocation.isNullOrEmpty()) contentLocationBytes = toIsoBytes( - contentLocation - ) - if (!transactionId.isNullOrEmpty()) transactionIdBytes = toIsoBytes( - transactionId - ) + val contentLocationBytes: ByteArray? = contentLocation?.takeUnless { it.isEmpty() }?.let(::toIsoBytes) + val transactionIdBytes: ByteArray? = transactionId?.takeUnless { it.isEmpty() }?.let(::toIsoBytes) val slideDeck = SlideDeck(context, MmsNotificationAttachment(status, messageSize)) return NotificationMmsMessageRecord( id, recipient, recipient, @@ -1277,7 +1263,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa ) } - private fun getMediaMmsMessageRecord(cursor: Cursor): MediaMmsMessageRecord { + private fun getMediaMmsMessageRecord(cursor: Cursor, getQuote: Boolean): MediaMmsMessageRecord { val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)) val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)) val dateReceived = cursor.getLong( @@ -1328,7 +1314,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa .filterNot { o: DatabaseAttachment? -> o in contactAttachments } .filterNot { o: DatabaseAttachment? -> o in previewAttachments } ) - val quote = getQuote(cursor) + val quote = if (getQuote) getQuote(cursor) else null val reactions = get(context).reactionDatabase().getReactions(cursor) return MediaMmsMessageRecord( id, recipient, recipient, @@ -1381,7 +1367,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID)) val quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR)) if (quoteId == 0L || quoteAuthor.isNullOrBlank()) return null - val retrievedQuote = get(context).mmsSmsDatabase().getMessageFor(quoteId, quoteAuthor) + val retrievedQuote = get(context).mmsSmsDatabase().getMessageFor(quoteId, quoteAuthor, false) val quoteText = retrievedQuote?.body val quoteMissing = retrievedQuote == null val quoteDeck = ( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index da4f39f0c1..0fbd2ac7b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -97,9 +97,13 @@ public class MmsSmsDatabase extends Database { } public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) { + return getMessageFor(timestamp, serializedAuthor, true); + } + + public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor, boolean getQuote) { try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) { - MmsSmsDatabase.Reader reader = readerFor(cursor); + MmsSmsDatabase.Reader reader = readerFor(cursor, getQuote); MessageRecord messageRecord; boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor); @@ -638,7 +642,11 @@ public class MmsSmsDatabase extends Database { } public Reader readerFor(@NonNull Cursor cursor) { - return new Reader(cursor); + return readerFor(cursor, true); + } + + public Reader readerFor(@NonNull Cursor cursor, boolean getQuote) { + return new Reader(cursor, getQuote); } @NotNull @@ -661,11 +669,13 @@ public class MmsSmsDatabase extends Database { public class Reader implements Closeable { private final Cursor cursor; + private final boolean getQuote; private SmsDatabase.Reader smsReader; private MmsDatabase.Reader mmsReader; - public Reader(Cursor cursor) { + public Reader(Cursor cursor, boolean getQuote) { this.cursor = cursor; + this.getQuote = getQuote; } private SmsDatabase.Reader getSmsReader() { @@ -678,7 +688,7 @@ public class MmsSmsDatabase extends Database { private MmsDatabase.Reader getMmsReader() { if (mmsReader == null) { - mmsReader = DatabaseComponent.get(context).mmsDatabase().readerFor(cursor); + mmsReader = DatabaseComponent.get(context).mmsDatabase().readerFor(cursor, getQuote); } return mmsReader; From cd302f9f27a3e2c6dc1b6dda3c788dcec4d87c0e Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 28 May 2024 12:57:50 +0930 Subject: [PATCH 2/5] Move unapprovedMessageCount to IO --- .../securesms/home/HomeActivity.kt | 15 ++--- .../securesms/home/HomeAdapter.kt | 6 +- .../securesms/home/HomeViewModel.kt | 64 +++++++++++++------ 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index ada4ceda66..79684d83d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -185,7 +185,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), binding.seedReminderView.isVisible = false } } - setupMessageRequestsBanner() // Set up recycler view binding.globalSearchInputLayout.listener = this homeAdapter.setHasStableIds(true) @@ -218,9 +217,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), // Subscribe to threads and update the UI lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - homeViewModel.threads + homeViewModel.data .filterNotNull() // We don't actually want the null value here as it indicates a loading state (maybe we need a loading state?) - .collectLatest { threads -> + .collectLatest { data -> val manager = binding.recyclerView.layoutManager as LinearLayoutManager val firstPos = manager.findFirstCompletelyVisibleItemPosition() val offsetTop = if(firstPos >= 0) { @@ -228,9 +227,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view) } ?: 0 } else 0 - homeAdapter.data = threads + homeAdapter.data = data if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) } - setupMessageRequestsBanner() + setupMessageRequestsBanner(data.unapprovedConversationCount, data.hasHiddenMessageRequests) updateEmptyState() } } @@ -341,10 +340,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), binding.newConversationButton.isVisible = !isShown } - private fun setupMessageRequestsBanner() { - val messageRequestCount = threadDb.unapprovedConversationCount + private fun setupMessageRequestsBanner(messageRequestCount: Int, hasHiddenMessageRequests: Boolean) { // Set up message requests - if (messageRequestCount > 0 && !textSecurePreferences.hasHiddenMessageRequests()) { + if (messageRequestCount > 0 && !hasHiddenMessageRequests) { with(ViewMessageRequestBannerBinding.inflate(layoutInflater)) { unreadCountTextView.text = messageRequestCount.toString() timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString( @@ -664,7 +662,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), text("Hide message requests?") button(R.string.yes) { textSecurePreferences.setHasHiddenMessageRequests() - setupMessageRequestsBanner() homeViewModel.tryReload() } button(R.string.no) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt index 63e0fed5b6..d94037f28c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt @@ -25,11 +25,9 @@ class HomeAdapter( var header: View? = null - var data: HomeViewModel.Data = HomeViewModel.Data(emptyList(), emptySet()) + var data: HomeViewModel.Data = HomeViewModel.Data(emptyList(), 0, false, emptySet()) set(newData) { - if (field === newData) { - return - } + if (field === newData) return val diff = HomeDiffUtil(field, newData, context, configFactory) val diffResult = DiffUtil.calculateDiff(diff) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt index dccebd4156..09da7c80a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt @@ -15,11 +15,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.withContext +import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.ThreadDatabase @@ -30,9 +33,10 @@ import dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQu @HiltViewModel class HomeViewModel @Inject constructor( - private val threadDb: ThreadDatabase, - private val contentResolver: ContentResolver, - @ApplicationContextQualifier private val context: Context, + private val threadDb: ThreadDatabase, + private val contentResolver: ContentResolver, + private val prefs: TextSecurePreferences, + @ApplicationContextQualifier private val context: Context, ) : ViewModel() { // SharedFlow that emits whenever the user asks us to reload the conversation private val manualReloadTrigger = MutableSharedFlow( @@ -46,8 +50,18 @@ class HomeViewModel @Inject constructor( * This flow will emit whenever the user asks us to reload the conversation list or * whenever the conversation list changes. */ - val threads: StateFlow = combine(observeConversationList(), observeTypingStatus(), ::Data) - .stateIn(viewModelScope, SharingStarted.Eagerly, null) + val data: StateFlow = combine( + observeConversationList(), + unapprovedConversationCount(), + hasHiddenMessageRequestsFlow(), + observeTypingStatus(), + ::Data + ).stateIn(viewModelScope, SharingStarted.Eagerly, null) + + private fun hasHiddenMessageRequestsFlow() = TextSecurePreferences.events + .filter { it == TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS } + .map { prefs.hasHiddenMessageRequests() } + .onStart { emit(prefs.hasHiddenMessageRequests()) } private fun observeTypingStatus(): Flow> = ApplicationContext.getInstance(context).typingStatusRepository @@ -56,30 +70,38 @@ class HomeViewModel @Inject constructor( .onStart { emit(emptySet()) } .distinctUntilChanged() + private fun unapprovedConversationCount() = + contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI) + .flowOn(Dispatchers.IO) + .map { threadDb.unapprovedConversationCount } + .onStart { emit(threadDb.unapprovedConversationCount) } + @Suppress("OPT_IN_USAGE") private fun observeConversationList(): Flow> = merge( - manualReloadTrigger, - contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI)) - .debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS) - .onStart { emit(Unit) } - .mapLatest { _ -> - withContext(Dispatchers.IO) { - threadDb.approvedConversationList.use { openCursor -> - val reader = threadDb.readerFor(openCursor) - buildList(reader.count) { - while (true) { - add(reader.next ?: break) - } - } + manualReloadTrigger, + contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI) + ) + .flowOn(Dispatchers.IO) + .debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS) + .onStart { emit(Unit) } + .mapLatest { _ -> + threadDb.approvedConversationList.use { openCursor -> + val reader = threadDb.readerFor(openCursor) + buildList(reader.count) { + while (true) { + add(reader.next ?: break) } } } + } fun tryReload() = manualReloadTrigger.tryEmit(Unit) data class Data( - val threads: List, - val typingThreadIDs: Set + val threads: List, + val unapprovedConversationCount: Int, + val hasHiddenMessageRequests: Boolean, + val typingThreadIDs: Set ) companion object { From 04215f74e1dcf43f8e5c8e174d5bfe3f7d541e08 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 28 May 2024 16:44:49 +0930 Subject: [PATCH 3/5] Bind message requests in HomeAdapter --- .../securesms/home/HomeActivity.kt | 34 +--------- .../securesms/home/HomeAdapter.kt | 57 +++++++++++----- .../securesms/home/HomeViewModel.kt | 68 ++++++++++++------- 3 files changed, 86 insertions(+), 73 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 79684d83d5..c063f30538 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -16,7 +16,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collect @@ -28,7 +27,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R import network.loki.messenger.databinding.ActivityHomeBinding -import network.loki.messenger.databinding.ViewMessageRequestBannerBinding import network.loki.messenger.libsession_util.ConfigBase import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -74,13 +72,11 @@ import org.thoughtcrime.securesms.preferences.SettingsActivity import org.thoughtcrime.securesms.showMuteDialog import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities -import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.IP2Country import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.show import java.io.IOException -import java.util.Locale import javax.inject.Inject @AndroidEntryPoint @@ -113,7 +109,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), get() = textSecurePreferences.getLocalNumber()!! private val homeAdapter: HomeAdapter by lazy { - HomeAdapter(context = this, configFactory = configFactory, listener = this) + HomeAdapter(context = this, configFactory = configFactory, listener = this, ::showMessageRequests, ::hideMessageRequests) } private val globalSearchAdapter = GlobalSearchAdapter { model -> @@ -229,7 +225,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } else 0 homeAdapter.data = data if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) } - setupMessageRequestsBanner(data.unapprovedConversationCount, data.hasHiddenMessageRequests) updateEmptyState() } } @@ -340,33 +335,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), binding.newConversationButton.isVisible = !isShown } - private fun setupMessageRequestsBanner(messageRequestCount: Int, hasHiddenMessageRequests: Boolean) { - // Set up message requests - if (messageRequestCount > 0 && !hasHiddenMessageRequests) { - with(ViewMessageRequestBannerBinding.inflate(layoutInflater)) { - unreadCountTextView.text = messageRequestCount.toString() - timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString( - this@HomeActivity, - Locale.getDefault(), - threadDb.latestUnapprovedConversationTimestamp - ) - root.setOnClickListener { showMessageRequests() } - root.setOnLongClickListener { hideMessageRequests(); true } - root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) - val hadHeader = homeAdapter.hasHeaderView() - homeAdapter.header = root - if (hadHeader) homeAdapter.notifyItemChanged(0) - else homeAdapter.notifyItemInserted(0) - } - } else { - val hadHeader = homeAdapter.hasHeaderView() - homeAdapter.header = null - if (hadHeader) { - homeAdapter.notifyItemRemoved(0) - } - } - } - private fun updateLegacyConfigView() { binding.configOutdatedView.isVisible = ConfigBase.isNewConfigEnabled(textSecurePreferences.hasForcedNewConfig(), SnodeAPI.nowWithOffset) && textSecurePreferences.getHasLegacyConfig() diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt index d94037f28c..5203c6ac1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt @@ -9,13 +9,18 @@ import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_ID import network.loki.messenger.R +import network.loki.messenger.databinding.ViewMessageRequestBannerBinding import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.util.DateUtils +import java.util.Locale class HomeAdapter( private val context: Context, private val configFactory: ConfigFactory, - private val listener: ConversationClickListener + private val listener: ConversationClickListener, + private val showMessageRequests: () -> Unit, + private val hideMessageRequests: () -> Unit, ) : RecyclerView.Adapter(), ListUpdateCallback { companion object { @@ -23,22 +28,31 @@ class HomeAdapter( private const val ITEM = 1 } - var header: View? = null + var messageRequests: HomeViewModel.MessageRequests? = null + set(value) { + val hadHeader = hasHeaderView() + field = value + if (value != null) { + if (hadHeader) notifyItemChanged(0) else notifyItemInserted(0) + } else if (hadHeader) notifyItemRemoved(0) + } - var data: HomeViewModel.Data = HomeViewModel.Data(emptyList(), 0, false, emptySet()) + var data: HomeViewModel.Data = HomeViewModel.Data() set(newData) { if (field === newData) return + messageRequests = newData.messageRequests + val diff = HomeDiffUtil(field, newData, context, configFactory) val diffResult = DiffUtil.calculateDiff(diff) field = newData diffResult.dispatchUpdatesTo(this as ListUpdateCallback) } - fun hasHeaderView(): Boolean = header != null + fun hasHeaderView(): Boolean = messageRequests != null private val headerCount: Int - get() = if (header == null) 0 else 1 + get() = if (messageRequests == null) 0 else 1 override fun onInserted(position: Int, count: Int) { notifyItemRangeInserted(position + headerCount, count) @@ -67,7 +81,11 @@ class HomeAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = when (viewType) { HEADER -> { - HeaderFooterViewHolder(header!!) + ViewMessageRequestBannerBinding.inflate(LayoutInflater.from(parent.context)).apply { + root.setOnClickListener { showMessageRequests() } + root.setOnLongClickListener { hideMessageRequests(); true } + root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + }.let(::HeaderFooterViewHolder) } ITEM -> { val conversationView = LayoutInflater.from(parent.context).inflate(R.layout.view_conversation, parent, false) as ConversationView @@ -83,19 +101,27 @@ class HomeAdapter( } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - if (holder is ConversationViewHolder) { - val offset = if (hasHeaderView()) position - 1 else position - val thread = data.threads[offset] - val isTyping = data.typingThreadIDs.contains(thread.threadId) - holder.view.bind(thread, isTyping, glide) + when (holder) { + is HeaderFooterViewHolder -> { + holder.binding.run { + messageRequests?.let { + unreadCountTextView.text = it.count + timestampTextView.text = it.timestamp + } + } + } + is ConversationViewHolder -> { + val offset = if (hasHeaderView()) position - 1 else position + val thread = data.threads[offset] + val isTyping = data.typingThreadIDs.contains(thread.threadId) + holder.view.bind(thread, isTyping, glide) + } } } override fun onViewRecycled(holder: RecyclerView.ViewHolder) { if (holder is ConversationViewHolder) { holder.view.recycle() - } else { - super.onViewRecycled(holder) } } @@ -107,6 +133,5 @@ class HomeAdapter( class ConversationViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view) - class HeaderFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) - -} \ No newline at end of file + class HeaderFooterViewHolder(val binding: ViewMessageRequestBannerBinding) : RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt index 09da7c80a0..32e276939e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt @@ -2,11 +2,13 @@ package org.thoughtcrime.securesms.home import android.content.ContentResolver import android.content.Context +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -20,6 +22,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import org.session.libsession.utilities.TextSecurePreferences @@ -27,7 +30,9 @@ import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.observeChanges +import java.util.Locale import javax.inject.Inject import dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQualifier @@ -52,13 +57,13 @@ class HomeViewModel @Inject constructor( */ val data: StateFlow = combine( observeConversationList(), - unapprovedConversationCount(), - hasHiddenMessageRequestsFlow(), observeTypingStatus(), + messageRequests(), ::Data - ).stateIn(viewModelScope, SharingStarted.Eagerly, null) + ) + .stateIn(viewModelScope, SharingStarted.Eagerly, null) - private fun hasHiddenMessageRequestsFlow() = TextSecurePreferences.events + private fun hasHiddenMessageRequests() = TextSecurePreferences.events .filter { it == TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS } .map { prefs.hasHiddenMessageRequests() } .onStart { emit(prefs.hasHiddenMessageRequests()) } @@ -70,40 +75,55 @@ class HomeViewModel @Inject constructor( .onStart { emit(emptySet()) } .distinctUntilChanged() - private fun unapprovedConversationCount() = - contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI) - .flowOn(Dispatchers.IO) - .map { threadDb.unapprovedConversationCount } - .onStart { emit(threadDb.unapprovedConversationCount) } + private fun messageRequests() = combine( + unapprovedConversationCount(), + hasHiddenMessageRequests(), + latestUnapprovedConversationTimestamp(), + ::createMessageRequests + ) + + private fun unapprovedConversationCount() = reloadTriggersAndContentChanges() + .map { threadDb.unapprovedConversationCount } + + private fun latestUnapprovedConversationTimestamp() = reloadTriggersAndContentChanges() + .map { threadDb.latestUnapprovedConversationTimestamp } @Suppress("OPT_IN_USAGE") - private fun observeConversationList(): Flow> = merge( + private fun observeConversationList(): Flow> = reloadTriggersAndContentChanges() + .mapLatest { _ -> + threadDb.approvedConversationList.use { openCursor -> + threadDb.readerFor(openCursor).run { generateSequence { next }.toList() } + } + } + + @OptIn(FlowPreview::class) + private fun reloadTriggersAndContentChanges() = merge( manualReloadTrigger, contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI) ) .flowOn(Dispatchers.IO) .debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS) .onStart { emit(Unit) } - .mapLatest { _ -> - threadDb.approvedConversationList.use { openCursor -> - val reader = threadDb.readerFor(openCursor) - buildList(reader.count) { - while (true) { - add(reader.next ?: break) - } - } - } - } fun tryReload() = manualReloadTrigger.tryEmit(Unit) data class Data( - val threads: List, - val unapprovedConversationCount: Int, - val hasHiddenMessageRequests: Boolean, - val typingThreadIDs: Set + val threads: List = emptyList(), + val typingThreadIDs: Set = emptySet(), + val messageRequests: MessageRequests? = null ) + fun createMessageRequests( + count: Int, + hidden: Boolean, + timestamp: Long + ) = if (count > 0 && !hidden) MessageRequests( + count.toString(), + DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), timestamp) + ) else null + + data class MessageRequests(val count: String, val timestamp: String) + companion object { private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L } From 4fab64e6eefe161f1482825a8d07bab9eff3253d Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 28 May 2024 19:29:24 +0930 Subject: [PATCH 4/5] Schedule share pref --- .../main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt index 32e276939e..fa18a995b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt @@ -65,6 +65,7 @@ class HomeViewModel @Inject constructor( private fun hasHiddenMessageRequests() = TextSecurePreferences.events .filter { it == TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS } + .flowOn(Dispatchers.IO) .map { prefs.hasHiddenMessageRequests() } .onStart { emit(prefs.hasHiddenMessageRequests()) } From 69c50e6822eb82c1ec4a50d027345cd434035e84 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 29 May 2024 10:29:47 +0930 Subject: [PATCH 5/5] Dont reassign messageRequests if value didn't change --- app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt index 5203c6ac1b..571adb7358 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt @@ -30,6 +30,7 @@ class HomeAdapter( var messageRequests: HomeViewModel.MessageRequests? = null set(value) { + if (field == value) return val hadHeader = hasHeaderView() field = value if (value != null) {