From ebf4f023704158a8c4a716961baa06e49880dd32 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 20 Mar 2024 14:28:21 +1030 Subject: [PATCH 01/14] Fix disappearing messages option explanation --- .../securesms/conversation/disappearingmessages/State.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt index cc968df398..ced4cc0035 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt @@ -68,7 +68,7 @@ enum class ExpiryType( AFTER_SEND( ExpiryMode::AfterSend, R.string.expiration_type_disappear_after_send, - R.string.expiration_type_disappear_after_read_description, + R.string.expiration_type_disappear_after_send_description, R.string.AccessibilityId_disappear_after_send_option ); From b54efd6206a009019dcdf2defed6bcf5c35505a5 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 14 May 2024 13:13:38 +0930 Subject: [PATCH 02/14] Clamp initial page of MediaPreview --- .../securesms/MediaPreviewActivity.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index b74638fec7..4c3c45ab53 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -21,6 +21,7 @@ import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.database.Cursor; +import android.database.CursorIndexOutOfBoundsException; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -526,23 +527,24 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im @Override public void onLoadFinished(@NonNull Loader> loader, @Nullable Pair data) { - if (data != null) { - CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent); - mediaPager.setAdapter(adapter); - adapter.setActive(true); + if (data == null) return; - viewModel.setCursor(this, data.first, leftIsRecent); + CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent); + mediaPager.setAdapter(adapter); + adapter.setActive(true); - if (restartItem >= 0 || data.second >= 0) { - int item = restartItem >= 0 ? restartItem : data.second; - mediaPager.setCurrentItem(item); + viewModel.setCursor(this, data.first, leftIsRecent); - if (item == 0) { - viewPagerListener.onPageSelected(0); - } - } else { - Log.w(TAG, "one of restartItem "+restartItem+" and data.second "+data.second+" would cause OOB exception"); - } + int item = restartItem >= 0 && restartItem < adapter.getCount() ? restartItem : Math.max(Math.min(data.second, adapter.getCount() - 1), 0); + + try { + mediaPager.setCurrentItem(item); + } catch (CursorIndexOutOfBoundsException e) { + throw new RuntimeException("restartItem = " + restartItem + ", data.second = " + data.second + " leftIsRecent = " + leftIsRecent, e); + } + + if (item == 0) { + viewPagerListener.onPageSelected(0); } } From be0b8007513a49cfc0dae7e3cd58997b83f68a83 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 14 May 2024 13:36:12 +0930 Subject: [PATCH 03/14] Move adapter to field --- .../securesms/MediaPreviewActivity.java | 59 +++++++------------ 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 4c3c45ab53..5f9ca6fecc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -146,6 +146,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); } }; + private MediaItemAdapter adapter; public static Intent getPreviewIntent(Context context, MediaPreviewArgs args) { return getPreviewIntent(context, args.getSlide(), args.getMmsRecord(), args.getThread()); @@ -379,7 +380,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im if (conversationRecipient != null) { getSupportLoaderManager().restartLoader(0, null, this); } else { - mediaPager.setAdapter(new SingleItemPagerAdapter(this, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize)); + adapter = new SingleItemPagerAdapter(this, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize); + mediaPager.setAdapter(adapter); if (initialCaption != null) { detailsContainer.setVisibility(View.VISIBLE); @@ -507,13 +509,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im } private @Nullable MediaItem getCurrentMediaItem() { - MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter(); - - if (adapter != null) { - return adapter.getMediaItemFor(mediaPager.getCurrentItem()); - } else { - return null; - } + if (adapter == null) return null; + return adapter.getMediaItemFor(mediaPager.getCurrentItem()); } public static boolean isContentTypeSupported(final String contentType) { @@ -529,9 +526,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im public void onLoadFinished(@NonNull Loader> loader, @Nullable Pair data) { if (data == null) return; - CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent); + adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent); mediaPager.setAdapter(adapter); - adapter.setActive(true); viewModel.setCursor(this, data.first, leftIsRecent); @@ -562,26 +558,22 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im if (currentPage != -1 && currentPage != position) onPageUnselected(currentPage); currentPage = position; - MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter(); + if (adapter == null) return; - if (adapter != null) { - MediaItem item = adapter.getMediaItemFor(position); - if (item.recipient != null) item.recipient.addListener(MediaPreviewActivity.this); - viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position); - updateActionBar(); - } + MediaItem item = adapter.getMediaItemFor(position); + if (item.recipient != null) item.recipient.addListener(MediaPreviewActivity.this); + viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position); + updateActionBar(); } public void onPageUnselected(int position) { - MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter(); + if (adapter == null) return; - if (adapter != null) { - MediaItem item = adapter.getMediaItemFor(position); - if (item.recipient != null) item.recipient.removeListener(MediaPreviewActivity.this); + MediaItem item = adapter.getMediaItemFor(position); + if (item.recipient != null) item.recipient.removeListener(MediaPreviewActivity.this); - adapter.pause(position); - } + adapter.pause(position); } @Override @@ -595,7 +587,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im } } - private static class SingleItemPagerAdapter extends PagerAdapter implements MediaItemAdapter { + private static class SingleItemPagerAdapter extends MediaItemAdapter { private final GlideRequests glideRequests; private final Window window; @@ -667,7 +659,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im } } - private static class CursorPagerAdapter extends PagerAdapter implements MediaItemAdapter { + private static class CursorPagerAdapter extends MediaItemAdapter { private final WeakHashMap mediaViews = new WeakHashMap<>(); @@ -677,7 +669,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im private final Cursor cursor; private final boolean leftIsRecent; - private boolean active; private int autoPlayPosition; CursorPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, @@ -692,15 +683,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im this.leftIsRecent = leftIsRecent; } - public void setActive(boolean active) { - this.active = active; - notifyDataSetChanged(); - } - @Override public int getCount() { - if (!active) return 0; - else return cursor.getCount(); + return cursor.getCount(); } @Override @@ -802,9 +787,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im } } - interface MediaItemAdapter { - MediaItem getMediaItemFor(int position); - void pause(int position); - @Nullable View getPlaybackControls(int position); + abstract static class MediaItemAdapter extends PagerAdapter { + abstract MediaItem getMediaItemFor(int position); + abstract void pause(int position); + @Nullable abstract View getPlaybackControls(int position); } } From cee06bf7ee3a31ed631413ef815befaa81d3aec2 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 14 May 2024 13:43:25 +0930 Subject: [PATCH 04/14] Remove invalid ViewPagerListener before updating pager data --- .../org/thoughtcrime/securesms/MediaPreviewActivity.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 5f9ca6fecc..ed3af774b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -287,9 +287,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im mediaPager = findViewById(R.id.media_pager); mediaPager.setOffscreenPageLimit(1); - viewPagerListener = new ViewPagerListener(); - mediaPager.addOnPageChangeListener(viewPagerListener); - albumRail = findViewById(R.id.media_preview_album_rail); albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false); @@ -526,6 +523,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im public void onLoadFinished(@NonNull Loader> loader, @Nullable Pair data) { if (data == null) return; + mediaPager.removeOnPageChangeListener(viewPagerListener); + adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent); mediaPager.setAdapter(adapter); @@ -533,6 +532,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im int item = restartItem >= 0 && restartItem < adapter.getCount() ? restartItem : Math.max(Math.min(data.second, adapter.getCount() - 1), 0); + viewPagerListener = new ViewPagerListener(); + mediaPager.addOnPageChangeListener(viewPagerListener); + try { mediaPager.setCurrentItem(item); } catch (CursorIndexOutOfBoundsException e) { From 41dde125303d29e01504423e127bae11b0cbca59 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 14 May 2024 14:35:21 +0930 Subject: [PATCH 05/14] Clamp MediaPreview#getCursorPosition --- .../thoughtcrime/securesms/MediaPreviewActivity.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index ed3af774b5..7e4f3a4e01 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -219,13 +219,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } - @TargetApi(VERSION_CODES.JELLY_BEAN) - private void setFullscreenIfPossible() { - if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { - getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN); - } - } - @Override public void onModified(Recipient recipient) { Util.runOnMain(this::updateActionBar); @@ -760,8 +753,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im } private int getCursorPosition(int position) { - if (leftIsRecent) return position; - else return cursor.getCount() - 1 - position; + int unclamped = leftIsRecent ? position : cursor.getCount() - 1 - position; + return Math.max(Math.min(unclamped, cursor.getCount() - 1), 0); } } From 9f150391888af2cc2299fc9bc73b3448911cf91a Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 14 May 2024 15:25:45 +0930 Subject: [PATCH 06/14] Add more info to unselect exception --- .../org/thoughtcrime/securesms/MediaPreviewActivity.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 7e4f3a4e01..2e67becbfd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -565,8 +565,12 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im public void onPageUnselected(int position) { if (adapter == null) return; - MediaItem item = adapter.getMediaItemFor(position); - if (item.recipient != null) item.recipient.removeListener(MediaPreviewActivity.this); + try { + MediaItem item = adapter.getMediaItemFor(position); + if (item.recipient != null) item.recipient.removeListener(MediaPreviewActivity.this); + } catch (CursorIndexOutOfBoundsException e) { + throw new RuntimeException("position = " + position + " leftIsRecent = " + leftIsRecent, e); + } adapter.pause(position); } From 2002b6f397b8c33d7b628e15ed6a23598c537514 Mon Sep 17 00:00:00 2001 From: fanchao Date: Mon, 20 May 2024 16:11:43 +1000 Subject: [PATCH 07/14] Fixes SES-1931 debouncer crash --- .../ConversationNotificationDebouncer.kt | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ConversationNotificationDebouncer.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ConversationNotificationDebouncer.kt index 84e1b9b20a..5620814190 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ConversationNotificationDebouncer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ConversationNotificationDebouncer.kt @@ -5,9 +5,9 @@ import android.content.Context import org.session.libsession.utilities.Debouncer import org.thoughtcrime.securesms.ApplicationContext -class ConversationNotificationDebouncer(private val context: Context) { +class ConversationNotificationDebouncer(private val context: ApplicationContext) { private val threadIDs = mutableSetOf() - private val handler = (context.applicationContext as ApplicationContext).conversationListNotificationHandler + private val handler = context.conversationListNotificationHandler private val debouncer = Debouncer(handler, 100) companion object { @@ -17,20 +17,28 @@ class ConversationNotificationDebouncer(private val context: Context) { @Synchronized fun get(context: Context): ConversationNotificationDebouncer { if (::shared.isInitialized) { return shared } - shared = ConversationNotificationDebouncer(context) + shared = ConversationNotificationDebouncer(context.applicationContext as ApplicationContext) return shared } } fun notify(threadID: Long) { - threadIDs.add(threadID) + synchronized(threadIDs) { + threadIDs.add(threadID) + } + debouncer.publish { publish() } } private fun publish() { - for (threadID in threadIDs.toList()) { + val toNotify = synchronized(threadIDs) { + val copy = threadIDs.toList() + threadIDs.clear() + copy + } + + for (threadID in toNotify) { context.contentResolver.notifyChange(DatabaseContentProviders.Conversation.getUriForThread(threadID), null) } - threadIDs.clear() } } \ No newline at end of file From 27fcc66e7f2983fb5196f7046c1c6edd72ea4736 Mon Sep 17 00:00:00 2001 From: fanchao Date: Tue, 21 May 2024 14:55:10 +1000 Subject: [PATCH 08/14] Defensive check --- .../java/org/thoughtcrime/securesms/audio/AudioRecorder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java index 35cbf16b63..835777fdcf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java @@ -45,7 +45,8 @@ public class AudioRecorder { Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId()); try { if (audioCodec != null) { - throw new AssertionError("We can only record once at a time."); + Log.e(TAG, "We can only record once at a time."); + return; } ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe(); From ee9a0a610c347234cb84b2caae95395b31aa1220 Mon Sep 17 00:00:00 2001 From: fanchao Date: Wed, 22 May 2024 11:13:26 +1000 Subject: [PATCH 09/14] Logging --- .../java/org/thoughtcrime/securesms/audio/AudioRecorder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java index 835777fdcf..fd265337f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java @@ -45,7 +45,7 @@ public class AudioRecorder { Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId()); try { if (audioCodec != null) { - Log.e(TAG, "We can only record once at a time."); + Log.e(TAG, "Trying to start recording while another recording is in progress, exiting..."); return; } From f90cea8fe2cc0f58e9ee5418142d6c167efece26 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 23 May 2024 18:59:09 +0930 Subject: [PATCH 10/14] 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 11/14] 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 12/14] 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 13/14] 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 14/14] 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) {