diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index dea2071d1..a280b8012 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -390,7 +390,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO } private void initializeProfileManager() { - this.profileManager = new ProfileManager(); + this.profileManager = new ProfileManager(this, configFactory); } private void initializeTypingStatusSender() { 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 f77e7edad..c3b69c832 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 @@ -420,7 +420,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // only update the conversation every 3 seconds maximum // channel is rendezvous and shouldn't block on try send calls as often as we want val bufferedFlow = bufferedLastSeenChannel.consumeAsFlow() - .debounce(3.seconds) + .debounce(1.seconds) bufferedFlow.collectLatest { withContext(Dispatchers.IO) { storage.markConversationAsRead(viewModel.threadId, SnodeAPI.nowWithOffset) @@ -496,6 +496,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { handleRecyclerViewScrolled() } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + + } }) } @@ -951,13 +955,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom binding.typingIndicatorViewContainer.isVisible - showOrHidScrollToBottomButton() + showOrHideScrollToBottomButton() val firstVisiblePosition = layoutManager?.findFirstVisibleItemPosition() ?: -1 unreadCount = min(unreadCount, firstVisiblePosition).coerceAtLeast(0) updateUnreadCountIndicator() } - private fun showOrHidScrollToBottomButton(show: Boolean = true) { + private fun showOrHideScrollToBottomButton(show: Boolean = true) { binding?.scrollToBottomButton?.isVisible = show && !isScrolledToBottom && adapter.itemCount > 0 } @@ -1130,7 +1134,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } ViewUtil.hideKeyboard(this, visibleMessageView) binding?.reactionsShade?.isVisible = true - showOrHidScrollToBottomButton(false) + showOrHideScrollToBottomButton(false) binding?.conversationRecyclerView?.suppressLayout(true) reactionDelegate.setOnActionSelectedListener(ReactionsToolbarListener(message)) reactionDelegate.setOnHideListener(object: ConversationReactionOverlay.OnHideListener { @@ -1138,7 +1142,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe binding?.reactionsShade?.let { ViewUtil.fadeOut(it, resources.getInteger(R.integer.reaction_scrubber_hide_duration), View.GONE) } - showOrHidScrollToBottomButton(true) + showOrHideScrollToBottomButton(true) } override fun onHide() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt index 39ca7c691..3509ffd58 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt @@ -8,6 +8,7 @@ import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog import network.loki.messenger.R import network.loki.messenger.databinding.DialogBlockedBinding +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog @@ -35,7 +36,7 @@ class BlockedDialog(private val recipient: Recipient) : BaseDialog() { } private fun unblock() { - DatabaseComponent.get(requireContext()).recipientDatabase().setBlocked(recipient, false) + MessagingModuleConfiguration.shared.storage.setBlocked(listOf(recipient), false) dismiss() } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index af2faaaca..e460574b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -268,14 +268,6 @@ public class RecipientDatabase extends Database { notifyRecipientListeners(); } - public void setBlocked(@NonNull Recipient recipient, boolean blocked) { - ContentValues values = new ContentValues(); - values.put(BLOCK, blocked ? 1 : 0); - updateOrInsert(recipient.getAddress(), values); - recipient.resolve().setBlocked(blocked); - notifyRecipientListeners(); - } - public void setBlocked(@NonNull List recipients, boolean blocked) { SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); 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 88aa81b18..dcf956e48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -191,9 +191,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF if (!targetRecipient.isGroupRecipient) { val recipientDb = DatabaseComponent.get(context).recipientDatabase() if (isUserSender || isUserBlindedSender) { - recipientDb.setApproved(targetRecipient, true) + setRecipientApproved(targetRecipient, true) } else { - recipientDb.setApprovedMe(targetRecipient, true) + setRecipientApprovedMe(targetRecipient, true) } } if (message.isMediaMessage() || attachments.isNotEmpty()) { @@ -790,7 +790,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF 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) + val (url, key) = contact.profilePicture.let { it.url to it.key } // set or clear the avatar recipientDatabase.setProfileAvatar(recipient, url) recipientDatabase.setProfileKey(recipient, key) @@ -826,11 +826,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF recipientDatabase.setApprovedMe(recipient, true) } if (contact.isApproved == true) { - recipientDatabase.setApproved(recipient, true) + setRecipientApproved(recipient, true) threadDatabase.setHasSent(threadId, true) } if (contact.isBlocked == true) { - recipientDatabase.setBlocked(recipient, true) + setBlocked(listOf(recipient), true) threadDatabase.deleteConversation(threadId) } } @@ -993,10 +993,16 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF override fun setRecipientApproved(recipient: Recipient, approved: Boolean) { DatabaseComponent.get(context).recipientDatabase().setApproved(recipient, approved) + configFactory.contacts?.upsertContact(recipient.address.serialize()) { + this.approved = approved + } } override fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean) { DatabaseComponent.get(context).recipientDatabase().setApprovedMe(recipient, approvedMe) + configFactory.contacts?.upsertContact(recipient.address.serialize()) { + this.approvedMe = approvedMe + } } override fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) { @@ -1126,9 +1132,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF DatabaseComponent.get(context).reactionDatabase().deleteMessageReactions(MessageId(messageId, mms)) } - override fun unblock(toUnblock: List) { + override fun setBlocked(recipients: List, isBlocked: Boolean) { val recipientDb = DatabaseComponent.get(context).recipientDatabase() - recipientDb.setBlocked(toUnblock, false) + recipientDb.setBlocked(recipients, isBlocked) + recipients.filter { it.isContactRecipient }.forEach { recipient -> + configFactory.contacts?.upsertContact(recipient.address.serialize()) { + this.blocked = true + } + } + val contactsConfig = configFactory.contacts ?: return + if (contactsConfig.needsDump()) { + configFactory.persist(contactsConfig) + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) + } } override fun blockedContacts(): List { 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 de89a99bb..a3087a417 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -749,7 +749,7 @@ public class ThreadDatabase extends Database { } else { MarkReadReceiver.process(context, messages); } - ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, false, 0); + ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, threadId); setLastSeen(threadId, lastSeenTime); } 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 e505702a2..13c74980e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -52,6 +52,7 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.MmsSmsDatabase import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.dependencies.ConfigFactory @@ -90,6 +91,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), @Inject lateinit var threadDb: ThreadDatabase @Inject lateinit var mmsSmsDatabase: MmsSmsDatabase @Inject lateinit var recipientDatabase: RecipientDatabase + @Inject lateinit var storage: Storage @Inject lateinit var groupDatabase: GroupDatabase @Inject lateinit var textSecurePreferences: TextSecurePreferences @Inject lateinit var configFactory: ConfigFactory @@ -515,7 +517,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ -> lifecycleScope.launch(Dispatchers.IO) { - recipientDatabase.setBlocked(thread.recipient, true) + storage.setBlocked(listOf(thread.recipient), true) withContext(Dispatchers.Main) { binding.recyclerView.adapter!!.notifyDataSetChanged() dialog.dismiss() @@ -531,7 +533,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ -> lifecycleScope.launch(Dispatchers.IO) { - recipientDatabase.setBlocked(thread.recipient, false) + storage.setBlocked(listOf(thread.recipient), false) withContext(Dispatchers.Main) { binding.recyclerView.adapter!!.notifyDataSetChanged() dialog.dismiss() diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt index 9c0a436eb..022407bdf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt @@ -55,7 +55,7 @@ class BlockedContactsViewModel @Inject constructor(private val storage: Storage) } fun unblock(toUnblock: List) { - storage.unblock(toUnblock) + storage.setBlocked(toUnblock, false) } data class BlockedContactsViewState( diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt index 66b64480c..b880c10ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.repository +import org.jetbrains.annotations.Contract import org.session.libsession.database.MessageDataProvider import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.control.MessageRequestResponse @@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.database.MmsSmsDatabase import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.SessionJobDatabase import org.thoughtcrime.securesms.database.SmsDatabase +import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.ThreadRecord @@ -83,6 +85,7 @@ class DefaultConversationRepository @Inject constructor( private val mmsDb: MmsDatabase, private val mmsSmsDb: MmsSmsDatabase, private val recipientDb: RecipientDatabase, + private val storage: Storage, private val lokiMessageDb: LokiMessageDatabase, private val sessionJobDb: SessionJobDatabase, private val configFactory: ConfigFactory @@ -127,8 +130,10 @@ class DefaultConversationRepository @Inject constructor( } } + // This assumes that recipient.isContactRecipient is true + @Contract override fun setBlocked(recipient: Recipient, blocked: Boolean) { - recipientDb.setBlocked(recipient, blocked) + storage.setBlocked(listOf(recipient), blocked) } override fun deleteLocally(recipient: Recipient, message: MessageRecord) { @@ -141,7 +146,7 @@ class DefaultConversationRepository @Inject constructor( } override fun setApproved(recipient: Recipient, isApproved: Boolean) { - recipientDb.setApproved(recipient, isApproved) + storage.setRecipientApproved(recipient, isApproved) } override suspend fun deleteForEveryone( @@ -272,7 +277,7 @@ class DefaultConversationRepository @Inject constructor( } override suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf = suspendCoroutine { continuation -> - recipientDb.setApproved(recipient, true) + storage.setRecipientApproved(recipient, true) val message = MessageRequestResponse(true) MessageSender.send(message, Destination.from(recipient.address)) .success { diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt index a1a4199d8..8d115aa4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt @@ -1,14 +1,17 @@ package org.thoughtcrime.securesms.sskenvironment import android.content.Context +import network.loki.messenger.libsession_util.util.UserPic import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities -class ProfileManager : SSKEnvironment.ProfileManagerProtocol { +class ProfileManager(private val context: Context, private val configFactory: ConfigFactory) : SSKEnvironment.ProfileManagerProtocol { override fun setNickname(context: Context, recipient: Recipient, nickname: String?) { val sessionID = recipient.address.serialize() @@ -20,6 +23,7 @@ class ProfileManager : SSKEnvironment.ProfileManagerProtocol { contact.nickname = nickname contactDatabase.setContact(contact) } + contactUpdatedInternal(contact) } override fun setName(context: Context, recipient: Recipient, name: String) { @@ -37,6 +41,7 @@ class ProfileManager : SSKEnvironment.ProfileManagerProtocol { val database = DatabaseComponent.get(context).recipientDatabase() database.setProfileName(recipient, name) recipient.notifyListeners() + contactUpdatedInternal(contact) } override fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String) { @@ -52,6 +57,7 @@ class ProfileManager : SSKEnvironment.ProfileManagerProtocol { contact.profilePictureURL = profilePictureURL contactDatabase.setContact(contact) } + contactUpdatedInternal(contact) } override fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray?) { @@ -68,10 +74,28 @@ class ProfileManager : SSKEnvironment.ProfileManagerProtocol { // Old API val database = DatabaseComponent.get(context).recipientDatabase() database.setProfileKey(recipient, profileKey) + contactUpdatedInternal(contact) } override fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode) { val database = DatabaseComponent.get(context).recipientDatabase() database.setUnidentifiedAccessMode(recipient, unidentifiedAccessMode) } + + private fun contactUpdatedInternal(contact: Contact) { + val contactConfig = configFactory.contacts ?: return + contactConfig.upsertContact(contact.sessionID) { + this.name = contact.name.orEmpty() + this.nickname = contact.nickname.orEmpty() + val url = contact.profilePictureURL + val key = contact.profilePictureEncryptionKey + if (!url.isNullOrEmpty() && key != null && key.size == 32) { + this.profilePicture = UserPic(url, key) + } + } + if (contactConfig.needsDump()) { + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) + } + } + } \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt index 7a548fc9b..c30ef8852 100644 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -62,6 +62,25 @@ class Contacts(pointer: Long) : ConfigBase(pointer) { external fun all(): List external fun set(contact: Contact) external fun erase(sessionId: String): Boolean + + /** + * Similar to [updateIfExists], but will create the underlying contact if it doesn't exist before passing to [updateFunction] + */ + fun upsertContact(sessionId: String, updateFunction: Contact.()->Unit) { + val contact = getOrConstruct(sessionId) + updateFunction(contact) + set(contact) + } + + /** + * Updates the contact by sessionId with a given [updateFunction], and applies to the underlying config. + * the [updateFunction] doesn't run if there is no contact + */ + fun updateIfExists(sessionId: String, updateFunction: Contact.()->Unit) { + val contact = get(sessionId) ?: return + updateFunction(contact) + set(contact) + } } class UserProfile(pointer: Long) : ConfigBase(pointer) { diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index 98d3a9a63..ef7eec8f0 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -204,7 +204,7 @@ interface StorageProtocol { fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean) fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long) fun deleteReactions(messageId: Long, mms: Boolean) - fun unblock(toUnblock: List) + fun setBlocked(recipients: List, isBlocked: Boolean) fun blockedContacts(): List // Shared configs 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 bc09376d6..c804ba002 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 @@ -150,7 +150,9 @@ data class ConfigurationSyncJob(val destination: Destination): Job { // store the new hash in list of hashes to track against configFactory.appendHash(config, insertHash) // dump and write config after successful - configFactory.persist(config) + if (config.needsDump()) { // usually this will be true? + configFactory.persist(config) + } } } catch (e: Exception) { Log.e(TAG, "Error performing batch request", e) 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 bd1b1afae..08b531e4a 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 @@ -163,7 +163,9 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti } } // process new results - configFactory.persist(forConfigObject) + if (forConfigObject.needsDump()) { + configFactory.persist(forConfigObject) + } } private fun poll(snode: Snode, deferred: Deferred): Promise {