From 983cfdd2da0e5969d7eac88f10dfb26ddeb3958f Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:18:26 +1100 Subject: [PATCH 01/12] Remove unused code --- .../libsession/database/StorageProtocol.kt | 1 - .../securesms/database/Storage.kt | 5 --- .../securesms/database/ThreadDatabase.java | 40 ------------------- 3 files changed, 46 deletions(-) diff --git a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt index 1d99e66c52..9cb4e34574 100644 --- a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -142,7 +142,6 @@ interface StorageProtocol { fun getOrCreateThreadIdFor(address: Address): Long fun getThreadId(address: Address): Long? fun getThreadIdForMms(mmsId: Long): Long - fun getLastUpdated(threadID: Long): Long fun trimThreadBefore(threadID: Long, timestamp: Long) fun getMessageCount(threadID: Long): Long fun getTotalPinned(): Int diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index a52279598e..d964fd249a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -894,11 +894,6 @@ open class Storage @Inject constructor( } } - override fun getLastUpdated(threadID: Long): Long { - val threadDB = threadDatabase - return threadDB.getLastUpdated(threadID) - } - override fun trimThreadBefore(threadID: Long, timestamp: Long) { val threadDB = threadDatabase threadDB.trimThreadBefore(threadID, timestamp) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 068ab51fbc..724bad0181 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -514,31 +514,6 @@ public List getThreads(@Nullable Collection add } } - /** - * @return All threads in the database, with their thread ID and Address. Note that - * threads don't necessarily mean conversations, as whether you have a conversation - * or not depend on the config data. This method returns all threads that exist - * in the database, normally this is useful only for data integrity purposes. - */ - public List> getAllThreads() { - return getAllThreads(getReadableDatabase()); - } - - private List> getAllThreads(SQLiteDatabase db) { - final String query = "SELECT " + ID + ", " + ADDRESS + " FROM " + TABLE_NAME + " WHERE nullif(" + ADDRESS + ", '') IS NOT NULL"; - try (Cursor cursor = db.rawQuery(query, null)) { - List> threads = new ArrayList<>(cursor.getCount()); - while (cursor.moveToNext()) { - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); - String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)); - if (address != null && !address.isEmpty()) { - threads.add(new kotlin.Pair<>(Address.fromSerialized(address), threadId)); - } - } - return threads; - } - } - /** * @param threadId * @param timestamp @@ -595,21 +570,6 @@ public Pair getLastSeenAndHasSent(long threadId) { } } - public long getLastUpdated(long threadId) { - SQLiteDatabase db = getReadableDatabase(); - Cursor cursor = db.query(TABLE_NAME, new String[]{THREAD_CREATION_DATE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null); - - try { - if (cursor != null && cursor.moveToFirst()) { - return cursor.getLong(0); - } - - return -1L; - } finally { - if (cursor != null) cursor.close(); - } - } - public int getMessageCount(long threadId) { SQLiteDatabase db = getReadableDatabase(); String[] columns = new String[]{MESSAGE_COUNT}; From 116aa5e853239b2fd07b5e0bd63abf9d581d0b2a Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:58:16 +1100 Subject: [PATCH 02/12] WIP --- app/build.gradle.kts | 2 +- .../libsession/database/StorageProtocol.kt | 8 +- .../messages/ExpirationConfiguration.kt | 6 +- .../ReceivedMessageProcessor.kt | 16 +- .../utilities/TextSecurePreferences.kt | 7 - .../utilities/recipients/RecipientData.kt | 1 + .../securesms/configs/ConfigToDatabaseSync.kt | 52 +-- .../DisappearingMessagesViewModel.kt | 4 +- .../conversation/v2/ConversationActivityV2.kt | 34 +- .../v2/messages/ControlMessageView.kt | 3 +- .../settings/ConversationSettingsNavHost.kt | 20 +- .../securesms/database/LokiMessageDatabase.kt | 2 +- .../securesms/database/MmsDatabase.kt | 36 +- .../securesms/database/MmsSmsColumns.java | 2 + .../securesms/database/MmsSmsDatabase.java | 26 +- .../securesms/database/MmsSmsDatabaseSQL.kt | 6 +- .../securesms/database/RecipientRepository.kt | 1 + .../securesms/database/SmsDatabase.java | 55 ++- .../securesms/database/Storage.kt | 74 +--- .../securesms/database/ThreadDatabase.java | 399 +----------------- .../securesms/database/ThreadDatabaseExt.kt | 195 +++++++++ .../database/helpers/SQLCipherOpenHelper.java | 11 +- .../database/model/ThreadRecord.java | 95 ----- .../securesms/database/model/ThreadRecord.kt | 48 +++ .../dependencies/OnAppStartupComponents.kt | 3 - .../securesms/home/HomeActivity.kt | 6 +- .../securesms/home/HomeDiffUtil.kt | 2 +- .../notifications/DefaultMessageNotifier.kt | 5 +- .../DeleteNotificationReceiver.kt | 13 +- .../notifications/MarkReadReceiver.kt | 10 +- .../securesms/util/SharedConfigUtils.kt | 48 ++- 31 files changed, 427 insertions(+), 763 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 73c2c09f84..2834925672 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -190,7 +190,7 @@ android { enableUnitTestCoverage = false signingConfig = signingConfigs.getByName("debug") - applicationIdSuffix = ".${name}" +// applicationIdSuffix = ".${name}" enablePermissiveNetworkSecurityConfig(true) devNetDefaultOn(false) setAlternativeAppName("Session Debug") diff --git a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt index 9cb4e34574..78f48fee60 100644 --- a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -148,7 +148,6 @@ interface StorageProtocol { suspend fun getTotalSentProBadges(): Int suspend fun getTotalSentLongMessages(): Int fun setPinned(address: Address, isPinned: Boolean) - fun isRead(threadId: Long) : Boolean fun setThreadCreationDate(threadId: Long, newDate: Long) fun getLastLegacyRecipient(threadRecipient: String): String? fun setLastLegacyRecipient(threadRecipient: String, senderRecipient: String?) @@ -175,7 +174,8 @@ interface StorageProtocol { attachments: List, runThreadUpdate: Boolean ): MessageId? - fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean = false, updateNotification: Boolean = true) + + fun updateConversationLastSeenIfNeeded(threadAddress: Address.Conversable, lastSeenTime: Long) /** * Marks the conversation as read up to and including the message with [messageId]. It will @@ -185,13 +185,11 @@ interface StorageProtocol { */ fun markConversationAsReadUpToMessage(messageId: MessageId) fun markConversationAsUnread(threadId: Long) - fun getLastSeen(threadId: Long): Long + fun getLastSeen(threadAddress: Address.Conversable): Long fun ensureMessageHashesAreSender(hashes: Set, sender: String, closedGroupId: String): Boolean - fun updateThread(threadId: Long, unarchive: Boolean) fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) fun insertMessageRequestResponseFromYou(threadId: Long) fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) - fun conversationHasOutgoing(userPublicKey: String): Boolean fun deleteMessagesByHash(threadId: Long, hashes: List) fun deleteMessagesByUser(threadId: Long, userSessionId: String) fun clearAllMessages(threadId: Long): List diff --git a/app/src/main/java/org/session/libsession/messaging/messages/ExpirationConfiguration.kt b/app/src/main/java/org/session/libsession/messaging/messages/ExpirationConfiguration.kt index 8e3ab18b22..a484a8e9d8 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/ExpirationConfiguration.kt +++ b/app/src/main/java/org/session/libsession/messaging/messages/ExpirationConfiguration.kt @@ -7,11 +7,7 @@ data class ExpirationConfiguration( val expiryMode: ExpiryMode = ExpiryMode.NONE, val updatedTimestampMs: Long = 0 ) { - val isEnabled = expiryMode.expirySeconds > 0 - - companion object { - val isNewConfigEnabled = true - } + val isEnabled get() = expiryMode.expirySeconds > 0 } data class ExpirationDatabaseMetadata( diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt index bae87cf23b..23b1a729c2 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt @@ -101,18 +101,12 @@ class ReceivedMessageProcessor @Inject constructor( try { return block(context) } finally { - for (threadId in context.threadIDs.values) { - if (context.maxOutgoingMessageTimestamp > 0L && - context.maxOutgoingMessageTimestamp > storage.getLastSeen(threadId) - ) { - storage.markConversationAsRead( - threadId, - context.maxOutgoingMessageTimestamp, - force = true - ) - } + for ((threadAddress, threadId) in context.threadIDs) { + storage.updateConversationLastSeenIfNeeded( + threadAddress = threadAddress, + context.maxOutgoingMessageTimestamp, + ) - storage.updateThread(threadId, true) notificationManager.updateNotification(this.context, threadId) } diff --git a/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 810bb9aa71..adcb28a355 100644 --- a/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -7,7 +7,6 @@ import android.provider.Settings import androidx.annotation.ArrayRes import androidx.annotation.StyleRes import androidx.camera.core.CameraSelector -import androidx.core.app.NotificationCompat import androidx.core.content.edit import androidx.preference.PreferenceManager.getDefaultSharedPreferences import dagger.hilt.android.qualifiers.ApplicationContext @@ -263,8 +262,6 @@ interface TextSecurePreferences { var migratedToDisablingKDF: Boolean var migratedToMultiPartConfig: Boolean - var migratedDisappearingMessagesToMessageContent: Boolean - var selectedActivityAliasName: String? var inAppReviewState: String? @@ -1037,10 +1034,6 @@ class AppTextSecurePreferences @Inject constructor( get() = getBooleanPreference(TextSecurePreferences.MIGRATED_TO_MULTIPART_CONFIG, false) set(value) = setBooleanPreference(TextSecurePreferences.MIGRATED_TO_MULTIPART_CONFIG, value) - override var migratedDisappearingMessagesToMessageContent: Boolean - get() = getBooleanPreference("migrated_disappearing_messages_to_message_content", false) - set(value) = setBooleanPreference("migrated_disappearing_messages_to_message_content", value) - override fun getConfigurationMessageSynced(): Boolean { return getBooleanPreference(TextSecurePreferences.CONFIGURATION_SYNCED, false) } diff --git a/app/src/main/java/org/session/libsession/utilities/recipients/RecipientData.kt b/app/src/main/java/org/session/libsession/utilities/recipients/RecipientData.kt index 26506c7ca9..3b2fbe577b 100644 --- a/app/src/main/java/org/session/libsession/utilities/recipients/RecipientData.kt +++ b/app/src/main/java/org/session/libsession/utilities/recipients/RecipientData.kt @@ -141,6 +141,7 @@ sealed interface RecipientData { val approvedMe: Boolean, val blocked: Boolean, val expiryMode: ExpiryMode, + val createdAt: Instant, override val priority: Long, override val proData: ProData?, override val profileUpdatedAt: Instant?, diff --git a/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt b/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt index 4abe47990e..837f330b09 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt @@ -6,14 +6,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.libsession_util.ReadableGroupInfoConfig -import network.loki.messenger.libsession_util.util.Conversation import network.loki.messenger.libsession_util.util.UserPic import org.session.libsession.avatars.AvatarCacheCleaner import org.session.libsession.database.StorageProtocol @@ -26,11 +21,8 @@ import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigFactoryProtocol -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.UserConfigType import org.session.libsession.utilities.allConfigAddresses import org.session.libsession.utilities.getGroup -import org.session.libsession.utilities.userConfigsChanged import org.session.libsignal.crypto.ecc.DjbECPrivateKey import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.ECKeyPair @@ -38,7 +30,6 @@ import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.auth.AuthAwareComponent import org.thoughtcrime.securesms.auth.LoggedInState -import org.thoughtcrime.securesms.auth.LoginStateRepository import org.thoughtcrime.securesms.database.CommunityDatabase import org.thoughtcrime.securesms.database.DraftDatabase import org.thoughtcrime.securesms.database.GroupDatabase @@ -53,11 +44,8 @@ import org.thoughtcrime.securesms.database.SmsDatabase import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.dependencies.ManagerScope -import org.thoughtcrime.securesms.dependencies.OnAppStartupComponent import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.util.SessionMetaProtocol -import org.thoughtcrime.securesms.util.castAwayType -import java.util.EnumSet import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -92,18 +80,10 @@ class ConfigToDatabaseSync @Inject constructor( @param:ManagerScope private val scope: CoroutineScope, ) : AuthAwareComponent { override suspend fun doWhileLoggedIn(loggedInState: LoggedInState) { - combine( - conversationRepository.conversationListAddressesFlow, - configFactory.userConfigsChanged(EnumSet.of(UserConfigType.CONVO_INFO_VOLATILE)) - .castAwayType() - .onStart { emit(Unit) } - .map { _ -> configFactory.withUserConfigs { it.convoInfoVolatile.all() } }, - ::Pair - ).distinctUntilChanged() - .collectLatest { (conversations, convoInfo) -> + conversationRepository.conversationListAddressesFlow + .collectLatest { conversations -> try { ensureConversations(conversations, loggedInState.accountId) - updateConvoVolatile(convoInfo) } catch (e: Exception) { Log.e(TAG, "Error updating conversations from config", e) } @@ -337,32 +317,4 @@ class ConfigToDatabaseSync @Inject constructor( private val MmsMessageRecord.containsAttachment: Boolean get() = this.slideDeck.slides.isNotEmpty() && !this.slideDeck.isVoiceNote - - private fun updateConvoVolatile(convos: List) { - for (conversation in convos.asSequence().filterNotNull()) { - val address: Address.Conversable = when (conversation) { - is Conversation.OneToOne -> Address.Standard(AccountId(conversation.accountId)) - is Conversation.LegacyGroup -> Address.LegacyGroup(conversation.groupId) - is Conversation.Community -> Address.Community(serverUrl = conversation.baseCommunityInfo.baseUrl, room = conversation.baseCommunityInfo.room) - is Conversation.ClosedGroup -> Address.Group(AccountId(conversation.accountId)) // New groups will be managed bia libsession - is Conversation.BlindedOneToOne -> { - // Not supported yet - continue - } - } - - val threadId = threadDatabase.getThreadIdIfExistsFor(address) - - if (threadId != -1L) { - if (conversation.lastRead > storage.getLastSeen(threadId)) { - storage.markConversationAsRead( - threadId, - conversation.lastRead, - force = true - ) - storage.updateThread(threadId, false) - } - } - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt index cb9a40ca23..bba9238d63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt @@ -29,7 +29,6 @@ import org.thoughtcrime.securesms.ui.UINavigator @HiltViewModel(assistedFactory = DisappearingMessagesViewModel.Factory::class) class DisappearingMessagesViewModel @AssistedInject constructor( @Assisted private val address: Address, - @Assisted("isNewConfigEnabled") private val isNewConfigEnabled: Boolean, @Assisted("showDebugOptions") private val showDebugOptions: Boolean, @Assisted private val navigator: UINavigator, @param:ApplicationContext private val context: Context, @@ -39,7 +38,7 @@ class DisappearingMessagesViewModel @AssistedInject constructor( private val _state = MutableStateFlow( State( - isNewConfigEnabled = isNewConfigEnabled, + isNewConfigEnabled = true, showDebugOptions = showDebugOptions ) ) @@ -95,7 +94,6 @@ class DisappearingMessagesViewModel @AssistedInject constructor( interface Factory { fun create( address: Address, - @Assisted("isNewConfigEnabled") isNewConfigEnabled: Boolean, @Assisted("showDebugOptions") showDebugOptions: Boolean, navigator: UINavigator ): DisappearingMessagesViewModel diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 9a2f871fe5..67847e84bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -84,7 +84,6 @@ import network.loki.messenger.databinding.ActivityConversationV2Binding import network.loki.messenger.libsession_util.util.ExpiryMode import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.GroupManagerV2 -import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.applyExpiryMode import org.session.libsession.messaging.messages.control.DataExtractionNotification import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage @@ -209,7 +208,6 @@ import org.thoughtcrime.securesms.util.adapter.runWhenLaidOut import org.thoughtcrime.securesms.util.drawToBitmap import org.thoughtcrime.securesms.util.fadeIn import org.thoughtcrime.securesms.util.fadeOut -import org.thoughtcrime.securesms.util.getConversationUnread import org.thoughtcrime.securesms.util.isFullyScrolled import org.thoughtcrime.securesms.util.isNearBottom import org.thoughtcrime.securesms.util.push @@ -375,7 +373,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, private val adapter by lazy { val adapter = ConversationAdapter( this, - storage.getLastSeen(viewModel.threadId), + storage.getLastSeen(viewModel.address), false, onItemPress = { message, position, view, event -> handlePress(message, position, view, event) @@ -633,9 +631,10 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, try { when (it) { is Long -> { - if (storage.getLastSeen(viewModel.threadId) < it) { - storage.markConversationAsRead(viewModel.threadId, it) - } + storage.updateConversationLastSeenIfNeeded( + viewModel.address, + it + ) } is MessageId -> { @@ -826,24 +825,6 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } else { if (firstLoad.getAndSet(false)) { scrollToFirstUnreadMessageOrBottom() - - // On the first load, check if there unread messages - if (unreadCount == 0 && adapter.itemCount > 0) { - lifecycleScope.launch(Dispatchers.Default) { - val isUnread = configFactory.withUserConfigs { - it.convoInfoVolatile.getConversationUnread( - viewModel.address, - ) - } - - if (isUnread) { - storage.markConversationAsRead( - viewModel.threadId, - clock.currentTimeMills() - ) - } - } - } } else { // If there are new data updated, we'll try to stay scrolled at the bottom (if we were at the bottom). // scrolled to bottom has a leniency of 50dp, so if we are within the 50dp but not fully at the bottom, scroll down @@ -1049,8 +1030,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, private fun setUpOutdatedClientBanner() { val legacyRecipient = viewModel.legacyBannerRecipient(this) - val shouldShowLegacy = ExpirationConfiguration.isNewConfigEnabled && - legacyRecipient != null + val shouldShowLegacy = legacyRecipient != null binding.conversationHeader.outdatedDisappearingBanner.isVisible = shouldShowLegacy if (shouldShowLegacy) { @@ -1244,7 +1224,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, return } - val lastSeenTimestamp = threadDb.getLastSeenAndHasSent(viewModel.threadId).first() + val lastSeenTimestamp = storage.getLastSeen(viewModel.address) val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return binding.conversationRecyclerView.runWhenLaidOut { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt index 4cc0276c5e..072b7c8577 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt @@ -114,8 +114,7 @@ class ControlMessageView : LinearLayout { expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn) } - followSetting.isVisible = ExpirationConfiguration.isNewConfigEnabled - && !message.isOutgoing + followSetting.isVisible = !message.isOutgoing && messageContent.expiryMode != (message.individualRecipient?.expiryMode ?: ExpiryMode.NONE) && !threadRecipient.isGroupOrCommunityRecipient diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsNavHost.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsNavHost.kt index 3460c6f325..6ea58edd49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsNavHost.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsNavHost.kt @@ -14,31 +14,39 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.NavBackStackEntry import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable import network.loki.messenger.BuildConfig -import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.utilities.Address import org.session.libsignal.utilities.AccountId import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesViewModel import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.DisappearingMessagesScreen -import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.* +import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteAllMedia +import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteConversationSettings +import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteDisappearingMessages +import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteGroupMembers +import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteInviteAccountIdToGroup +import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteInviteToCommunity +import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteInviteToGroup +import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteManageAdmins +import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteManageMembers +import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteNotifications +import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RoutePromoteMembers import org.thoughtcrime.securesms.conversation.v2.settings.notification.NotificationSettingsScreen import org.thoughtcrime.securesms.conversation.v2.settings.notification.NotificationSettingsViewModel -import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel import org.thoughtcrime.securesms.groups.GroupMembersViewModel import org.thoughtcrime.securesms.groups.InviteMembersViewModel import org.thoughtcrime.securesms.groups.ManageGroupAdminsViewModel +import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel import org.thoughtcrime.securesms.groups.PromoteMembersViewModel -import org.thoughtcrime.securesms.groups.compose.ManageGroupMembersScreen import org.thoughtcrime.securesms.groups.compose.GroupMembersScreen import org.thoughtcrime.securesms.groups.compose.InviteAccountIdScreen import org.thoughtcrime.securesms.groups.compose.InviteContactsScreen import org.thoughtcrime.securesms.groups.compose.ManageGroupAdminsScreen +import org.thoughtcrime.securesms.groups.compose.ManageGroupMembersScreen import org.thoughtcrime.securesms.groups.compose.PromoteMembersScreen import org.thoughtcrime.securesms.home.startconversation.newmessage.NewMessageViewModel import org.thoughtcrime.securesms.home.startconversation.newmessage.State @@ -46,7 +54,6 @@ import org.thoughtcrime.securesms.media.MediaOverviewScreen import org.thoughtcrime.securesms.media.MediaOverviewViewModel import org.thoughtcrime.securesms.ui.NavigationAction import org.thoughtcrime.securesms.ui.ObserveAsEvents -import org.thoughtcrime.securesms.ui.OpenURLAlertDialog import org.thoughtcrime.securesms.ui.UINavigator import org.thoughtcrime.securesms.ui.handleIntent import org.thoughtcrime.securesms.ui.horizontalSlideComposable @@ -414,7 +421,6 @@ fun ConversationSettingsNavHost( hiltViewModel { factory -> factory.create( address = address, - isNewConfigEnabled = ExpirationConfiguration.isNewConfigEnabled, showDebugOptions = BuildConfig.BUILD_TYPE != "release", navigator = navigator ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt index 2283612690..a924e18bf9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt @@ -29,7 +29,7 @@ class LokiMessageDatabase(context: Context, helper: Provider if (cursor.moveToFirst()) { @@ -212,7 +206,6 @@ class MmsDatabase @Inject constructor( ) groupReceiptDatabase .update(ourAddress, id, status, timestamp) - threadDatabase.update(threadId, false) } } } @@ -297,7 +290,7 @@ class MmsDatabase @Inject constructor( " WHERE " + ID + " = ?", arrayOf(id.toString() + "") ) if (threadId.isPresent) { - threadDatabase.update(threadId.get(), false) + threadDatabase.notifyThreadUpdated(threadId.get()) } } @@ -513,7 +506,7 @@ class MmsDatabase @Inject constructor( contentValues = contentValues, ) if (runThreadUpdate) { - threadDatabase.update(threadId, true) + threadDatabase.notifyThreadUpdated(threadId) } return Optional.of(InsertResult(messageId, threadId)) } @@ -639,16 +632,7 @@ class MmsDatabase @Inject constructor( -1 ) } - with (threadDatabase) { - val lastSeen = getLastSeenAndHasSent(threadId).first() - if (lastSeen < message.sentTimeMillis) { - setLastSeen(threadId, message.sentTimeMillis) - } - setHasSent(threadId, true) - if (runThreadUpdate) { - update(threadId, true) - } - } + return messageId } @@ -752,7 +736,7 @@ class MmsDatabase @Inject constructor( if (updateThread) { for (threadId in deletedMessagesThreadIDs) { - threadDatabase.update(threadId, false) + threadDatabase.notifyThreadUpdated(threadId) } } @@ -971,9 +955,8 @@ class MmsDatabase @Inject constructor( * @param outgoing if true only delete outgoing messages, if false only delete incoming messages, if null delete both. */ private fun deleteExpirationTimerMessages(threadId: Long, outgoing: Boolean? = null) { - val outgoingClause = outgoing?.takeIf { ExpirationConfiguration.isNewConfigEnabled }?.let { - val comparison = if (it) "IN" else "NOT IN" - " AND $MESSAGE_BOX & ${MmsSmsColumns.Types.BASE_TYPE_MASK} $comparison (${MmsSmsColumns.Types.OUTGOING_MESSAGE_TYPES.joinToString()})" + val outgoingClause = outgoing?.let { + " AND $IS_OUTGOING" } ?: "" val where = "$THREAD_ID = ? AND $MESSAGE_CONTENT->>'$.${MessageContent.DISCRIMINATOR}' == '${DisappearingMessageUpdate.TYPE_NAME}' " + outgoingClause @@ -1262,5 +1245,10 @@ class MmsDatabase @Inject constructor( db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN $PRO_PROFILE_FEATURES INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN $PRO_MESSAGE_FEATURES INTEGER NOT NULL DEFAULT 0") } + + fun addOutgoingColumn(db: SupportSQLiteDatabase) { + val outgoingTypeSet = MmsSmsColumns.Types.OUTGOING_MESSAGE_TYPES.joinToString(separator = ",", prefix = "(", postfix = ")") + db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN $IS_OUTGOING BOOLEAN GENERATED ALWAYS AS (($MESSAGE_BOX & ${MmsSmsColumns.Types.BASE_TYPE_MASK}) IN ${outgoingTypeSet}) VIRTUAL") + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 5decc3301d..fac2991415 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -46,6 +46,8 @@ public interface MmsSmsColumns { public static final String PRO_MESSAGE_FEATURES = "pro_message_features"; public static final String PRO_PROFILE_FEATURES = "pro_profile_features"; + public static final String IS_OUTGOING = "is_outgoing"; + public static class Types { protected static final long TOTAL_MASK = 0xFFFFFFFF; 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 1aa7363678..55f244b671 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -18,6 +18,7 @@ import static org.thoughtcrime.securesms.database.MmsDatabase.MESSAGE_BOX; import static org.thoughtcrime.securesms.database.MmsSmsColumns.ID; +import static org.thoughtcrime.securesms.database.MmsSmsColumns.IS_OUTGOING; import static org.thoughtcrime.securesms.database.MmsSmsColumns.NOTIFIED; import static org.thoughtcrime.securesms.database.MmsSmsColumns.READ; import static org.thoughtcrime.securesms.database.MmsSmsColumns.THREAD_ID; @@ -333,24 +334,14 @@ public Pair getMaxTimestampInThreadUpTo(@NonNull final MessageId mes } } - private String buildOutgoingConditionForNotifications() { - return "(" + TRANSPORT + " = '" + MMS_TRANSPORT + "' AND " + - "(" + MESSAGE_BOX + " & " + MmsSmsColumns.Types.BASE_TYPE_MASK + ") IN (" + buildOutgoingTypesList() + "))" + - " OR " + - "(" + TRANSPORT + " = '" + SMS_TRANSPORT + "' AND " + - "(" + SmsDatabase.TYPE + " & " + MmsSmsColumns.Types.BASE_TYPE_MASK + ") IN (" + buildOutgoingTypesList() + "))"; - } - public Cursor getUnreadIncomingForNotifications(int maxRows) { - String outgoing = buildOutgoingConditionForNotifications(); - String selection = "(" + READ + " = 0 AND " + NOTIFIED + " = 0 AND NOT (" + outgoing + "))"; + String selection = "(" + READ + " = 0 AND " + NOTIFIED + " = 0 AND NOT (" + IS_OUTGOING + "))"; String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; String limitStr = maxRows > 0 ? String.valueOf(maxRows) : null; return queryTables(PROJECTION_ALL, selection, true, null, order, limitStr); } public Cursor getOutgoingWithUnseenReactionsForNotifications(int maxRows) { - String outgoing = buildOutgoingConditionForNotifications(); String lastSeenQuery = "SELECT " + ThreadDatabase.LAST_SEEN + " FROM " + ThreadDatabase.TABLE_NAME + @@ -361,7 +352,7 @@ public Cursor getOutgoingWithUnseenReactionsForNotifications(int maxRows) { String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; String limitStr = maxRows > 0 ? String.valueOf(maxRows) : null; - return queryTables(PROJECTION_ALL, outgoing, true, reactionSelection, order, limitStr); + return queryTables(PROJECTION_ALL, IS_OUTGOING, true, reactionSelection, order, limitStr); } public Set
getAllReferencedAddresses() { @@ -384,17 +375,6 @@ public Set
getAllReferencedAddresses() { return out; } - /** Builds the comma-separated list of base types that represent - * *outgoing* messages (same helper as before). */ - private String buildOutgoingTypesList() { - long[] types = MmsSmsColumns.Types.OUTGOING_MESSAGE_TYPES; - StringBuilder sb = new StringBuilder(types.length * 3); - for (int i = 0; i < types.length; i++) { - if (i > 0) sb.append(','); - sb.append(types[i]); - } - return sb.toString(); - } public int getUnreadCount(long threadId) { String selection = READ + " = 0 AND " + NOTIFIED + " = 0 AND " + MmsSmsColumns.THREAD_ID + " = " + threadId; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabaseSQL.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabaseSQL.kt index e4682ee9e6..fb6bdea78d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabaseSQL.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabaseSQL.kt @@ -113,7 +113,8 @@ fun buildMmsSmsCombinedQuery( ${MmsSmsColumns.HAS_MENTION}, ($smsHashQuery) AS ${MmsSmsColumns.SERVER_HASH}, ${MmsSmsColumns.PRO_MESSAGE_FEATURES}, - ${MmsSmsColumns.PRO_PROFILE_FEATURES} + ${MmsSmsColumns.PRO_PROFILE_FEATURES}, + ${MmsSmsColumns.IS_OUTGOING} FROM ${SmsDatabase.TABLE_NAME} $whereStatement """ @@ -204,7 +205,8 @@ fun buildMmsSmsCombinedQuery( ${MmsSmsColumns.HAS_MENTION}, ($mmsHashQuery) AS ${MmsSmsColumns.SERVER_HASH}, ${MmsSmsColumns.PRO_MESSAGE_FEATURES}, - ${MmsSmsColumns.PRO_PROFILE_FEATURES} + ${MmsSmsColumns.PRO_PROFILE_FEATURES}, + ${MmsSmsColumns.IS_OUTGOING} FROM ${MmsDatabase.TABLE_NAME} $whereStatement """ diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt index 2cd4c8c8d9..8bb0245a00 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt @@ -641,6 +641,7 @@ class RecipientRepository @Inject constructor( priority = contact.priority, proData = null, // final ProData will be calculated later profileUpdatedAt = contact.profileUpdatedEpochSeconds.secondsToInstant(), + createdAt = Instant.ofEpochSecond(contact.createdEpochSeconds) ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 7a6204dfe6..a0d02c1491 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -50,6 +50,7 @@ import java.io.Closeable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; @@ -63,6 +64,8 @@ import dagger.Lazy; import dagger.hilt.android.qualifiers.ApplicationContext; +import kotlin.collections.ArraysKt; +import kotlin.collections.CollectionsKt; import network.loki.messenger.libsession_util.protocol.ProFeature; import network.loki.messenger.libsession_util.protocol.ProMessageFeature; import network.loki.messenger.libsession_util.protocol.ProProfileFeature; @@ -142,6 +145,21 @@ public static void addProFeatureColumns(SupportSQLiteDatabase db) { db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + PRO_PROFILE_FEATURES + " INTEGER NOT NULL DEFAULT 0"); } + public static void addOutgoingColumn(SupportSQLiteDatabase db) { + final String allOutgoingMessageTypeSet = ArraysKt.joinToString( + Types.OUTGOING_MESSAGE_TYPES, + ",", + "(", + ")", + -1, + "", + (value) -> Long.toString(value) + ); + + db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + IS_OUTGOING + + " BOOLEAN GENERATED ALWAYS AS ((" + TYPE + " & " + MmsSmsColumns.Types.BASE_TYPE_MASK +") IN " + allOutgoingMessageTypeSet + ") VIRTUAL"); + } + private static final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache(); private static final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache(); @@ -175,7 +193,7 @@ private void updateTypeBitmask(long id, long maskOff, long maskOn) { long threadId = getThreadIdForMessage(id); - threadDatabase.get().update(threadId, false); + threadDatabase.get().notifyThreadUpdated(threadId); } public long getThreadIdForMessage(long id) { @@ -254,7 +272,7 @@ public void markExpireStarted(long id, long startedAtTimestamp) { "WHERE " + ID + " = ? RETURNING " + THREAD_ID, startedAtTimestamp, id)) { if (cursor.moveToNext()) { long threadId = cursor.getLong(0); - threadDatabase.get().update(threadId, false); + threadDatabase.get().notifyThreadUpdated(threadId); } } } @@ -306,17 +324,7 @@ private int getOutgoingProFeatureCountInternal(@NonNull String columnName, long SQLiteDatabase db = getReadableDatabase(); // outgoing clause - StringBuilder outgoingTypes = new StringBuilder(); - long[] types = MmsSmsColumns.Types.OUTGOING_MESSAGE_TYPES; - for (int i = 0; i < types.length; i++) { - if (i > 0) outgoingTypes.append(","); - outgoingTypes.append(types[i]); - } - - String outgoingSelection = - "(" + TYPE + " & " + MmsSmsColumns.Types.BASE_TYPE_MASK + ") IN (" + outgoingTypes + ")"; - - String where = "(" + columnName + " & " + featureMask + ") != 0 AND " + outgoingSelection; + String where = "(" + columnName + " & " + featureMask + ") != 0 AND " + IS_OUTGOING; try (Cursor cursor = db.query(TABLE_NAME, new String[]{"COUNT(*)"}, where, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { @@ -378,7 +386,7 @@ public void incrementReceiptCount(SyncMessageId messageId, boolean deliveryRecei ID + " = ?", new String[] {String.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))}); - threadDatabase.get().update(threadId, false); + threadDatabase.get().notifyThreadUpdated(threadId); foundMessage = true; } } @@ -486,7 +494,7 @@ protected Optional insertMessageInbox(IncomingTextMessage message, long messageId = db.insert(TABLE_NAME, null, values); if (runThreadUpdate) { - threadDatabase.get().update(threadId, true); + threadDatabase.get().notifyThreadUpdated(threadId); } return Optional.of(new InsertResult(messageId, threadId)); @@ -565,20 +573,7 @@ public long insertMessageOutbox(long threadId, OutgoingTextMessage message, return -1; } - SQLiteDatabase db = getWritableDatabase(); - long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues); - - if (runThreadUpdate) { - threadDatabase.get().update(threadId, true); - } - long lastSeen = threadDatabase.get().getLastSeenAndHasSent(threadId).first(); - if (lastSeen < message.getSentTimestampMillis()) { - threadDatabase.get().setLastSeen(threadId, message.getSentTimestampMillis()); - } - - threadDatabase.get().setHasSent(threadId, true); - - return messageId; + return getWritableDatabase().insert(TABLE_NAME, ADDRESS, contentValues); } @Override public List getExpiredMessageIDs(long nowMills) { @@ -672,7 +667,7 @@ private boolean doDeleteMessages(final boolean updateThread, @NonNull final Stri if (updateThread) { for (final long threadId : deletedMessageThreadIds) { - threadDatabase.get().update(threadId, false); + threadDatabase.get().notifyThreadUpdated(threadId); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index d964fd249a..2c2d148341 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -73,8 +73,8 @@ import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.util.FilenameUtils import org.thoughtcrime.securesms.util.SessionMetaProtocol -import java.time.Instant -import java.time.ZoneId +import org.thoughtcrime.securesms.util.get +import org.thoughtcrime.securesms.util.getOrConstructConvo import javax.inject.Inject import javax.inject.Provider import javax.inject.Singleton @@ -125,8 +125,8 @@ open class Storage @Inject constructor( return attachmentDatabase.getAttachmentsForMessage(mmsMessageId) } - override fun getLastSeen(threadId: Long): Long { - return threadDatabase.getLastSeenAndHasSent(threadId)?.first() ?: 0L + override fun getLastSeen(threadAddress: Address.Conversable): Long { + return configFactory.withUserConfigs { it.convoInfoVolatile.get(threadAddress) }?.lastRead ?: 0L } override fun ensureMessageHashesAreSender( @@ -175,31 +175,23 @@ open class Storage @Inject constructor( return messages.map { it.second } // return the message hashes } - override fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean, updateNotification: Boolean) { - val threadDb = threadDatabase - val threadAddress = threadDb.getRecipientForThreadId(threadId) ?: return - // don't set the last read in the volatile if we didn't set it in the DB - if (!threadDb.markAllAsRead(threadId, lastSeenTime, force, updateNotification) && !force) return - - // don't process configs for inbox recipients - if (threadAddress.isCommunityInbox) return - - val currentLastRead = threadDb.getLastSeenAndHasSent(threadId).first() - + override fun updateConversationLastSeenIfNeeded( + threadAddress: Address.Conversable, + lastSeenTime: Long + ) { configFactory.withMutableUserConfigs { configs -> - val config = configs.convoInfoVolatile - val convo = getConvo( - threadAddress = threadAddress, - config = config, - groupConfig = configs.userGroups - ) ?: return@withMutableUserConfigs - convo.lastRead = lastSeenTime + val convo = configs.getOrConstructConvo(threadAddress) + val currentLastRead = convo.lastRead - if(convo.unread){ + if (convo.unread) { convo.unread = lastSeenTime < currentLastRead } - config.set(convo) + if (lastSeenTime > currentLastRead) { + convo.lastRead = lastSeenTime + } + + configs.convoInfoVolatile.set(convo) } } @@ -208,16 +200,11 @@ open class Storage @Inject constructor( if (maxTimestampMillsAndThreadId != null) { val threadId = maxTimestampMillsAndThreadId.second val maxTimestamp = maxTimestampMillsAndThreadId.first - if (getLastSeen(threadId) < maxTimestamp) { - Log.d(TAG, "Marking last seen for thread $threadId as ${Instant.ofEpochMilli(maxTimestamp).atZone( - ZoneId.systemDefault())}") - markConversationAsRead( - threadId = threadId, - lastSeenTime = maxTimestamp, - force = false, - updateNotification = true - ) - } + val threadAddress = threadDatabase.getRecipientForThreadId(threadId) as? Address.Conversable ?: return + updateConversationLastSeenIfNeeded( + threadAddress = threadAddress, + lastSeenTime = maxTimestamp + ) } } @@ -272,11 +259,6 @@ open class Storage @Inject constructor( } } - override fun updateThread(threadId: Long, unarchive: Boolean) { - val threadDb = threadDatabase - threadDb.update(threadId, unarchive) - } - override fun persist( threadRecipient: Recipient, message: VisibleMessage, @@ -1005,11 +987,6 @@ open class Storage @Inject constructor( } } - override fun isRead(threadId: Long) : Boolean { - val threadDB = threadDatabase - return threadDB.isRead(threadId) - } - override fun setThreadCreationDate(threadId: Long, newDate: Long) { val threadDb = threadDatabase threadDb.setCreationDate(threadId, newDate) @@ -1121,15 +1098,6 @@ open class Storage @Inject constructor( smsDatabase.insertCallMessage(callMessage) } - override fun conversationHasOutgoing(userPublicKey: String): Boolean { - val database = threadDatabase - val threadId = database.getThreadIdIfExistsFor(userPublicKey) - - if (threadId == -1L) return false - - return database.getLastSeenAndHasSent(threadId).second() ?: false - } - override fun getLastInboxMessageId(server: String): Long? { return lokiAPIDatabase.getLastInboxMessageId(server) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 724bad0181..687ebf3377 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -17,13 +17,11 @@ */ package org.thoughtcrime.securesms.database; -import static org.thoughtcrime.securesms.database.GroupDatabase.GROUP_ID; import static org.thoughtcrime.securesms.database.GroupDatabase.TYPED_GROUP_PROJECTION; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; -import android.net.Uri; import com.annimon.stream.Stream; @@ -35,31 +33,15 @@ import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.AddressKt; import org.session.libsession.utilities.ConfigFactoryProtocol; -import org.session.libsession.utilities.ConfigFactoryProtocolKt; import org.session.libsession.utilities.GroupUtil; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.AccountId; import org.session.libsignal.utilities.Log; -import org.session.libsignal.utilities.Pair; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; -import org.thoughtcrime.securesms.database.model.GroupThreadStatus; -import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.content.MessageContent; -import org.thoughtcrime.securesms.dependencies.OnAppStartupComponent; -import org.thoughtcrime.securesms.mms.Slide; -import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.notifications.MarkReadProcessor; -import org.thoughtcrime.securesms.util.SharedConfigUtilsKt; -import java.io.Closeable; -import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -79,11 +61,9 @@ import kotlinx.coroutines.flow.Flow; import kotlinx.coroutines.flow.MutableSharedFlow; import kotlinx.coroutines.flow.SharedFlowKt; -import kotlinx.serialization.json.Json; -import network.loki.messenger.libsession_util.util.GroupInfo; @Singleton -public class ThreadDatabase extends Database implements OnAppStartupComponent { +public class ThreadDatabase extends Database { private static final String TAG = ThreadDatabase.class.getSimpleName(); @@ -229,12 +209,10 @@ public static void migrateLegacyCommunityAddresses(final SQLiteDatabase db) { private final MutableSharedFlow updateNotifications = SharedFlowKt.MutableSharedFlow(0, 256, BufferOverflow.DROP_OLDEST); - private final Json json; - private final TextSecurePreferences prefs; - private final Lazy<@NonNull RecipientRepository> recipientRepository; - private final Lazy<@NonNull MmsSmsDatabase> mmsSmsDatabase; - private final Lazy<@NonNull ConfigFactoryProtocol> configFactory; + final Lazy<@NonNull RecipientRepository> recipientRepository; + final Lazy<@NonNull MmsSmsDatabase> mmsSmsDatabase; + final Lazy<@NonNull ConfigFactoryProtocol> configFactory; private final Lazy<@NonNull MessageNotifier> messageNotifier; private final Lazy<@NonNull MmsDatabase> mmsDatabase; private final Lazy<@NonNull SmsDatabase> smsDatabase; @@ -249,9 +227,7 @@ public ThreadDatabase(@dagger.hilt.android.qualifiers.ApplicationContext Context Lazy<@NonNull MessageNotifier> messageNotifier, Lazy<@NonNull MmsDatabase> mmsDatabase, Lazy<@NonNull SmsDatabase> smsDatabase, - Lazy<@NonNull MarkReadProcessor> markReadProcessor, - TextSecurePreferences prefs, - Json json) { + Lazy<@NonNull MarkReadProcessor> markReadProcessor) { super(context, databaseHelper); this.recipientRepository = recipientRepository; this.mmsSmsDatabase = mmsSmsDatabase; @@ -260,17 +236,6 @@ public ThreadDatabase(@dagger.hilt.android.qualifiers.ApplicationContext Context this.mmsDatabase = mmsDatabase; this.smsDatabase = smsDatabase; this.markReadProcessor = markReadProcessor; - - this.json = json; - this.prefs = prefs; - } - - @Override - public void onPostAppStarted() { - if (!prefs.getMigratedDisappearingMessagesToMessageContent()) { - migrateDisappearingMessagesToMessageContent(); - prefs.setMigratedDisappearingMessagesToMessageContent(true); - } } @NonNull @@ -278,54 +243,6 @@ public Flow getUpdateNotifications() { return updateNotifications; } - // As we migrate disappearing messages to MessageContent, we need to ensure that - // if they appear in the snippet, they have to be re-generated with the new MessageContent. - private void migrateDisappearingMessagesToMessageContent() { - String sql = "SELECT " + ID + " FROM " + TABLE_NAME + - " WHERE " + SNIPPET_TYPE + " & " + MmsSmsColumns.Types.EXPIRATION_TIMER_UPDATE_BIT + " != 0"; - try (final Cursor cursor = getReadableDatabase().rawQuery(sql)) { - while (cursor.moveToNext()) { - update(cursor.getLong(0), false); - } - } - } - - private void updateThread(long threadId, long count, String body, @Nullable Uri attachment, @Nullable MessageContent messageContent, - long date, int status, int deliveryReceiptCount, long type, boolean unarchive, - long expiresIn, int readReceiptCount) - { - ContentValues contentValues = new ContentValues(7); - contentValues.put(THREAD_CREATION_DATE, date - date % 1000); - contentValues.put(MESSAGE_COUNT, count); - if (!body.isEmpty()) { - contentValues.put(SNIPPET, body); - } - contentValues.put(SNIPPET_CONTENT, messageContent == null ? null : json.encodeToString(MessageContent.Companion.serializer(), messageContent)); - contentValues.put(SNIPPET_URI, attachment == null ? null : attachment.toString()); - contentValues.put(SNIPPET_TYPE, type); - contentValues.put(STATUS, status); - contentValues.put(DELIVERY_RECEIPT_COUNT, deliveryReceiptCount); - contentValues.put(READ_RECEIPT_COUNT, readReceiptCount); - contentValues.put(EXPIRES_IN, expiresIn); - - if (unarchive) { contentValues.put(ARCHIVED, 0); } - - SQLiteDatabase db = getWritableDatabase(); - db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""}); - - updateUnreadCounts(threadId); - } - - public void clearSnippet(long threadId){ - ContentValues contentValues = new ContentValues(1); - - contentValues.put(SNIPPET, ""); - contentValues.put(SNIPPET_CONTENT, ""); - - SQLiteDatabase db = getWritableDatabase(); - db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""}); - notifyThreadUpdated(threadId); - } public void deleteThread(long threadId) { SQLiteDatabase db = getWritableDatabase(); @@ -415,7 +332,6 @@ public void trimThreadBefore(long threadId, long timestamp) { Log.i("ThreadDatabase", "Trimming thread: " + threadId + " before :"+timestamp); smsDatabase.get().deleteMessagesInThreadBeforeDate(threadId, timestamp); mmsDatabase.get().deleteMessagesInThreadBeforeDate(threadId, timestamp, false); - update(threadId, false); notifyThreadUpdated(threadId); } @@ -468,106 +384,12 @@ public void setCreationDate(long threadId, long date) { if (updated > 0) notifyThreadUpdated(threadId); } - public void setCreationDates(@NonNull final Map dates) { - if (dates.isEmpty()) return; - - final SQLiteDatabase db = getWritableDatabase(); - db.beginTransaction(); - - ContentValues contentValues = new ContentValues(1); - - try { - for (Map.Entry entry : dates.entrySet()) { - contentValues.put(THREAD_CREATION_DATE, entry.getValue().toInstant().toEpochMilli()); - db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(entry.getKey())}); - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - for (Long threadId : dates.keySet()) { - notifyThreadUpdated(threadId); - } - } - - @NonNull - public List getThreads(@Nullable Collection addresses) { + @NonNull + public List getThreads(@Nullable Collection addresses) { if (addresses == null || addresses.isEmpty()) return Collections.emptyList(); - final String query = createQuery( - TABLE_NAME + "." + ADDRESS + " IN (SELECT value FROM json_each(?))" - ); - - final String selectionArg = new JSONArray(CollectionsKt.map(addresses, Address::getAddress)).toString(); - - try (final Cursor cursor = getReadableDatabase().rawQuery(query, selectionArg)) { - final ArrayList threads = new ArrayList<>(cursor.getCount()); - final Reader reader = new Reader(cursor); - ThreadRecord thread; - while ((thread = reader.getNext()) != null) { - threads.add(thread); - } - - return threads; - } - } - - /** - * @param threadId - * @param timestamp - * @return true if we have set the last seen for the thread, false if there were no messages in the thread - */ - public boolean setLastSeen(long threadId, long timestamp) { - // edge case where we set the last seen time for a conversation before it loads messages (joining community for example) - Address forThreadId = getRecipientForThreadId(threadId); - if (mmsSmsDatabase.get().getConversationCount(threadId) <= 0 && forThreadId != null && AddressKt.isCommunity(forThreadId)) return false; - - SQLiteDatabase db = getWritableDatabase(); - - ContentValues contentValues = new ContentValues(1); - long lastSeenTime = timestamp == -1 ? SnodeAPI.getNowWithOffset() : timestamp; - contentValues.put(LAST_SEEN, lastSeenTime); - db.beginTransaction(); - db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)}); - updateUnreadCounts(threadId); - db.setTransactionSuccessful(); - db.endTransaction(); - notifyThreadUpdated(threadId); - return true; - } - - private void updateUnreadCounts(long threadId) { - String smsCountSubQuery = "SELECT COUNT(*) FROM "+SmsDatabase.TABLE_NAME+" AS s WHERE t."+ID+" = s."+SmsDatabase.THREAD_ID+" AND s."+SmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND s."+SmsDatabase.READ+" = 0"; - String smsMentionCountSubQuery = "SELECT COUNT(*) FROM "+SmsDatabase.TABLE_NAME+" AS s WHERE t."+ID+" = s."+SmsDatabase.THREAD_ID+" AND s."+SmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND s."+SmsDatabase.READ+" = 0 AND s."+SmsDatabase.HAS_MENTION+" = 1"; - String smsReactionCountSubQuery = "SELECT COUNT(*) FROM "+SmsDatabase.TABLE_NAME+" AS s WHERE t."+ID+" = s."+SmsDatabase.THREAD_ID+" AND s."+SmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND s."+SmsDatabase.REACTIONS_UNREAD+" = 1"; - String mmsCountSubQuery = "SELECT COUNT(*) FROM "+MmsDatabase.TABLE_NAME+" AS m WHERE t."+ID+" = m."+MmsDatabase.THREAD_ID+" AND m."+MmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND m."+MmsDatabase.READ+" = 0"; - String mmsMentionCountSubQuery = "SELECT COUNT(*) FROM "+MmsDatabase.TABLE_NAME+" AS m WHERE t."+ID+" = m."+MmsDatabase.THREAD_ID+" AND m."+MmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND m."+MmsDatabase.READ+" = 0 AND m."+MmsDatabase.HAS_MENTION+" = 1"; - String mmsReactionCountSubQuery = "SELECT COUNT(*) FROM "+MmsDatabase.TABLE_NAME+" AS m WHERE t."+ID+" = m."+MmsDatabase.THREAD_ID+" AND m."+MmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND m."+MmsDatabase.REACTIONS_UNREAD+" = 1"; - String allSmsUnread = "(("+smsCountSubQuery+") + ("+smsReactionCountSubQuery+"))"; - String allMmsUnread = "(("+mmsCountSubQuery+") + ("+mmsReactionCountSubQuery+"))"; - String allUnread = "(("+allSmsUnread+") + ("+allMmsUnread+"))"; - String allUnreadMention = "(("+smsMentionCountSubQuery+") + ("+mmsMentionCountSubQuery+"))"; - - String reflectUpdates = "UPDATE "+TABLE_NAME+" AS t SET "+UNREAD_COUNT+" = "+allUnread+", "+UNREAD_MENTION_COUNT+" = "+allUnreadMention+" WHERE "+ID+" = ?"; - getWritableDatabase().rawExecSQL(reflectUpdates, threadId); - } - - - public Pair getLastSeenAndHasSent(long threadId) { - SQLiteDatabase db = getReadableDatabase(); - Cursor cursor = db.query(TABLE_NAME, new String[]{LAST_SEEN, HAS_SENT}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null); - - try { - if (cursor != null && cursor.moveToFirst()) { - return new Pair<>(cursor.getLong(0), cursor.getLong(1) == 1); - } - - return new Pair<>(-1L, false); - } finally { - if (cursor != null) cursor.close(); - } + return ThreadDatabaseExtKt.queryThreads(this, addresses); } public int getMessageCount(long threadId) { @@ -646,213 +468,8 @@ public long getOrCreateThreadIdFor(Address address) { return null; } - public void setHasSent(long threadId, boolean hasSent) { - ContentValues contentValues = new ContentValues(1); - final int hasSentValue = hasSent ? 1 : 0; - contentValues.put(HAS_SENT, hasSentValue); - - if (getWritableDatabase().update(TABLE_NAME, contentValues, ID + " = ? AND " + HAS_SENT + " != ?", - new String[] {String.valueOf(threadId), String.valueOf(hasSentValue)}) > 0) { - notifyThreadUpdated(threadId); - } - } - - - public boolean update(long threadId, boolean unarchive) { - long count = mmsSmsDatabase.get().getConversationCount(threadId); - - try (MmsSmsDatabase.Reader reader = mmsSmsDatabase.get().readerFor(mmsSmsDatabase.get().getConversationSnippet(threadId))) { - MessageRecord record = null; - if (reader != null) { - record = reader.getNext(); - while (record != null && record.isDeleted()) { - record = reader.getNext(); - } - } - - if (record != null && !record.isDeleted()) { - updateThread(threadId, count, getFormattedBodyFor(record), getAttachmentUriFor(record), record.getMessageContent(), - record.getTimestamp(), record.getDeliveryStatus(), record.getDeliveryReceiptCount(), - record.getType(), unarchive, record.getExpiresIn(), record.getReadReceiptCount()); - return false; - } else { - // for empty threads or if there is only deleted messages, show an empty snippet - clearSnippet(threadId); - return false; - } - } finally { - notifyThreadUpdated(threadId); - } - } - - public boolean isRead(long threadId) { - SQLiteDatabase db = getReadableDatabase(); - // Only ask for the "READ" column - String[] projection = {READ}; - String selection = ID + " = ?"; - String[] args = {String.valueOf(threadId)}; - - Cursor cursor = db.query(TABLE_NAME, projection, selection, args, null, null, null); - try { - if (cursor != null && cursor.moveToFirst()) { - // READ is stored as 1 = read, 0 = unread - return cursor.getInt(0) == 1; - } - return false; - } finally { - if (cursor != null) cursor.close(); - } - } - - /** - * @param threadId - * @param lastSeenTime - * @param force - * @param updateNotifications - if true, update the notification state. Set to false if you already came from a notification interaction - * @return true if we have set the last seen for the thread, false if there were no messages in the thread - */ - public boolean markAllAsRead(long threadId, long lastSeenTime, boolean force, boolean updateNotifications) { - if (mmsSmsDatabase.get().getConversationCount(threadId) <= 0 && !force) return false; - List messages = setRead(threadId, lastSeenTime); - markReadProcessor.get().process(messages); - if(updateNotifications) messageNotifier.get().updateNotification(context, threadId); - return setLastSeen(threadId, lastSeenTime); - } - - private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) { - if (messageRecord.isMms()) { - MmsMessageRecord record = (MmsMessageRecord) messageRecord; - String attachmentString = record.getSlideDeck().getBody(); - if (!attachmentString.isEmpty()) { - if (!messageRecord.getBody().isEmpty()) { - attachmentString = attachmentString + ": " + messageRecord.getBody(); - } - return attachmentString; - } - } - return messageRecord.getBody(); - } - - private @Nullable Uri getAttachmentUriFor(MessageRecord record) { - if (!record.isMms() || record.isMmsNotification()) return null; - - SlideDeck slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck(); - Slide thumbnail = slideDeck.getThumbnailSlide(); - - if (thumbnail != null) { - return thumbnail.getThumbnailUri(); - } - - return null; - } - - private @NonNull String createQuery(@NonNull String where) { - String projection = Util.join(COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION, ","); - return "SELECT " + projection + " FROM " + TABLE_NAME + - " LEFT OUTER JOIN " + RecipientSettingsDatabase.TABLE_NAME + - " ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientSettingsDatabase.TABLE_NAME + "." + RecipientSettingsDatabase.COL_ADDRESS + - " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + - " ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + - " LEFT OUTER JOIN " + LokiMessageDatabase.groupInviteTable + - " ON "+ TABLE_NAME + "." + ID + " = " + LokiMessageDatabase.groupInviteTable+"."+LokiMessageDatabase.invitingSessionId + - " WHERE " + where; - } - public void notifyThreadUpdated(long threadId) { Log.d(TAG, "Notifying thread updated: " + threadId); updateNotifications.tryEmit(threadId); } - - private class Reader implements Closeable { - - private final Cursor cursor; - - public Reader(Cursor cursor) { - this.cursor = cursor; - } - - public int getCount() { - return cursor == null ? 0 : cursor.getCount(); - } - - public ThreadRecord getNext() { - if (cursor == null || !cursor.moveToNext()) - return null; - - return getCurrent(); - } - - public ThreadRecord getCurrent() { - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID)); - Address address = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.ADDRESS))); - - Recipient recipient = recipientRepository.get().getRecipientSync(address); - String body = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET)); - long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.THREAD_CREATION_DATE)); - long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT)); - int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT)); - int unreadMentionCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_MENTION_COUNT)); - long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE)); - int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS)); - int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.DELIVERY_RECEIPT_COUNT)); - int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.READ_RECEIPT_COUNT)); - long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN)); - String invitingAdmin = cursor.getString(cursor.getColumnIndexOrThrow(LokiMessageDatabase.invitingSessionId)); - String messageContentJson = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_CONTENT)); - - if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { - readReceiptCount = 0; - } - - MessageRecord lastMessage = null; - - if (count > 0) { - lastMessage = mmsSmsDatabase.get().getLastMessage(threadId, false, false); - } - - final GroupThreadStatus groupThreadStatus; - if (recipient.isGroupV2Recipient()) { - GroupInfo.ClosedGroupInfo group = ConfigFactoryProtocolKt.getGroup( - configFactory.get(), - new AccountId(recipient.getAddress().toString()) - ); - if (group != null && group.getDestroyed()) { - groupThreadStatus = GroupThreadStatus.Destroyed; - } else if (group != null && group.getKicked()) { - groupThreadStatus = GroupThreadStatus.Kicked; - } else { - groupThreadStatus = GroupThreadStatus.None; - } - } else { - groupThreadStatus = GroupThreadStatus.None; - } - - final boolean isUnread = address instanceof Address.Conversable && - configFactory.get().withUserConfigs(configs -> - SharedConfigUtilsKt.getConversationUnread( - configs.getConvoInfoVolatile(), (Address.Conversable) address)); - - MessageContent messageContent; - try { - messageContent = (messageContentJson == null || messageContentJson.isEmpty()) ? null : json.decodeFromString( - MessageContent.Companion.serializer(), - messageContentJson - ); - } catch (Exception e) { - Log.e(TAG, "Failed to parse message content for thread: " + threadId, e); - messageContent = null; - } - - return new ThreadRecord(body, lastMessage, recipient, date, count, - unreadCount, unreadMentionCount, threadId, deliveryReceiptCount, status, type, - lastSeen, readReceiptCount, invitingAdmin, groupThreadStatus, messageContent, isUnread); - } - - @Override - public void close() { - if (cursor != null) { - cursor.close(); - } - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt new file mode 100644 index 0000000000..8db779626e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt @@ -0,0 +1,195 @@ +package org.thoughtcrime.securesms.database + +import androidx.core.database.getStringOrNull +import androidx.sqlite.db.transaction +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.Address.Companion.toAddress +import org.session.libsession.utilities.recipients.RecipientData +import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.util.asSequence +import org.thoughtcrime.securesms.util.get + +fun ThreadDatabase.queryThreads(addresses: Collection): List { + val convoInfo = configFactory.get().withUserConfigs { configs -> + addresses.associateWith { address -> + configs.convoInfoVolatile.get(address) + } + } + + // For our query, we need to fill in some information first before the main query can be done. + // The information we are filling in is: + // 1. The address of the thread + // 2. The "lastRead" timestamp from the volatile config + // 3. The "unread" status from the volatile config + return readableDatabase.transaction(exclusive = false) { + //language=roomsql + execSQL( + """ + CREATE TEMP TABLE IF NOT EXISTS threads_query_input( + address TEXT PRIMARY KEY, + last_read INTEGER NOT NULL, + unread BOOLEAN NOT NULL + ) + """ + ) + + //language=roomsql + execSQL("DELETE FROM threads_query_input WHERE 1") + + //language=roomsql + compileStatement( + """ + INSERT OR REPLACE INTO threads_query_input (address, last_read, unread) + VALUES (?, ?, ?) + """ + ).use { stmt -> + convoInfo.forEach { (address, convo) -> + stmt.clearBindings() + stmt.bindString(1, address.address) + stmt.bindLong(2, convo?.lastRead ?: 0L) + stmt.bindLong(3, if (convo?.unread == true) 1 else 0) + stmt.executeInsert() + } + } + + //language=roomsql + val result = query( + """ + SELECT + threads.${ThreadDatabase.ID}, + input.address, + + -- Query the groupInviteTable to find out who invited the user to this group + (SELECT ${LokiMessageDatabase.invitingSessionId} FROM ${LokiMessageDatabase.groupInviteTable} WHERE ${LokiMessageDatabase.threadID} = threads.${ThreadDatabase.ID} LIMIT 1) AS invitingAdminId, + + -- Return unread as is + input.unread, + + -- Count unread sms + ( + SELECT COUNT(*) + FROM ${SmsDatabase.TABLE_NAME} s + WHERE s.${SmsDatabase.THREAD_ID} = threads.${ThreadDatabase.ID} + AND ${SmsDatabase.DATE_SENT} > input.last_read + AND NOT s.${SmsDatabase.READ} + AND NOT s.${MmsSmsColumns.IS_DELETED} + ) AS smsUnreadCount, + + -- Count unread sms with mention + ( + SELECT COUNT(*) + FROM ${SmsDatabase.TABLE_NAME} s + WHERE s.${SmsDatabase.THREAD_ID} = threads.${ThreadDatabase.ID} + AND ${SmsDatabase.DATE_SENT} > input.last_read + AND NOT s.${SmsDatabase.READ} + AND s.${SmsDatabase.HAS_MENTION} + AND NOT s.${MmsSmsColumns.IS_DELETED} + ) AS smsUnreadMentionCount, + + -- Count unread mms + ( + SELECT COUNT(*) + FROM ${MmsDatabase.TABLE_NAME} m + WHERE m.${MmsSmsColumns.THREAD_ID} = threads.${ThreadDatabase.ID} + AND ${MmsDatabase.DATE_SENT} > input.last_read + AND NOT m.${MmsSmsColumns.READ} + AND NOT m.${MmsSmsColumns.IS_DELETED} + ) AS mmsUnreadCount, + + -- Count unread mms with mention + ( + SELECT COUNT(*) + FROM ${MmsDatabase.TABLE_NAME} m + WHERE m.${MmsSmsColumns.THREAD_ID} = threads.${ThreadDatabase.ID} + AND ${MmsDatabase.DATE_SENT} > input.last_read + AND NOT m.${MmsSmsColumns.READ} + AND m.${MmsSmsColumns.HAS_MENTION} + AND NOT m.${MmsSmsColumns.IS_DELETED} + ) AS mmsUnreadMentionCount, + + -- Count sms + ( + SELECT COUNT(*) + FROM ${SmsDatabase.TABLE_NAME} s + WHERE s.${SmsDatabase.THREAD_ID} = threads.${ThreadDatabase.ID} + ), + + -- Count mms + ( + SELECT COUNT(*) + FROM ${MmsDatabase.TABLE_NAME} m + WHERE m.${MmsSmsColumns.THREAD_ID} = threads.${ThreadDatabase.ID} + ) + FROM ${ThreadDatabase.TABLE_NAME} AS threads + INNER JOIN threads_query_input AS input ON threads.${ThreadDatabase.ADDRESS} = input.address + """ + ).use { cursor -> + cursor.asSequence() + .map { cursor -> + val threadId = cursor.getLong(0) + val threadAddress = cursor.getString(1).toAddress() as Address.Conversable + val invitingAdminId = cursor.getStringOrNull(2) + val unread = cursor.getLong(3) != 0L + val smsUnreadCount = cursor.getLong(4) + val smsUnreadMentionCount = cursor.getLong(5) + val mmsUnreadCount = cursor.getLong(6) + val mmsUnreadMentionCount = cursor.getLong(7) + val smsCount = cursor.getLong(8) + val mmsCount = cursor.getLong(9) + + val threadRecipient = recipientRepository.get().getRecipientSync(threadAddress) + val lastMessage = mmsSmsDatabase.get().getLastMessage( + /* threadId = */ threadId, + /* includeReactions = */ false, + /* getQuote = */ false + ) + + val date = when { + lastMessage != null -> lastMessage.dateReceived + threadRecipient.data is RecipientData.Contact -> threadRecipient.data.name + else -> 0L + } + + ThreadRecord( + threadId = threadId, + recipient = threadRecipient, + lastMessage = lastMessage, + count = smsCount.toInt() + mmsCount.toInt(), + unreadCount = smsUnreadCount.toInt() + mmsUnreadCount.toInt(), + unreadMentionCount = smsUnreadMentionCount.toInt() + mmsUnreadMentionCount.toInt(), + isUnread = unread, + date = 0L, + invitingAdminId = invitingAdminId + ) + } + .toList() + } + + // Clean up our temp table + //language=roomsql + execSQL("DROP TABLE threads_query_input") + + result + } +} + +fun ThreadDatabase.threadContainsOutgoingMessage(threadId: Long): Boolean { + //language=roomsql + val hasOutgoingSms = readableDatabase.rawQuery(""" + SELECT 1 FROM ${SmsDatabase.TABLE_NAME} + WHERE ${SmsDatabase.THREAD_ID} = ? + AND ${SmsDatabase.IS_OUTGOING} + AND NOT ${MmsSmsColumns.IS_DELETED} + LIMIT 1 + """).use { it.count > 0 } + + if (hasOutgoingSms) return true + + return readableDatabase.rawQuery(""" + SELECT 1 FROM ${MmsDatabase.TABLE_NAME} + WHERE ${MmsSmsColumns.THREAD_ID} = ? + AND ${MmsSmsColumns.IS_OUTGOING} + AND NOT ${MmsSmsColumns.IS_DELETED} + LIMIT 1 + """).use { it.count > 0 } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index d82ba6b813..7f6abd47f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -106,9 +106,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV55 = 76; private static final int lokiV56 = 77; private static final int lokiV57 = 78; + private static final int lokiV58 = 79; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes - private static final int DATABASE_VERSION = lokiV57; + private static final int DATABASE_VERSION = lokiV58; private static final int MIN_DATABASE_VERSION = lokiV7; public static final String DATABASE_NAME = "session.db"; @@ -275,6 +276,9 @@ public void onCreate(SQLiteDatabase db) { MmsDatabase.Companion.addProFeatureColumns(db); SmsDatabase.addProFeatureColumns(db); RecipientSettingsDatabase.Companion.migrateProStatusToProData(db); + + SmsDatabase.addOutgoingColumn(db); + MmsDatabase.Companion.addOutgoingColumn(db); } @Override @@ -624,6 +628,11 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { RecipientSettingsDatabase.Companion.migrateProStatusToProData(db); } + if (oldVersion < lokiV58) { + SmsDatabase.addOutgoingColumn(db); + MmsDatabase.Companion.addOutgoingColumn(db); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java deleted file mode 100644 index 5ed38dc3ef..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2012 Moxie Marlinspike - * Copyright (C) 2013-2017 Open Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.database.model; - -import static org.session.libsession.utilities.StringSubstitutionConstants.AUTHOR_KEY; -import static org.session.libsession.utilities.StringSubstitutionConstants.MESSAGE_SNIPPET_KEY; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.squareup.phrase.Phrase; - -import org.session.libsession.utilities.AddressKt; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.recipients.RecipientNamesKt; -import org.thoughtcrime.securesms.database.model.content.MessageContent; - -import network.loki.messenger.R; - -/** - * The message record model which represents thread heading messages. - * - * @author Moxie Marlinspike - * - */ -public class ThreadRecord extends DisplayRecord { - public @Nullable final MessageRecord lastMessage; - private final long count; - private final int unreadCount; - private final int unreadMentionCount; - private final long lastSeen; - private final String invitingAdminId; - private final boolean isUnread; - - @NonNull - public final GroupThreadStatus groupThreadStatus; - - public ThreadRecord(@NonNull String body, - @Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount, - int unreadMentionCount, long threadId, int deliveryReceiptCount, int status, - long snippetType, - long lastSeen, int readReceiptCount, String invitingAdminId, - @NonNull GroupThreadStatus groupThreadStatus, - @Nullable MessageContent messageContent, - boolean isUnread) - { - super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount, messageContent); - this.lastMessage = lastMessage; - this.count = count; - this.unreadCount = unreadCount; - this.unreadMentionCount = unreadMentionCount; - this.lastSeen = lastSeen; - this.invitingAdminId = invitingAdminId; - this.groupThreadStatus = groupThreadStatus; - this.isUnread = isUnread; - } - - - public long getCount() { return count; } - - public int getUnreadCount() { return unreadCount; } - - public int getUnreadMentionCount() { return unreadMentionCount; } - - public long getDate() { return getDateReceived(); } - - public long getLastSeen() { return lastSeen; } - - public boolean isPinned() { return getRecipient().isPinned(); } - - public String getInvitingAdminId() { - return invitingAdminId; - } - - public boolean isUnread() { - return isUnread; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.kt new file mode 100644 index 0000000000..3a7aa644d8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.kt @@ -0,0 +1,48 @@ +package org.thoughtcrime.securesms.database.model + +import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.recipients.RecipientData + +data class ThreadRecord( + val lastMessage: MessageRecord?, + val threadId: Long, + val recipient: Recipient, + val count: Int, // total message count + val unreadCount: Int, // unread message count + val unreadMentionCount: Int, // unread mention count + val date: Long, + val isUnread: Boolean, + val invitingAdminId: String?, +) { + val isDelivered: Boolean + get() = lastMessage?.isDelivered == true + + val isFailed: Boolean + get() = lastMessage?.isFailed == true + + val isSent: Boolean + get() = lastMessage?.isSent == true + + val isPending: Boolean + get() = lastMessage?.isPending == true + + val isPinned: Boolean + get() = recipient.isPinned + + val isRead: Boolean + get() = lastMessage?.isRead == true + + val isOutgoing: Boolean + get() = lastMessage?.isOutgoing == true + + val groupThreadStatus: GroupThreadStatus + get() { + val group = recipient.data as? RecipientData.Group + + return when { + group?.kicked == true -> GroupThreadStatus.Kicked + group?.destroyed == true -> GroupThreadStatus.Destroyed + else -> GroupThreadStatus.None + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/OnAppStartupComponents.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/OnAppStartupComponents.kt index 5276e16650..d4d5340b78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/OnAppStartupComponents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/OnAppStartupComponents.kt @@ -4,7 +4,6 @@ import org.session.libsession.messaging.notifications.TokenFetcher import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerManager import org.session.libsession.snode.SnodeClock import org.thoughtcrime.securesms.auth.AuthAwareComponentsHandler -import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.disguise.AppDisguiseManager import org.thoughtcrime.securesms.emoji.EmojiIndexLoader import org.thoughtcrime.securesms.groups.ExpiredGroupManager @@ -41,7 +40,6 @@ class OnAppStartupComponents private constructor( appDisguiseManager: AppDisguiseManager, tokenFetcher: TokenFetcher, versionDataFetcher: VersionDataFetcher, - threadDatabase: ThreadDatabase, emojiIndexLoader: EmojiIndexLoader, subscriptionCoordinator: SubscriptionCoordinator, authAwareHandler: AuthAwareComponentsHandler, @@ -61,7 +59,6 @@ class OnAppStartupComponents private constructor( appDisguiseManager, tokenFetcher, versionDataFetcher, - threadDatabase, emojiIndexLoader, subscriptionCoordinator, authAwareHandler, 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 eb5c55a74b..d0e4b39349 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -14,7 +14,6 @@ import androidx.compose.animation.Crossfade import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -746,7 +745,10 @@ class HomeActivity : ScreenLockActionBarActivity(), private fun markAllAsRead(thread: ThreadRecord) { lifecycleScope.launch(Dispatchers.Default) { - storage.markConversationAsRead(thread.threadId, clock.currentTimeMills()) + storage.updateConversationLastSeenIfNeeded( + thread.recipient.address as Address.Conversable, + clock.currentTimeMills() + ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt index 0749b6838e..5264dabeb3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.home import android.content.Context import androidx.recyclerview.widget.DiffUtil +import org.session.libsession.utilities.recipients.displayName import org.thoughtcrime.securesms.conversation.v2.messages.MessageFormatter class HomeDiffUtil( @@ -68,7 +69,6 @@ class HomeDiffUtil( oldItem.isDelivered == newItem.isDelivered && oldItem.isSent == newItem.isSent && oldItem.isPending == newItem.isPending && - oldItem.lastSeen == newItem.lastSeen && oldItem.isUnread == newItem.isUnread && old.isTyping == new.isTyping ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt index b530a8c0ab..02d6bc56df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt @@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.NotifyType +import org.thoughtcrime.securesms.database.threadContainsOutgoingMessage import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.util.AvatarUtils @@ -169,7 +170,7 @@ class DefaultMessageNotifier @Inject constructor( if (recipient != null && !recipient.isGroupOrCommunityRecipient && threadDatabase.getMessageCount( threadId ) == 1 && - !(recipient.approved || threadDatabase.getLastSeenAndHasSent(threadId).second()) + !(recipient.approved || threadDatabase.threadContainsOutgoingMessage(threadId)) ) { removeHasHiddenMessageRequests(context) } @@ -591,7 +592,7 @@ class DefaultMessageNotifier @Inject constructor( // Handle message requests early val isMessageRequest = !threadRecipient.isGroupOrCommunityRecipient && !threadRecipient.approved && - !threadDatabase.getLastSeenAndHasSent(threadId).second() + !threadDatabase.threadContainsOutgoingMessage(threadId) // Do not repeat request notifications once the thread has >1 messages if (isMessageRequest) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.kt index 064b10c507..3c25682a2d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.kt @@ -9,9 +9,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier +import org.session.libsession.utilities.Address import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.SmsDatabase import org.thoughtcrime.securesms.database.Storage +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.dependencies.ManagerScope import javax.inject.Inject @@ -33,6 +35,7 @@ class DeleteNotificationReceiver : BroadcastReceiver() { @Inject lateinit var smsDb: SmsDatabase @Inject lateinit var mmsDb: MmsDatabase @Inject lateinit var storage: Storage + @Inject lateinit var threadDatabase: ThreadDatabase override fun onReceive(context: Context, intent: Intent) { if (intent.action != DELETE_NOTIFICATION_ACTION) return @@ -48,12 +51,10 @@ class DeleteNotificationReceiver : BroadcastReceiver() { try { withContext(Dispatchers.IO) { val now = System.currentTimeMillis() - for(threadId in threadIds){ - storage.markConversationAsRead( - threadId = threadId, - lastSeenTime = now, - force = false, - updateNotification = false + for (threadId in threadIds){ + storage.updateConversationLastSeenIfNeeded( + threadAddress = threadDatabase.getRecipientForThreadId(threadId) as? Address.Conversable ?: continue, + lastSeenTime = now ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt index 6b1cd80d4d..c652268849 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt @@ -9,7 +9,9 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.session.libsession.database.StorageProtocol import org.session.libsession.snode.SnodeClock +import org.session.libsession.utilities.Address import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.ThreadDatabase import javax.inject.Inject @AndroidEntryPoint @@ -20,6 +22,9 @@ class MarkReadReceiver : BroadcastReceiver() { @Inject lateinit var clock: SnodeClock + @Inject + lateinit var threadDatabase: ThreadDatabase + override fun onReceive(context: Context, intent: Intent) { if (CLEAR_ACTION != intent.action) return @@ -29,10 +34,9 @@ class MarkReadReceiver : BroadcastReceiver() { val currentTime = clock.currentTimeMills() threadIds.forEach { Log.i(TAG, "Marking as read: $it") - storage.markConversationAsRead( - threadId = it, + storage.updateConversationLastSeenIfNeeded( + threadAddress = threadDatabase.getRecipientForThreadId(it) as? Address.Conversable ?: return@forEach, lastSeenTime = currentTime, - force = true ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt index ab6ab6a111..f5e7239e66 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt @@ -1,27 +1,59 @@ package org.thoughtcrime.securesms.util import network.loki.messenger.libsession_util.ReadableConversationVolatileConfig +import network.loki.messenger.libsession_util.util.Conversation import org.session.libsession.utilities.Address +import org.session.libsession.utilities.MutableUserConfigs - -fun ReadableConversationVolatileConfig.getConversationUnread(recipientAddress: Address.Conversable): Boolean { - return when (recipientAddress) { +fun ReadableConversationVolatileConfig.get(address: Address.Conversable): Conversation? { + return when (address) { is Address.Standard -> { - getOneToOne(recipientAddress.accountId.hexString)?.unread == true + getOneToOne(address.accountId.hexString) } is Address.Group -> { - getClosedGroup(recipientAddress.accountId.hexString)?.unread == true + getClosedGroup(address.accountId.hexString) } is Address.LegacyGroup -> { - getLegacyClosedGroup(recipientAddress.groupPublicKeyHex)?.unread == true + getLegacyClosedGroup(address.groupPublicKeyHex) } is Address.Community -> { - getCommunity(baseUrl = recipientAddress.serverUrl, room = recipientAddress.room)?.unread == true + getCommunity(baseUrl = address.serverUrl, room = address.room) } - is Address.CommunityBlindedId -> false + is Address.CommunityBlindedId -> { + getBlindedOneToOne(address.blindedId.address) + } } } + +fun MutableUserConfigs.getOrConstructConvo(address: Address.Conversable): Conversation { + return when (address) { + is Address.Standard -> { + convoInfoVolatile.getOrConstructOneToOne(address.accountId.hexString) + } + + is Address.Group -> { + convoInfoVolatile.getOrConstructClosedGroup(address.accountId.hexString) + } + + is Address.LegacyGroup -> { + convoInfoVolatile.getOrConstructLegacyGroup(address.groupPublicKeyHex) + } + + is Address.Community -> { + convoInfoVolatile.getOrConstructCommunity(baseUrl = address.serverUrl, room = address.room, pubKeyHex = requireNotNull( + userGroups.getCommunityInfo( + baseUrl = address.serverUrl, + room = address.room + ) + ) { "Community does not exist" }.community.pubKeyHex) + } + + is Address.CommunityBlindedId -> { + convoInfoVolatile.getOrConstructedBlindedOneToOne(address.blindedId.address) + } + } +} \ No newline at end of file From 532ac5470c38f1fadce321ce8b68bf1796ba611f Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:58:57 +1100 Subject: [PATCH 03/12] Handling mark read automatically --- .../securesms/auth/AuthAwareComponents.kt | 3 + .../securesms/database/MmsDatabase.kt | 121 +++++++++--------- .../securesms/database/SmsDatabase.java | 76 +++-------- .../securesms/database/SmsDatabaseExt.kt | 50 ++++++++ .../notifications/MarkReadProcessor.kt | 109 +++++++++++++++- .../service/ExpiringMessageManager.kt | 32 +++-- 6 files changed, 260 insertions(+), 131 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabaseExt.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/auth/AuthAwareComponents.kt b/app/src/main/java/org/thoughtcrime/securesms/auth/AuthAwareComponents.kt index f273de03eb..799fcef38d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/auth/AuthAwareComponents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/auth/AuthAwareComponents.kt @@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.groups.handler.CleanupInvitationHandler import org.thoughtcrime.securesms.groups.handler.DestroyedGroupSync import org.thoughtcrime.securesms.groups.handler.RemoveGroupMemberHandler import org.thoughtcrime.securesms.notifications.BackgroundPollManager +import org.thoughtcrime.securesms.notifications.MarkReadProcessor import org.thoughtcrime.securesms.notifications.PushRegistrationHandler import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.service.ExpiringMessageManager @@ -41,6 +42,7 @@ class AuthAwareComponents( proStatusManager: Lazy, pollerManager: Lazy, backgroundPollManager: Lazy, + markReadProcessor: Lazy, ): this( components = listOf>( expiringMessageManager, @@ -56,6 +58,7 @@ class AuthAwareComponents( proStatusManager, pollerManager, backgroundPollManager, + markReadProcessor, ) ) } \ No newline at end of file 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 7171947a45..7d9878c7fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context import android.database.Cursor +import androidx.collection.MutableLongSet import androidx.sqlite.db.SupportSQLiteDatabase import com.annimon.stream.Stream import dagger.Lazy @@ -27,7 +28,6 @@ import kotlinx.serialization.json.Json import org.json.JSONArray import org.json.JSONException import org.json.JSONObject -import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.signal.IncomingMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.messaging.sending_receiving.attachments.Attachment @@ -347,10 +347,16 @@ class MmsDatabase @Inject constructor( } override fun markExpireStarted(messageId: Long, startedTimestamp: Long) { - val contentValues = ContentValues() - contentValues.put(EXPIRE_STARTED, startedTimestamp) - val db = writableDatabase - db.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString())) + //language=roomsql + writableDatabase.rawQuery(""" + UPDATE $TABLE_NAME SET $EXPIRE_STARTED = ? + WHERE $ID = ? + RETURNING $THREAD_ID + """, startedTimestamp, messageId).use { cursor -> + if (cursor.moveToNext()) { + threadDatabase.notifyThreadUpdated(cursor.getLong(0)) + } + } } fun markAsNotified(id: Long) { @@ -362,56 +368,59 @@ class MmsDatabase @Inject constructor( fun setMessagesRead(threadId: Long, beforeTime: Long): List { return setMessagesRead( - THREAD_ID + " = ? AND (" + READ + " = 0) AND " + DATE_SENT + " <= ?", - arrayOf(threadId.toString(), beforeTime.toString()) + "$THREAD_ID = ? AND ($READ = 0) AND $DATE_SENT <= ?", + threadId, + beforeTime ) } fun setMessagesRead(threadId: Long): List { - return setMessagesRead( - THREAD_ID + " = ? AND (" + READ + " = 0)", - arrayOf(threadId.toString()) + return setMessagesRead("$THREAD_ID = ? AND ($READ = 0)",threadId ) } - private fun setMessagesRead(where: String, arguments: Array?): List { - val database = writableDatabase - val result: MutableList = LinkedList() - var cursor: Cursor? = null - database.beginTransaction() - try { - cursor = database.query( - TABLE_NAME, - arrayOf(ID, ADDRESS, DATE_SENT, MESSAGE_BOX, EXPIRES_IN, EXPIRE_STARTED), - where, - arguments, - null, - null, - null - ) - while (cursor != null && cursor.moveToNext()) { - if (MmsSmsColumns.Types.isSecureType(cursor.getLong(3))) { - val timestamp = cursor.getLong(2) - val syncMessageId = SyncMessageId(fromSerialized(cursor.getString(1)), timestamp) - val expirationInfo = ExpirationInfo( - id = MessageId(cursor.getLong(0), mms = true), - timestamp = timestamp, - expiresIn = cursor.getLong(4), - expireStarted = cursor.getLong(5), + private fun setMessagesRead(where: String, vararg args: Any?): List { + val updatedThreadIDs = MutableLongSet(1) + + //language=roomsql + val messages = writableDatabase.rawQuery(""" + UPDATE $TABLE_NAME + SET $READ = 1 + WHERE $where + RETURNING $ID, + $ADDRESS, + $THREAD_ID, + $DATE_SENT, + $EXPIRES_IN, + $EXPIRE_STARTED + """, *args).use { cursor -> + cursor.asSequence() + .map { + val timestamp = cursor.getLong(3) + + updatedThreadIDs += cursor.getLong(2) + + MarkedMessageInfo( + syncMessageId = SyncMessageId( + cursor.getString(1).toAddress(), + timestamp + ), + expirationInfo = ExpirationInfo( + id = MessageId(cursor.getLong(0), true), + timestamp = timestamp, + expiresIn = cursor.getLong(4), + expireStarted = cursor.getLong(5) + ) ) - result.add(MarkedMessageInfo(syncMessageId, expirationInfo)) } - } - val contentValues = ContentValues() - contentValues.put(READ, 1) - contentValues.put(REACTIONS_UNREAD, 0) - database.update(TABLE_NAME, contentValues, where, arguments) - database.setTransactionSuccessful() - } finally { - cursor?.close() - database.endTransaction() + .toList() + } + + updatedThreadIDs.forEach { + threadDatabase.notifyThreadUpdated(it) } - return result + + return messages } @@ -766,7 +775,7 @@ class MmsDatabase @Inject constructor( contentValues.put(THREAD_ID, toId) val db = writableDatabase - db.update(SmsDatabase.TABLE_NAME, contentValues, "$THREAD_ID = ?", arrayOf("$fromId")) + db.update(TABLE_NAME, contentValues, "$THREAD_ID = ?", arrayOf("$fromId")) } fun deleteThread(threadId: Long, updateThread: Boolean) { @@ -939,25 +948,15 @@ class MmsDatabase @Inject constructor( fun readerFor(cursor: Cursor?, getQuote: Boolean = true) = Reader(cursor, getQuote) - fun setQuoteMissing(messageId: Long): Int { - val contentValues = ContentValues() - contentValues.put(QUOTE_MISSING, 1) - val database = writableDatabase - return database!!.update( - TABLE_NAME, - contentValues, - "$ID = ?", - arrayOf(messageId.toString()) - ) - } - /** * @param outgoing if true only delete outgoing messages, if false only delete incoming messages, if null delete both. */ private fun deleteExpirationTimerMessages(threadId: Long, outgoing: Boolean? = null) { - val outgoingClause = outgoing?.let { - " AND $IS_OUTGOING" - } ?: "" + val outgoingClause = when (outgoing) { + null -> "" + true -> " AND $IS_OUTGOING" + false -> " AND NOT $IS_OUTGOING" + } val where = "$THREAD_ID = ? AND $MESSAGE_CONTENT->>'$.${MessageContent.DISCRIMINATOR}' == '${DisappearingMessageUpdate.TYPE_NAME}' " + outgoingClause diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index a0d02c1491..fb522215a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -43,20 +43,16 @@ import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.guava.Optional; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; -import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.pro.ProFeatureExtKt; import java.io.Closeable; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Set; import javax.inject.Inject; import javax.inject.Provider; @@ -65,11 +61,7 @@ import dagger.Lazy; import dagger.hilt.android.qualifiers.ApplicationContext; import kotlin.collections.ArraysKt; -import kotlin.collections.CollectionsKt; import network.loki.messenger.libsession_util.protocol.ProFeature; -import network.loki.messenger.libsession_util.protocol.ProMessageFeature; -import network.loki.messenger.libsession_util.protocol.ProProfileFeature; -import network.loki.messenger.libsession_util.util.BitSet; /** * Database for storage of SMS messages. @@ -164,7 +156,7 @@ public static void addOutgoingColumn(SupportSQLiteDatabase db) { private static final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache(); private final RecipientRepository recipientRepository; - private final Lazy<@NonNull ThreadDatabase> threadDatabase; + final Lazy<@NonNull ThreadDatabase> threadDatabase; private final Lazy<@NonNull ReactionDatabase> reactionDatabase; @Inject @@ -187,25 +179,14 @@ private void updateTypeBitmask(long id, long maskOff, long maskOn) { Log.i("MessageDatabase", "Updating ID: " + id + " to base type: " + maskOn); SQLiteDatabase db = getWritableDatabase(); - db.execSQL("UPDATE " + TABLE_NAME + + try (final Cursor cursor = db.rawQuery("UPDATE " + TABLE_NAME + " SET " + TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + " )" + - " WHERE " + ID + " = ?", new String[] {id+""}); - - long threadId = getThreadIdForMessage(id); - - threadDatabase.get().notifyThreadUpdated(threadId); - } - - public long getThreadIdForMessage(long id) { - String sql = "SELECT " + THREAD_ID + " FROM " + TABLE_NAME + " WHERE " + ID + " = ?"; - String[] sqlArgs = new String[] {id+""}; - SQLiteDatabase db = getReadableDatabase(); - - try (Cursor cursor = db.rawQuery(sql, sqlArgs)) { - if (cursor != null && cursor.moveToFirst()) - return cursor.getLong(0); - else - return -1; + " WHERE " + ID + " = ?" + + " RETURNING " + THREAD_ID, id)) { + if (cursor.moveToNext()) { + long threadId = cursor.getLong(0); + threadDatabase.get().notifyThreadUpdated(threadId); + } } } @@ -264,9 +245,6 @@ public void markAsDeleted(long messageId, boolean isOutgoing, String displayedMe @Override public void markExpireStarted(long id, long startedAtTimestamp) { - ContentValues contentValues = new ContentValues(); - contentValues.put(EXPIRE_STARTED, startedAtTimestamp); - SQLiteDatabase db = getWritableDatabase(); try (final Cursor cursor = db.rawQuery("UPDATE " + TABLE_NAME + " SET " + EXPIRE_STARTED + " = ? " + "WHERE " + ID + " = ? RETURNING " + THREAD_ID, startedAtTimestamp, id)) { @@ -404,37 +382,21 @@ public void incrementReceiptCount(SyncMessageId messageId, boolean deliveryRecei } public List setMessagesRead(long threadId, long beforeTime) { - return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0) AND " + DATE_SENT + " <= ?", new String[]{threadId+"", beforeTime+""}); + return SmsDatabaseExtKt.setMessagesRead( + this, + THREAD_ID + " = ? AND (" + READ + " = 0) AND " + DATE_SENT + " <= ?", + threadId, + beforeTime + ); } public List setMessagesRead(long threadId) { - return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0)", new String[] {String.valueOf(threadId)}); + return SmsDatabaseExtKt.setMessagesRead( + this, + THREAD_ID + " = ? AND (" + READ + " = 0)", + threadId + ); } - private List setMessagesRead(String where, String[] arguments) { - SQLiteDatabase database = getWritableDatabase(); - List results = new LinkedList<>(); - database.beginTransaction(); - try (final Cursor cursor = database.query(TABLE_NAME, new String[] {ID, ADDRESS, DATE_SENT, TYPE, EXPIRES_IN, EXPIRE_STARTED}, where, arguments, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - long timestamp = cursor.getLong(2); - SyncMessageId syncMessageId = new SyncMessageId(Address.fromSerialized(cursor.getString(1)), timestamp); - ExpirationInfo expirationInfo = new ExpirationInfo(new MessageId(cursor.getLong(0), false), timestamp, cursor.getLong(4), cursor.getLong(5)); - - results.add(new MarkedMessageInfo(syncMessageId, expirationInfo)); - } - - ContentValues contentValues = new ContentValues(); - contentValues.put(READ, 1); - contentValues.put(REACTIONS_UNREAD, 0); - - database.update(TABLE_NAME, contentValues, where, arguments); - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - } - - return results; - } public void updateSentTimestamp(long messageId, long newTimestamp) { SQLiteDatabase db = getWritableDatabase(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabaseExt.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabaseExt.kt new file mode 100644 index 0000000000..8c0596883a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabaseExt.kt @@ -0,0 +1,50 @@ +package org.thoughtcrime.securesms.database + +import androidx.collection.MutableLongSet +import org.session.libsession.utilities.Address.Companion.toAddress +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.util.asSequence + +fun SmsDatabase.setMessagesRead(where: String, vararg args: Any?): List { + val updatedThreadIDs = MutableLongSet(1) + + //language=roomsql + val messages = writableDatabase.rawQuery(""" + UPDATE ${SmsDatabase.TABLE_NAME} + SET ${SmsDatabase.READ} = 1 + WHERE $where + RETURNING ${SmsDatabase.ID}, + ${SmsDatabase.ADDRESS}, + ${SmsDatabase.THREAD_ID}, + ${SmsDatabase.DATE_SENT}, + ${SmsDatabase.EXPIRES_IN}, + ${SmsDatabase.EXPIRE_STARTED} + """, *args).use { cursor -> + cursor.asSequence() + .map { + val timestamp = cursor.getLong(3) + + updatedThreadIDs += cursor.getLong(2) + + MarkedMessageInfo( + syncMessageId = MessagingDatabase.SyncMessageId( + cursor.getString(1).toAddress(), + timestamp + ), + expirationInfo = ExpirationInfo( + id = MessageId(cursor.getLong(0), false), + timestamp = timestamp, + expiresIn = cursor.getLong(4), + expireStarted = cursor.getLong(5) + ) + ) + } + .toList() + } + + updatedThreadIDs.forEach { + threadDatabase.get().notifyThreadUpdated(it) + } + + return messages +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadProcessor.kt index 04cfd04597..730e66576d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadProcessor.kt @@ -2,20 +2,33 @@ package org.thoughtcrime.securesms.notifications import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.scan import kotlinx.coroutines.launch +import network.loki.messenger.libsession_util.util.Conversation import org.session.libsession.database.StorageProtocol import org.session.libsession.database.userAuth import org.session.libsession.messaging.messages.control.ReadReceipt import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeClock +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled +import org.session.libsession.utilities.UserConfigType import org.session.libsession.utilities.associateByNotNull import org.session.libsession.utilities.isGroupOrCommunity import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.RecipientData +import org.session.libsession.utilities.userConfigsChanged +import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.auth.AuthAwareComponent +import org.thoughtcrime.securesms.auth.LoggedInState import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.MarkedMessageInfo @@ -25,8 +38,13 @@ import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.SmsDatabase import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.content.DisappearingMessageUpdate +import org.thoughtcrime.securesms.dependencies.ManagerScope +import org.thoughtcrime.securesms.util.castAwayType +import java.util.EnumSet import javax.inject.Inject +import javax.inject.Singleton +@Singleton class MarkReadProcessor @Inject constructor( @param:ApplicationContext private val context: Context, private val recipientRepository: RecipientRepository, @@ -38,7 +56,88 @@ class MarkReadProcessor @Inject constructor( private val storage: StorageProtocol, private val snodeClock: SnodeClock, private val lokiMessageDatabase: LokiMessageDatabase, -) { + private val configFactory: ConfigFactoryProtocol, + @param:ManagerScope private val scope: CoroutineScope, +) : AuthAwareComponent { + + override suspend fun doWhileLoggedIn(loggedInState: LoggedInState) { + processLastSeenChanges() + } + + private suspend fun processLastSeenChanges() { + + data class LastSeenChanges( + val previousSeen: Map? = null, + val currentSeen: Map? = null, + ) + + // Observe the config changes, figuring out the individual changes to each conversation + configFactory.userConfigsChanged( + onlyConfigTypes = EnumSet.of(UserConfigType.CONVO_INFO_VOLATILE), + debounceMills = 500L + ).castAwayType() + .onStart { emit(Unit) } + .map { + buildMap { + configFactory.withUserConfigs { configs -> + configs.convoInfoVolatile.all() + }.forEach { convo -> + val address = when (convo) { + is Conversation.ClosedGroup -> + Address.Group(AccountId(convo.accountId)) + + is Conversation.Community -> + Address.Community( + serverUrl = convo.baseCommunityInfo.baseUrl, + room = convo.baseCommunityInfo.room + ) + + is Conversation.OneToOne -> + Address.Standard(AccountId(convo.accountId)) + + is Conversation.BlindedOneToOne, + is Conversation.LegacyGroup, + null -> null + } + + if (address != null && convo != null) { + put(address, convo.lastRead) + } + } + } + } + .distinctUntilChanged() + .scan(LastSeenChanges()) { acc, current -> + acc.copy( + currentSeen = current, + previousSeen = acc.currentSeen, + ) + } + .distinctUntilChanged() + .collectLatest { (previousSeen, currentSeen) -> + if (previousSeen != null && currentSeen != null) { + currentSeen + .asSequence() + .filter { (key, value) -> + previousSeen[key] != value + } + .forEach { changed -> + val threadId = threadDb.getThreadIdIfExistsFor(changed.key) + if (threadId != -1L) { + val allUnreadMessages = buildList { + addAll(smsDatabase.setMessagesRead(threadId, changed.value)) + addAll(mmsDatabase.setMessagesRead(threadId, changed.value)) + } + + Log.d(TAG, "Processing mark read for ${changed.key.debugString}, messageCount = ${allUnreadMessages.size}") + process(allUnreadMessages) + } + } + } + } + } + + fun process( markedReadMessages: List ) { @@ -64,11 +163,12 @@ class MarkReadProcessor @Inject constructor( smsDatabase } + Log.d(TAG, "Marking message ${it.expirationInfo.id.id} as started for disappear after read") db.markExpireStarted(it.expirationInfo.id.id, snodeClock.currentTimeMills()) } - hashToDisappearAfterReadMessage(context, markedReadMessages)?.let { hashToMessages -> - GlobalScope.launch { + hashToDisappearAfterReadMessage(markedReadMessages)?.let { hashToMessages -> + scope.launch { try { shortenExpiryOfDisappearingAfterRead(hashToMessages) } catch (e: Exception) { @@ -79,7 +179,6 @@ class MarkReadProcessor @Inject constructor( } private fun hashToDisappearAfterReadMessage( - context: Context, markedReadMessages: List ): Map? { return markedReadMessages diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt index 696259092b..aa49ad2bfe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt @@ -12,8 +12,8 @@ import org.session.libsession.messaging.messages.signal.IncomingMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.Address -import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Address.Companion.toAddress +import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.auth.AuthAwareComponent @@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.auth.LoggedInState import org.thoughtcrime.securesms.auth.LoginStateRepository import org.thoughtcrime.securesms.database.MessagingDatabase import org.thoughtcrime.securesms.database.MmsDatabase +import org.thoughtcrime.securesms.database.MmsSmsDatabase import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.SmsDatabase import org.thoughtcrime.securesms.database.Storage @@ -71,7 +72,7 @@ class ExpiringMessageManager @Inject constructor( val sentTimestamp = message.sentTimestamp val groupAddress = message.groupPublicKey?.toAddress() as? Address.GroupLike val expiresInMillis = message.expiryMode.expiryMillis - val address = fromSerialized(senderPublicKey!!) + val address = senderPublicKey!!.toAddress() var recipient = recipientRepository.getRecipientSync(address) // if the sender is blocked, we don't display the update, except if it's in a closed group @@ -100,7 +101,11 @@ class ExpiringMessageManager @Inject constructor( dataExtractionNotification = null ) //insert the timer update message - mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, threadId, runThreadUpdate = true) + mmsDatabase.insertSecureDecryptedMessageInbox( + mediaMessage, + threadId, + runThreadUpdate = true + ) .orNull() ?.let { MessageId(it.messageId, mms = true) } } catch (ioe: IOException) { @@ -119,7 +124,8 @@ class ExpiringMessageManager @Inject constructor( val groupId = message.groupPublicKey?.toAddress() as? Address.GroupLike val duration = message.expiryMode.expiryMillis try { - val serializedAddress = groupId ?: (message.syncTarget ?: message.recipient!!).toAddress() + val serializedAddress = + groupId ?: (message.syncTarget ?: message.recipient!!).toAddress() message.threadID = storage.get().getOrCreateThreadIdFor(serializedAddress) val content = DisappearingMessageUpdate(message.expiryMode) @@ -198,7 +204,8 @@ class ExpiringMessageManager @Inject constructor( // If we receive a message that is sent from ourselves (aka the sync message), we // will start the expiry timer regardless if (message.expiryMode is ExpiryMode.AfterSend || - (message.expiryMode != ExpiryMode.NONE && message.isSenderSelf)) { + (message.expiryMode != ExpiryMode.NONE && message.isSenderSelf) + ) { getDatabase(messageId.mms) .markExpireStarted(messageId.id, clock.currentTimeMills()) } @@ -209,7 +216,10 @@ class ExpiringMessageManager @Inject constructor( val expiredMessages = db.getExpiredMessageIDs(clock.currentTimeMills()) if (expiredMessages.isNotEmpty()) { - Log.d(TAG, "Deleting ${expiredMessages.size} expired messages from ${db.javaClass.simpleName}") + Log.d( + TAG, + "Deleting ${expiredMessages.size} expired messages from ${db.javaClass.simpleName}" + ) for (messageId in expiredMessages) { try { db.deleteMessage(messageId) @@ -230,12 +240,18 @@ class ExpiringMessageManager @Inject constructor( if (nextExpiration > 0) { val delayMills = nextExpiration - now - Log.d(TAG, "Wait for up to $delayMills ms for next expiration in ${db.javaClass.simpleName}") + Log.d( + TAG, + "Wait for up to $delayMills ms for next expiration in ${db.javaClass.simpleName}" + ) withTimeoutOrNull(delayMills) { dbChanges.first() } } else { - Log.d(TAG, "No next expiration found, waiting for any change in ${db.javaClass.simpleName}") + Log.d( + TAG, + "No next expiration found, waiting for any change in ${db.javaClass.simpleName}" + ) // If there are no next expiration, just wait for any change in the database dbChanges.first() } From cbe2cb2f69676634b484ea6f0f3eb69894ffa2b7 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:42:40 +1100 Subject: [PATCH 04/12] Optimize --- .../libsession/database/StorageProtocol.kt | 1 + .../securesms/database/MmsDatabase.kt | 11 +++-- .../securesms/database/Storage.kt | 13 +++++- .../securesms/database/ThreadDatabase.java | 43 ------------------- .../securesms/database/ThreadDatabaseExt.kt | 2 +- .../AndroidAutoHeardReceiver.java | 31 +++++-------- .../notifications/AndroidAutoReplyReceiver.kt | 18 +++++--- .../DeleteNotificationReceiver.kt | 5 +-- .../notifications/MarkReadReceiver.kt | 10 ++++- .../notifications/RemoteReplyReceiver.kt | 13 +++--- 10 files changed, 58 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt index 78f48fee60..e4c01bdc86 100644 --- a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -176,6 +176,7 @@ interface StorageProtocol { ): MessageId? fun updateConversationLastSeenIfNeeded(threadAddress: Address.Conversable, lastSeenTime: Long) + fun updateConversationLastSeenIfNeeded(threadId: Long, lastSeenTime: Long) /** * Marks the conversation as read up to and including the message with [messageId]. It will 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 7d9878c7fb..8477a747e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -642,6 +642,10 @@ class MmsDatabase @Inject constructor( ) } + if (runThreadUpdate) { + threadDatabase.notifyThreadUpdated(threadId) + } + return messageId } @@ -720,8 +724,9 @@ class MmsDatabase @Inject constructor( where: String, vararg whereArgs: Any?): Boolean { val deletedMessageIDs: MutableList - val deletedMessagesThreadIDs = hashSetOf() + val deletedMessagesThreadIDs = MutableLongSet(1) + //language=roomsql writableDatabase.rawQuery( "DELETE FROM $TABLE_NAME WHERE $where RETURNING $ID, $THREAD_ID", *whereArgs @@ -744,9 +749,7 @@ class MmsDatabase @Inject constructor( } if (updateThread) { - for (threadId in deletedMessagesThreadIDs) { - threadDatabase.notifyThreadUpdated(threadId) - } + deletedMessagesThreadIDs.forEach(threadDatabase::notifyThreadUpdated) } return deletedMessageIDs.isNotEmpty() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 2c2d148341..b82d973a0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -195,6 +195,17 @@ open class Storage @Inject constructor( } } + override fun updateConversationLastSeenIfNeeded( + threadId: Long, + lastSeenTime: Long + ) { + val threadAddress = threadDatabase.getRecipientForThreadId(threadId) as? Address.Conversable ?: return + updateConversationLastSeenIfNeeded( + threadAddress = threadAddress, + lastSeenTime = lastSeenTime + ) + } + override fun markConversationAsReadUpToMessage(messageId: MessageId) { val maxTimestampMillsAndThreadId = mmsSmsDatabase.getMaxTimestampInThreadUpTo(messageId) if (maxTimestampMillsAndThreadId != null) { @@ -1010,8 +1021,6 @@ open class Storage @Inject constructor( mmsDatabase.deleteMessagesFrom(threadID, fromUser.toString()) } - threadDb.setRead(threadID, true) - return true } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 687ebf3377..e1d961df88 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -31,7 +31,6 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; -import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.ConfigFactoryProtocol; import org.session.libsession.utilities.GroupUtil; @@ -46,7 +45,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -335,47 +333,6 @@ public void trimThreadBefore(long threadId, long timestamp) { notifyThreadUpdated(threadId); } - public List setRead(long threadId, long lastReadTime) { - - final List smsRecords = smsDatabase.get().setMessagesRead(threadId, lastReadTime); - final List mmsRecords = mmsDatabase.get().setMessagesRead(threadId, lastReadTime); - - ContentValues contentValues = new ContentValues(2); - contentValues.put(READ, smsRecords.isEmpty() && mmsRecords.isEmpty()); - contentValues.put(LAST_SEEN, lastReadTime); - - SQLiteDatabase db = getWritableDatabase(); - db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""}); - - notifyThreadUpdated(threadId); - - return CollectionsKt.plus(smsRecords, mmsRecords); - } - - public List setRead(long threadId, boolean lastSeen) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(READ, 1); - contentValues.put(UNREAD_COUNT, 0); - contentValues.put(UNREAD_MENTION_COUNT, 0); - - if (lastSeen) { - contentValues.put(LAST_SEEN, SnodeAPI.getNowWithOffset()); - } - - SQLiteDatabase db = getWritableDatabase(); - db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""}); - - final List smsRecords = smsDatabase.get().setMessagesRead(threadId); - final List mmsRecords = mmsDatabase.get().setMessagesRead(threadId); - - notifyThreadUpdated(threadId); - - return new LinkedList() {{ - addAll(smsRecords); - addAll(mmsRecords); - }}; - } - public void setCreationDate(long threadId, long date) { ContentValues contentValues = new ContentValues(1); contentValues.put(THREAD_CREATION_DATE, date); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt index 8db779626e..7b1c2128c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt @@ -192,4 +192,4 @@ fun ThreadDatabase.threadContainsOutgoingMessage(threadId: Long): Boolean { AND NOT ${MmsSmsColumns.IS_DELETED} LIMIT 1 """).use { it.count > 0 } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java index 62173b3390..ae2530edd5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java @@ -26,14 +26,8 @@ import androidx.core.app.NotificationManagerCompat; import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.database.MarkedMessageInfo; -import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.dependencies.DatabaseComponent; - -import java.util.LinkedList; -import java.util.List; +import org.session.libsession.snode.SnodeClock; +import org.thoughtcrime.securesms.database.Storage; import javax.inject.Inject; @@ -50,9 +44,9 @@ public class AndroidAutoHeardReceiver extends BroadcastReceiver { public static final String THREAD_IDS_EXTRA = "car_heard_thread_ids"; public static final String NOTIFICATION_ID_EXTRA = "car_notification_id"; - @Inject MarkReadProcessor markReadProcessor; + @Inject Storage storage; @Inject MessageNotifier messageNotifier; - @Inject ThreadDatabase threadDb; + @Inject SnodeClock clock; @SuppressLint("StaticFieldLeak") @Override @@ -70,19 +64,14 @@ public void onReceive(final Context context, Intent intent) new AsyncTask() { @Override protected Void doInBackground(Void... params) { - List messageIdsCollection = new LinkedList<>(); - - for (long threadId : threadIds) { - Log.i(TAG, "Marking meassage as read: " + threadId); - List messageIds = threadDb.setRead(threadId, true); - - messageIdsCollection.addAll(messageIds); - } + long now = clock.currentTimeMills(); + for (long threadId : threadIds) { + storage.updateConversationLastSeenIfNeeded(threadId, now); + } - messageNotifier.updateNotification(context); - markReadProcessor.process(messageIdsCollection); + messageNotifier.updateNotification(context); - return null; + return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.kt index f6ba38c84d..5e64443437 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.kt @@ -28,15 +28,16 @@ import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.sending_receiving.MessageSender -import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier import org.session.libsession.snode.SnodeAPI.nowWithOffset +import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.Address import org.session.libsession.utilities.isGroupOrCommunity import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.SmsDatabase +import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.mms.MmsException import org.thoughtcrime.securesms.pro.ProStatusManager @@ -63,7 +64,7 @@ class AndroidAutoReplyReceiver : BroadcastReceiver() { lateinit var messageNotifier: MessageNotifier @Inject - lateinit var markReadProcessor: MarkReadProcessor + lateinit var storage: Storage @Inject lateinit var messageSender: MessageSender @@ -71,6 +72,9 @@ class AndroidAutoReplyReceiver : BroadcastReceiver() { @Inject lateinit var proStatusManager: ProStatusManager + @Inject + lateinit var clock: SnodeClock + @SuppressLint("StaticFieldLeak") override fun onReceive(context: Context, intent: Intent) { if (REPLY_ACTION != intent.getAction()) return @@ -97,7 +101,7 @@ class AndroidAutoReplyReceiver : BroadcastReceiver() { val message = VisibleMessage() message.text = responseText.toString() proStatusManager.addProFeatures(message) - message.sentTimestamp = nowWithOffset + message.sentTimestamp = clock.currentTimeMills() messageSender.send(message, address!!) val expiryMode = recipientRepository.getRecipientSync(address).expiryMode val expiresInMillis = expiryMode.expiryMillis @@ -142,10 +146,14 @@ class AndroidAutoReplyReceiver : BroadcastReceiver() { ) } - val messageIds = threadDatabase.setRead(replyThreadId, true) + if (address is Address.Conversable) { + storage.updateConversationLastSeenIfNeeded( + threadAddress = address, + lastSeenTime = clock.currentTimeMills() + ) + } messageNotifier.updateNotification(context) - markReadProcessor.process(messageIds) return null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.kt index 3c25682a2d..f01157149c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.kt @@ -9,11 +9,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier -import org.session.libsession.utilities.Address import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.SmsDatabase import org.thoughtcrime.securesms.database.Storage -import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.dependencies.ManagerScope import javax.inject.Inject @@ -35,7 +33,6 @@ class DeleteNotificationReceiver : BroadcastReceiver() { @Inject lateinit var smsDb: SmsDatabase @Inject lateinit var mmsDb: MmsDatabase @Inject lateinit var storage: Storage - @Inject lateinit var threadDatabase: ThreadDatabase override fun onReceive(context: Context, intent: Intent) { if (intent.action != DELETE_NOTIFICATION_ACTION) return @@ -53,7 +50,7 @@ class DeleteNotificationReceiver : BroadcastReceiver() { val now = System.currentTimeMillis() for (threadId in threadIds){ storage.updateConversationLastSeenIfNeeded( - threadAddress = threadDatabase.getRecipientForThreadId(threadId) as? Address.Conversable ?: continue, + threadId = threadId, lastSeenTime = now ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt index c652268849..57dfd1c223 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt @@ -5,13 +5,14 @@ import android.content.Context import android.content.Intent import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.session.libsession.database.StorageProtocol import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.Address import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.dependencies.ManagerScope import javax.inject.Inject @AndroidEntryPoint @@ -25,12 +26,17 @@ class MarkReadReceiver : BroadcastReceiver() { @Inject lateinit var threadDatabase: ThreadDatabase + @Inject + @ManagerScope + lateinit var scope: CoroutineScope + override fun onReceive(context: Context, intent: Intent) { if (CLEAR_ACTION != intent.action) return val threadIds = intent.getLongArrayExtra(THREAD_IDS_EXTRA) ?: return NotificationManagerCompat.from(context).cancel(intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1)) - GlobalScope.launch { + + scope.launch { val currentTime = clock.currentTimeMills() threadIds.forEach { Log.i(TAG, "Marking as read: $it") diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.kt index 40513eaec5..3c007fe8a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.kt @@ -28,7 +28,6 @@ import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.sending_receiving.MessageSender -import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.Address @@ -48,7 +47,6 @@ import javax.inject.Inject */ @AndroidEntryPoint class RemoteReplyReceiver : BroadcastReceiver() { - @Inject lateinit var threadDatabase: ThreadDatabase @Inject @@ -69,9 +67,6 @@ class RemoteReplyReceiver : BroadcastReceiver() { @Inject lateinit var recipientRepository: RecipientRepository - @Inject - lateinit var markReadProcessor: MarkReadProcessor - @Inject lateinit var messageSender: MessageSender @@ -153,10 +148,14 @@ class RemoteReplyReceiver : BroadcastReceiver() { } } - val messageIds = threadDatabase.setRead(threadId, true) + if (address is Address.Conversable) { + storage.updateConversationLastSeenIfNeeded( + threadAddress = address, + lastSeenTime = clock.currentTimeMills() + ) + } messageNotifier.updateNotification(context) - markReadProcessor.process(messageIds) return null } From 764deff154d7dacaabb00f8f33846fb6b44d4d30 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:48:10 +1100 Subject: [PATCH 05/12] Remove debug code --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2834925672..73c2c09f84 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -190,7 +190,7 @@ android { enableUnitTestCoverage = false signingConfig = signingConfigs.getByName("debug") -// applicationIdSuffix = ".${name}" + applicationIdSuffix = ".${name}" enablePermissiveNetworkSecurityConfig(true) devNetDefaultOn(false) setAlternativeAppName("Session Debug") From 0d19fce765a5fa2c580b4dddcb87bde2848a7a0a Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Tue, 16 Dec 2025 09:53:24 +1100 Subject: [PATCH 06/12] Fixed test issues --- .../DisappearingMessagesViewModelTest.kt | 17 +++++++++++------ .../v2/ConversationViewModelTest.kt | 4 +++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt index 1c6b3b18df..7765095164 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt @@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsD import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.OptionsCardData import org.thoughtcrime.securesms.ui.UINavigator +import java.time.Instant import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours @@ -253,7 +254,8 @@ class DisappearingMessagesViewModelTest : BaseViewModelTest() { expiryMode = ExpiryMode.NONE, priority = 1, proData = null, - profileUpdatedAt = null + profileUpdatedAt = null, + createdAt = Instant.now(), ) ) ) @@ -307,7 +309,8 @@ class DisappearingMessagesViewModelTest : BaseViewModelTest() { expiryMode = ExpiryMode.AfterSend(time.inWholeSeconds), priority = 1, proData = null, - profileUpdatedAt = null + profileUpdatedAt = null, + createdAt = Instant.now(), ) ) ) @@ -368,7 +371,8 @@ class DisappearingMessagesViewModelTest : BaseViewModelTest() { expiryMode = ExpiryMode.AfterSend(time.inWholeSeconds), priority = 1, proData = null, - profileUpdatedAt = null + profileUpdatedAt = null, + createdAt = Instant.now(), ) ) ) @@ -429,7 +433,8 @@ class DisappearingMessagesViewModelTest : BaseViewModelTest() { expiryMode = ExpiryMode.AfterRead(time.inWholeSeconds), priority = 1, proData = null, - profileUpdatedAt = null + profileUpdatedAt = null, + createdAt = Instant.now(), ) ) ) @@ -492,7 +497,8 @@ class DisappearingMessagesViewModelTest : BaseViewModelTest() { expiryMode = ExpiryMode.AfterRead(time.inWholeSeconds), priority = 1, proData = null, - profileUpdatedAt = null + profileUpdatedAt = null, + createdAt = Instant.now(), ) ) ) @@ -562,7 +568,6 @@ class DisappearingMessagesViewModelTest : BaseViewModelTest() { context = application, disappearingMessages = disappearingMessages, navigator = navigator, - isNewConfigEnabled = true, showDebugOptions = false, recipientRepository = mock { onBlocking { getRecipient(recipient.address) } doReturn recipient diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt index a93273784d..6e8a5fa3eb 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt @@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUtils +import java.time.Instant import java.time.ZonedDateTime private val STANDARD_ADDRESS = @@ -71,7 +72,8 @@ class ConversationViewModelTest : BaseViewModelTest() { approvedMe = true, blocked = false, expiryMode = ExpiryMode.NONE, - 1, + createdAt = Instant.now(), + priority = 1, proData = null, profileUpdatedAt = null ) From 82ad649e27f02df8e5ffeb10be50c258e92c3b21 Mon Sep 17 00:00:00 2001 From: fanchao Date: Tue, 16 Dec 2025 15:53:55 +1100 Subject: [PATCH 07/12] Merge issues --- .../utilities/recipients/RecipientData.kt | 29 ++++++++++++------- .../securesms/database/RecipientRepository.kt | 11 +------ .../securesms/database/ThreadDatabaseExt.kt | 6 ++-- .../notifications/MarkReadProcessor.kt | 1 + 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/session/libsession/utilities/recipients/RecipientData.kt b/app/src/main/java/org/session/libsession/utilities/recipients/RecipientData.kt index 3b2fbe577b..f171d2b9f1 100644 --- a/app/src/main/java/org/session/libsession/utilities/recipients/RecipientData.kt +++ b/app/src/main/java/org/session/libsession/utilities/recipients/RecipientData.kt @@ -9,7 +9,9 @@ import okhttp3.HttpUrl.Companion.toHttpUrl import org.session.libsession.messaging.open_groups.GroupMemberRole import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.utilities.Address +import org.session.libsession.utilities.recipients.RemoteFile.Companion.toRemoteFile import org.session.libsignal.utilities.AccountId +import org.thoughtcrime.securesms.util.DateUtils.Companion.secondsToInstant import java.time.Instant /** @@ -134,18 +136,24 @@ sealed interface RecipientData { * A recipient that was saved in your contact config. */ data class Contact( - val name: String, - val nickname: String?, - override val avatar: RemoteFile.Encrypted?, - val approved: Boolean, - val approvedMe: Boolean, - val blocked: Boolean, - val expiryMode: ExpiryMode, - val createdAt: Instant, - override val priority: Long, + private val configData: network.loki.messenger.libsession_util.util.Contact, override val proData: ProData?, - override val profileUpdatedAt: Instant?, ) : RecipientData { + val name: String get() = configData.name + val nickname: String? get() = configData.nickname.takeIf { it.isNotBlank() } + val approved: Boolean get() = configData.approved + val approvedMe: Boolean get() = configData.approvedMe + val blocked: Boolean get() = configData.blocked + val createdAt: Instant get() = Instant.ofEpochSecond(configData.createdEpochSeconds) + override val priority: Long get() = configData.priority + override val profileUpdatedAt: Instant? get() = configData.profileUpdatedEpochSeconds + .secondsToInstant() + + val expiryMode: ExpiryMode get() = configData.expiryMode + + override val avatar: RemoteFile? + get() = configData.profilePicture.toRemoteFile() + val displayName: String get() = nickname?.takeIf { it.isNotBlank() } ?: name @@ -187,6 +195,7 @@ sealed interface RecipientData { val kicked: Boolean get() = groupInfo.kicked val destroyed: Boolean get() = groupInfo.destroyed val shouldPoll: Boolean get() = groupInfo.shouldPoll + val joinedAt: Instant get() = Instant.ofEpochSecond(groupInfo.joinedAtSecs) override val profileUpdatedAt: Instant? get() = null diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt index 5adc5f6414..58d9b7305d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt @@ -633,17 +633,8 @@ class RecipientRepository @Inject constructor( } RecipientData.Contact( - name = contact.name, - nickname = contact.nickname.takeIf { it.isNotBlank() }, - avatar = contact.profilePicture.toRemoteFile(), - approved = contact.approved, - approvedMe = contact.approvedMe, - blocked = contact.blocked, - expiryMode = contact.expiryMode, - priority = contact.priority, + configData = contact, proData = null, // final ProData will be calculated later - profileUpdatedAt = contact.profileUpdatedEpochSeconds.secondsToInstant(), - createdAt = Instant.ofEpochSecond(contact.createdEpochSeconds) ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt index 7b1c2128c5..b4cb05d91f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt @@ -5,6 +5,7 @@ import androidx.sqlite.db.transaction import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.recipients.RecipientData +import org.session.libsession.utilities.withUserConfigs import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.util.asSequence import org.thoughtcrime.securesms.util.get @@ -146,7 +147,8 @@ fun ThreadDatabase.queryThreads(addresses: Collection): Lis val date = when { lastMessage != null -> lastMessage.dateReceived - threadRecipient.data is RecipientData.Contact -> threadRecipient.data.name + threadRecipient.data is RecipientData.Contact -> threadRecipient.data.createdAt.toEpochMilli() + threadRecipient.data is RecipientData.Group -> threadRecipient.data.joinedAt.toEpochMilli() else -> 0L } @@ -158,7 +160,7 @@ fun ThreadDatabase.queryThreads(addresses: Collection): Lis unreadCount = smsUnreadCount.toInt() + mmsUnreadCount.toInt(), unreadMentionCount = smsUnreadMentionCount.toInt() + mmsUnreadMentionCount.toInt(), isUnread = unread, - date = 0L, + date = date, invitingAdminId = invitingAdminId ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadProcessor.kt index 730e66576d..4eda2a9d1c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadProcessor.kt @@ -25,6 +25,7 @@ import org.session.libsession.utilities.isGroupOrCommunity import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.RecipientData import org.session.libsession.utilities.userConfigsChanged +import org.session.libsession.utilities.withUserConfigs import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.auth.AuthAwareComponent From 49588face97c2f14d41013c39d6ec93298c2dbb2 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:10:13 +1100 Subject: [PATCH 08/12] Fixed tests and added comments --- .../securesms/database/SmsDatabase.java | 12 +-- .../DisappearingMessagesViewModelTest.kt | 77 +++++++------------ .../v2/ConversationViewModelTest.kt | 22 +++--- 3 files changed, 43 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index fb522215a7..d46a57a39a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -140,12 +140,12 @@ public static void addProFeatureColumns(SupportSQLiteDatabase db) { public static void addOutgoingColumn(SupportSQLiteDatabase db) { final String allOutgoingMessageTypeSet = ArraysKt.joinToString( Types.OUTGOING_MESSAGE_TYPES, - ",", - "(", - ")", - -1, - "", - (value) -> Long.toString(value) + /* separator */ ",", + /* prefix */ "(", + /* postfix */ ")", + /* limit */ -1, + /* truncated */ "", + /* transform */ (value) -> Long.toString(value) ); db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + IS_OUTGOING + diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt index 7765095164..0e5890aebd 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import network.loki.messenger.R import network.loki.messenger.libsession_util.util.Bytes +import network.loki.messenger.libsession_util.util.Contact import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupInfo import org.junit.Rule @@ -245,17 +246,12 @@ class DisappearingMessagesViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(Recipient( address = STANDARD_ADDRESS, data = RecipientData.Contact( - name = "Contact", - nickname = null, - avatar = null, - approved = true, - approvedMe = true, - blocked = false, - expiryMode = ExpiryMode.NONE, - priority = 1, - proData = null, - profileUpdatedAt = null, - createdAt = Instant.now(), + configData = Contact( + id = "contact-id", + name = "Contact", + expiryMode = ExpiryMode.NONE + ), + proData = null ) ) ) @@ -300,17 +296,12 @@ class DisappearingMessagesViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(Recipient( address = STANDARD_ADDRESS, data = RecipientData.Contact( - name = "Contact", - nickname = null, - avatar = null, - approved = true, - approvedMe = true, - blocked = false, - expiryMode = ExpiryMode.AfterSend(time.inWholeSeconds), - priority = 1, + configData = Contact( + id = "contact-id", + name = "Contact", + expiryMode = ExpiryMode.AfterSend(time.inWholeSeconds), + ), proData = null, - profileUpdatedAt = null, - createdAt = Instant.now(), ) ) ) @@ -362,17 +353,12 @@ class DisappearingMessagesViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(Recipient( address = STANDARD_ADDRESS, data = RecipientData.Contact( - name = "Contact", - nickname = null, - avatar = null, - approved = true, - approvedMe = true, - blocked = false, - expiryMode = ExpiryMode.AfterSend(time.inWholeSeconds), - priority = 1, + configData = Contact( + id = "contact-id", + name = "Contact", + expiryMode = ExpiryMode.AfterSend(time.inWholeSeconds), + ), proData = null, - profileUpdatedAt = null, - createdAt = Instant.now(), ) ) ) @@ -424,17 +410,12 @@ class DisappearingMessagesViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(Recipient( address = STANDARD_ADDRESS, data = RecipientData.Contact( - name = "Contact", - nickname = null, - avatar = null, - approved = true, - approvedMe = true, - blocked = false, - expiryMode = ExpiryMode.AfterRead(time.inWholeSeconds), - priority = 1, + configData = Contact( + id = "contact-id", + name = "Contact", + expiryMode = ExpiryMode.AfterRead(time.inWholeSeconds), + ), proData = null, - profileUpdatedAt = null, - createdAt = Instant.now(), ) ) ) @@ -488,17 +469,11 @@ class DisappearingMessagesViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(Recipient( address = STANDARD_ADDRESS, data = RecipientData.Contact( - name = "Contact", - nickname = null, - avatar = null, - approved = true, - approvedMe = true, - blocked = false, - expiryMode = ExpiryMode.AfterRead(time.inWholeSeconds), - priority = 1, + configData = Contact( + id = "contact-id", + expiryMode = ExpiryMode.AfterRead(time.inWholeSeconds), + ), proData = null, - profileUpdatedAt = null, - createdAt = Instant.now(), ) ) ) diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt index 6e8a5fa3eb..a7cc27213d 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf +import network.loki.messenger.libsession_util.util.Contact import network.loki.messenger.libsession_util.util.ExpiryMode import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat @@ -36,7 +37,6 @@ import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUtils -import java.time.Instant import java.time.ZonedDateTime private val STANDARD_ADDRESS = @@ -65,17 +65,17 @@ class ConversationViewModelTest : BaseViewModelTest() { private val standardRecipient = Recipient( address = STANDARD_ADDRESS, data = RecipientData.Contact( - name = "Test User", - nickname = "Test User", - avatar = null, - approved = true, - approvedMe = true, - blocked = false, - expiryMode = ExpiryMode.NONE, - createdAt = Instant.now(), - priority = 1, + configData = Contact( + id = "contact-1", + name = "Test User", + nickname = "Test User", + approved = true, + approvedMe = true, + blocked = false, + expiryMode = ExpiryMode.NONE, + createdEpochSeconds = System.currentTimeMillis() / 1000L, + ), proData = null, - profileUpdatedAt = null ) ) From 0bf3a064d815c90d257dc72f885f7896ace9739f Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:58:38 +1100 Subject: [PATCH 09/12] Add missing place to notify thread updated --- .../securesms/database/MmsDatabase.kt | 3 + .../securesms/database/SmsDatabase.java | 55 +++++++++++-------- 2 files changed, 35 insertions(+), 23 deletions(-) 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 8477a747e5..568cbbefd5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -234,6 +234,9 @@ class MmsDatabase @Inject constructor( if (it.moveToFirst()) it.getLong(0) else null } + if (threadId != null) { + threadDatabase.notifyThreadUpdated(threadId) + } } fun getThreadIdForMessage(id: Long): Long { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index d46a57a39a..6260ecf2fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -24,6 +24,9 @@ import android.database.Cursor; import androidx.collection.ArraySet; +import androidx.collection.LongList; +import androidx.collection.LongSet; +import androidx.collection.MutableLongSet; import androidx.sqlite.db.SupportSQLiteDatabase; import com.annimon.stream.Stream; @@ -60,6 +63,7 @@ import dagger.Lazy; import dagger.hilt.android.qualifiers.ApplicationContext; +import kotlin.Unit; import kotlin.collections.ArraysKt; import network.loki.messenger.libsession_util.protocol.ProFeature; @@ -389,19 +393,16 @@ public List setMessagesRead(long threadId, long beforeTime) { beforeTime ); } - public List setMessagesRead(long threadId) { - return SmsDatabaseExtKt.setMessagesRead( - this, - THREAD_ID + " = ? AND (" + READ + " = 0)", - threadId - ); - } - public void updateSentTimestamp(long messageId, long newTimestamp) { SQLiteDatabase db = getWritableDatabase(); - db.rawExecSQL("UPDATE " + TABLE_NAME + " SET " + DATE_SENT + " = ? " + - "WHERE " + ID + " = ?", newTimestamp, messageId); + try (final Cursor cursor = db.rawQuery("UPDATE " + TABLE_NAME + " SET " + DATE_SENT + " = ? " + + "WHERE " + ID + " = ? RETURNING " + THREAD_ID, newTimestamp, messageId)) { + if (cursor.moveToNext()) { + long threadId = cursor.getLong(0); + threadDatabase.get().notifyThreadUpdated(threadId); + } + } } protected Optional insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runThreadUpdate) { @@ -535,7 +536,13 @@ public long insertMessageOutbox(long threadId, OutgoingTextMessage message, return -1; } - return getWritableDatabase().insert(TABLE_NAME, ADDRESS, contentValues); + final long id = getWritableDatabase().insert(TABLE_NAME, ADDRESS, contentValues); + + if (runThreadUpdate) { + threadDatabase.get().notifyThreadUpdated(threadId); + } + + return id; } @Override public List getExpiredMessageIDs(long nowMills) { @@ -575,7 +582,8 @@ public void deleteMessage(long messageId) { } @Override - public void deleteMessages(Collection messageIds) {doDeleteMessages(true, + public void deleteMessages(Collection messageIds) { + doDeleteMessages(true, ID + " IN (SELECT value FROM json_each(?))", new JSONArray(messageIds).toString() ); @@ -616,24 +624,25 @@ private boolean isDuplicate(OutgoingTextMessage message, long threadId) { } } - private boolean doDeleteMessages(final boolean updateThread, @NonNull final String where, @Nullable final Object...args) { + private void doDeleteMessages(final boolean updateThread, @NonNull final String where, @Nullable final Object...args) { final String sql = "DELETE FROM " + TABLE_NAME + " WHERE " + where + " RETURNING " + THREAD_ID; - final HashSet deletedMessageThreadIds = new HashSet<>(); + final MutableLongSet deletedMessageThreadIds = updateThread ? new MutableLongSet() : null; try (final Cursor cursor = getWritableDatabase().rawQuery(sql, args)) { - while (cursor.moveToNext()) { - final long threadId = cursor.getLong(0); - deletedMessageThreadIds.add(threadId); + if (deletedMessageThreadIds != null) { + while (cursor.moveToNext()) { + final long threadId = cursor.getLong(0); + deletedMessageThreadIds.add(threadId); + } } } - if (updateThread) { - for (final long threadId : deletedMessageThreadIds) { - threadDatabase.get().notifyThreadUpdated(threadId); - } + if (deletedMessageThreadIds != null) { + deletedMessageThreadIds.forEach(id -> { + threadDatabase.get().notifyThreadUpdated(id); + return Unit.INSTANCE; + }); } - - return !deletedMessageThreadIds.isEmpty(); } void deleteMessagesFrom(long threadId, String fromUser) { From f07c53b7b873916ab56956a9a995d733e0091185 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:20:51 +1100 Subject: [PATCH 10/12] Tidy up --- .../securesms/database/MmsDatabase.kt | 33 ++++++------------- .../securesms/database/SmsDatabase.java | 2 +- .../service/ExpiringMessageManager.kt | 2 -- 3 files changed, 11 insertions(+), 26 deletions(-) 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 568cbbefd5..a033bc4f7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -370,33 +370,20 @@ class MmsDatabase @Inject constructor( } fun setMessagesRead(threadId: Long, beforeTime: Long): List { - return setMessagesRead( - "$THREAD_ID = ? AND ($READ = 0) AND $DATE_SENT <= ?", - threadId, - beforeTime - ) - } - - fun setMessagesRead(threadId: Long): List { - return setMessagesRead("$THREAD_ID = ? AND ($READ = 0)",threadId - ) - } - - private fun setMessagesRead(where: String, vararg args: Any?): List { val updatedThreadIDs = MutableLongSet(1) //language=roomsql val messages = writableDatabase.rawQuery(""" - UPDATE $TABLE_NAME - SET $READ = 1 - WHERE $where - RETURNING $ID, - $ADDRESS, - $THREAD_ID, - $DATE_SENT, - $EXPIRES_IN, - $EXPIRE_STARTED - """, *args).use { cursor -> + UPDATE $TABLE_NAME + SET $READ = 1 + WHERE $THREAD_ID = ? AND ($READ = 0) AND $DATE_SENT <= ? + RETURNING $ID, + $ADDRESS, + $THREAD_ID, + $DATE_SENT, + $EXPIRES_IN, + $EXPIRE_STARTED + """, threadId, beforeTime).use { cursor -> cursor.asSequence() .map { val timestamp = cursor.getLong(3) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 6260ecf2fb..4351fe781e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -598,7 +598,7 @@ public void updateThreadId(long fromId, long toId) { db.update(TABLE_NAME, contentValues, THREAD_ID + " = ?", new String[] {fromId + ""}); } - private boolean isDuplicate(IncomingTextMessage message, long threadId) { + private boolean isDuplicate(IncomingTextMessage message, long threadId) { SQLiteDatabase database = getReadableDatabase(); Cursor cursor = database.query(TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?", new String[]{String.valueOf(message.getSentTimestampMillis()), message.getSender().toString(), String.valueOf(threadId)}, diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt index aa49ad2bfe..b6ed94932d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt @@ -13,7 +13,6 @@ import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.toAddress -import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.auth.AuthAwareComponent @@ -21,7 +20,6 @@ import org.thoughtcrime.securesms.auth.LoggedInState import org.thoughtcrime.securesms.auth.LoginStateRepository import org.thoughtcrime.securesms.database.MessagingDatabase import org.thoughtcrime.securesms.database.MmsDatabase -import org.thoughtcrime.securesms.database.MmsSmsDatabase import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.SmsDatabase import org.thoughtcrime.securesms.database.Storage From 50d4bd081238fc2c07a46b90fe5c2b02ae8f664d Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:49:28 +1100 Subject: [PATCH 11/12] Fixed missing thread notification --- .../messaging/sending_receiving/ReceivedMessageProcessor.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt index b3b208b265..270eface78 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt @@ -109,6 +109,7 @@ class ReceivedMessageProcessor @Inject constructor( ) notificationManager.updateNotification(this.context, threadId) + threadDatabase.notifyThreadUpdated(threadId) } // Handle pending community reactions From f1122d0c1f6b1caf5726b877d526f81dbf9b3f44 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:42:45 +1100 Subject: [PATCH 12/12] Optimise thread query --- .../securesms/configs/ConfigToDatabaseSync.kt | 11 ++++++ .../securesms/database/ThreadDatabase.java | 6 ++-- .../securesms/database/ThreadDatabaseExt.kt | 20 +++++++---- .../securesms/util/SharedConfigUtils.kt | 35 +++++++++++++++++-- 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt b/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt index 3262338256..ea952f2cc1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt @@ -51,6 +51,8 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.util.SessionMetaProtocol +import org.thoughtcrime.securesms.util.erase +import org.thoughtcrime.securesms.util.get import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -114,6 +116,15 @@ class ConfigToDatabaseSync @Inject constructor( // If you can find out what it does, please remove it. SessionMetaProtocol.clearReceivedMessages() + // Remove all convo info + configFactory.withMutableUserConfigs { configs -> + result.deletedThreads.keys.forEach { address -> + if (address is Address.Conversable) { + configs.convoInfoVolatile.erase(address) + } + } + } + // Some type of convo require additional cleanup, we'll go through them here for ((address, threadId) in result.deletedThreads) { storage.cancelPendingMessageSendJobs(threadId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index e1d961df88..1eeded33e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -23,6 +23,8 @@ import android.content.Context; import android.database.Cursor; +import androidx.collection.ArrayMap; + import com.annimon.stream.Stream; import net.zetetic.database.sqlcipher.SQLiteDatabase; @@ -284,7 +286,7 @@ public EnsureThreadsResult ensureThreads(@NonNull final Iterable(cursor.getCount()); + deletedThreads = new ArrayMap<>(cursor.getCount()); while (cursor.moveToNext()) { deletedThreads.put( Address.fromSerialized(cursor.getString(1)), @@ -299,7 +301,7 @@ public EnsureThreadsResult ensureThreads(@NonNull final Iterable(cursor.getCount()); + createdThreads = new ArrayMap<>(cursor.getCount()); while (cursor.moveToNext()) { createdThreads.put( Address.fromSerialized(cursor.getString(1)), diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt index b4cb05d91f..26932e98e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabaseExt.kt @@ -1,7 +1,9 @@ package org.thoughtcrime.securesms.database +import androidx.collection.SimpleArrayMap import androidx.core.database.getStringOrNull import androidx.sqlite.db.transaction +import network.loki.messenger.libsession_util.util.Conversation import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.recipients.RecipientData @@ -12,9 +14,16 @@ import org.thoughtcrime.securesms.util.get fun ThreadDatabase.queryThreads(addresses: Collection): List { val convoInfo = configFactory.get().withUserConfigs { configs -> - addresses.associateWith { address -> - configs.convoInfoVolatile.get(address) + val out = SimpleArrayMap() + + for (address in addresses) { + val convo = configs.convoInfoVolatile.get(address) + if (convo != null) { + out.put(address, convo) + } } + + out } // For our query, we need to fill in some information first before the main query can be done. @@ -44,7 +53,8 @@ fun ThreadDatabase.queryThreads(addresses: Collection): Lis VALUES (?, ?, ?) """ ).use { stmt -> - convoInfo.forEach { (address, convo) -> + addresses.forEach { address -> + val convo = convoInfo[address] stmt.clearBindings() stmt.bindString(1, address.address) stmt.bindLong(2, convo?.lastRead ?: 0L) @@ -72,7 +82,6 @@ fun ThreadDatabase.queryThreads(addresses: Collection): Lis FROM ${SmsDatabase.TABLE_NAME} s WHERE s.${SmsDatabase.THREAD_ID} = threads.${ThreadDatabase.ID} AND ${SmsDatabase.DATE_SENT} > input.last_read - AND NOT s.${SmsDatabase.READ} AND NOT s.${MmsSmsColumns.IS_DELETED} ) AS smsUnreadCount, @@ -82,7 +91,6 @@ fun ThreadDatabase.queryThreads(addresses: Collection): Lis FROM ${SmsDatabase.TABLE_NAME} s WHERE s.${SmsDatabase.THREAD_ID} = threads.${ThreadDatabase.ID} AND ${SmsDatabase.DATE_SENT} > input.last_read - AND NOT s.${SmsDatabase.READ} AND s.${SmsDatabase.HAS_MENTION} AND NOT s.${MmsSmsColumns.IS_DELETED} ) AS smsUnreadMentionCount, @@ -93,7 +101,6 @@ fun ThreadDatabase.queryThreads(addresses: Collection): Lis FROM ${MmsDatabase.TABLE_NAME} m WHERE m.${MmsSmsColumns.THREAD_ID} = threads.${ThreadDatabase.ID} AND ${MmsDatabase.DATE_SENT} > input.last_read - AND NOT m.${MmsSmsColumns.READ} AND NOT m.${MmsSmsColumns.IS_DELETED} ) AS mmsUnreadCount, @@ -103,7 +110,6 @@ fun ThreadDatabase.queryThreads(addresses: Collection): Lis FROM ${MmsDatabase.TABLE_NAME} m WHERE m.${MmsSmsColumns.THREAD_ID} = threads.${ThreadDatabase.ID} AND ${MmsDatabase.DATE_SENT} > input.last_read - AND NOT m.${MmsSmsColumns.READ} AND m.${MmsSmsColumns.HAS_MENTION} AND NOT m.${MmsSmsColumns.IS_DELETED} ) AS mmsUnreadMentionCount, diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt index f5e7239e66..cd08d2cc28 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.util +import network.loki.messenger.libsession_util.MutableConversationVolatileConfig import network.loki.messenger.libsession_util.ReadableConversationVolatileConfig import network.loki.messenger.libsession_util.util.Conversation import org.session.libsession.utilities.Address @@ -29,6 +30,30 @@ fun ReadableConversationVolatileConfig.get(address: Address.Conversable): Conver } } +fun MutableConversationVolatileConfig.erase(address: Address.Conversable) { + when (address) { + is Address.Standard -> { + eraseOneToOne(address.accountId.hexString) + } + + is Address.Group -> { + eraseClosedGroup(address.accountId.hexString) + } + + is Address.LegacyGroup -> { + eraseLegacyClosedGroup(address.groupPublicKeyHex) + } + + is Address.Community -> { + eraseCommunity(baseUrl = address.serverUrl, room = address.room) + } + + is Address.CommunityBlindedId -> { + eraseBlindedOneToOne(address.blindedId.address) + } + } +} + fun MutableUserConfigs.getOrConstructConvo(address: Address.Conversable): Conversation { return when (address) { is Address.Standard -> { @@ -44,12 +69,18 @@ fun MutableUserConfigs.getOrConstructConvo(address: Address.Conversable): Conver } is Address.Community -> { - convoInfoVolatile.getOrConstructCommunity(baseUrl = address.serverUrl, room = address.room, pubKeyHex = requireNotNull( + val community = requireNotNull( userGroups.getCommunityInfo( baseUrl = address.serverUrl, room = address.room ) - ) { "Community does not exist" }.community.pubKeyHex) + ) { "Community does not exist" } + + convoInfoVolatile.getOrConstructCommunity( + baseUrl = address.serverUrl, + room = address.room, + pubKeyHex = community.community.pubKeyHex + ) } is Address.CommunityBlindedId -> {