diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 93957ec58f..be077b8d47 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -479,7 +479,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO poller.setUserPublicKey(userPublicKey); return; } - poller = new Poller(configFactory); + poller = new Poller(configFactory, new Timer()); } public void startPollingIfNeeded() { 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 567266f77b..9b80618115 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -128,8 +128,27 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF override fun markConversationAsRead(threadId: Long, lastSeenTime: Long) { val threadDb = DatabaseComponent.get(context).threadDatabase() - getRecipientForThread(threadId)?.let { - threadDb.markAllAsRead(threadId, it.isGroupRecipient, lastSeenTime) + getRecipientForThread(threadId)?.let { recipient -> + threadDb.markAllAsRead(threadId, recipient.isGroupRecipient, lastSeenTime) + configFactory.convoVolatile?.let { config -> + val convo = when { + // recipient closed group + recipient.isClosedGroupRecipient -> config.getOrConstructLegacyClosedGroup(recipient.address.serialize()) + // recipient is open group + recipient.isOpenGroupRecipient -> { + val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return + Conversation.OpenGroup.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) -> + config.getOrConstructOpenGroup(base, room, pubKey) + } ?: return + } + // otherwise recipient is one to one + recipient.isContactRecipient -> config.getOrConstructOneToOne(recipient.address.serialize()) + else -> throw NullPointerException("Weren't expecting to have a convo with address ${recipient.address.serialize()}") + } + convo.lastRead = lastSeenTime + config.set(convo) + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) + } } } @@ -346,8 +365,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF } Log.d("Loki-DBG", "Should update thread $threadId") if (threadId >= 0) { - DatabaseComponent.get(context).threadDatabase() - .setLastSeen(threadId, conversation.lastRead) + markConversationAsRead(threadId, conversation.lastRead) + updateThread(threadId, false) } } } 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 4ee8d10056..de89a99bb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -64,8 +64,6 @@ import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.util.SessionMetaProtocol; import java.io.Closeable; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -101,8 +99,6 @@ public class ThreadDatabase extends Database { public static final String HAS_SENT = "has_sent"; public static final String IS_PINNED = "is_pinned"; - public static final String LAST_SEEN_TRIGGER = "thread_last_seen_trigger"; - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " + MESSAGE_COUNT + " INTEGER DEFAULT 0, " + ADDRESS + " TEXT, " + SNIPPET + " TEXT, " + @@ -522,9 +518,19 @@ public class ThreadDatabase extends Database { contentValues.put(LAST_SEEN, lastSeenTime); db.beginTransaction(); db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)}); - String countSubQuery = "SELECT COUNT(*) FROM "+SmsDatabase.TABLE_NAME+" AS s INNER JOIN "+TABLE_NAME+" AS t ON s.thread_id = t._id WHERE t._id = ? AND s."+SmsDatabase.DATE_SENT+" > t."+LAST_SEEN+""; - String reflectUpdates = "UPDATE "+TABLE_NAME+" AS t SET "+UNREAD_COUNT+" = ("+countSubQuery+") WHERE t."+ID+" = ?"; - db.query(reflectUpdates, new String[]{threadId+"", 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+" = ?"; + db.execSQL(reflectUpdates, new Object[]{threadId}); db.setTransactionSuccessful(); db.endTransaction(); notifyConversationListListeners(); 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 c720dc291d..201ea05a11 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 @@ -88,10 +88,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV39 = 60; private static final int lokiV40 = 61; private static final int lokiV41 = 62; - private static final int lokiV42 = 63; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes - private static final int DATABASE_VERSION = lokiV42; + private static final int DATABASE_VERSION = lokiV41; private static final int MIN_DATABASE_VERSION = lokiV7; private static final String CIPHER3_DATABASE_NAME = "signal.db"; public static final String DATABASE_NAME = "signal_v4.db"; @@ -596,10 +595,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(ConfigDatabase.CREATE_CONFIG_TABLE_COMMAND); } - if (oldVersion < lokiV42) { -// db.execSQL(ThreadDatabase.getCreateLastSeenTrigger()); - } - db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt index dfbb82bc26..055e00b35f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt @@ -107,14 +107,14 @@ class ConfigFactory(private val context: Context, } override fun persist(forConfigObject: ConfigBase) { + listeners.forEach { listener -> + listener.notifyUpdates(forConfigObject) + } when (forConfigObject) { is UserProfile -> persistUserConfigDump() is Contacts -> persistContactsConfigDump() is ConversationVolatileConfig -> persistConvoVolatileConfigDump() } - listeners.forEach { listener -> - listener.notifyUpdates(forConfigObject) - } } override fun appendHash(configObject: ConfigBase, hash: String) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index 0cbfffbaec..bd1a5ab545 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -176,7 +176,7 @@ class BatchMessageReceiveJob( } // increment unreads, notify, and update thread // last seen will be the current last seen if not changed (re-computes the read counts for thread record) - storage.markConversationAsRead(threadId, 0) + storage.markConversationAsRead(threadId, myLastSeen) storage.updateThread(threadId, true) SSKEnvironment.shared.notificationManager.updateNotification(context, threadId) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt index 4175bea5d6..7f4f64944b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt @@ -20,7 +20,7 @@ data class ConfigurationSyncJob(val destination: Destination): Job { override var failureCount: Int = 0 override val maxFailureCount: Int = 1 - override suspend fun execute() { + override suspend fun execute(dispatcherName: String) { val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() val delegate = delegate @@ -37,7 +37,7 @@ data class ConfigurationSyncJob(val destination: Destination): Job { || (destination is Destination.Contact && destination.publicKey != userPublicKey) ) { Log.w(TAG, "No need to run config sync job, TODO") - delegate?.handleJobSucceeded(this) + delegate?.handleJobSucceeded(this, dispatcherName) return } @@ -52,7 +52,7 @@ data class ConfigurationSyncJob(val destination: Destination): Job { ).filter { config -> config.needsPush() } // don't run anything if we don't need to push anything - if (configsRequiringPush.isEmpty()) return delegate.handleJobSucceeded(this) + if (configsRequiringPush.isEmpty()) return delegate.handleJobSucceeded(this, dispatcherName) // allow null results here so the list index matches configsRequiringPush val batchObjects: List?> = configsRequiringPush.map { config -> @@ -79,7 +79,7 @@ data class ConfigurationSyncJob(val destination: Destination): Job { if (batchObjects.any { it == null }) { // stop running here, something like a signing error occurred - return delegate.handleJobFailedPermanently(this, NullPointerException("One or more requests had a null batch request info")) + return delegate.handleJobFailedPermanently(this, dispatcherName, NullPointerException("One or more requests had a null batch request info")) } val allRequests = mutableListOf() @@ -145,9 +145,9 @@ data class ConfigurationSyncJob(val destination: Destination): Job { } } catch (e: Exception) { Log.e(TAG, "Error performing batch request", e) - return delegate.handleJobFailedPermanently(this, e) + return delegate.handleJobFailedPermanently(this, dispatcherName, e) } - delegate.handleJobSucceeded(this) + delegate.handleJobSucceeded(this, dispatcherName) } fun Destination.destinationPublicKey(): String = when (this) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index bd06e292a0..6166e32da3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -26,6 +26,7 @@ import org.session.libsession.snode.RawResponse import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeModule import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsession.utilities.WindowDebouncer import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.Snode @@ -35,12 +36,14 @@ import java.util.TimerTask private class PromiseCanceledException : Exception("Promise canceled.") -class Poller(private val configFactory: ConfigFactoryProtocol) { +class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Timer) { var userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: "" private var hasStarted: Boolean = false private val usedSnodes: MutableSet = mutableSetOf() var isCaughtUp = false var configPollingJob: Job? = null + + val configDebouncer = WindowDebouncer(3000, debounceTimer) // region Settings companion object { @@ -208,10 +211,12 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { if (key == Namespace.DEFAULT) { processPersonalMessages(snode, body) } else { - when (ConfigBase.kindFor(key)) { - UserProfile::class.java -> processConfig(snode, body, key, configFactory.user) - Contacts::class.java -> processConfig(snode, body, key, configFactory.contacts) - ConversationVolatileConfig::class.java -> processConfig(snode, body, key, configFactory.convoVolatile) + configDebouncer.publish { + when (ConfigBase.kindFor(key)) { + UserProfile::class.java -> processConfig(snode, body, key, configFactory.user) + Contacts::class.java -> processConfig(snode, body, key, configFactory.contacts) + ConversationVolatileConfig::class.java -> processConfig(snode, body, key, configFactory.convoVolatile) + } } } }