feat: add basic message read logic for synchronizing last reads, need to modify the query to use the last seen instead of the unread count in a subquery possibly for thread display record

This commit is contained in:
0x330a 2023-02-15 17:29:29 +11:00
parent 03a343d832
commit 2f2ebe9451
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C
14 changed files with 189 additions and 67 deletions

View File

@ -40,6 +40,7 @@ import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPol
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
import org.session.libsession.snode.SnodeModule;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.ConfigFactoryUpdateListener;
import org.session.libsession.utilities.ProfilePictureUtilities;
import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences;
@ -115,6 +116,7 @@ import dagger.hilt.android.HiltAndroidApp;
import kotlin.Unit;
import kotlinx.coroutines.Job;
import network.loki.messenger.BuildConfig;
import network.loki.messenger.libsession_util.ConfigBase;
/**
* Will be called once when the TextSecure process is created.
@ -125,7 +127,7 @@ import network.loki.messenger.BuildConfig;
* @author Moxie Marlinspike
*/
@HiltAndroidApp
public class ApplicationContext extends Application implements DefaultLifecycleObserver {
public class ApplicationContext extends Application implements DefaultLifecycleObserver, ConfigFactoryUpdateListener {
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
@ -199,6 +201,11 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
return this.persistentLogger;
}
@Override
public void notifyUpdates(@NonNull ConfigBase forConfigObject) {
// forward to the config factory / storage ig
storage.notifyConfigUpdates(forConfigObject);
}
@Override
public void onCreate() {
DatabaseModule.init(this);

View File

@ -399,6 +399,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(id.toString()))
}
fun setMessagesRead(threadId: Long, beforeTime: Long): List<MarkedMessageInfo> {
return setMessagesRead(
THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1) AND " + DATE_RECEIVED + " <= ?",
arrayOf(threadId.toString(), beforeTime.toString())
)
}
fun setMessagesRead(threadId: Long): List<MarkedMessageInfo> {
return setMessagesRead(
THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)",

View File

@ -321,6 +321,9 @@ public class SmsDatabase extends MessagingDatabase {
}
}
public List<MarkedMessageInfo> setMessagesRead(long threadId, long beforeTime) {
return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1) AND " + DATE_RECEIVED + " <= ?", new String[]{threadId+"", beforeTime+""});
}
public List<MarkedMessageInfo> setMessagesRead(long threadId) {
return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)", new String[] {String.valueOf(threadId)});
}

View File

@ -2,6 +2,11 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.net.Uri
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserProfile
import network.loki.messenger.libsession_util.util.Conversation
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.BlindedIdMapping
@ -52,21 +57,26 @@ import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.ApplicationContext
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.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.SessionMetaProtocol
import java.security.MessageDigest
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol {
class Storage(context: Context, helper: SQLCipherOpenHelper, private val configFactory: ConfigFactory) : Database(context, helper), StorageProtocol {
override fun getUserPublicKey(): String? {
return TextSecurePreferences.getLocalNumber(context)
@ -262,6 +272,81 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return DatabaseComponent.get(context).lokiAPIDatabase().getAuthToken(id)
}
override fun notifyConfigUpdates(forConfigObject: ConfigBase) {
notifyUpdates(forConfigObject)
}
fun notifyUpdates(forConfigObject: ConfigBase) {
when (forConfigObject) {
is UserProfile -> updateUser(forConfigObject)
is Contacts -> updateContacts(forConfigObject)
is ConversationVolatileConfig -> updateConvoVolatile(forConfigObject)
}
}
private fun updateUser(userProfile: UserProfile) {
val userPublicKey = getUserPublicKey() ?: return
// would love to get rid of recipient and context from this
val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
// update name
val name = userProfile.getName() ?: return
val userPic = userProfile.getPic()
val profileManager = SSKEnvironment.shared.profileManager
if (name.isNotEmpty()) {
TextSecurePreferences.setProfileName(context, name)
profileManager.setName(context, recipient, name)
}
// update pfp
if (userPic == null) {
// clear picture if userPic is null
TextSecurePreferences.setProfileKey(context, null)
ProfileKeyUtil.setEncodedProfileKey(context, null)
profileManager.setProfileKey(context, recipient, null)
setUserProfilePictureURL(null)
} else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty()
&& TextSecurePreferences.getProfilePictureURL(context) != userPic.url) {
val profileKey = Base64.encodeBytes(userPic.key)
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
profileManager.setProfileKey(context, recipient, userPic.key)
setUserProfilePictureURL(userPic.url)
}
}
private fun updateContacts(contacts: Contacts) {
val extracted = contacts.all().toList()
extracted.forEach { contact ->
val address = Address.fromSerialized(contact.id)
val settings = getRecipientSettings(address) ?: run {
// new contact, store it
}
contact.name
contact.approved
contact.approvedMe
contact.blocked
contact.nickname
contact.profilePicture
}
}
private fun updateConvoVolatile(convos: ConversationVolatileConfig) {
val extracted = convos.all()
for (conversation in extracted) {
val threadId = when (conversation) {
is Conversation.OneToOne -> conversation.sessionId.let {
getOrCreateThreadIdFor(fromSerialized(it))
}
is Conversation.LegacyClosedGroup -> conversation.groupId.let {
getOrCreateThreadIdFor("", it,null)
}
is Conversation.OpenGroup -> conversation.baseUrl.let {
getOrCreateThreadIdFor("",null, it)
}
}
Log.d("Loki-DBG", "Should update thread $threadId")
}
}
override fun setAuthToken(room: String, server: String, newValue: String) {
val id = "$server.$room"
DatabaseComponent.get(context).lokiAPIDatabase().setAuthToken(id, newValue)
@ -455,6 +540,12 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun createGroup(groupId: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long) {
DatabaseComponent.get(context).groupDatabase().create(groupId, title, members, avatar, relay, admins, formationTimestamp)
val volatiles = configFactory.convoVolatile ?: return
val groupPublicKey = GroupUtil.doubleDecodeGroupId(groupId)
val groupVolatileConfig = volatiles.getOrConstructLegacyClosedGroup(groupPublicKey)
groupVolatileConfig.lastRead = formationTimestamp
volatiles.set(groupVolatileConfig)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
override fun isGroupActive(groupPublicKey: String): Boolean {
@ -660,6 +751,25 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return if (recipientSettings.isPresent) { recipientSettings.get() } else null
}
override fun addLibSessionContacts(contacts: List<LibSessionContact>) {
val recipientDatabase = DatabaseComponent.get(context).recipientDatabase()
val threadDatabase = DatabaseComponent.get(context).threadDatabase()
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
val moreContacts = contacts.filter { contact ->
val id = SessionId(contact.id)
id.prefix != IdPrefix.BLINDED || mappingDb.getBlindedIdMapping(contact.id).none { it.sessionId != null }
}
for (contact in moreContacts) {
val address = fromSerialized(contact.id)
val recipient = Recipient.from(context, address, true)
val (url, key) = contact.profilePicture?.let { it.url to it.key } ?: (null to null)
// set or clear the avatar
recipientDatabase.setProfileAvatar(recipient, url)
recipientDatabase.setProfileKey(recipient, key)
}
}
override fun addContacts(contacts: List<ConfigurationMessage.Contact>) {
val recipientDatabase = DatabaseComponent.get(context).recipientDatabase()
val threadDatabase = DatabaseComponent.get(context).threadDatabase()

View File

@ -295,6 +295,27 @@ public class ThreadDatabase extends Database {
notifyConversationListeners(threadId);
}
public List<MarkedMessageInfo> setRead(long threadId, long lastReadTime) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(READ, 1);
contentValues.put(UNREAD_COUNT, 0);
contentValues.put(UNREAD_MENTION_COUNT, 0);
contentValues.put(LAST_SEEN, lastReadTime);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
final List<MarkedMessageInfo> smsRecords = DatabaseComponent.get(context).smsDatabase().setMessagesRead(threadId, lastReadTime);
final List<MarkedMessageInfo> mmsRecords = DatabaseComponent.get(context).mmsDatabase().setMessagesRead(threadId, lastReadTime);
notifyConversationListListeners();
return new LinkedList<MarkedMessageInfo>() {{
addAll(smsRecords);
addAll(mmsRecords);
}};
}
public List<MarkedMessageInfo> setRead(long threadId, boolean lastSeen) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(READ, 1);

View File

@ -5,21 +5,14 @@ import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserProfile
import org.session.libsession.database.StorageProtocol
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.ConfigFactoryUpdateListener
import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
import org.session.libsignal.utilities.Base64
import org.thoughtcrime.securesms.database.ConfigDatabase
import java.util.concurrent.ConcurrentSkipListSet
class ConfigFactory(private val context: Context,
private val configDatabase: ConfigDatabase,
private val storage: StorageProtocol,
private val maybeGetUserInfo: ()->Pair<ByteArray, String>?):
ConfigFactoryProtocol {
@ -42,6 +35,10 @@ class ConfigFactory(private val context: Context,
private var _convoVolatileConfig: ConversationVolatileConfig? = null
private val convoHashes = ConcurrentSkipListSet<String>()
private val listeners: MutableList<ConfigFactoryUpdateListener> = mutableListOf()
fun registerListener(listener: ConfigFactoryUpdateListener) { listeners += listener }
fun unregisterListener(listener: ConfigFactoryUpdateListener) { listeners -= listener }
override val user: UserProfile? = synchronized(userLock) {
if (_userConfig == null) {
val (secretKey, publicKey) = maybeGetUserInfo() ?: return@synchronized null
@ -115,6 +112,9 @@ class ConfigFactory(private val context: Context,
is Contacts -> persistContactsConfigDump()
is ConversationVolatileConfig -> persistConvoVolatileConfigDump()
}
listeners.forEach { listener ->
listener.notifyUpdates(forConfigObject)
}
}
override fun appendHash(configObject: ConfigBase, hash: String) {
@ -125,14 +125,6 @@ class ConfigFactory(private val context: Context,
}
}
override fun notifyUpdates(forConfigObject: ConfigBase) {
when (forConfigObject) {
is UserProfile -> updateUser(forConfigObject)
is Contacts -> updateContacts(forConfigObject)
is ConversationVolatileConfig -> updateConvoVolatile(forConfigObject)
}
}
override fun getHashesFor(forConfigObject: ConfigBase): List<String> =
when (forConfigObject) {
is UserProfile -> userHashes.toList()
@ -147,42 +139,4 @@ class ConfigFactory(private val context: Context,
is ConversationVolatileConfig -> convoHashes.removeAll(deletedHashes)
}
private fun updateUser(userProfile: UserProfile) {
val (_, userPublicKey) = maybeGetUserInfo() ?: return
// would love to get rid of recipient and context from this
val recipient = Recipient.from(context, Address.fromSerialized(userPublicKey), false)
// update name
val name = userProfile.getName() ?: return
val userPic = userProfile.getPic()
val profileManager = SSKEnvironment.shared.profileManager
if (name.isNotEmpty()) {
TextSecurePreferences.setProfileName(context, name)
profileManager.setName(context, recipient, name)
}
// update pfp
if (userPic == null) {
// clear picture if userPic is null
TextSecurePreferences.setProfileKey(context, null)
ProfileKeyUtil.setEncodedProfileKey(context, null)
profileManager.setProfileKey(context, recipient, null)
storage.setUserProfilePictureURL(null)
} else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty()
&& TextSecurePreferences.getProfilePictureURL(context) != userPic.url) {
val profileKey = Base64.encodeBytes(userPic.key)
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
profileManager.setProfileKey(context, recipient, userPic.key)
storage.setUserProfilePictureURL(userPic.url)
}
}
private fun updateContacts(contacts: Contacts) {
}
private fun updateConvoVolatile(convos: ConversationVolatileConfig) {
}
}

View File

@ -6,7 +6,6 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import net.zetetic.database.sqlcipher.SQLiteDatabase
import org.session.libsession.database.MessageDataProvider
import org.thoughtcrime.securesms.attachments.DatabaseAttachmentProvider
import org.thoughtcrime.securesms.crypto.AttachmentSecret
@ -136,7 +135,7 @@ object DatabaseModule {
@Provides
@Singleton
fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = Storage(context,openHelper)
fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper, configFactory: ConfigFactory) = Storage(context,openHelper, configFactory)
@Provides
@Singleton

View File

@ -6,10 +6,10 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.session.libsession.utilities.ConfigFactoryUpdateListener
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.database.ConfigDatabase
import org.thoughtcrime.securesms.database.Storage
import javax.inject.Singleton
@Module
@ -23,12 +23,14 @@ object SessionUtilModule {
@Provides
@Singleton
fun provideConfigFactory(@ApplicationContext context: Context, configDatabase: ConfigDatabase, storage: Storage): ConfigFactory =
ConfigFactory(context, configDatabase, storage) {
fun provideConfigFactory(@ApplicationContext context: Context, configDatabase: ConfigDatabase): ConfigFactory =
ConfigFactory(context, configDatabase) {
val localUserPublicKey = TextSecurePreferences.getLocalNumber(context)
val secretKey = maybeUserEdSecretKey(context)
if (localUserPublicKey == null || secretKey == null) null
else secretKey to localUserPublicKey
}.apply {
registerListener(context as ConfigFactoryUpdateListener)
}
}

View File

@ -213,6 +213,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
}
val compoundPromise = all(promises)
compoundPromise.successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below
val userConfig = configFactory.user
if (isUpdatingProfilePicture && profilePicture != null) {
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
@ -220,11 +221,14 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
// new config
val url = TextSecurePreferences.getProfilePictureURL(this)
val profileKey = ProfileKeyUtil.getProfileKey(this)
if (!url.isNullOrEmpty() && !profileKey.isEmpty()) {
configFactory.user?.setPic(UserPic(url, profileKey))
if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
userConfig?.setPic(UserPic(url, profileKey))
}
}
if (profilePicture != null || displayName != null) {
if (userConfig != null && userConfig.needsDump()) {
configFactory.persist(userConfig)
}
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
}
}

View File

@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@ -83,7 +84,8 @@ class DefaultConversationRepository @Inject constructor(
private val mmsSmsDb: MmsSmsDatabase,
private val recipientDb: RecipientDatabase,
private val lokiMessageDb: LokiMessageDatabase,
private val sessionJobDb: SessionJobDatabase
private val sessionJobDb: SessionJobDatabase,
private val configFactory: ConfigFactory
) : ConversationRepository {
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {

View File

@ -2,6 +2,7 @@ package org.session.libsession.database
import android.content.Context
import android.net.Uri
import network.loki.messenger.libsession_util.ConfigBase
import org.session.libsession.messaging.BlindedIdMapping
import org.session.libsession.messaging.calls.CallMessageType
import org.session.libsession.messaging.contacts.Contact
@ -31,6 +32,7 @@ import org.session.libsession.utilities.recipients.Recipient.RecipientSettings
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
interface StorageProtocol {
@ -164,6 +166,7 @@ interface StorageProtocol {
fun setContact(contact: Contact)
fun getRecipientForThread(threadId: Long): Recipient?
fun getRecipientSettings(address: Address): RecipientSettings?
fun addLibSessionContacts(contacts: List<LibSessionContact>)
fun addContacts(contacts: List<ConfigurationMessage.Contact>)
// Attachments
@ -202,4 +205,7 @@ interface StorageProtocol {
fun deleteReactions(messageId: Long, mms: Boolean)
fun unblock(toUnblock: List<Recipient>)
fun blockedContacts(): List<Recipient>
// Shared configs
fun notifyConfigUpdates(forConfigObject: ConfigBase)
}

View File

@ -161,7 +161,6 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
}
// process new results
configFactory.persist(forConfigObject)
configFactory.notifyUpdates(forConfigObject)
}
private fun poll(snode: Snode, deferred: Deferred<Unit, Exception>): Promise<Unit, Exception> {

View File

@ -11,7 +11,10 @@ interface ConfigFactoryProtocol {
val convoVolatile: ConversationVolatileConfig?
fun persist(forConfigObject: ConfigBase)
fun appendHash(configObject: ConfigBase, hash: String)
fun notifyUpdates(forConfigObject: ConfigBase)
fun getHashesFor(forConfigObject: ConfigBase): List<String>
fun removeHashesFor(config: ConfigBase, deletedHashes: Set<String>): Boolean
}
interface ConfigFactoryUpdateListener {
fun notifyUpdates(forConfigObject: ConfigBase)
}

View File

@ -3,7 +3,6 @@ package org.session.libsession.utilities
import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.utilities.Hex
import java.io.IOException
import kotlin.jvm.Throws
object GroupUtil {
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
@ -97,4 +96,10 @@ object GroupUtil {
fun doubleDecodeGroupID(groupID: String): ByteArray {
return getDecodedGroupIDAsData(getDecodedGroupID(groupID))
}
@JvmStatic
@Throws(IOException::class)
fun doubleDecodeGroupId(groupID: String): String {
return Hex.toStringCondensed(getDecodedGroupIDAsData(getDecodedGroupID(groupID)))
}
}