diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 136a882f74..7fcdf8b7f4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -271,7 +271,6 @@ dependencies { implementation(libs.photoview) implementation(libs.glide) implementation(libs.compose) - implementation(libs.eventbus) implementation(libs.android.image.cropper) implementation(libs.subsampling.scale.image.view) { exclude(group = "com.android.support", module = "support-annotations") diff --git a/app/src/main/java/org/session/libsession/avatars/ContactPhoto.java b/app/src/main/java/org/session/libsession/avatars/ContactPhoto.java deleted file mode 100644 index 0d256fd38c..0000000000 --- a/app/src/main/java/org/session/libsession/avatars/ContactPhoto.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.session.libsession.avatars; - -import android.content.Context; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.bumptech.glide.load.Key; - -import java.io.IOException; -import java.io.InputStream; - -public interface ContactPhoto extends Key { - - InputStream openInputStream(Context context) throws IOException; - - @Nullable Uri getUri(@NonNull Context context); - - boolean isProfilePhoto(); -} diff --git a/app/src/main/java/org/session/libsession/avatars/GroupRecordContactPhoto.java b/app/src/main/java/org/session/libsession/avatars/GroupRecordContactPhoto.java deleted file mode 100644 index e033b55f23..0000000000 --- a/app/src/main/java/org/session/libsession/avatars/GroupRecordContactPhoto.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.session.libsession.avatars; - -import android.content.Context; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.session.libsession.database.StorageProtocol; -import org.session.libsession.messaging.MessagingModuleConfiguration; -import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.Conversions; -import org.session.libsession.utilities.GroupRecord; -import org.session.libsignal.utilities.guava.Optional; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.MessageDigest; - -public class GroupRecordContactPhoto implements ContactPhoto { - - private final @NonNull - Address address; - private final long avatarId; - - public GroupRecordContactPhoto(@NonNull Address address, long avatarId) { - this.address = address; - this.avatarId = avatarId; - } - - @Override - public InputStream openInputStream(Context context) throws IOException { - StorageProtocol groupDatabase = MessagingModuleConfiguration.getShared().getStorage(); - Optional groupRecord = Optional.of(groupDatabase.getGroup(address.toGroupString())); - - if (groupRecord.isPresent() && groupRecord.get().getAvatar() != null) { - return new ByteArrayInputStream(groupRecord.get().getAvatar()); - } - - throw new IOException("Couldn't load avatar for group: " + address.toGroupString()); - } - - @Override - public @Nullable Uri getUri(@NonNull Context context) { - return null; - } - - @Override - public boolean isProfilePhoto() { - return false; - } - - @Override - public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { - messageDigest.update(address.toString().getBytes()); - messageDigest.update(Conversions.longToByteArray(avatarId)); - } - - @Override - public boolean equals(Object other) { - if (other == null || !(other instanceof GroupRecordContactPhoto)) return false; - - GroupRecordContactPhoto that = (GroupRecordContactPhoto)other; - return this.address.equals(that.address) && this.avatarId == that.avatarId; - } - - @Override - public int hashCode() { - return this.address.hashCode() ^ (int) avatarId; - } - -} diff --git a/app/src/main/java/org/session/libsession/avatars/ProfileContactPhoto.java b/app/src/main/java/org/session/libsession/avatars/ProfileContactPhoto.java deleted file mode 100644 index 86669f9d41..0000000000 --- a/app/src/main/java/org/session/libsession/avatars/ProfileContactPhoto.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.session.libsession.avatars; - -import android.content.Context; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.session.libsession.utilities.Address; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.security.MessageDigest; - -public class ProfileContactPhoto implements ContactPhoto { - - private final @NonNull - Address address; - public final @NonNull String avatarObject; - - public ProfileContactPhoto(@NonNull Address address, @NonNull String avatarObject) { - this.address = address; - this.avatarObject = avatarObject; - } - - @Override - public InputStream openInputStream(Context context) throws FileNotFoundException { - return AvatarHelper.getInputStreamFor(context, address); - } - - @Override - public @Nullable Uri getUri(@NonNull Context context) { - return Uri.fromFile(AvatarHelper.getAvatarFile(context, address)); - } - - @Override - public boolean isProfilePhoto() { - return true; - } - - @Override - public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { - messageDigest.update(address.toString().getBytes()); - messageDigest.update(avatarObject.getBytes()); - } - - @Override - public boolean equals(Object other) { - if (other == null || !(other instanceof ProfileContactPhoto)) return false; - - ProfileContactPhoto that = (ProfileContactPhoto)other; - - return this.address.equals(that.address) && this.avatarObject.equals(that.avatarObject); - } - - @Override - public int hashCode() { - return address.hashCode() ^ avatarObject.hashCode(); - } -} diff --git a/app/src/main/java/org/session/libsession/avatars/SystemContactPhoto.java b/app/src/main/java/org/session/libsession/avatars/SystemContactPhoto.java deleted file mode 100644 index f4d31f7055..0000000000 --- a/app/src/main/java/org/session/libsession/avatars/SystemContactPhoto.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.session.libsession.avatars; - -import android.content.Context; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - - -import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.Conversions; - -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.security.MessageDigest; - -public class SystemContactPhoto implements ContactPhoto { - - private final @NonNull - Address address; - private final @NonNull Uri contactPhotoUri; - private final long lastModifiedTime; - - public SystemContactPhoto(@NonNull Address address, @NonNull Uri contactPhotoUri, long lastModifiedTime) { - this.address = address; - this.contactPhotoUri = contactPhotoUri; - this.lastModifiedTime = lastModifiedTime; - } - - @Override - public InputStream openInputStream(Context context) throws FileNotFoundException { - return context.getContentResolver().openInputStream(contactPhotoUri); - } - - @Override - public @Nullable Uri getUri(@NonNull Context context) { - return contactPhotoUri; - } - - @Override - public boolean isProfilePhoto() { - return false; - } - - @Override - public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { - messageDigest.update(address.toString().getBytes()); - messageDigest.update(contactPhotoUri.toString().getBytes()); - messageDigest.update(Conversions.longToByteArray(lastModifiedTime)); - } - - @Override - public boolean equals(Object other) { - if (other == null || !(other instanceof SystemContactPhoto)) return false; - - SystemContactPhoto that = (SystemContactPhoto)other; - - return this.address.equals(that.address) && this.contactPhotoUri.equals(that.contactPhotoUri) && this.lastModifiedTime == that.lastModifiedTime; - } - - @Override - public int hashCode() { - return address.hashCode() ^ contactPhotoUri.hashCode() ^ (int)lastModifiedTime; - } - -} diff --git a/app/src/main/java/org/session/libsession/database/MessageDataProvider.kt b/app/src/main/java/org/session/libsession/database/MessageDataProvider.kt index dbe35e6f4f..3d1e5a91e9 100644 --- a/app/src/main/java/org/session/libsession/database/MessageDataProvider.kt +++ b/app/src/main/java/org/session/libsession/database/MessageDataProvider.kt @@ -9,7 +9,6 @@ import org.session.libsession.messaging.sending_receiving.attachments.SessionSer import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream import org.session.libsession.utilities.Address import org.session.libsession.utilities.UploadResult -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceAttachmentStream import org.thoughtcrime.securesms.database.model.MessageId @@ -45,5 +44,4 @@ interface MessageDataProvider { fun getMessageBodyFor(timestamp: Long, author: String): String fun getAttachmentIDsFor(mmsMessageId: Long): List fun getLinkPreviewAttachmentIDFor(mmsMessageId: Long): Long? - fun getIndividualRecipientForMms(mmsId: Long): Recipient? } \ No newline at end of file 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 971fa482ee..b6a6e90e7c 100644 --- a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -2,14 +2,13 @@ package org.session.libsession.database import android.content.Context import android.net.Uri +import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.KeyPair import org.session.libsession.messaging.BlindedIdMapping import org.session.libsession.messaging.calls.CallMessageType -import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.MessageSendJob -import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.GroupUpdated import org.session.libsession.messaging.messages.control.MessageRequestResponse @@ -28,8 +27,8 @@ import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupDisplayInfo import org.session.libsession.utilities.GroupRecord +import org.session.libsession.utilities.recipients.RecipientSettings import org.session.libsession.utilities.recipients.Recipient -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 @@ -48,10 +47,8 @@ interface StorageProtocol { fun getUserX25519KeyPair(): ECKeyPair fun getUserBlindedAccountId(serverPublicKey: String): AccountId? fun getUserProfile(): Profile - fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?) - fun setBlocksCommunityMessageRequests(recipient: Recipient, blocksMessageRequests: Boolean) - fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) - fun clearUserPic(clearConfig: Boolean = true) + fun setBlocksCommunityMessageRequests(recipient: Address, blocksMessageRequests: Boolean) + // Signal fun getOrGenerateRegistrationID(): Int @@ -81,6 +78,7 @@ interface StorageProtocol { fun getAllOpenGroups(): Map fun updateOpenGroup(openGroup: OpenGroup) fun getOpenGroup(threadId: Long): OpenGroup? + fun getOpenGroup(address: Address): OpenGroup? suspend fun addOpenGroup(urlAsString: String) fun onOpenGroupAdded(server: String, room: String) fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean @@ -130,13 +128,10 @@ interface StorageProtocol { fun getGroup(groupID: String): GroupRecord? fun createGroup(groupID: String, title: String?, members: List
, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List
, formationTimestamp: Long) fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map, formationTimestamp: Long, encryptionKeyPair: ECKeyPair, expirationTimer: Int) - fun updateGroupConfig(groupPublicKey: String) fun isGroupActive(groupPublicKey: String): Boolean fun setActive(groupID: String, value: Boolean) - fun getZombieMembers(groupID: String): Set fun removeMember(groupID: String, member: Address) fun updateMembers(groupID: String, members: List
) - fun setZombieMembers(groupID: String, members: List
) fun getAllLegacyGroupPublicKeys(): Set fun getAllActiveClosedGroupPublicKeys(): Set fun addClosedGroupPublicKey(groupPublicKey: String) @@ -176,23 +171,17 @@ interface StorageProtocol { // Groups fun getAllGroups(includeInactive: Boolean): List - // Settings - fun setProfileSharing(address: Address, value: Boolean) - // Thread fun getOrCreateThreadIdFor(address: Address): Long fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long? fun getThreadId(publicKeyOrOpenGroupID: String): Long? fun getThreadId(openGroup: OpenGroup): Long? fun getThreadId(address: Address): Long? - fun getThreadId(recipient: Recipient): Long? fun getThreadIdForMms(mmsId: Long): Long fun getLastUpdated(threadID: Long): Long - fun trimThread(threadID: Long, threadLimit: Int) fun trimThreadBefore(threadID: Long, timestamp: Long) fun getMessageCount(threadID: Long): Long - fun setPinned(threadID: Long, isPinned: Boolean) - fun isPinned(threadID: Long): Boolean + fun setPinned(address: Address, isPinned: Boolean) fun deleteConversation(threadID: Long) fun setThreadCreationDate(threadId: Long, newDate: Long) fun getLastLegacyRecipient(threadRecipient: String): String? @@ -201,16 +190,11 @@ interface StorageProtocol { fun clearMedia(threadID: Long, fromUser: Address? = null): Boolean // Contacts - fun getContactWithAccountID(accountID: String): Contact? - fun getAllContacts(): Set - fun setContact(contact: Contact) fun deleteContactAndSyncConfig(accountId: String) - fun getRecipientForThread(threadId: Long): Recipient? + fun getRecipientForThread(threadId: Long): Address? fun getRecipientSettings(address: Address): RecipientSettings? fun syncLibSessionContacts(contacts: List, timestamp: Long?) - fun hasAutoDownloadFlagBeenSet(recipient: Recipient): Boolean - fun shouldAutoDownloadAttachments(recipient: Recipient): Boolean - fun setAutoDownloadAttachments(recipient: Recipient, shouldAutoDownloadAttachments: Boolean) + fun setAutoDownloadAttachments(recipient: Address, shouldAutoDownloadAttachments: Boolean) // Attachments fun getAttachmentDataUri(attachmentId: AttachmentId): Uri @@ -228,9 +212,6 @@ interface StorageProtocol { fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) fun insertMessageRequestResponseFromContact(response: MessageRequestResponse) fun insertMessageRequestResponseFromYou(threadId: Long) - fun setRecipientApproved(recipient: Recipient, approved: Boolean) - fun getRecipientApproved(address: Address): Boolean - fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean) fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) fun conversationHasOutgoing(userPublicKey: String): Boolean fun deleteMessagesByHash(threadId: Long, hashes: List) @@ -269,16 +250,11 @@ interface StorageProtocol { fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long) fun deleteReactions(messageId: MessageId) fun deleteReactions(messageIds: List, mms: Boolean) - fun setBlocked(recipients: Iterable, isBlocked: Boolean, fromConfigUpdate: Boolean = false) + fun setBlocked(recipients: Iterable
, isBlocked: Boolean, fromConfigUpdate: Boolean = false) fun blockedContacts(): List - fun getExpirationConfiguration(threadId: Long): ExpirationConfiguration? - fun setExpirationConfiguration(config: ExpirationConfiguration) + fun getExpirationConfiguration(threadId: Long): ExpiryMode + fun setExpirationConfiguration(address: Address, expiryMode: ExpiryMode) fun getExpiringMessages(messageIds: List = emptyList()): List> - fun updateDisappearingState( - messageSender: String, - threadID: Long, - disappearingState: Recipient.DisappearingState - ) // Shared configs fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean diff --git a/app/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt b/app/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt index 42d26e1de4..02989156c7 100644 --- a/app/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt +++ b/app/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt @@ -11,7 +11,7 @@ import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.Device import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.Toaster -import org.session.libsession.utilities.UsernameUtils +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.pro.ProStatusManager class MessagingModuleConfiguration( @@ -26,8 +26,8 @@ class MessagingModuleConfiguration( val clock: SnodeClock, val preferences: TextSecurePreferences, val deprecationManager: LegacyGroupDeprecationManager, - val usernameUtils: UsernameUtils, - val proStatusManager: ProStatusManager + val recipientRepository: RecipientRepository, + val proStatusManager: ProStatusManager, ) { companion object { diff --git a/app/src/main/java/org/session/libsession/messaging/contacts/Contact.kt b/app/src/main/java/org/session/libsession/messaging/contacts/Contact.kt deleted file mode 100644 index 6d899da758..0000000000 --- a/app/src/main/java/org/session/libsession/messaging/contacts/Contact.kt +++ /dev/null @@ -1,76 +0,0 @@ -package org.session.libsession.messaging.contacts - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import org.session.libsession.utilities.recipients.Recipient -import org.session.libsession.utilities.truncateIdForDisplay - -@Parcelize -class Contact( - val accountID: String, - /** - * The URL from which to fetch the contact's profile picture. - */ - var profilePictureURL: String? = null, - /** - * The file name of the contact's profile picture on local storage. - */ - var profilePictureFileName: String? = null, - /** - * The key with which the profile picture is encrypted. - */ - var profilePictureEncryptionKey: ByteArray? = null, - /** - * The ID of the thread associated with this contact. - */ - var threadID: Long? = null, - /** - * The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message). - */ - var name: String? = null, - /** - * The contact's nickname, if the user set one. - */ - var nickname: String? = null, -): Parcelable { - - constructor(id: String): this(accountID = id) - - /** - * The name to display in the UI. For local use only. - */ - fun displayName(context: ContactContext = ContactContext.REGULAR): String = nickname ?: when (context) { - ContactContext.REGULAR -> name - // In open groups, where it's more likely that multiple users have the same name, - // we display a bit of the Account ID after a user's display name for added context. - ContactContext.OPEN_GROUP -> name?.let { "$it (${truncateIdForDisplay(accountID)})" } - } ?: truncateIdForDisplay(accountID) - - enum class ContactContext { - REGULAR, OPEN_GROUP - } - - fun isValid(): Boolean { - if (profilePictureURL != null) { return profilePictureEncryptionKey != null } - if (profilePictureEncryptionKey != null) { return profilePictureURL != null} - return true - } - - override fun equals(other: Any?): Boolean { - return this.accountID == (other as? Contact)?.accountID - } - - override fun hashCode(): Int { - return accountID.hashCode() - } - - override fun toString(): String { - return nickname ?: name ?: accountID - } - - companion object { - fun contextForRecipient(recipient: Recipient): ContactContext { - return if (recipient.isCommunityRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt b/app/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt index 7e9aa23b58..c261533005 100644 --- a/app/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt +++ b/app/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt @@ -18,6 +18,7 @@ import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.toHexString +import java.util.regex.Pattern import kotlin.time.Duration.Companion.milliseconds object FileServerApi { @@ -28,6 +29,10 @@ object FileServerApi { val fileServerUrl: HttpUrl by lazy { FILE_SERVER_URL.toHttpUrl() } + val FILE_SERVER_FILE_URL_PATTERN: Pattern by lazy { + Pattern.compile("^https?://filev2\\.getsession\\.org/file/([a-zA-Z0-9]+)$", Pattern.CASE_INSENSITIVE) + } + sealed class Error(message: String) : Exception(message) { object ParsingFailed : Error("Invalid response.") object InvalidURL : Error("Invalid URL.") @@ -48,6 +53,15 @@ object FileServerApi { val useOnionRouting: Boolean = true ) + fun getFileIdFromUrl(url: String): String? { + val matcher = FILE_SERVER_FILE_URL_PATTERN.matcher(url) + return if (matcher.matches()) { + matcher.group(1) + } else { + null + } + } + private fun createBody(body: ByteArray?, parameters: Any?): RequestBody? { if (body != null) return RequestBody.create("application/octet-stream".toMediaType(), body) if (parameters == null) return null diff --git a/app/src/main/java/org/session/libsession/messaging/groups/GroupInviteException.kt b/app/src/main/java/org/session/libsession/messaging/groups/GroupInviteException.kt index 415a4a6f51..402c65dc5f 100644 --- a/app/src/main/java/org/session/libsession/messaging/groups/GroupInviteException.kt +++ b/app/src/main/java/org/session/libsession/messaging/groups/GroupInviteException.kt @@ -3,13 +3,12 @@ package org.session.libsession.messaging.groups import android.content.Context import com.squareup.phrase.Phrase import network.loki.messenger.R -import org.session.libsession.database.StorageProtocol +import org.session.libsession.utilities.Address import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.OTHER_NAME_KEY -import org.session.libsession.utilities.UsernameUtils -import org.session.libsession.utilities.truncateIdForDisplay +import org.thoughtcrime.securesms.database.RecipientRepository /** * Exception that occurs during a group invite. @@ -31,9 +30,9 @@ class GroupInviteException( } } - fun format(context: Context, usernameUtils: UsernameUtils): CharSequence { + fun format(context: Context, recipientRepository: RecipientRepository): CharSequence { val getInviteeName = { accountId: String -> - usernameUtils.getContactNameWithAccountID(accountId) + recipientRepository.getRecipientDisplayNameSync(Address.fromSerialized(accountId)) } val first = inviteeAccountIds.first().let(getInviteeName) diff --git a/app/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt b/app/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt index cd48ece0a1..b8ea9990ec 100644 --- a/app/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt +++ b/app/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt @@ -6,7 +6,6 @@ import org.session.libsession.messaging.messages.control.GroupUpdated import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage import org.session.libsignal.utilities.AccountId -import org.thoughtcrime.securesms.groups.GroupManagerV2Impl /** * Business logic handling group v2 operations like inviting members, @@ -109,7 +108,7 @@ interface GroupManagerV2 { senderIsVerifiedAdmin: Boolean, ) - fun setExpirationTimer(groupId: AccountId, mode: ExpiryMode, expiryChangeTimestampMs: Long) + fun setExpirationTimer(groupId: AccountId, mode: ExpiryMode) fun handleGroupInfoChange(message: GroupUpdated, groupId: AccountId) diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index ea76dbfea4..cd01781a72 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -18,6 +18,7 @@ import org.session.libsignal.streams.AttachmentCipherInputStream import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.model.MessageId import java.io.File import java.io.FileInputStream @@ -56,9 +57,11 @@ class AttachmentDownloadJob(val attachmentID: Long, val mmsMessageId: Long) : Jo */ fun eligibleForDownload(threadID: Long, storage: StorageProtocol, + recipientRepository: RecipientRepository, messageDataProvider: MessageDataProvider, mmsId: Long): Boolean { - val threadRecipient = storage.getRecipientForThread(threadID) ?: return false + val threadRecipient = storage.getRecipientForThread(threadID) + ?.let(recipientRepository::getRecipientSync) ?: return false // if we are the sender we are always eligible val selfSend = messageDataProvider.isOutgoingMessage(MessageId(mmsId, true)) @@ -66,7 +69,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val mmsMessageId: Long) : Jo return true } - return storage.shouldAutoDownloadAttachments(threadRecipient) + return threadRecipient.autoDownloadAttachments == true } } @@ -124,7 +127,13 @@ class AttachmentDownloadJob(val attachmentID: Long, val mmsMessageId: Long) : Jo return } - if (!eligibleForDownload(threadID, storage, messageDataProvider, mmsMessageId)) { + if (!eligibleForDownload( + threadID = threadID, + storage = storage, + recipientRepository = MessagingModuleConfiguration.shared.recipientRepository, + messageDataProvider = messageDataProvider, + mmsId = mmsMessageId + )) { handleFailure(Error.NoSender, null) return } diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt index 707883cbc4..33418f9a67 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt @@ -127,7 +127,7 @@ class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array< groupName = groupName.orEmpty(), underlying = firstError, ).format(MessagingModuleConfiguration.shared.context, - MessagingModuleConfiguration.shared.usernameUtils).let { + MessagingModuleConfiguration.shared.recipientRepository).let { withContext(Dispatchers.Main) { toaster.toast(it, Toast.LENGTH_LONG) } diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/app/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index 188f7cfaed..67f3b8bc4e 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -128,7 +128,6 @@ class JobQueue : JobDelegate { is MessageSendJob -> { txQueue.send(job) } - is RetrieveProfileAvatarJob, is AttachmentDownloadJob -> { mediaQueue.send(job) } @@ -233,7 +232,6 @@ class JobQueue : JobDelegate { GroupAvatarDownloadJob.KEY, BackgroundGroupAddJob.KEY, OpenGroupDeleteJob.KEY, - RetrieveProfileAvatarJob.KEY, InviteContactsJob.KEY, ) allJobTypes.forEach { type -> diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt deleted file mode 100644 index a60b3e2449..0000000000 --- a/app/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt +++ /dev/null @@ -1,145 +0,0 @@ -package org.session.libsession.messaging.jobs - -import network.loki.messenger.libsession_util.SessionEncrypt -import org.session.libsession.avatars.AvatarHelper -import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.utilities.Data -import org.session.libsession.utilities.AESGCM -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.DownloadUtilities.downloadFromFileServer -import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfileAvatarId -import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfilePictureURL -import org.session.libsession.utilities.Util.equals -import org.session.libsession.utilities.recipients.Recipient -import org.session.libsignal.exceptions.NonRetryableException -import org.session.libsignal.utilities.HTTP -import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Util.SECURE_RANDOM -import java.io.File -import java.io.FileOutputStream -import java.util.concurrent.ConcurrentSkipListSet - -class RetrieveProfileAvatarJob( - private val profileAvatar: String?, val recipientAddress: Address, - private val profileKey: ByteArray? -): Job { - override var delegate: JobDelegate? = null - override var id: String? = null - override var failureCount: Int = 0 - override val maxFailureCount: Int = 3 - - companion object { - val TAG = RetrieveProfileAvatarJob::class.simpleName - val KEY: String = "RetrieveProfileAvatarJob" - - // Keys used for database storage - private const val PROFILE_AVATAR_KEY = "profileAvatar" - private const val RECEIPIENT_ADDRESS_KEY = "recipient" - private const val PROFILE_KEY = "profileKey" - - val errorUrls = ConcurrentSkipListSet() - - } - - override suspend fun execute(dispatcherName: String) { - val delegate = delegate ?: return Log.w(TAG, "RetrieveProfileAvatarJob has no delegate method to work with!") - if (profileAvatar != null && profileAvatar in errorUrls) return delegate.handleJobFailed(this, dispatcherName, Exception("Profile URL 404'd this app instance")) - val context = MessagingModuleConfiguration.shared.context - val storage = MessagingModuleConfiguration.shared.storage - val recipient = Recipient.from(context, recipientAddress, true) - - if (profileKey == null || (profileKey.size != 32 && profileKey.size != 16)) { - return delegate.handleJobFailedPermanently(this, dispatcherName, Exception("Recipient profile key is gone!")) - } - - // Commit '78d1e9d' (fix: open group threads and avatar downloads) had this commented out so - // it's now limited to just the current user case - if ( - recipient.isLocalNumber && - AvatarHelper.avatarFileExists(context, recipient.resolve().address) && - equals(profileAvatar, recipient.resolve().profileAvatar) - ) { - Log.w(TAG, "Already retrieved profile avatar: $profileAvatar") - return - } - - if (profileAvatar.isNullOrEmpty()) { - Log.w(TAG, "Removing profile avatar for: " + recipient.address.toString()) - - if (recipient.isLocalNumber) { - setProfileAvatarId(context, SECURE_RANDOM.nextInt()) - setProfilePictureURL(context, null) - } - - AvatarHelper.delete(context, recipient.address) - storage.setProfilePicture(recipient, null, null) - return - } - - - try { - val downloaded = downloadFromFileServer(profileAvatar) - val decrypted = AESGCM.decrypt( - downloaded.data, - offset = downloaded.offset, - len = downloaded.len, - symmetricKey = profileKey - ) - - FileOutputStream(AvatarHelper.getAvatarFile(context, recipient.address)).use { out -> - out.write(decrypted) - } - - if (recipient.isLocalNumber) { - setProfileAvatarId(context, SECURE_RANDOM.nextInt()) - setProfilePictureURL(context, profileAvatar) - } - - storage.setProfilePicture(recipient, profileAvatar, profileKey) - } - catch (e: NonRetryableException){ - Log.e("Loki", "Failed to download profile avatar from non-retryable error", e) - errorUrls += profileAvatar - return delegate.handleJobFailedPermanently(this, dispatcherName, e) - } - catch (e: Exception) { - if(e is HTTP.HTTPRequestFailedException && e.statusCode == 404){ - Log.e("Loki", "Failed to download profile avatar from non-retryable error", e) - errorUrls += profileAvatar - return delegate.handleJobFailedPermanently(this, dispatcherName, e) - } else { - Log.e("Loki", "Failed to download profile avatar", e) - if (failureCount + 1 >= maxFailureCount) { - errorUrls += profileAvatar - } - return delegate.handleJobFailed(this, dispatcherName, e) - } - } - return delegate.handleJobSucceeded(this, dispatcherName) - } - - override fun serialize(): Data { - val data = Data.Builder() - .putString(PROFILE_AVATAR_KEY, profileAvatar) - .putString(RECEIPIENT_ADDRESS_KEY, recipientAddress.toString()) - - if (profileKey != null) { - data.putByteArray(PROFILE_KEY, profileKey) - } - - return data.build() - } - - override fun getFactoryKey(): String { - return KEY - } - - class Factory: Job.Factory { - override fun create(data: Data): RetrieveProfileAvatarJob { - val profileAvatar = if (data.hasString(PROFILE_AVATAR_KEY)) { data.getString(PROFILE_AVATAR_KEY) } else { null } - val recipientAddress = Address.fromSerialized(data.getString(RECEIPIENT_ADDRESS_KEY)) - val profileKey = data.getByteArray(PROFILE_KEY) - return RetrieveProfileAvatarJob(profileAvatar, recipientAddress, profileKey) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarWork.kt b/app/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarWork.kt deleted file mode 100644 index 8a18ac68c2..0000000000 --- a/app/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarWork.kt +++ /dev/null @@ -1,134 +0,0 @@ -package org.session.libsession.messaging.jobs - -import android.content.Context -import androidx.hilt.work.HiltWorker -import androidx.work.CoroutineWorker -import androidx.work.Data -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager -import androidx.work.WorkerParameters -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import kotlinx.coroutines.CancellationException -import org.session.libsession.avatars.AvatarHelper -import org.session.libsession.database.StorageProtocol -import org.session.libsession.utilities.AESGCM -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.DownloadUtilities.downloadFromFileServer -import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfileAvatarId -import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfilePictureURL -import org.session.libsession.utilities.Util -import org.session.libsession.utilities.recipients.Recipient -import org.session.libsignal.exceptions.NonRetryableException -import org.session.libsignal.utilities.HTTP -import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Util.SECURE_RANDOM -import java.io.FileOutputStream - -@HiltWorker -class RetrieveProfileAvatarWork @AssistedInject constructor( - @Assisted private val appContext: Context, - @Assisted parameters: WorkerParameters, - private val storage: StorageProtocol, -) : CoroutineWorker(appContext, parameters) { - override suspend fun doWork(): Result { - val recipientAddress = requireNotNull( - inputData.getString(DATA_ADDRESS)?.let(Address::fromSerialized) - ) { "Recipient address is required" } - - val profileAvatarUrl = requireNotNull(inputData.getString(DATA_URL)) { - "Profile avatar URL is required" - } - - val profileAvatarKey = requireNotNull(inputData.getByteArray(DATA_KEY)) { - "Profile avatar key is required" - } - - require(profileAvatarKey.size == 16 || profileAvatarKey.size == 32) { - "Profile avatar key must be either 16 or 32 bytes long" - } - - val recipient by lazy { Recipient.from(appContext, recipientAddress, false) } - - // Commit '78d1e9d' (fix: open group threads and avatar downloads) had this commented out so - // it's now limited to just the current user case - if ( - recipient.isLocalNumber && - AvatarHelper.avatarFileExists(appContext, recipientAddress) && - Util.equals(profileAvatarUrl, recipient.resolve().profileAvatar) - ) { - Log.w(TAG, "Already retrieved profile avatar: $profileAvatarUrl") - return Result.success() - } - - try { - val downloaded = downloadFromFileServer(profileAvatarUrl) - val decrypted = AESGCM.decrypt( - downloaded.data, - offset = downloaded.offset, - len = downloaded.len, - symmetricKey = profileAvatarKey - ) - - FileOutputStream(AvatarHelper.getAvatarFile(appContext, recipient.address)).use { out -> - out.write(decrypted) - } - - if (recipient.isLocalNumber) { - setProfileAvatarId(appContext, SECURE_RANDOM.nextInt()) - setProfilePictureURL(appContext, profileAvatarUrl) - } - - storage.setProfilePicture(recipient, profileAvatarUrl, profileAvatarKey) - return Result.success() - } - catch (e: NonRetryableException) { - Log.e("Loki", "Failed to download profile avatar from non-retryable error", e) - return Result.failure() - } - catch (e: CancellationException) { - throw e - } - catch (e: Exception) { - if(e is HTTP.HTTPRequestFailedException && e.statusCode == 404){ - Log.e(TAG, "Failed to download profile avatar from non-retryable error", e) - return Result.failure() - } else { - Log.e(TAG, "Failed to download profile avatar", e) - return Result.retry() - } - } - } - - companion object { - private const val DATA_ADDRESS = "address" - private const val DATA_URL = "url" - private const val DATA_KEY = "key" - - private const val TAG = "RetrieveProfileAvatarWork" - - fun schedule( - context: Context, - recipientAddress: Address, - profileAvatarUrl: String, - profileAvatarKey: ByteArray, - ) { - val uniqueWorkName = "retrieve-avatar-$recipientAddress" - - val data = Data.Builder() - .putString(DATA_ADDRESS, recipientAddress.toString()) - .putString(DATA_URL, profileAvatarUrl) - .putByteArray(DATA_KEY, profileAvatarKey) - .build() - - val workRequest = OneTimeWorkRequestBuilder() - .setInputData(data) - .build() - - WorkManager.getInstance(context) - .beginUniqueWork(uniqueWorkName, ExistingWorkPolicy.REPLACE, workRequest) - .enqueue() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt b/app/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt deleted file mode 100644 index a60b4fd5b5..0000000000 --- a/app/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt +++ /dev/null @@ -1,43 +0,0 @@ -package org.session.libsession.messaging.mentions - -import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.contacts.Contact -import java.util.Locale - -object MentionsManager { - var userPublicKeyCache = mutableMapOf>() // Thread ID to set of user hex encoded public keys - - fun cache(publicKey: String, threadID: Long) { - val cache = userPublicKeyCache[threadID] - if (cache != null) { - userPublicKeyCache[threadID] = cache.plus(publicKey) - } else { - userPublicKeyCache[threadID] = setOf( publicKey ) - } - } - - fun getMentionCandidates(query: String, threadID: Long, isOpenGroup: Boolean): List { - val cache = userPublicKeyCache[threadID] ?: return listOf() - // Prepare - val context = if (isOpenGroup) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR - val storage = MessagingModuleConfiguration.shared.storage - val userPublicKey = storage.getUserPublicKey() - // Gather candidates - var candidates: List = cache.mapNotNull { publicKey -> - val contact = storage.getContactWithAccountID(publicKey) - val displayName = contact?.displayName(context) ?: return@mapNotNull null - Mention(publicKey, displayName) - } - candidates = candidates.filter { it.publicKey != userPublicKey } - // Sort alphabetically first - candidates.sortedBy { it.displayName } - if (query.length >= 2) { - // Filter out any non-matching candidates - candidates = candidates.filter { it.displayName.lowercase(Locale.getDefault()).contains(query.lowercase(Locale.getDefault())) } - // Sort based on where in the candidate the query occurs - candidates.sortedBy { it.displayName.lowercase(Locale.getDefault()).indexOf(query.lowercase(Locale.getDefault())) } - } - // Return - return candidates - } -} diff --git a/app/src/main/java/org/session/libsession/messaging/messages/Message.kt b/app/src/main/java/org/session/libsession/messaging/messages/Message.kt index 366b49d24a..4c995bac0f 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/Message.kt +++ b/app/src/main/java/org/session/libsession/messaging/messages/Message.kt @@ -88,5 +88,5 @@ fun SignalServiceProtos.Content.expiryMode(): ExpiryMode = */ inline fun M.applyExpiryMode(thread: Long): M = apply { val storage = MessagingModuleConfiguration.shared.storage - expiryMode = storage.getExpirationConfiguration(thread)?.expiryMode?.coerceSendToRead(coerceDisappearAfterSendToRead) ?: ExpiryMode.NONE + expiryMode = storage.getExpirationConfiguration(thread).coerceSendToRead(coerceDisappearAfterSendToRead) } diff --git a/app/src/main/java/org/session/libsession/messaging/messages/ProfileUpdateHandler.kt b/app/src/main/java/org/session/libsession/messaging/messages/ProfileUpdateHandler.kt new file mode 100644 index 0000000000..de39ee3c97 --- /dev/null +++ b/app/src/main/java/org/session/libsession/messaging/messages/ProfileUpdateHandler.kt @@ -0,0 +1,153 @@ +package org.session.libsession.messaging.messages + +import dagger.Lazy +import network.loki.messenger.BuildConfig +import network.loki.messenger.libsession_util.util.UserPic +import org.session.libsession.database.StorageProtocol +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.IdPrefix +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.BlindedIdMappingDatabase +import org.thoughtcrime.securesms.database.RecipientDatabase +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This class handles the profile updates coming from user's messages. The messages can be + * from a 1to1, groups or community conversations. + * + * Although [handleProfileUpdate] takes an [Address] or [AccountId], this class can only handle + * the profile updates for **users**, not groups or communities' profile, as they have very different + * mechanisms and storage for their updates. + */ +@Singleton +class ProfileUpdateHandler @Inject constructor( + private val configFactory: ConfigFactoryProtocol, + private val recipientDatabase: RecipientDatabase, + private val blindedIdMappingDatabase: BlindedIdMappingDatabase, + private val prefs: TextSecurePreferences, + private val storage: Lazy, +) { + + fun handleProfileUpdate(sender: Address, updates: Updates, communityServerPubKey: String?) { + val senderAccountId = AccountId.fromStringOrNull(sender.address) + if (senderAccountId == null) { + Log.e(TAG, "Invalid sender address") + return + } + + handleProfileUpdate(senderAccountId, updates, communityServerPubKey) + } + + fun handleProfileUpdate(sender: AccountId, updates: Updates, communityServerPubKey: String?) { + if (sender.hexString == prefs.getLocalNumber() || + (communityServerPubKey != null && storage.get().getUserBlindedAccountId(communityServerPubKey) == sender) + ) { + Log.w(TAG, "Ignoring profile update for local number") + return + } + + + if (updates.name.isNullOrBlank() && + updates.pic == null && + updates.acceptsCommunityRequests == null + ) { + Log.i(TAG, "No valid profile updated data provided for $sender") + return + } + + + Log.d(TAG, "Handling profile update for $sender") + + // If the sender is blind, we need to figure out their real address first. + val actualSender = + if (sender.prefix == IdPrefix.BLINDED || sender.prefix == IdPrefix.BLINDEDV2) { + blindedIdMappingDatabase.getBlindedIdMapping(sender.hexString) + .firstNotNullOfOrNull { it.accountId } + ?.let(AccountId::fromStringOrNull) + ?: sender + } else { + sender + } + + if (actualSender.hexString == prefs.getLocalNumber()) { + Log.w(TAG, "Ignoring profile update for local number: $actualSender") + return + } + + // First, if the user is a contact, update the config and that's all we need to do. + val isExistingContact = actualSender.prefix == IdPrefix.STANDARD && + configFactory.withMutableUserConfigs { configs -> + val existingContact = configs.contacts.get(actualSender.hexString) + if (existingContact != null) { + configs.contacts.set( + existingContact.copy( + name = updates.name ?: existingContact.name, + profilePicture = updates.pic ?: existingContact.profilePicture + ) + ) + true + } else { + false + } + } + + if (isExistingContact && updates.acceptsCommunityRequests == null) { + Log.d(TAG, "Updated existing contact profile for $sender (actualSender: $actualSender)") + return + } + + // If the actual sender is still blinded or unknown contact, we need to update their + // settings in the recipient database instead, as we don't have a place in the config + // for them. + if (actualSender.prefix == IdPrefix.BLINDED || actualSender.prefix == IdPrefix.BLINDEDV2 || + actualSender.prefix == IdPrefix.STANDARD + ) { + Log.d(TAG, "Updating recipient profile for $actualSender") + recipientDatabase.updateProfile( + Address.fromSerialized(actualSender.hexString), + updates.name, + updates.pic, + updates.acceptsCommunityRequests + ) + return + } + + if (BuildConfig.DEBUG) { + throw IllegalArgumentException("Unsupported profile update for sender: $sender") + } + + Log.e(TAG, "Unsupported profile updating for sender: $sender") + } + + data class Updates( + val name: String? = null, + val pic: UserPic? = null, + val acceptsCommunityRequests: Boolean? = null, + ) { + constructor( + name: String?, + picUrl: String?, + picKey: ByteArray?, + acceptsCommunityRequests: Boolean? + ) : this( + name = name, + pic = if (!picUrl.isNullOrBlank() && picKey != null && picKey.size in VALID_PROFILE_KEY_LENGTH) { + UserPic(picUrl, picKey) + } else { + null + }, acceptsCommunityRequests = acceptsCommunityRequests + ) + } + + companion object { + const val TAG = "ProfileUpdateHandler" + + const val MAX_PROFILE_NAME_LENGTH = 100 + + private val VALID_PROFILE_KEY_LENGTH = listOf(16, 32) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingExpirationUpdateMessage.java b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingExpirationUpdateMessage.java index b42d6d1bde..cd411a3580 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingExpirationUpdateMessage.java +++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingExpirationUpdateMessage.java @@ -1,8 +1,8 @@ package org.session.libsession.messaging.messages.signal; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; +import org.session.libsession.utilities.Address; import org.session.libsession.utilities.DistributionTypes; -import org.session.libsession.utilities.recipients.Recipient; import java.util.Collections; import java.util.LinkedList; @@ -11,7 +11,7 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage private final String groupId; - public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn, long expireStartedAt, String groupId) { + public OutgoingExpirationUpdateMessage(Address recipient, long sentTimeMillis, long expiresIn, long expireStartedAt, String groupId) { super(recipient, "", new LinkedList(), sentTimeMillis, DistributionTypes.CONVERSATION, expiresIn, expireStartedAt, null, Collections.emptyList(), Collections.emptyList()); diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java index 86db70f435..4c1d7ead1b 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java +++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java @@ -3,12 +3,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.session.libsession.utilities.Address; import org.session.libsession.utilities.DistributionTypes; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.utilities.Contact; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; -import org.session.libsession.utilities.recipients.Recipient; import java.util.LinkedList; import java.util.List; @@ -18,7 +18,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { private final String groupID; private final boolean isUpdateMessage; - public OutgoingGroupMediaMessage(@NonNull Recipient recipient, + public OutgoingGroupMediaMessage(@NonNull Address recipient, @NonNull String body, @Nullable String groupId, @Nullable final Attachment avatar, diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java index 85dc0cc6a2..85ae7a0a0d 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java +++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java @@ -7,11 +7,11 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; +import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Contact; import org.session.libsession.utilities.DistributionTypes; import org.session.libsession.utilities.IdentityKeyMismatch; import org.session.libsession.utilities.NetworkFailure; -import org.session.libsession.utilities.recipients.Recipient; import java.util.Collections; import java.util.LinkedList; @@ -19,7 +19,7 @@ public class OutgoingMediaMessage { - private final Recipient recipient; + private final Address recipient; protected final String body; protected final List attachments; private final long sentTimeMillis; @@ -34,7 +34,7 @@ public class OutgoingMediaMessage { private final List contacts = new LinkedList<>(); private final List linkPreviews = new LinkedList<>(); - public OutgoingMediaMessage(Recipient recipient, String message, + public OutgoingMediaMessage(Address recipient, String message, List attachments, long sentTimeMillis, int subscriptionId, long expiresIn, long expireStartedAt, int distributionType, @@ -78,7 +78,7 @@ public OutgoingMediaMessage(OutgoingMediaMessage that) { } public static OutgoingMediaMessage from(VisibleMessage message, - Recipient recipient, + Address recipient, List attachments, @Nullable QuoteModel outgoingQuote, @Nullable LinkPreview linkPreview, @@ -94,7 +94,7 @@ public static OutgoingMediaMessage from(VisibleMessage message, Collections.emptyList(), previews, Collections.emptyList(), Collections.emptyList()); } - public Recipient getRecipient() { + public Address getRecipient() { return recipient; } diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java index e93c3c5986..9d8b5e9521 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java +++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java @@ -4,17 +4,17 @@ import androidx.annotation.Nullable; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; +import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Contact; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; -import org.session.libsession.utilities.recipients.Recipient; import java.util.Collections; import java.util.List; public class OutgoingSecureMediaMessage extends OutgoingMediaMessage { - public OutgoingSecureMediaMessage(Recipient recipient, String body, + public OutgoingSecureMediaMessage(Address recipient, String body, List attachments, long sentTimeMillis, int distributionType, diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.java b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.java index b62a75a1e3..c79a68102f 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.java +++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.java @@ -2,11 +2,11 @@ import org.session.libsession.messaging.messages.visible.OpenGroupInvitation; import org.session.libsession.messaging.messages.visible.VisibleMessage; -import org.session.libsession.utilities.recipients.Recipient; +import org.session.libsession.utilities.Address; import org.session.libsession.messaging.utilities.UpdateMessageData; public class OutgoingTextMessage { - private final Recipient recipient; + private final Address recipient; private final String message; private final int subscriptionId; private final long expiresIn; @@ -14,7 +14,7 @@ public class OutgoingTextMessage { private final long sentTimestampMillis; private boolean isOpenGroupInvitation = false; - public OutgoingTextMessage(Recipient recipient, String message, long expiresIn, long expireStartedAt, int subscriptionId, long sentTimestampMillis) { + public OutgoingTextMessage(Address recipient, String message, long expiresIn, long expireStartedAt, int subscriptionId, long sentTimestampMillis) { this.recipient = recipient; this.message = message; this.expiresIn = expiresIn; @@ -23,11 +23,11 @@ public OutgoingTextMessage(Recipient recipient, String message, long expiresIn, this.sentTimestampMillis = sentTimestampMillis; } - public static OutgoingTextMessage from(VisibleMessage message, Recipient recipient, long expiresInMillis, long expireStartedAt) { + public static OutgoingTextMessage from(VisibleMessage message, Address recipient, long expiresInMillis, long expireStartedAt) { return new OutgoingTextMessage(recipient, message.getText(), expiresInMillis, expireStartedAt, -1, message.getSentTimestamp()); } - public static OutgoingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation, Recipient recipient, Long sentTimestamp, long expiresInMillis, long expireStartedAt) { + public static OutgoingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation, Address recipient, Long sentTimestamp, long expiresInMillis, long expireStartedAt) { String url = openGroupInvitation.getUrl(); String name = openGroupInvitation.getName(); if (url == null || name == null) { return null; } @@ -54,7 +54,7 @@ public String getMessageBody() { return message; } - public Recipient getRecipient() { + public Address getRecipient() { return recipient; } diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 26915e8493..b1fe03d34f 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -293,10 +293,9 @@ object MessageSender { ?.let(MessagingModuleConfiguration.shared.storage::getThreadId) } ?.let(MessagingModuleConfiguration.shared.storage::getExpirationConfiguration) - ?.takeIf { it.isEnabled } - ?.expiryMode ?.takeIf { it is ExpiryMode.AfterSend || isSyncMessage } ?.expiryMillis + ?.takeIf { it > 0 } } // Open Groups @@ -548,8 +547,6 @@ object MessageSender { // only show the NTS if it is currently marked as hidden MessagingModuleConfiguration.shared.configFactory.withUserConfigs { it.userProfile.getNtsPriority() == PRIORITY_HIDDEN } ){ - // make sure note to self is not hidden - MessagingModuleConfiguration.shared.preferences.setHasHiddenNoteToSelf(false) // update config in case it was marked as hidden there MessagingModuleConfiguration.shared.configFactory.withMutableUserConfigs { it.userProfile.setNtsPriority(PRIORITY_VISIBLE) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 6be4745ed3..8f29d9872d 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -9,7 +9,6 @@ import network.loki.messenger.R import network.loki.messenger.libsession_util.ED25519 import network.loki.messenger.libsession_util.util.BlindKeyAPI import network.loki.messenger.libsession_util.util.ExpiryMode -import org.session.libsession.avatars.AvatarHelper import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.StorageProtocol import org.session.libsession.database.userAuth @@ -17,9 +16,8 @@ import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.messages.ExpirationConfiguration -import org.session.libsession.messaging.messages.ExpirationConfiguration.Companion.isNewConfigEnabled import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.messages.ProfileUpdateHandler import org.session.libsession.messaging.messages.control.CallMessage import org.session.libsession.messaging.messages.control.DataExtractionNotification import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate @@ -57,17 +55,15 @@ import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.guava.Optional import org.thoughtcrime.securesms.database.ConfigDatabase +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.ReactionRecord -import org.thoughtcrime.securesms.dependencies.ConfigFactory -import java.security.MessageDigest import java.security.SignatureException import kotlin.math.min internal fun MessageReceiver.isBlocked(publicKey: String): Boolean { - val context = MessagingModuleConfiguration.shared.context - val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false) - return recipient.isBlocked + val recipient = MessagingModuleConfiguration.shared.recipientRepository.getRecipientSync(Address.fromSerialized(publicKey)) + return recipient?.blocked == true } fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, threadId: Long, openGroupID: String?, groupv2Id: AccountId?) { @@ -115,13 +111,13 @@ fun MessageReceiver.messageIsOutdated(message: Message, threadId: Long, openGrou val userPublicKey = storage.getUserPublicKey()!! val threadRecipient = storage.getRecipientForThread(threadId) val conversationVisibleInConfig = storage.conversationInConfig( - if (message.groupPublicKey == null) threadRecipient?.address?.toString() else null, + if (message.groupPublicKey == null) threadRecipient?.toString() else null, message.groupPublicKey, openGroupID, true ) val canPerformChange = storage.canPerformConfigChange( - if (threadRecipient?.address?.toString() == userPublicKey) ConfigDatabase.USER_PROFILE_VARIANT else ConfigDatabase.CONTACTS_VARIANT, + if (threadRecipient?.toString() == userPublicKey) ConfigDatabase.USER_PROFILE_VARIANT else ConfigDatabase.CONTACTS_VARIANT, userPublicKey, message.sentTimestamp!! ) @@ -172,26 +168,6 @@ fun MessageReceiver.cancelTypingIndicatorsIfNeeded(senderPublicKey: String) { private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimerUpdate) { SSKEnvironment.shared.messageExpirationManager.insertExpirationTimerMessage(message) - - val isLegacyGroup = message.groupPublicKey != null && message.groupPublicKey?.startsWith(IdPrefix.GROUP.value) == false - - if (isNewConfigEnabled && !isLegacyGroup) return - - val module = MessagingModuleConfiguration.shared - try { - val threadId = Address.fromSerialized(message.groupPublicKey?.let(::doubleEncodeGroupID) ?: message.sender!!) - .let(module.storage::getOrCreateThreadIdFor) - - module.storage.setExpirationConfiguration( - ExpirationConfiguration( - threadId, - message.expiryMode, - message.sentTimestamp!! - ) - ) - } catch (e: Exception) { - Log.e("Loki", "Failed to update expiration configuration.") - } } private fun MessageReceiver.handleDataExtractionNotification(message: DataExtractionNotification) { @@ -292,10 +268,11 @@ class VisibleMessageHandlerContext( val threadId: Long, val openGroupID: String?, val storage: StorageProtocol, - val profileManager: SSKEnvironment.ProfileManagerProtocol, val groupManagerV2: GroupManagerV2, val messageExpirationManager: SSKEnvironment.MessageExpirationManagerProtocol, val messageDataProvider: MessageDataProvider, + val recipientRepository: RecipientRepository, + val profileUpdateHandler: ProfileUpdateHandler, ) { constructor(module: MessagingModuleConfiguration, threadId: Long, openGroupID: String?): this( @@ -303,10 +280,11 @@ class VisibleMessageHandlerContext( threadId = threadId, openGroupID = openGroupID, storage = module.storage, - profileManager = SSKEnvironment.shared.profileManager, groupManagerV2 = module.groupManagerV2, messageExpirationManager = SSKEnvironment.shared.messageExpirationManager, - messageDataProvider = module.messageDataProvider + messageDataProvider = module.messageDataProvider, + recipientRepository = module.recipientRepository, + profileUpdateHandler = SSKEnvironment.shared.profileUpdateHandler, ) val openGroup: OpenGroup? by lazy { @@ -331,7 +309,7 @@ class VisibleMessageHandlerContext( } val threadRecipient: Recipient? by lazy { - storage.getRecipientForThread(threadId) + storage.getRecipientForThread(threadId)?.let(recipientRepository::getRecipientSync) } } @@ -349,41 +327,17 @@ fun MessageReceiver.handleVisibleMessage( if (MessageReceiver.messageIsOutdated(message, context.threadId, context.openGroupID)) { return null } // Update profile if needed - val recipient = Recipient.from(context.context, Address.fromSerialized(messageSender!!), false) + val address = Address.fromSerialized(messageSender!!) if (runProfileUpdate) { - val profile = message.profile - val isUserBlindedSender = messageSender == context.userBlindedKey - if (profile != null && userPublicKey != messageSender && !isUserBlindedSender) { - val name = profile.displayName!! - if (name.isNotEmpty()) { - context.profileManager.setName(context.context, recipient, name) - } - val newProfileKey = profile.profileKey - - val needsProfilePicture = !AvatarHelper.avatarFileExists(context.context, Address.fromSerialized(messageSender)) - val profileKeyValid = newProfileKey?.isNotEmpty() == true && (newProfileKey.size == 16 || newProfileKey.size == 32) && profile.profilePictureURL?.isNotEmpty() == true - val profileKeyChanged = (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfileKey)) - - if ((profileKeyValid && profileKeyChanged) || (profileKeyValid && needsProfilePicture)) { - context.profileManager.setProfilePicture(context.context, recipient, profile.profilePictureURL, newProfileKey) - } else if (newProfileKey == null || newProfileKey.isEmpty() || profile.profilePictureURL.isNullOrEmpty()) { - context.profileManager.setProfilePicture(context.context, recipient, null, null) - } - } - - if (userPublicKey != messageSender && !isUserBlindedSender) { - context.storage.setBlocksCommunityMessageRequests(recipient, message.blocksMessageRequests) - } - - // update the disappearing / legacy banner for the sender - val disappearingState = when { - proto.dataMessage.expireTimer > 0 && !proto.hasExpirationType() -> Recipient.DisappearingState.LEGACY - else -> Recipient.DisappearingState.UPDATED - } - context.storage.updateDisappearingState( - messageSender, - context.threadId, - disappearingState + context.profileUpdateHandler.handleProfileUpdate( + sender = address, + updates = ProfileUpdateHandler.Updates( + name = message.profile?.displayName, + picUrl = message.profile?.profilePictureURL, + picKey = message.profile?.profileKey, + acceptsCommunityRequests = !message.blocksMessageRequests + ), + communityServerPubKey = context.openGroup?.publicKey ) } // Handle group invite response if new closed group @@ -601,17 +555,16 @@ private fun MessageReceiver.handleGroupUpdated(message: GroupUpdated, closedGrou } // Update profile if needed - if (message.profile != null && !message.isSenderSelf) { - val profile = message.profile - val recipient = Recipient.from(MessagingModuleConfiguration.shared.context, Address.fromSerialized(message.sender!!), false) - val profileManager = SSKEnvironment.shared.profileManager - if (profile.displayName?.isNotEmpty() == true) { - profileManager.setName(MessagingModuleConfiguration.shared.context, recipient, profile.displayName) - } - if (profile.profileKey?.isNotEmpty() == true && !profile.profilePictureURL.isNullOrEmpty()) { - profileManager.setProfilePicture(MessagingModuleConfiguration.shared.context, recipient, profile.profilePictureURL, profile.profileKey) - } - } + SSKEnvironment.shared.profileUpdateHandler.handleProfileUpdate( + sender = Address.fromSerialized(message.sender!!), + updates = ProfileUpdateHandler.Updates( + name = message.profile?.displayName, + picUrl = message.profile?.profilePictureURL, + picKey = message.profile?.profileKey, + acceptsCommunityRequests = null, + ), + communityServerPubKey = null // Groupv2 is not a community + ) when { inner.hasInviteMessage() -> handleNewLibSessionClosedGroupMessage(message) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/MessageNotifier.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/MessageNotifier.kt index 8de01ca53e..0573561c9f 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/MessageNotifier.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/MessageNotifier.kt @@ -1,13 +1,11 @@ package org.session.libsession.messaging.sending_receiving.notifications import android.content.Context -import org.session.libsession.utilities.recipients.Recipient interface MessageNotifier { fun setHomeScreenVisible(isVisible: Boolean) fun setVisibleThread(threadId: Long) fun setLastDesktopActivityTimestamp(timestamp: Long) - fun notifyMessageDeliveryFailed(context: Context?, recipient: Recipient?, threadId: Long) fun cancelDelayedNotifications() fun updateNotification(context: Context) fun updateNotification(context: Context, threadId: Long) diff --git a/app/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt b/app/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt index 796dd77bed..69155b4f93 100644 --- a/app/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt +++ b/app/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt @@ -2,41 +2,51 @@ package org.session.libsession.messaging.utilities import android.content.Context import com.squareup.phrase.Phrase -import network.loki.messenger.libsession_util.getOrNull import network.loki.messenger.R +import network.loki.messenger.libsession_util.getOrNull import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.calls.CallMessageType.CALL_FIRST_MISSED import org.session.libsession.messaging.calls.CallMessageType.CALL_INCOMING import org.session.libsession.messaging.calls.CallMessageType.CALL_MISSED import org.session.libsession.messaging.calls.CallMessageType.CALL_OUTGOING -import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage.Kind.SCREENSHOT +import org.session.libsession.utilities.Address import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ExpirationUtil -import org.session.libsession.utilities.getExpirationTypeDisplayValue -import org.session.libsession.utilities.truncateIdForDisplay -import org.session.libsignal.utilities.Log import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY import org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.OTHER_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY +import org.session.libsession.utilities.getExpirationTypeDisplayValue import org.session.libsession.utilities.getGroup import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.Log object UpdateMessageBuilder { const val TAG = "UpdateMessageBuilder" val storage = MessagingModuleConfiguration.shared.storage - val usernameUtils = MessagingModuleConfiguration.shared.usernameUtils + val recipientRepository = MessagingModuleConfiguration.shared.recipientRepository - private fun getGroupMemberName(memberId: String, groupId: AccountId? = null) = - usernameUtils.getContactNameWithAccountID(memberId, groupId) + private fun getGroupMemberName(senderAddress: String, groupV2Id: AccountId? = null): String { + return recipientRepository.getRecipientDisplayNameSync( + address = Address.fromSerialized(senderAddress), + // There's additional way to getting a group member's name via config system. + fallbackName = { + groupV2Id?.let { gid -> + MessagingModuleConfiguration.shared.configFactory.withGroupConfigs(gid) { + it.groupMembers.getOrNull(senderAddress)?.name + } + } + } + ) + } @JvmStatic fun buildGroupUpdateMessage( @@ -402,7 +412,7 @@ object UpdateMessageBuilder { } fun buildCallMessage(context: Context, type: CallMessageType, senderId: String): String { - val senderName = usernameUtils.getContactNameWithAccountID(senderId) + val senderName = recipientRepository.getRecipientDisplayNameSync(Address.fromSerialized(senderId)) return when (type) { CALL_INCOMING -> Phrase.from(context, R.string.callsCalledYou).put(NAME_KEY, senderName) diff --git a/app/src/main/java/org/session/libsession/utilities/AESGCM.kt b/app/src/main/java/org/session/libsession/utilities/AESGCM.kt index 4685b5767c..536bd310f8 100644 --- a/app/src/main/java/org/session/libsession/utilities/AESGCM.kt +++ b/app/src/main/java/org/session/libsession/utilities/AESGCM.kt @@ -4,6 +4,8 @@ import androidx.annotation.WorkerThread import network.loki.messenger.libsession_util.Curve25519 import network.loki.messenger.libsession_util.SessionEncrypt import org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK +import org.session.libsignal.utilities.ByteArraySlice +import org.session.libsignal.utilities.ByteArraySlice.Companion.view import org.session.libsignal.utilities.ByteUtil import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Util @@ -53,15 +55,18 @@ internal object AESGCM { /** * Sync. Don't call from the main thread. */ - internal fun encrypt(plaintext: ByteArray, symmetricKey: ByteArray): ByteArray { + fun encrypt(plaintext: ByteArraySlice, symmetricKey: ByteArray): ByteArray { val iv = Util.getSecretBytes(ivSize) synchronized(CIPHER_LOCK) { val cipher = Cipher.getInstance("AES/GCM/NoPadding") cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv)) - return ByteUtil.combine(iv, cipher.doFinal(plaintext)) + return ByteUtil.combine(iv, cipher.doFinal(plaintext.data, plaintext.offset, plaintext.len)) } } + internal fun encrypt(plaintext: ByteArray, symmetricKey: ByteArray): ByteArray = + encrypt(plaintext.view(), symmetricKey) + /** * Sync. Don't call from the main thread. */ diff --git a/app/src/main/java/org/session/libsession/utilities/Address.kt b/app/src/main/java/org/session/libsession/utilities/Address.kt index 6fc8c545d8..9ddba07a26 100644 --- a/app/src/main/java/org/session/libsession/utilities/Address.kt +++ b/app/src/main/java/org/session/libsession/utilities/Address.kt @@ -1,23 +1,13 @@ package org.session.libsession.utilities -import android.content.Context -import android.os.Parcel import android.os.Parcelable -import android.util.Pair -import androidx.annotation.VisibleForTesting +import kotlinx.parcelize.Parcelize import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Util -import org.session.libsignal.utilities.guava.Optional import java.util.LinkedList -import java.util.concurrent.atomic.AtomicReference -import java.util.regex.Matcher -import java.util.regex.Pattern - -class Address private constructor(address: String) : Parcelable, Comparable { - private val address: String = address.lowercase() - - constructor(`in`: Parcel) : this(`in`.readString()!!) {} +@Parcelize +data class Address private constructor(val address: String) : Parcelable, Comparable
{ val isLegacyGroup: Boolean get() = GroupUtil.isLegacyClosedGroup(address) val isGroupV2: Boolean @@ -50,97 +40,16 @@ class Address private constructor(address: String) : Parcelable, Comparable - private val localCountryCode: String - private val ALPHA_PATTERN = Pattern.compile("[a-zA-Z]") - fun format(number: String?): String { - return number ?: "Unknown" - } - - private fun parseAreaCode(e164Number: String, countryCode: Int): String? { - when (countryCode) { - 1 -> return e164Number.substring(2, 5) - 55 -> return e164Number.substring(3, 5) - } - return null - } - - private fun applyAreaCodeRules(localNumber: Optional, testNumber: String): String { - if (!localNumber.isPresent || !localNumber.get().areaCode.isPresent) { - return testNumber - } - val matcher: Matcher - when (localNumber.get().countryCode) { - 1 -> { - matcher = US_NO_AREACODE.matcher(testNumber) - if (matcher.matches()) { - return localNumber.get().areaCode.toString() + matcher.group() - } - } - 55 -> { - matcher = BR_NO_AREACODE.matcher(testNumber) - if (matcher.matches()) { - return localNumber.get().areaCode.toString() + matcher.group() - } - } - } - return testNumber - } + override fun compareTo(other: Address): Int = address.compareTo(other.address) - private class PhoneNumber internal constructor(val e164Number: String, val countryCode: Int, areaCode: String?) { - val areaCode: Optional - - init { - this.areaCode = Optional.fromNullable(areaCode) - } - } - - companion object { - private val TAG = ExternalAddressFormatter::class.java.simpleName - private val SHORT_COUNTRIES: HashSet = object : HashSet() { - init { - add("NU") - add("TK") - add("NC") - add("AC") - } - } - private val US_NO_AREACODE = Pattern.compile("^(\\d{7})$") - private val BR_NO_AREACODE = Pattern.compile("^(9?\\d{8})$") - } - - init { - localNumber = Optional.absent() - this.localCountryCode = localCountryCode - } - } + val debugString: String + get() = "Address(address=${address.substring(0, address.length.coerceAtMost(5))}...)" companion object { - @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { - override fun createFromParcel(`in`: Parcel): Address = Address(`in`) - override fun newArray(size: Int): Array = arrayOfNulls(size) - } val UNKNOWN = Address("Unknown") - private val TAG = Address::class.java.simpleName - private val cachedFormatter = AtomicReference>() - - @JvmStatic - fun fromSerialized(serialized: String): Address = Address(serialized) @JvmStatic - fun fromExternal(context: Context, external: String?): Address = fromSerialized(external!!) + fun fromSerialized(serialized: String): Address = Address(serialized.lowercase()) @JvmStatic fun fromSerializedList(serialized: String, delimiter: Char): List
{ diff --git a/app/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt b/app/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt index 66734f463a..964e38ab5c 100644 --- a/app/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt +++ b/app/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt @@ -1,6 +1,7 @@ package org.session.libsession.utilities import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onStart @@ -24,8 +25,8 @@ import network.loki.messenger.libsession_util.ReadableUserGroupsConfig import network.loki.messenger.libsession_util.ReadableUserProfile import network.loki.messenger.libsession_util.util.ConfigPush import network.loki.messenger.libsession_util.util.GroupInfo +import network.loki.messenger.libsession_util.util.UserPic import org.session.libsession.snode.SwarmAuth -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.AccountId interface ConfigFactoryProtocol { @@ -112,6 +113,11 @@ enum class UserConfigType(val namespace: Int) { USER_GROUPS(Namespace.USER_GROUPS()), } +val ConfigFactoryProtocol.currentUserName: String get() = withUserConfigs { it.userProfile.getName().orEmpty() } +val ConfigFactoryProtocol.currentUserProfile: UserPic? get() = withUserConfigs { configs -> + configs.userProfile.getPic().takeIf { it.url.isNotBlank() } +} + /** * Shortcut to get the group info for a closed group. Equivalent to: `withUserConfigs { it.userGroups.getClosedGroup(groupId) }` */ @@ -120,39 +126,17 @@ fun ConfigFactoryProtocol.getGroup(groupId: AccountId): GroupInfo.ClosedGroupInf } /** - * Shortcut to check if the current user was kicked from a given group V2 (as a Recipient) - */ -fun ConfigFactoryProtocol.wasKickedFromGroupV2(group: Recipient) = - group.isGroupV2Recipient && getGroup(AccountId(group.address.toString()))?.kicked == true - -/** - * Shortcut to check if the a given group is destroyed + * Flow that emits when the user configs are modified or merged. */ -fun ConfigFactoryProtocol.isGroupDestroyed(group: Recipient) = - group.isGroupV2Recipient && getGroup(AccountId(group.address.toString()))?.destroyed == true - -/** - * Wait until all user configs are pushed to the server. - * - * This function is not essential to the pushing of the configs, the config push will schedule - * itself upon changes, so this function is purely observatory. - * - * This function will check the user configs immediately, if nothing needs to be pushed, it will return immediately. - * - * @return True if all user configs are pushed, false if the timeout is reached. - */ -suspend fun ConfigFactoryProtocol.waitUntilUserConfigsPushed(timeoutMills: Long = 10_000L): Boolean { - fun needsPush() = withUserConfigs { configs -> - UserConfigType.entries.any { configs.getConfig(it).needsPush() } - } - - return withTimeoutOrNull(timeoutMills){ - configUpdateNotifications - .onStart { emit(ConfigUpdateNotification.UserConfigsModified) } // Trigger the filtering immediately - .filter { it == ConfigUpdateNotification.UserConfigsModified && !needsPush() } - .first() - } != null -} +fun ConfigFactoryProtocol.userConfigsChanged(debounceMills: Long = 0L): Flow<*> = + configUpdateNotifications.filter { it is ConfigUpdateNotification.UserConfigsModified || it is ConfigUpdateNotification.UserConfigsMerged } + .let { flow -> + if (debounceMills > 0) { + flow.debounce(debounceMills) + } else { + flow + } + } /** * Wait until all configs of given group are pushed to the server. diff --git a/app/src/main/java/org/session/libsession/utilities/ProfileKeyUtil.java b/app/src/main/java/org/session/libsession/utilities/ProfileKeyUtil.java index 4550965ae7..b3034df568 100644 --- a/app/src/main/java/org/session/libsession/utilities/ProfileKeyUtil.java +++ b/app/src/main/java/org/session/libsession/utilities/ProfileKeyUtil.java @@ -1,9 +1,6 @@ package org.session.libsession.utilities; -import android.content.Context; - import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import org.session.libsignal.utilities.Base64; @@ -13,22 +10,8 @@ public class ProfileKeyUtil { public static final int PROFILE_KEY_BYTES = 32; - public static synchronized @NonNull byte[] getProfileKey(@NonNull Context context) { - try { - String encodedProfileKey = TextSecurePreferences.getProfileKey(context); - - if (encodedProfileKey == null) { - encodedProfileKey = Util.getSecret(PROFILE_KEY_BYTES); - TextSecurePreferences.setProfileKey(context, encodedProfileKey); - } - return Base64.decode(encodedProfileKey); - } catch (IOException e) { - throw new AssertionError(e); - } - } - - public static synchronized @NonNull byte[] getProfileKeyFromEncodedString(String encodedProfileKey) { + public static @NonNull byte[] getProfileKeyFromEncodedString(String encodedProfileKey) { try { return Base64.decode(encodedProfileKey); } catch (IOException e) { @@ -36,11 +19,7 @@ public class ProfileKeyUtil { } } - public static synchronized @NonNull String generateEncodedProfileKey(@NonNull Context context) { + public static @NonNull String generateEncodedProfileKey() { return Util.getSecret(PROFILE_KEY_BYTES); } - - public static synchronized void setEncodedProfileKey(@NonNull Context context, @Nullable String key) { - TextSecurePreferences.setProfileKey(context, key); - } } diff --git a/app/src/main/java/org/session/libsession/utilities/ProfilePictureModifiedEvent.kt b/app/src/main/java/org/session/libsession/utilities/ProfilePictureModifiedEvent.kt deleted file mode 100644 index f06ca2ec0f..0000000000 --- a/app/src/main/java/org/session/libsession/utilities/ProfilePictureModifiedEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.session.libsession.utilities - -import org.session.libsession.utilities.recipients.Recipient - -data class ProfilePictureModifiedEvent(val recipient: Recipient) \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt b/app/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt index b682fbf193..843f4adff7 100644 --- a/app/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt +++ b/app/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt @@ -2,86 +2,72 @@ package org.session.libsession.utilities import android.content.Context import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import network.loki.messenger.libsession_util.util.Bytes -import network.loki.messenger.libsession_util.util.UserPic import okio.Buffer -import org.session.libsession.avatars.AvatarHelper -import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.file_server.FileServerApi import org.session.libsession.snode.utilities.await -import org.session.libsession.utilities.Address.Companion.fromSerialized -import org.session.libsession.utilities.TextSecurePreferences.Companion.getLastProfilePictureUpload -import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber -import org.session.libsession.utilities.TextSecurePreferences.Companion.getProfileKey -import org.session.libsession.utilities.TextSecurePreferences.Companion.setLastProfilePictureUpload import org.session.libsignal.streams.DigestingRequestBody import org.session.libsignal.streams.ProfileCipherOutputStream import org.session.libsignal.streams.ProfileCipherOutputStreamFactory -import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.ProfileAvatarData import org.session.libsignal.utilities.retryIfNeeded import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream import java.util.Date object ProfilePictureUtilities { @OptIn(DelicateCoroutinesApi::class) fun resubmitProfilePictureIfNeeded(context: Context) { - GlobalScope.launch(Dispatchers.IO) { - // Files expire on the file server after a while, so we simply re-upload the user's profile picture - // at a certain interval to ensure it's always available. - val userPublicKey = getLocalNumber(context) ?: return@launch - val now = Date().time - val lastProfilePictureUpload = getLastProfilePictureUpload(context) - if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return@launch - - // Don't generate a new profile key here; we do that when the user changes their profile picture - Log.d("Loki-Avatar", "Uploading Avatar Started") - val encodedProfileKey = getProfileKey(context) - try { - // Read the file into a byte array - val inputStream = AvatarHelper.getInputStreamFor( - context, - fromSerialized(userPublicKey) - ) - val baos = ByteArrayOutputStream() - var count: Int - val buffer = ByteArray(1024) - while ((inputStream.read(buffer, 0, buffer.size) - .also { count = it }) != -1 - ) { - baos.write(buffer, 0, count) - } - baos.flush() - val profilePicture = baos.toByteArray() - // Re-upload it - val url = upload( - profilePicture, - encodedProfileKey!!, - context - ) - - // Update the last profile picture upload date - setLastProfilePictureUpload( - context, - Date().time - ) - - // update config with new URL for reuploaded file - val profileKey = ProfileKeyUtil.getProfileKey(context) - MessagingModuleConfiguration.shared.configFactory.withMutableUserConfigs { - it.userProfile.setPic(UserPic(url, Bytes(profileKey))) - } - - Log.d("Loki-Avatar", "Uploading Avatar Finished") - } catch (e: Exception) { - Log.e("Loki-Avatar", "Uploading avatar failed.") - } - } +// GlobalScope.launch(Dispatchers.IO) { +// // Files expire on the file server after a while, so we simply re-upload the user's profile picture +// // at a certain interval to ensure it's always available. +// val userPublicKey = getLocalNumber(context) ?: return@launch +// val now = Date().time +// val lastProfilePictureUpload = getLastProfilePictureUpload(context) +// if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return@launch +// +// // Don't generate a new profile key here; we do that when the user changes their profile picture +// Log.d("Loki-Avatar", "Uploading Avatar Started") +// val encodedProfileKey = getProfileKey(context) +// try { +// // Read the file into a byte array +// val inputStream = AvatarHelper.getInputStreamFor( +// context, +// fromSerialized(userPublicKey) +// ) +// val baos = ByteArrayOutputStream() +// var count: Int +// val buffer = ByteArray(1024) +// while ((inputStream.read(buffer, 0, buffer.size) +// .also { count = it }) != -1 +// ) { +// baos.write(buffer, 0, count) +// } +// baos.flush() +// val profilePicture = baos.toByteArray() +// // Re-upload it +// val url = upload( +// profilePicture, +// encodedProfileKey!!, +// context +// ) +// +// // Update the last profile picture upload date +// setLastProfilePictureUpload( +// context, +// Date().time +// ) +// +// // update config with new URL for reuploaded file +// val profileKey = ProfileKeyUtil.getProfileKey(context) +// MessagingModuleConfiguration.shared.configFactory.withMutableUserConfigs { +// it.userProfile.setPic(UserPic(url, Bytes(profileKey))) +// } +// +// Log.d("Loki-Avatar", "Uploading Avatar Finished") +// } catch (e: Exception) { +// Log.e("Loki-Avatar", "Uploading avatar failed.") +// } +// } } suspend fun upload(profilePicture: ByteArray, encodedProfileKey: String, context: Context): String { @@ -113,7 +99,7 @@ object ProfilePictureUtilities { TextSecurePreferences.setLastProfilePictureUpload(context, Date().time) val url = "${FileServerApi.FILE_SERVER_URL}/file/$id" - TextSecurePreferences.setProfilePictureURL(context, url) +// TextSecurePreferences.setProfilePictureURL(context, url) return url } diff --git a/app/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt b/app/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt index 4e92594d3c..a41885fc33 100644 --- a/app/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt +++ b/app/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt @@ -1,19 +1,18 @@ package org.session.libsession.utilities import android.content.Context -import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.messages.ProfileUpdateHandler import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier -import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.database.model.MessageId class SSKEnvironment( val typingIndicators: TypingIndicatorsProtocol, val readReceiptManager: ReadReceiptManagerProtocol, - val profileManager: ProfileManagerProtocol, val notificationManager: MessageNotifier, - val messageExpirationManager: MessageExpirationManagerProtocol + val messageExpirationManager: MessageExpirationManagerProtocol, + val profileUpdateHandler: ProfileUpdateHandler, ) { interface TypingIndicatorsProtocol { @@ -26,17 +25,6 @@ class SSKEnvironment( fun processReadReceipts(context: Context, fromRecipientId: String, sentTimestamps: List, readTimestamp: Long) } - interface ProfileManagerProtocol { - companion object { - const val NAME_PADDED_LENGTH = 100 - } - - fun setNickname(context: Context, recipient: Recipient, nickname: String?) - fun setName(context: Context, recipient: Recipient, name: String?) - fun setProfilePicture(context: Context, recipient: Recipient, profilePictureURL: String?, profileKey: ByteArray?) - fun contactUpdatedInternal(contact: Contact): String? - } - interface MessageExpirationManagerProtocol { fun insertExpirationTimerMessage(message: ExpirationTimerUpdate) @@ -57,11 +45,17 @@ class SSKEnvironment( fun configure(typingIndicators: TypingIndicatorsProtocol, readReceiptManager: ReadReceiptManagerProtocol, - profileManager: ProfileManagerProtocol, notificationManager: MessageNotifier, - messageExpirationManager: MessageExpirationManagerProtocol) { + messageExpirationManager: MessageExpirationManagerProtocol, + profileUpdateHandler: ProfileUpdateHandler) { if (Companion::shared.isInitialized) { return } - shared = SSKEnvironment(typingIndicators, readReceiptManager, profileManager, notificationManager, messageExpirationManager) + shared = SSKEnvironment( + typingIndicators = typingIndicators, + readReceiptManager = readReceiptManager, + notificationManager = notificationManager, + messageExpirationManager = messageExpirationManager, + profileUpdateHandler = profileUpdateHandler + ) } } } 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 3e3cade903..5cb96acbbf 100644 --- a/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -25,7 +25,6 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_ import org.session.libsession.utilities.TextSecurePreferences.Companion.ENVIRONMENT import org.session.libsession.utilities.TextSecurePreferences.Companion.FOLLOW_SYSTEM_SETTINGS import org.session.libsession.utilities.TextSecurePreferences.Companion.HAS_HIDDEN_MESSAGE_REQUESTS -import org.session.libsession.utilities.TextSecurePreferences.Companion.HAS_HIDDEN_NOTE_TO_SELF import org.session.libsession.utilities.TextSecurePreferences.Companion.HAVE_SHOWN_A_NOTIFICATION_ABOUT_TOKEN_PAGE import org.session.libsession.utilities.TextSecurePreferences.Companion.HIDE_PASSWORD import org.session.libsession.utilities.TextSecurePreferences.Companion.LAST_VACUUM_TIME @@ -91,14 +90,6 @@ interface TextSecurePreferences { fun setHasSeenGIFMetaDataWarning() fun isGifSearchInGridLayout(): Boolean fun setIsGifSearchInGridLayout(isGrid: Boolean) - fun getProfileKey(): String? - fun setProfileKey(key: String?) - fun setProfileName(name: String?) - fun getProfileName(): String? - fun setProfileAvatarId(id: Int) - fun getProfileAvatarId(): Int - fun setProfilePictureURL(url: String?) - fun getProfilePictureURL(): String? fun getNotificationPriority(): Int fun getMessageBodyTextSize(): Int fun setPreferredCameraDirection(value: CameraSelector) @@ -178,8 +169,6 @@ interface TextSecurePreferences { fun setForceIncomingMessagesAsPro(hidden: Boolean) fun forcePostPro(): Boolean fun setForcePostPro(hidden: Boolean) - fun hasHiddenNoteToSelf(): Boolean - fun setHasHiddenNoteToSelf(hidden: Boolean) fun setShownCallWarning(): Boolean fun setShownCallNotification(): Boolean fun isCallNotificationsEnabled(): Boolean @@ -294,7 +283,6 @@ interface TextSecurePreferences { const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time" const val LAST_OPEN_DATE = "pref_last_open_date" const val HAS_HIDDEN_MESSAGE_REQUESTS = "pref_message_requests_hidden" - const val HAS_HIDDEN_NOTE_TO_SELF = "pref_note_to_self_hidden" const val SET_FORCE_CURRENT_USER_PRO = "pref_force_current_user_pro" const val SET_FORCE_INCOMING_MESSAGE_PRO = "pref_force_incoming_message_pro" const val SET_FORCE_POST_PRO = "pref_force_post_pro" @@ -543,46 +531,6 @@ interface TextSecurePreferences { setBooleanPreference(context, GIF_GRID_LAYOUT, isGrid) } - @JvmStatic - fun getProfileKey(context: Context): String? { - return getStringPreference(context, PROFILE_KEY_PREF, null) - } - - @JvmStatic - fun setProfileKey(context: Context, key: String?) { - setStringPreference(context, PROFILE_KEY_PREF, key) - } - - @JvmStatic - fun setProfileName(context: Context, name: String?) { - setStringPreference(context, PROFILE_NAME_PREF, name) - _events.tryEmit(PROFILE_NAME_PREF) - } - - @JvmStatic - fun getProfileName(context: Context): String? { - return getStringPreference(context, PROFILE_NAME_PREF, null) - } - - @JvmStatic - fun setProfileAvatarId(context: Context, id: Int) { - setIntegerPreference(context, PROFILE_AVATAR_ID_PREF, id) - } - - @JvmStatic - fun getProfileAvatarId(context: Context): Int { - return getIntegerPreference(context, PROFILE_AVATAR_ID_PREF, 0) - } - - fun setProfilePictureURL(context: Context, url: String?) { - setStringPreference(context, PROFILE_AVATAR_URL_PREF, url) - } - - @JvmStatic - fun getProfilePictureURL(context: Context): String? { - return getStringPreference(context, PROFILE_AVATAR_URL_PREF, null) - } - @JvmStatic fun getNotificationPrivacy(context: Context): NotificationPrivacyPreference { return NotificationPrivacyPreference(getStringPreference(context, NOTIFICATION_PRIVACY_PREF, "all")) @@ -1178,39 +1126,6 @@ class AppTextSecurePreferences @Inject constructor( setBooleanPreference(TextSecurePreferences.GIF_GRID_LAYOUT, isGrid) } - override fun getProfileKey(): String? { - return getStringPreference(TextSecurePreferences.PROFILE_KEY_PREF, null) - } - - override fun setProfileKey(key: String?) { - setStringPreference(TextSecurePreferences.PROFILE_KEY_PREF, key) - } - - override fun setProfileName(name: String?) { - setStringPreference(TextSecurePreferences.PROFILE_NAME_PREF, name) - _events.tryEmit(TextSecurePreferences.PROFILE_NAME_PREF) - } - - override fun getProfileName(): String? { - return getStringPreference(TextSecurePreferences.PROFILE_NAME_PREF, null) - } - - override fun setProfileAvatarId(id: Int) { - setIntegerPreference(TextSecurePreferences.PROFILE_AVATAR_ID_PREF, id) - } - - override fun getProfileAvatarId(): Int { - return getIntegerPreference(TextSecurePreferences.PROFILE_AVATAR_ID_PREF, 0) - } - - override fun setProfilePictureURL(url: String?) { - setStringPreference(TextSecurePreferences.PROFILE_AVATAR_URL_PREF, url) - } - - override fun getProfilePictureURL(): String? { - return getStringPreference(TextSecurePreferences.PROFILE_AVATAR_URL_PREF, null) - } - override fun getNotificationPriority(): Int { return getStringPreference( TextSecurePreferences.NOTIFICATION_PRIORITY_PREF, NotificationCompat.PRIORITY_HIGH.toString())!!.toInt() @@ -1594,16 +1509,6 @@ class AppTextSecurePreferences @Inject constructor( setBooleanPreference(HAS_HIDDEN_MESSAGE_REQUESTS, hidden) _events.tryEmit(HAS_HIDDEN_MESSAGE_REQUESTS) } - - override fun hasHiddenNoteToSelf(): Boolean { - return getBooleanPreference(HAS_HIDDEN_NOTE_TO_SELF, false) - } - - override fun setHasHiddenNoteToSelf(hidden: Boolean) { - setBooleanPreference(HAS_HIDDEN_NOTE_TO_SELF, hidden) - _events.tryEmit(HAS_HIDDEN_NOTE_TO_SELF) - } - override fun forceCurrentUserAsPro(): Boolean { return getBooleanPreference(SET_FORCE_CURRENT_USER_PRO, false) } diff --git a/app/src/main/java/org/session/libsession/utilities/UsernameUtils.kt b/app/src/main/java/org/session/libsession/utilities/UsernameUtils.kt deleted file mode 100644 index beb61769f5..0000000000 --- a/app/src/main/java/org/session/libsession/utilities/UsernameUtils.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.session.libsession.utilities - -import org.session.libsession.messaging.contacts.Contact -import org.session.libsignal.utilities.AccountId - -interface UsernameUtils { - fun getCurrentUsernameWithAccountIdFallback(): String - - fun getCurrentUsername(): String? - - fun saveCurrentUserName(name: String) - - fun getContactNameWithAccountID( - accountID: String, - groupId: AccountId? = null, - contactContext: Contact.ContactContext = Contact.ContactContext.REGULAR - ): String - - fun getContactNameWithAccountID( - contact: Contact?, - accountID: String, - groupId: AccountId? = null, - contactContext: Contact.ContactContext = Contact.ContactContext.REGULAR - ): String -} \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/utilities/Util.kt b/app/src/main/java/org/session/libsession/utilities/Util.kt index 5afa6c09bd..50215e321c 100644 --- a/app/src/main/java/org/session/libsession/utilities/Util.kt +++ b/app/src/main/java/org/session/libsession/utilities/Util.kt @@ -271,7 +271,7 @@ object Util { } @JvmStatic - fun getSecret(size: Int): String? { + fun getSecret(size: Int): String { val secret = getSecretBytes(size) return Base64.encodeBytes(secret) } diff --git a/app/src/main/java/org/session/libsession/utilities/recipients/Recipient.java b/app/src/main/java/org/session/libsession/utilities/recipients/Recipient.java deleted file mode 100644 index 71c0874477..0000000000 --- a/app/src/main/java/org/session/libsession/utilities/recipients/Recipient.java +++ /dev/null @@ -1,1083 +0,0 @@ -/* - * Copyright (C) 2011 Whisper Systems - * 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.session.libsession.utilities.recipients; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.annimon.stream.function.Consumer; - -import org.greenrobot.eventbus.EventBus; -import org.session.libsession.avatars.ContactColors; -import org.session.libsession.avatars.ContactPhoto; -import org.session.libsession.avatars.GroupRecordContactPhoto; -import org.session.libsession.avatars.ProfileContactPhoto; -import org.session.libsession.avatars.SystemContactPhoto; -import org.session.libsession.avatars.TransparentContactPhoto; -import org.session.libsession.messaging.MessagingModuleConfiguration; -import org.session.libsession.messaging.contacts.Contact; -import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.FutureTaskListener; -import org.session.libsession.utilities.GroupRecord; -import org.session.libsession.utilities.GroupUtil; -import org.session.libsession.utilities.ListenableFutureTask; -import org.session.libsession.utilities.MaterialColor; -import org.session.libsession.utilities.ProfilePictureModifiedEvent; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.UsernameUtils; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.recipients.RecipientProvider.RecipientDetails; -import org.session.libsignal.utilities.Log; -import org.session.libsignal.utilities.guava.Optional; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.ExecutionException; - -public class Recipient implements RecipientModifiedListener, Cloneable { - - private static final String TAG = Recipient.class.getSimpleName(); - private static final RecipientProvider provider = new RecipientProvider(); - - private final Set listeners = Collections.newSetFromMap(new WeakHashMap()); - - private final @NonNull Address address; - private final @NonNull List participants = new LinkedList<>(); - - private final Context context; - private @Nullable String name; - private @Nullable String customLabel; - private boolean resolving; - private boolean isLocalNumber; - - private @Nullable Uri systemContactPhoto; - private @Nullable Long groupAvatarId; - private Uri contactUri; - private @Nullable Uri messageRingtone = null; - private @Nullable Uri callRingtone = null; - public long mutedUntil = 0; - public int notifyType = 0; - private boolean autoDownloadAttachments = false; - private boolean blocked = false; - private boolean approved = false; - private boolean approvedMe = false; - private DisappearingState disappearingState = null; - private VibrateState messageVibrate = VibrateState.DEFAULT; - private VibrateState callVibrate = VibrateState.DEFAULT; - private int expireMessages = 0; - private Optional defaultSubscriptionId = Optional.absent(); - private @NonNull RegisteredState registered = RegisteredState.UNKNOWN; - - private @Nullable MaterialColor color; - private @Nullable byte[] profileKey; - private @Nullable String profileName; - private @Nullable String profileAvatar; - private boolean profileSharing; - private String notificationChannel; - private boolean forceSmsSelection; - private boolean blocksCommunityMessageRequests; - - @SuppressWarnings("ConstantConditions") - public static @NonNull Recipient from(@NonNull Context context, @NonNull Address address, boolean asynchronous) { - if (address == null) throw new AssertionError(address); - return provider.getRecipient(context, address, Optional.absent(), Optional.absent(), asynchronous); - } - - @SuppressWarnings("ConstantConditions") - public static @NonNull Recipient from(@NonNull Context context, @NonNull Address address, @NonNull Optional settings, @NonNull Optional groupRecord, boolean asynchronous) { - if (address == null) throw new AssertionError(address); - return provider.getRecipient(context, address, settings, groupRecord, asynchronous); - } - - public static void applyCached(@NonNull Address address, Consumer consumer) { - Optional recipient = provider.getCached(address); - if (recipient.isPresent()) consumer.accept(recipient.get()); - } - - public static boolean removeCached(@NonNull Address address) { - return provider.removeCached(address); - } - - Recipient(@NonNull Context context, - @NonNull Address address, - @Nullable Recipient stale, - @NonNull Optional details, - @NonNull ListenableFutureTask future) - { - this.context = context.getApplicationContext(); - this.address = address; - this.color = null; - this.resolving = true; - - if (stale != null) { - this.name = stale.name; - this.contactUri = stale.contactUri; - this.systemContactPhoto = stale.systemContactPhoto; - this.groupAvatarId = stale.groupAvatarId; - this.isLocalNumber = stale.isLocalNumber; - this.color = stale.color; - this.customLabel = stale.customLabel; - this.messageRingtone = stale.messageRingtone; - this.callRingtone = stale.callRingtone; - this.mutedUntil = stale.mutedUntil; - this.blocked = stale.blocked; - this.approved = stale.approved; - this.approvedMe = stale.approvedMe; - this.messageVibrate = stale.messageVibrate; - this.callVibrate = stale.callVibrate; - this.expireMessages = stale.expireMessages; - this.defaultSubscriptionId = stale.defaultSubscriptionId; - this.registered = stale.registered; - this.notificationChannel = stale.notificationChannel; - this.profileKey = stale.profileKey; - this.profileName = stale.profileName; - this.profileAvatar = stale.profileAvatar; - this.profileSharing = stale.profileSharing; - this.forceSmsSelection = stale.forceSmsSelection; - this.notifyType = stale.notifyType; - this.disappearingState = stale.disappearingState; - this.autoDownloadAttachments = stale.autoDownloadAttachments; - - this.participants.clear(); - this.participants.addAll(stale.participants); - } - - if (details.isPresent()) { - this.name = details.get().name; - this.systemContactPhoto = details.get().systemContactPhoto; - this.groupAvatarId = details.get().groupAvatarId; - this.isLocalNumber = details.get().isLocalNumber; - this.color = details.get().color; - this.messageRingtone = details.get().messageRingtone; - this.callRingtone = details.get().callRingtone; - this.mutedUntil = details.get().mutedUntil; - this.blocked = details.get().blocked; - this.approved = details.get().approved; - this.approvedMe = details.get().approvedMe; - this.messageVibrate = details.get().messageVibrateState; - this.callVibrate = details.get().callVibrateState; - this.expireMessages = details.get().expireMessages; - this.defaultSubscriptionId = details.get().defaultSubscriptionId; - this.registered = details.get().registered; - this.notificationChannel = details.get().notificationChannel; - this.profileKey = details.get().profileKey; - this.profileName = details.get().profileName; - this.profileAvatar = details.get().profileAvatar; - this.profileSharing = details.get().profileSharing; - this.forceSmsSelection = details.get().forceSmsSelection; - this.notifyType = details.get().notifyType; - this.autoDownloadAttachments = details.get().autoDownloadAttachments; - this.blocksCommunityMessageRequests = details.get().blocksCommunityMessageRequests; - this.disappearingState = details.get().disappearingState; - - this.participants.clear(); - this.participants.addAll(details.get().participants); - } - - future.addListener(new FutureTaskListener() { - @Override - public void onSuccess(RecipientDetails result) { - if (result != null) { - synchronized (Recipient.this) { - Recipient.this.name = result.name; - Recipient.this.contactUri = result.contactUri; - Recipient.this.systemContactPhoto = result.systemContactPhoto; - Recipient.this.groupAvatarId = result.groupAvatarId; - Recipient.this.isLocalNumber = result.isLocalNumber; - Recipient.this.color = result.color; - Recipient.this.customLabel = result.customLabel; - Recipient.this.messageRingtone = result.messageRingtone; - Recipient.this.callRingtone = result.callRingtone; - Recipient.this.mutedUntil = result.mutedUntil; - Recipient.this.blocked = result.blocked; - Recipient.this.approved = result.approved; - Recipient.this.approvedMe = result.approvedMe; - Recipient.this.messageVibrate = result.messageVibrateState; - Recipient.this.callVibrate = result.callVibrateState; - Recipient.this.expireMessages = result.expireMessages; - Recipient.this.defaultSubscriptionId = result.defaultSubscriptionId; - Recipient.this.registered = result.registered; - Recipient.this.notificationChannel = result.notificationChannel; - Recipient.this.profileKey = result.profileKey; - Recipient.this.profileName = result.profileName; - Recipient.this.profileAvatar = result.profileAvatar; - Recipient.this.profileSharing = result.profileSharing; - Recipient.this.forceSmsSelection = result.forceSmsSelection; - Recipient.this.notifyType = result.notifyType; - Recipient.this.disappearingState = result.disappearingState; - Recipient.this.autoDownloadAttachments = result.autoDownloadAttachments; - Recipient.this.blocksCommunityMessageRequests = result.blocksCommunityMessageRequests; - - - Recipient.this.participants.clear(); - Recipient.this.participants.addAll(result.participants); - Recipient.this.resolving = false; - - if (!listeners.isEmpty()) { - for (Recipient recipient : participants) recipient.addListener(Recipient.this); - } - - Recipient.this.notifyAll(); - } - - notifyListeners(); - } - } - - @Override - public void onFailure(ExecutionException error) { - Log.w(TAG, error); - } - }); - } - - Recipient(@NonNull Context context, @NonNull Address address, @NonNull RecipientDetails details) { - this.context = context.getApplicationContext(); - this.address = address; - this.contactUri = details.contactUri; - this.name = details.name; - this.systemContactPhoto = details.systemContactPhoto; - this.groupAvatarId = details.groupAvatarId; - this.isLocalNumber = details.isLocalNumber; - this.color = details.color; - this.customLabel = details.customLabel; - this.messageRingtone = details.messageRingtone; - this.callRingtone = details.callRingtone; - this.mutedUntil = details.mutedUntil; - this.notifyType = details.notifyType; - this.autoDownloadAttachments = details.autoDownloadAttachments; - this.blocked = details.blocked; - this.approved = details.approved; - this.approvedMe = details.approvedMe; - this.messageVibrate = details.messageVibrateState; - this.callVibrate = details.callVibrateState; - this.expireMessages = details.expireMessages; - this.defaultSubscriptionId = details.defaultSubscriptionId; - this.registered = details.registered; - this.notificationChannel = details.notificationChannel; - this.profileKey = details.profileKey; - this.profileName = details.profileName; - this.profileAvatar = details.profileAvatar; - this.profileSharing = details.profileSharing; - this.forceSmsSelection = details.forceSmsSelection; - this.blocksCommunityMessageRequests = details.blocksCommunityMessageRequests; - - this.participants.addAll(details.participants); - this.resolving = false; - } - - public boolean isLocalNumber() { - return isLocalNumber; - } - - public synchronized @Nullable Uri getContactUri() { - return this.contactUri; - } - - public void setContactUri(@Nullable Uri contactUri) { - boolean notify = false; - - synchronized (this) { - if (!Util.equals(contactUri, this.contactUri)) { - this.contactUri = contactUri; - notify = true; - } - } - - if (notify) notifyListeners(); - } - - public synchronized @NonNull String getName() { - UsernameUtils usernameUtils = MessagingModuleConfiguration.getShared().getUsernameUtils(); - String accountID = this.address.toString(); - if (isGroupOrCommunityRecipient()) { - if (this.name == null) { - List names = new LinkedList<>(); - for (Recipient recipient : participants) { - names.add(recipient.name); - } - return Util.join(names, ", "); - } else { - return this.name; - } - } else if (isCommunityInboxRecipient()){ - String inboxID = GroupUtil.getDecodedOpenGroupInboxAccountId(accountID); - return usernameUtils.getContactNameWithAccountID(inboxID, null, Contact.ContactContext.OPEN_GROUP); - } else { - return usernameUtils.getContactNameWithAccountID(accountID, null, Contact.ContactContext.REGULAR); - } - } - - public void setName(@Nullable String name) { - boolean notify = false; - - synchronized (this) { - if (!Util.equals(this.name, name)) { - this.name = name; - notify = true; - } - } - - if (notify) notifyListeners(); - } - - public boolean getBlocksCommunityMessageRequests() { - return blocksCommunityMessageRequests; - } - - public void setBlocksCommunityMessageRequests(boolean blocksCommunityMessageRequests) { - synchronized (this) { - this.blocksCommunityMessageRequests = blocksCommunityMessageRequests; - } - - notifyListeners(); - } - - public synchronized @NonNull MaterialColor getColor() { - if (isGroupOrCommunityRecipient()) return MaterialColor.GROUP; - else if (color != null) return color; - else if (name != null) return ContactColors.generateFor(name); - else return ContactColors.UNKNOWN_COLOR; - } - - public void setColor(@NonNull MaterialColor color) { - synchronized (this) { - this.color = color; - } - - notifyListeners(); - } - - public @NonNull Address getAddress() { - return address; - } - - public synchronized @Nullable String getCustomLabel() { - return customLabel; - } - - public void setCustomLabel(@Nullable String customLabel) { - boolean notify = false; - - synchronized (this) { - if (!Util.equals(customLabel, this.customLabel)) { - this.customLabel = customLabel; - notify = true; - } - } - - if (notify) notifyListeners(); - } - - public synchronized Optional getDefaultSubscriptionId() { - return defaultSubscriptionId; - } - - public void setDefaultSubscriptionId(Optional defaultSubscriptionId) { - synchronized (this) { - this.defaultSubscriptionId = defaultSubscriptionId; - } - - notifyListeners(); - } - - public synchronized @Nullable String getProfileName() { - return profileName; - } - - public void setProfileName(@Nullable String profileName) { - synchronized (this) { - this.profileName = profileName; - } - - notifyListeners(); - } - - public synchronized @Nullable String getProfileAvatar() { - return profileAvatar; - } - - public void setProfileAvatar(@Nullable String profileAvatar) { - synchronized (this) { - this.profileAvatar = profileAvatar; - } - - notifyListeners(); - EventBus.getDefault().post(new ProfilePictureModifiedEvent(this)); - } - - public synchronized boolean isProfileSharing() { - return profileSharing; - } - - public void setProfileSharing(boolean value) { - synchronized (this) { - this.profileSharing = value; - } - - notifyListeners(); - } - - public boolean isGroupOrCommunityRecipient() { - return address.isGroupOrCommunity(); - } - - public boolean isContactRecipient() { - return address.isContact(); - } - public boolean is1on1() { return address.isContact() && !isLocalNumber; } - - public boolean isCommunityRecipient() { - return address.isCommunity(); - } - - public boolean isCommunityOutboxRecipient() { - return address.isCommunityOutbox(); - } - - public boolean isCommunityInboxRecipient() { - return address.isCommunityInbox(); - } - - public boolean isLegacyGroupRecipient() { - return address.isLegacyGroup(); - } - - public boolean isGroupRecipient() { - return address.isGroup(); - } - - public boolean isGroupV2Recipient() { - return address.isGroupV2(); - } - - - @Deprecated - public boolean isPushGroupRecipient() { - return address.isGroupOrCommunity(); - } - - public @NonNull synchronized List getParticipants() { - return new LinkedList<>(participants); - } - - public void setParticipants(@NonNull List participants) { - synchronized (this) { - this.participants.clear(); - this.participants.addAll(participants); - } - - notifyListeners(); - } - - public synchronized void addListener(RecipientModifiedListener listener) { - if (listeners.isEmpty()) { - for (Recipient recipient : participants) recipient.addListener(this); - } - listeners.add(listener); - } - - public synchronized void removeListener(RecipientModifiedListener listener) { - listeners.remove(listener); - - if (listeners.isEmpty()) { - for (Recipient recipient : participants) recipient.removeListener(this); - } - } - - public synchronized @NonNull Drawable getFallbackContactPhotoDrawable(Context context, boolean inverted) { - return (new TransparentContactPhoto()).asDrawable(context, getColor().toAvatarColor(context), inverted); - } - - public synchronized @Nullable ContactPhoto getContactPhoto() { - if (isLocalNumber) return new ProfileContactPhoto(address, String.valueOf(TextSecurePreferences.getProfileAvatarId(context))); - else if (isGroupOrCommunityRecipient() && groupAvatarId != null) return new GroupRecordContactPhoto(address, groupAvatarId); - else if (systemContactPhoto != null) return new SystemContactPhoto(address, systemContactPhoto, 0); - else if (profileAvatar != null) return new ProfileContactPhoto(address, profileAvatar); - else return null; - } - - public void setSystemContactPhoto(@Nullable Uri systemContactPhoto) { - boolean notify = false; - - synchronized (this) { - if (!Util.equals(systemContactPhoto, this.systemContactPhoto)) { - this.systemContactPhoto = systemContactPhoto; - notify = true; - } - } - - if (notify) notifyListeners(); - } - - public void setGroupAvatarId(@Nullable Long groupAvatarId) { - boolean notify = false; - - synchronized (this) { - if (!Util.equals(this.groupAvatarId, groupAvatarId)) { - this.groupAvatarId = groupAvatarId; - notify = true; - } - } - - if (notify) notifyListeners(); - } - - @Nullable - public synchronized Long getGroupAvatarId() { - return groupAvatarId; - } - - public synchronized @Nullable Uri getMessageRingtone() { - if (messageRingtone != null && messageRingtone.getScheme() != null && messageRingtone.getScheme().startsWith("file")) { - return null; - } - - return messageRingtone; - } - - public void setMessageRingtone(@Nullable Uri ringtone) { - synchronized (this) { - this.messageRingtone = ringtone; - } - - notifyListeners(); - } - - public synchronized @Nullable Uri getCallRingtone() { - if (callRingtone != null && callRingtone.getScheme() != null && callRingtone.getScheme().startsWith("file")) { - return null; - } - - return callRingtone; - } - - public void setCallRingtone(@Nullable Uri ringtone) { - synchronized (this) { - this.callRingtone = ringtone; - } - - notifyListeners(); - } - - public synchronized boolean isMuted() { - return System.currentTimeMillis() <= mutedUntil; - } - - public void setMuted(long mutedUntil) { - synchronized (this) { - this.mutedUntil = mutedUntil; - } - - notifyListeners(); - } - - public void setNotifyType(int notifyType) { - synchronized (this) { - this.notifyType = notifyType; - } - - notifyListeners(); - } - - public boolean getAutoDownloadAttachments() { - return autoDownloadAttachments; - } - - public void setAutoDownloadAttachments(boolean autoDownloadAttachments) { - synchronized (this) { - this.autoDownloadAttachments = autoDownloadAttachments; - } - - notifyListeners(); - } - - public synchronized boolean isBlocked() { - return blocked; - } - - public void setBlocked(boolean blocked) { - synchronized (this) { - this.blocked = blocked; - } - - notifyListeners(); - } - - public synchronized boolean isApproved() { - return approved; - } - - public void setApproved(boolean approved) { - synchronized (this) { - this.approved = approved; - } - - notifyListeners(); - } - - public synchronized boolean hasApprovedMe() { - return approvedMe; - } - - public void setHasApprovedMe(boolean approvedMe) { - synchronized (this) { - this.approvedMe = approvedMe; - } - - notifyListeners(); - } - - public synchronized VibrateState getMessageVibrate() { - return messageVibrate; - } - - public void setMessageVibrate(VibrateState vibrate) { - synchronized (this) { - this.messageVibrate = vibrate; - } - - notifyListeners(); - } - - public synchronized VibrateState getCallVibrate() { - return callVibrate; - } - - public void setCallVibrate(VibrateState vibrate) { - synchronized (this) { - this.callVibrate = vibrate; - } - - notifyListeners(); - } - - public synchronized int getExpireMessages() { - return expireMessages; - } - - public void setExpireMessages(int expireMessages) { - synchronized (this) { - this.expireMessages = expireMessages; - } - - notifyListeners(); - } - - public synchronized DisappearingState getDisappearingState() { - return disappearingState; - } - - public void setDisappearingState(DisappearingState disappearingState) { - synchronized (this) { - this.disappearingState = disappearingState; - } - - notifyListeners(); - } - - public synchronized RegisteredState getRegistered() { - if (isPushGroupRecipient()) return RegisteredState.REGISTERED; - - return registered; - } - - public void setRegistered(@NonNull RegisteredState value) { - boolean notify = false; - - synchronized (this) { - if (this.registered != value) { - this.registered = value; - notify = true; - } - } - - if (notify) notifyListeners(); - } - - public synchronized @Nullable String getNotificationChannel() { - return notificationChannel; - } - - public void setNotificationChannel(@Nullable String value) { - boolean notify = false; - - synchronized (this) { - if (!Util.equals(this.notificationChannel, value)) { - this.notificationChannel = value; - notify = true; - } - } - - if (notify) notifyListeners(); - } - - public boolean isForceSmsSelection() { - return forceSmsSelection; - } - - public void setForceSmsSelection(boolean value) { - synchronized (this) { - this.forceSmsSelection = value; - } - - notifyListeners(); - } - - public synchronized @Nullable byte[] getProfileKey() { - return profileKey; - } - - public void setProfileKey(@Nullable byte[] profileKey) { - synchronized (this) { - this.profileKey = profileKey; - } - - notifyListeners(); - } - - public synchronized Recipient resolve() { - while (resolving) Util.wait(this, 0); - return this; - } - - public synchronized boolean showCallMenu() { - return !isGroupOrCommunityRecipient() && hasApprovedMe() && isApproved(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Recipient recipient = (Recipient) o; - return resolving == recipient.resolving - && mutedUntil == recipient.mutedUntil - && notifyType == recipient.notifyType - && blocked == recipient.blocked - && approved == recipient.approved - && approvedMe == recipient.approvedMe - && expireMessages == recipient.expireMessages - && address.equals(recipient.address) - && Objects.equals(name, recipient.name) - && Objects.equals(customLabel, recipient.customLabel) - && Objects.equals(groupAvatarId, recipient.groupAvatarId) - && Arrays.equals(profileKey, recipient.profileKey) - && Objects.equals(profileName, recipient.profileName) - && Objects.equals(profileAvatar, recipient.profileAvatar) - && blocksCommunityMessageRequests == recipient.blocksCommunityMessageRequests; - } - - @Override - public int hashCode() { - int result = Objects.hash( - address, - name, - customLabel, - resolving, - groupAvatarId, - mutedUntil, - notifyType, - blocked, - approved, - approvedMe, - expireMessages, - profileName, - profileAvatar, - blocksCommunityMessageRequests - ); - result = 31 * result + Arrays.hashCode(profileKey); - return result; - } - - public void notifyListeners() { - Set localListeners; - - synchronized (this) { - localListeners = new HashSet<>(listeners); - } - - for (RecipientModifiedListener listener : localListeners) - listener.onModified(this); - } - - @Override - public void onModified(Recipient recipient) { - notifyListeners(); - } - - public synchronized boolean isResolving() { - return resolving; - } - - public enum VibrateState { - DEFAULT(0), ENABLED(1), DISABLED(2); - - private final int id; - - VibrateState(int id) { - this.id = id; - } - - public int getId() { - return id; - } - - public static VibrateState fromId(int id) { - return values()[id]; - } - } - - public enum DisappearingState { - LEGACY(0), UPDATED(1); - - private final int id; - - DisappearingState(int id) { - this.id = id; - } - - public int getId() { - return id; - } - - public static DisappearingState fromId(int id) { - return values()[id]; - } - } - - public enum RegisteredState { - UNKNOWN(0), REGISTERED(1), NOT_REGISTERED(2); - - private final int id; - - RegisteredState(int id) { - this.id = id; - } - - public int getId() { - return id; - } - - public static RegisteredState fromId(int id) { - return values()[id]; - } - } - - public static class RecipientSettings { - private final boolean blocked; - private final boolean approved; - private final boolean approvedMe; - private final long muteUntil; - private final int notifyType; - private final boolean autoDownloadAttachments; - private final DisappearingState disappearingState; - private final VibrateState messageVibrateState; - private final VibrateState callVibrateState; - private final Uri messageRingtone; - private final Uri callRingtone; - private final MaterialColor color; - private final int defaultSubscriptionId; - private final int expireMessages; - private final RegisteredState registered; - private final byte[] profileKey; - private final String systemDisplayName; - private final String systemContactPhoto; - private final String systemPhoneLabel; - private final String systemContactUri; - private final String signalProfileName; - private final String signalProfileAvatar; - private final boolean profileSharing; - private final String notificationChannel; - private final boolean forceSmsSelection; - private final boolean blocksCommunityMessageRequests; - - public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil, - int notifyType, - boolean autoDownloadAttachments, - @NonNull DisappearingState disappearingState, - @NonNull VibrateState messageVibrateState, - @NonNull VibrateState callVibrateState, - @Nullable Uri messageRingtone, - @Nullable Uri callRingtone, - @Nullable MaterialColor color, - int defaultSubscriptionId, - int expireMessages, - @NonNull RegisteredState registered, - @Nullable byte[] profileKey, - @Nullable String systemDisplayName, - @Nullable String systemContactPhoto, - @Nullable String systemPhoneLabel, - @Nullable String systemContactUri, - @Nullable String signalProfileName, - @Nullable String signalProfileAvatar, - boolean profileSharing, - @Nullable String notificationChannel, - boolean forceSmsSelection, - boolean blocksCommunityMessageRequests - ) - { - this.blocked = blocked; - this.approved = approved; - this.approvedMe = approvedMe; - this.muteUntil = muteUntil; - this.notifyType = notifyType; - this.autoDownloadAttachments = autoDownloadAttachments; - this.disappearingState = disappearingState; - this.messageVibrateState = messageVibrateState; - this.callVibrateState = callVibrateState; - this.messageRingtone = messageRingtone; - this.callRingtone = callRingtone; - this.color = color; - this.defaultSubscriptionId = defaultSubscriptionId; - this.expireMessages = expireMessages; - this.registered = registered; - this.profileKey = profileKey; - this.systemDisplayName = systemDisplayName; - this.systemContactPhoto = systemContactPhoto; - this.systemPhoneLabel = systemPhoneLabel; - this.systemContactUri = systemContactUri; - this.signalProfileName = signalProfileName; - this.signalProfileAvatar = signalProfileAvatar; - this.profileSharing = profileSharing; - this.notificationChannel = notificationChannel; - this.forceSmsSelection = forceSmsSelection; - this.blocksCommunityMessageRequests = blocksCommunityMessageRequests; - } - - public @Nullable MaterialColor getColor() { - return color; - } - - public boolean isBlocked() { - return blocked; - } - - public boolean isApproved() { - return approved; - } - - public boolean hasApprovedMe() { - return approvedMe; - } - - public long getMuteUntil() { - return muteUntil; - } - - public int getNotifyType() { - return notifyType; - } - - public @NonNull DisappearingState getDisappearingState() { - return disappearingState; - } - - public boolean getAutoDownloadAttachments() { - return autoDownloadAttachments; - } - - public @NonNull VibrateState getMessageVibrateState() { - return messageVibrateState; - } - - public @NonNull VibrateState getCallVibrateState() { - return callVibrateState; - } - - public @Nullable Uri getMessageRingtone() { - return messageRingtone; - } - - public @Nullable Uri getCallRingtone() { - return callRingtone; - } - - public Optional getDefaultSubscriptionId() { - return defaultSubscriptionId != -1 ? Optional.of(defaultSubscriptionId) : Optional.absent(); - } - - public int getExpireMessages() { - return expireMessages; - } - - public RegisteredState getRegistered() { - return registered; - } - - public @Nullable byte[] getProfileKey() { - return profileKey; - } - - public @Nullable String getSystemDisplayName() { - return systemDisplayName; - } - - public @Nullable String getSystemContactPhotoUri() { - return systemContactPhoto; - } - - public @Nullable String getSystemPhoneLabel() { - return systemPhoneLabel; - } - - public @Nullable String getSystemContactUri() { - return systemContactUri; - } - - public @Nullable String getProfileName() { - return signalProfileName; - } - - public @Nullable String getProfileAvatar() { - return signalProfileAvatar; - } - - public boolean isProfileSharing() { - return profileSharing; - } - - public @Nullable String getNotificationChannel() { - return notificationChannel; - } - - public boolean isForceSmsSelection() { - return forceSmsSelection; - } - - public boolean getBlocksCommunityMessageRequests() { - return blocksCommunityMessageRequests; - } - - } - - @NonNull - @Override - public Recipient clone() throws CloneNotSupportedException { - return (Recipient) super.clone(); - } -} diff --git a/app/src/main/java/org/session/libsession/utilities/recipients/Recipient.kt b/app/src/main/java/org/session/libsession/utilities/recipients/Recipient.kt new file mode 100644 index 0000000000..9b1bf14757 --- /dev/null +++ b/app/src/main/java/org/session/libsession/utilities/recipients/Recipient.kt @@ -0,0 +1,244 @@ +package org.session.libsession.utilities.recipients + +import androidx.annotation.IntDef +import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED +import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE +import network.loki.messenger.libsession_util.util.Bytes +import network.loki.messenger.libsession_util.util.ExpiryMode +import network.loki.messenger.libsession_util.util.UserPic +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.truncateIdForDisplay +import org.thoughtcrime.securesms.database.RecipientDatabase +import java.time.ZonedDateTime + +data class Recipient( + val basic: BasicRecipient, + val mutedUntil: ZonedDateTime?, + val autoDownloadAttachments: Boolean?, + @get:NotifyType + val notifyType: Int, + val acceptsCommunityMessageRequests: Boolean, + val notificationChannel: String? = null, +) { + val isLocalNumber: Boolean get() = basic.isLocalNumber + val address: Address get() = basic.address + val avatar: RemoteFile? get() = basic.avatar + + val isGroupOrCommunityRecipient: Boolean get() = basic.isGroupOrCommunityRecipient + val isCommunityRecipient: Boolean get() = basic.isCommunityRecipient + val isCommunityInboxRecipient: Boolean get() = basic.isCommunityInboxRecipient + val isCommunityOutboxRecipient: Boolean get() = basic.isCommunityOutboxRecipient + val isGroupV2Recipient: Boolean get() = basic.isGroupV2Recipient + val isLegacyGroupRecipient: Boolean get() = basic.isLegacyGroupRecipient + val isContactRecipient: Boolean get() = basic.isContactRecipient + val is1on1: Boolean get() = basic.is1on1 + val isGroupRecipient: Boolean get() = basic.isGroupRecipient + + val displayName: String get() = basic.displayName + val expiryMode: ExpiryMode get() = when (basic) { + is BasicRecipient.Self -> basic.expiryMode + is BasicRecipient.Contact -> basic.expiryMode + is BasicRecipient.Group -> basic.expiryMode + else -> ExpiryMode.NONE + } + + val approved: Boolean get() = when (basic) { + is BasicRecipient.Contact -> basic.approved + is BasicRecipient.Group -> basic.approved + else -> true + } + + val approvedMe: Boolean get() = (basic as? BasicRecipient.Contact)?.approvedMe ?: true + val blocked: Boolean get() = when (basic) { + is BasicRecipient.Generic -> basic.blocked + is BasicRecipient.Contact -> basic.blocked + else -> false + } + + val priority: Long get() = basic.priority + + val isPinned: Boolean get() = priority == PRIORITY_PINNED + + @JvmOverloads + fun isMuted(now: ZonedDateTime = ZonedDateTime.now()): Boolean { + return mutedUntil?.isAfter(now) == true + } + + val showCallMenu: Boolean + get() = !isGroupOrCommunityRecipient && approvedMe && approved + + val mutedUntilMills: Long? + get() = mutedUntil?.toInstant()?.toEpochMilli() + + companion object { + fun empty(address: Address): Recipient { + return Recipient( + basic = BasicRecipient.Generic(address), + mutedUntil = null, + autoDownloadAttachments = true, + notifyType = RecipientDatabase.NOTIFY_TYPE_ALL, + acceptsCommunityMessageRequests = false, + ) + } + } +} + +sealed interface BasicRecipient { + val address: Address + val isLocalNumber: Boolean + val displayName: String + val avatar: RemoteFile? + val priority: Long + + /** + * A recipient that is backed by the config system. + */ + sealed interface ConfigBasedRecipient : BasicRecipient + + data class Generic( + override val address: Address, + override val displayName: String = "", + override val avatar: RemoteFile? = null, + override val isLocalNumber: Boolean = false, + val blocked: Boolean = false, + override val priority: Long = PRIORITY_VISIBLE, + ) : BasicRecipient + + /** + * Yourself. + */ + data class Self( + val name: String, + override val address: Address, + override val avatar: RemoteFile.Encrypted?, + val expiryMode: ExpiryMode, + val acceptsCommunityMessageRequests: Boolean, + override val priority: Long, + ) : ConfigBasedRecipient { + override val displayName: String + get() = name + + override val isLocalNumber: Boolean + get() = true + } + + /** + * A recipient that is your **real** contact. + */ + data class Contact( + override val address: Address, + val name: String, + val nickname: String?, + override val avatar: RemoteFile.Encrypted?, + val approved: Boolean, + val approvedMe: Boolean, + val blocked: Boolean, + val expiryMode: ExpiryMode, + override val priority: Long, + ) : ConfigBasedRecipient { + override val displayName: String + get() = nickname?.takeIf { it.isNotBlank() } ?: name + + override val isLocalNumber: Boolean + get() = false + } + + /** + * A recipient that is a groupv2. + */ + data class Group( + override val address: Address, + val name: String, + override val avatar: RemoteFile.Encrypted?, + val expiryMode: ExpiryMode, + val approved: Boolean, + override val priority: Long, + ) : ConfigBasedRecipient { + override val displayName: String + get() = name + + override val isLocalNumber: Boolean + get() = false + } +} + +val BasicRecipient.isGroupOrCommunityRecipient: Boolean get() = address.isGroupOrCommunity +val BasicRecipient.isCommunityRecipient: Boolean get() = address.isCommunity +val BasicRecipient.isCommunityInboxRecipient: Boolean get() = address.isCommunityInbox +val BasicRecipient.isCommunityOutboxRecipient: Boolean get() = address.isCommunityOutbox +val BasicRecipient.isGroupV2Recipient: Boolean get() = address.isGroupV2 +val BasicRecipient.isLegacyGroupRecipient: Boolean get() = address.isLegacyGroup +val BasicRecipient.isContactRecipient: Boolean get() = address.isContact +val BasicRecipient.is1on1: Boolean get() = !isLocalNumber && address.isContact +val BasicRecipient.isGroupRecipient: Boolean get() = address.isGroup + + +/** + * Represents a remote file that can be downloaded. + */ +sealed interface RemoteFile { + data class Encrypted(val url: String, val key: Bytes) : RemoteFile + data class Community(val communityServerBaseUrl: String, val roomId: String, val fileId: String) : RemoteFile + companion object { + fun UserPic.toRecipientAvatar(): Encrypted? { + return when { + url.isBlank() -> null + else -> Encrypted( + url = url, + key = key + ) + } + } + + fun from(url: String, bytes: ByteArray?): RemoteFile? { + return if (url.isNotBlank() && bytes != null && bytes.isNotEmpty()) { + Encrypted(url, Bytes(bytes)) + } else { + null + } + } + } +} + +/** + * Represents local database data for a recipient. + */ +class RecipientSettings( + val blocked: Boolean, + val approved: Boolean, + val approvedMe: Boolean, + val muteUntil: Long, + val notifyType: Int, + val autoDownloadAttachments: Boolean?, + val expireMessages: Int, + val profileKey: ByteArray?, + val systemDisplayName: String?, + val profileName: String?, + val profileAvatar: String?, + val blocksCommunityMessagesRequests: Boolean +) { + val profilePic: UserPic? get() = + if (!profileAvatar.isNullOrBlank() && profileKey != null) { + UserPic(profileAvatar, profileKey) + } else { + null + } +} + +@Retention(AnnotationRetention.SOURCE) +@IntDef(RecipientDatabase.NOTIFY_TYPE_MENTIONS, RecipientDatabase.NOTIFY_TYPE_ALL, RecipientDatabase.NOTIFY_TYPE_NONE) +annotation class NotifyType + + +fun RemoteFile.toUserPic(): UserPic? { + return when (this) { + is RemoteFile.Encrypted -> UserPic(url, key) + is RemoteFile.Community -> null + } +} + +inline fun Recipient?.displayNameOrFallback(fallbackName: () -> String? = { null }, address: String): String { + return (this?.displayName ?: fallbackName()) + ?.takeIf { it.isNotBlank() } + ?: truncateIdForDisplay(address) +} \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/utilities/recipients/RecipientExporter.java b/app/src/main/java/org/session/libsession/utilities/recipients/RecipientExporter.java deleted file mode 100644 index f01a8174ae..0000000000 --- a/app/src/main/java/org/session/libsession/utilities/recipients/RecipientExporter.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.session.libsession.utilities.recipients; - -import android.content.Intent; -import android.provider.ContactsContract; -import android.text.TextUtils; - -import org.session.libsession.utilities.Address; - -import static android.content.Intent.ACTION_INSERT_OR_EDIT; - -public final class RecipientExporter { - - public static RecipientExporter export(Recipient recipient) { - return new RecipientExporter(recipient); - } - - private final Recipient recipient; - - private RecipientExporter(Recipient recipient) { - this.recipient = recipient; - } - - public Intent asAddContactIntent() { - Intent intent = new Intent(ACTION_INSERT_OR_EDIT); - intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); - addNameToIntent(intent, recipient.getProfileName()); - addAddressToIntent(intent, recipient.getAddress()); - return intent; - } - - private static void addNameToIntent(Intent intent, String profileName) { - if (!TextUtils.isEmpty(profileName)) { - intent.putExtra(ContactsContract.Intents.Insert.NAME, profileName); - } - } - - private static void addAddressToIntent(Intent intent, Address address) { - if (address.isContact()) { - intent.putExtra(ContactsContract.Intents.Insert.PHONE, address.toString()); - } else { - throw new RuntimeException("Cannot export Recipient with neither phone nor email"); - } - } -} diff --git a/app/src/main/java/org/session/libsession/utilities/recipients/RecipientModifiedListener.java b/app/src/main/java/org/session/libsession/utilities/recipients/RecipientModifiedListener.java deleted file mode 100644 index a537f8fdcb..0000000000 --- a/app/src/main/java/org/session/libsession/utilities/recipients/RecipientModifiedListener.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.session.libsession.utilities.recipients; - - -public interface RecipientModifiedListener { - public void onModified(Recipient recipient); -} diff --git a/app/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java b/app/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java deleted file mode 100644 index bb65805268..0000000000 --- a/app/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright (C) 2011 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.session.libsession.utilities.recipients; - -import android.content.Context; -import android.net.Uri; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import network.loki.messenger.R; -import org.session.libsession.messaging.MessagingModuleConfiguration; -import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.GroupRecord; -import org.session.libsession.utilities.ListenableFutureTask; -import org.session.libsession.utilities.MaterialColor; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.recipients.Recipient.DisappearingState; -import org.session.libsession.utilities.recipients.Recipient.RecipientSettings; -import org.session.libsession.utilities.recipients.Recipient.RegisteredState; -import org.session.libsession.utilities.recipients.Recipient.VibrateState; -import org.session.libsignal.utilities.guava.Optional; - -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; - -class RecipientProvider { - - @SuppressWarnings("unused") - private static final String TAG = RecipientProvider.class.getSimpleName(); - - private static final RecipientCache recipientCache = new RecipientCache(); - private static final ExecutorService asyncRecipientResolver = Util.newSingleThreadedLifoExecutor(); - - @NonNull Recipient getRecipient(@NonNull Context context, @NonNull Address address, @NonNull Optional settings, @NonNull Optional groupRecord, boolean asynchronous) { - Recipient cachedRecipient = recipientCache.get(address); - - if (cachedRecipient != null && (asynchronous || !cachedRecipient.isResolving()) && ((!groupRecord.isPresent() && !settings.isPresent()) || !cachedRecipient.isResolving())) { - return cachedRecipient; - } - - Optional prefetchedRecipientDetails = createPrefetchedRecipientDetails(context, address, settings, groupRecord); - - if (asynchronous) { - cachedRecipient = new Recipient(context, address, cachedRecipient, prefetchedRecipientDetails, getRecipientDetailsAsync(context, address, settings, groupRecord)); - } else { - cachedRecipient = new Recipient(context, address, getRecipientDetailsSync(context, address, settings, groupRecord, false)); - } - - recipientCache.set(address, cachedRecipient); - return cachedRecipient; - } - - @NonNull Optional getCached(@NonNull Address address) { - return Optional.fromNullable(recipientCache.get(address)); - } - - boolean removeCached(@NonNull Address address) { - return recipientCache.remove(address); - } - - private @NonNull Optional createPrefetchedRecipientDetails(@NonNull Context context, @NonNull Address address, - @NonNull Optional settings, - @NonNull Optional groupRecord) - { - if (address.isGroupOrCommunity() && settings.isPresent() && groupRecord.isPresent()) { - return Optional.of(getGroupRecipientDetails(context, address, groupRecord, settings, true)); - } else if (!address.isGroupOrCommunity() && settings.isPresent()) { - boolean isLocalNumber = address.toString().equals(TextSecurePreferences.getLocalNumber(context)); - return Optional.of(new RecipientDetails(null, null, !TextUtils.isEmpty(settings.get().getSystemDisplayName()), isLocalNumber, settings.get(), null)); - } - - return Optional.absent(); - } - - private @NonNull ListenableFutureTask getRecipientDetailsAsync(final Context context, final @NonNull Address address, final @NonNull Optional settings, final @NonNull Optional groupRecord) - { - Callable task = () -> getRecipientDetailsSync(context, address, settings, groupRecord, true); - - ListenableFutureTask future = new ListenableFutureTask<>(task); - asyncRecipientResolver.submit(future); - return future; - } - - private @NonNull RecipientDetails getRecipientDetailsSync(Context context, @NonNull Address address, Optional settings, Optional groupRecord, boolean nestedAsynchronous) { - if (address.isLegacyGroup() || address.isCommunity()) return getGroupRecipientDetails(context, address, groupRecord, settings, nestedAsynchronous); - else return getIndividualRecipientDetails(context, address, settings); - } - - private @NonNull RecipientDetails getIndividualRecipientDetails(Context context, @NonNull Address address, Optional settings) { - if (!settings.isPresent()) { - settings = Optional.fromNullable(MessagingModuleConfiguration.getShared().getStorage().getRecipientSettings(address)); - } - - boolean systemContact = settings.isPresent() && !TextUtils.isEmpty(settings.get().getSystemDisplayName()); - boolean isLocalNumber = address.toString().equals(TextSecurePreferences.getLocalNumber(context)); - return new RecipientDetails(null, null, systemContact, isLocalNumber, settings.orNull(), null); - } - - private @NonNull RecipientDetails getGroupRecipientDetails(Context context, Address groupId, Optional groupRecord, Optional settings, boolean asynchronous) { - - if (!groupRecord.isPresent()) { - groupRecord = Optional.fromNullable(MessagingModuleConfiguration.getShared().getStorage().getGroup(groupId.toGroupString())); - } - - if (!settings.isPresent()) { - - settings = Optional.fromNullable(MessagingModuleConfiguration.getShared().getStorage().getRecipientSettings(groupId)); - } - - if (groupRecord.isPresent()) { - String title = groupRecord.get().getTitle(); - List
memberAddresses = groupRecord.get().getMembers(); - List members = new LinkedList<>(); - Long avatarId = null; - - for (Address memberAddress : memberAddresses) { - members.add(getRecipient(context, memberAddress, Optional.absent(), Optional.absent(), asynchronous)); - } - - if (groupRecord.get().getAvatar() != null && groupRecord.get().getAvatar().length > 0) { - avatarId = groupRecord.get().getAvatarId(); - } - - return new RecipientDetails(title, avatarId, false, false, settings.orNull(), members); - } - - return new RecipientDetails(context.getString(R.string.groupUnknown), null, false, false, settings.orNull(), null); - } - - static class RecipientDetails { - @Nullable final String name; - @Nullable final String customLabel; - @Nullable final Uri systemContactPhoto; - @Nullable final Uri contactUri; - @Nullable final Long groupAvatarId; - @Nullable final MaterialColor color; - @Nullable final Uri messageRingtone; - @Nullable final Uri callRingtone; - final long mutedUntil; - final int notifyType; - @Nullable final DisappearingState disappearingState; - final boolean autoDownloadAttachments; - @Nullable final VibrateState messageVibrateState; - @Nullable final VibrateState callVibrateState; - final boolean blocked; - final boolean approved; - final boolean approvedMe; - final int expireMessages; - @NonNull final List participants; - @Nullable final String profileName; - final Optional defaultSubscriptionId; - @NonNull final RegisteredState registered; - @Nullable final byte[] profileKey; - @Nullable final String profileAvatar; - final boolean profileSharing; - final boolean systemContact; - final boolean isLocalNumber; - @Nullable final String notificationChannel; - final boolean forceSmsSelection; - final boolean blocksCommunityMessageRequests; - - RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId, - boolean systemContact, boolean isLocalNumber, @Nullable RecipientSettings settings, - @Nullable List participants) - { - this.groupAvatarId = groupAvatarId; - this.systemContactPhoto = settings != null ? Util.uri(settings.getSystemContactPhotoUri()) : null; - this.customLabel = settings != null ? settings.getSystemPhoneLabel() : null; - this.contactUri = settings != null ? Util.uri(settings.getSystemContactUri()) : null; - this.color = settings != null ? settings.getColor() : null; - this.messageRingtone = settings != null ? settings.getMessageRingtone() : null; - this.callRingtone = settings != null ? settings.getCallRingtone() : null; - this.mutedUntil = settings != null ? settings.getMuteUntil() : 0; - this.notifyType = settings != null ? settings.getNotifyType() : 0; - this.autoDownloadAttachments = settings != null && settings.getAutoDownloadAttachments(); - this.disappearingState = settings != null ? settings.getDisappearingState() : null; - this.messageVibrateState = settings != null ? settings.getMessageVibrateState() : null; - this.callVibrateState = settings != null ? settings.getCallVibrateState() : null; - this.blocked = settings != null && settings.isBlocked(); - this.approved = settings != null && settings.isApproved(); - this.approvedMe = settings != null && settings.hasApprovedMe(); - this.expireMessages = settings != null ? settings.getExpireMessages() : 0; - this.participants = participants == null ? new LinkedList<>() : participants; - this.profileName = settings != null ? settings.getProfileName() : null; - this.defaultSubscriptionId = settings != null ? settings.getDefaultSubscriptionId() : Optional.absent(); - this.registered = settings != null ? settings.getRegistered() : RegisteredState.UNKNOWN; - this.profileKey = settings != null ? settings.getProfileKey() : null; - this.profileAvatar = settings != null ? settings.getProfileAvatar() : null; - this.profileSharing = settings != null && settings.isProfileSharing(); - this.systemContact = systemContact; - this.isLocalNumber = isLocalNumber; - this.notificationChannel = settings != null ? settings.getNotificationChannel() : null; - this.forceSmsSelection = settings != null && settings.isForceSmsSelection(); - this.blocksCommunityMessageRequests = settings != null && settings.getBlocksCommunityMessageRequests(); - - if (name == null && settings != null) this.name = settings.getSystemDisplayName(); - else this.name = name; - } - } - - private static class RecipientCache { - - private final Map cache = new ConcurrentHashMap<>(1000); - - public Recipient get(Address address) { - return cache.get(address); - } - - public void set(Address address, Recipient recipient) { - cache.put(address, recipient); - } - - public boolean remove(Address address) { - return cache.remove(address) != null; - } - - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.kt b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.kt index 542a3341e7..b9c3b3e11b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.kt @@ -47,6 +47,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration.Companion.configure import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager +import org.session.libsession.messaging.messages.ProfileUpdateHandler import org.session.libsession.messaging.notifications.TokenFetcher import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerManager @@ -57,11 +58,9 @@ import org.session.libsession.utilities.Device import org.session.libsession.utilities.Environment import org.session.libsession.utilities.ProfilePictureUtilities.resubmitProfilePictureIfNeeded import org.session.libsession.utilities.SSKEnvironment.Companion.configure -import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences.Companion.pushSuffix import org.session.libsession.utilities.Toaster -import org.session.libsession.utilities.UsernameUtils import org.session.libsession.utilities.WindowDebouncer import org.session.libsignal.utilities.HTTP.isConnectedToNetwork import org.session.libsignal.utilities.JsonUtil @@ -71,6 +70,7 @@ import org.thoughtcrime.securesms.components.TypingStatusSender import org.thoughtcrime.securesms.configs.ConfigUploader import org.thoughtcrime.securesms.database.EmojiSearchDatabase import org.thoughtcrime.securesms.database.LokiAPIDatabase +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.EmojiSearchData import org.thoughtcrime.securesms.debugmenu.DebugActivity @@ -151,7 +151,6 @@ class ApplicationContext : Application(), DefaultLifecycleObserver, @Inject lateinit var pushRegistrationHandler: Lazy @Inject lateinit var tokenFetcher: Lazy @Inject lateinit var groupManagerV2: Lazy - @Inject lateinit var profileManager: Lazy @Inject lateinit var callMessageProcessor: Lazy private var messagingModuleConfiguration: MessagingModuleConfiguration? = null @@ -187,9 +186,9 @@ class ApplicationContext : Application(), DefaultLifecycleObserver, @Inject lateinit var webRtcCallBridge: Lazy @Inject lateinit var legacyGroupDeprecationManager: Lazy @Inject lateinit var cleanupInvitationHandler: Lazy - @Inject lateinit var usernameUtils: Lazy @Inject lateinit var pollerManager: Lazy @Inject lateinit var proStatusManager: Lazy + @Inject lateinit var recipientRepository: Lazy @Inject lateinit var backgroundPollManager: Lazy // Exists here only to start upon app starts @@ -206,6 +205,9 @@ class ApplicationContext : Application(), DefaultLifecycleObserver, @Inject lateinit var openGroupPollerManager: Lazy + @Inject + lateinit var profileUpdateHandler: Lazy + @Volatile var isAppVisible: Boolean = false @@ -287,7 +289,7 @@ class ApplicationContext : Application(), DefaultLifecycleObserver, clock = snodeClock.get(), preferences = textSecurePreferences.get(), deprecationManager = legacyGroupDeprecationManager.get(), - usernameUtils = usernameUtils.get(), + recipientRepository = recipientRepository.get(), proStatusManager = proStatusManager.get() ) @@ -302,8 +304,11 @@ class ApplicationContext : Application(), DefaultLifecycleObserver, val useTestNet = textSecurePreferences.get().getEnvironment() == Environment.TEST_NET configure(apiDB.get(), broadcaster!!, useTestNet) configure( - typingStatusRepository.get(), readReceiptManager.get(), profileManager.get(), - messageNotifier, expiringMessageManager.get() + typingIndicators = typingStatusRepository.get(), + readReceiptManager = readReceiptManager.get(), + notificationManager = messageNotifier, + messageExpirationManager = expiringMessageManager.get(), + profileUpdateHandler = profileUpdateHandler.get(), ) initializeWebRtc() initializeBlobProvider() @@ -355,7 +360,6 @@ class ApplicationContext : Application(), DefaultLifecycleObserver, pushRegistrationHandler.get() tokenFetcher.get() groupManagerV2.get() - profileManager.get() callMessageProcessor.get() configUploader.get() adminStateSync.get() @@ -375,7 +379,6 @@ class ApplicationContext : Application(), DefaultLifecycleObserver, pollerManager.get() legacyGroupDeprecationManager.get() cleanupInvitationHandler.get() - usernameUtils.get() backgroundPollManager.get() appVisibilityManager.get() groupPollerManager.get() diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.kt index a9c0d0aa03..3d4d09f7f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.kt @@ -36,6 +36,7 @@ import android.view.ViewTreeObserver import android.view.Window import android.widget.Toast import androidx.activity.viewModels +import androidx.core.content.IntentCompat import androidx.core.graphics.ColorUtils import androidx.core.graphics.drawable.toDrawable import androidx.core.util.Pair @@ -64,19 +65,16 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt import org.session.libsession.snode.SnodeAPI.nowWithOffset import org.session.libsession.utilities.Address import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY -import org.session.libsession.utilities.Util.runOnMain import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.recipients.Recipient -import org.session.libsession.utilities.recipients.RecipientModifiedListener import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.ShareActivity import org.thoughtcrime.securesms.components.MediaView import org.thoughtcrime.securesms.components.dialogs.DeleteMediaPreviewDialog -import org.thoughtcrime.securesms.conversation.v2.DimensionUnit import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader import org.thoughtcrime.securesms.database.model.MmsMessageRecord -import org.thoughtcrime.securesms.media.MediaOverviewActivity.Companion.createIntent +import org.thoughtcrime.securesms.media.MediaOverviewActivity import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel.PreviewData import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter @@ -99,7 +97,7 @@ import kotlin.math.min * Activity for displaying media attachments in-app */ @AndroidEntryPoint -class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedListener, +class MediaPreviewActivity : ScreenLockActionBarActivity(), LoaderManager.LoaderCallbacks?>, RailItemListener, MediaView.FullscreenToggleListener { private lateinit var binding: MediaPreviewActivityBinding @@ -107,7 +105,7 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis private var initialMediaType: String? = null private var initialMediaSize: Long = 0 private var initialCaption: String? = null - private var conversationRecipient: Recipient? = null + private var conversationRecipient: Address? = null private var leftIsRecent = false private val viewModel: MediaPreviewViewModel by viewModels() private var viewPagerListener: ViewPagerListener? = null @@ -120,6 +118,9 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis @Inject lateinit var dateUtils: DateUtils + @Inject + lateinit var recipientRepository: RecipientRepository + override val applyDefaultWindowInsets: Boolean get() = false @@ -253,10 +254,6 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) } - override fun onModified(recipient: Recipient) { - runOnMain { this.updateActionBar() } - } - override fun onRailItemClicked(distanceFromActive: Int) { binding.mediaPager.currentItem = binding.mediaPager.currentItem + distanceFromActive } @@ -277,7 +274,7 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis if (mediaItem.outgoing) supportActionBar?.title = getString(R.string.you) else if (mediaItem.recipient != null) supportActionBar?.title = - mediaItem.recipient.name + mediaItem.recipient.displayName else supportActionBar?.title = "" supportActionBar?.subtitle = relativeTimeSpan @@ -329,8 +326,9 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis } private fun initializeResources() { - val address = intent.getParcelableExtra
( - ADDRESS_EXTRA + conversationRecipient = IntentCompat.getParcelableExtra(intent, + ADDRESS_EXTRA, + Address::class.java ) initialMediaUri = intent.data @@ -338,16 +336,6 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0) initialCaption = intent.getStringExtra(CAPTION_EXTRA) leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false) - - conversationRecipient = if (address != null) { - Recipient.from( - this, - address, - true - ) - } else { - null - } } override fun onConfigurationChanged(newConfig: Configuration) { @@ -428,7 +416,7 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis } private fun showOverview() { - conversationRecipient?.address?.let { startActivity(createIntent(this, it)) } + conversationRecipient?.let { startActivity(MediaOverviewActivity.createIntent(this, it)) } } private fun forward() { @@ -512,13 +500,13 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis .format().toString() private fun sendMediaSavedNotificationIfNeeded() { - if (conversationRecipient == null || conversationRecipient?.isGroupOrCommunityRecipient == true) return + if (conversationRecipient == null || conversationRecipient?.isGroupOrCommunity == true) return val message = DataExtractionNotification( MediaSaved( nowWithOffset ) ) - send(message, conversationRecipient!!.address) + send(message, conversationRecipient!!) } @SuppressLint("StaticFieldLeak") @@ -542,7 +530,7 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis inflater.inflate(R.menu.media_preview, menu) val isDeprecatedLegacyGroup = conversationRecipient != null && - conversationRecipient?.isLegacyGroupRecipient == true && + conversationRecipient?.isLegacyGroup == true && deprecationManager.deprecationState.value == LegacyGroupDeprecationManager.DeprecationState.DEPRECATED if (!isMediaInDb || isDeprecatedLegacyGroup) { @@ -655,7 +643,6 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis try { val item = adapter!!.getMediaItemFor(position) - if (item.recipient != null) item.recipient.addListener(this@MediaPreviewActivity) viewModel.setActiveAlbumRailItem(this@MediaPreviewActivity, position) updateActionBar() } catch (e: Exception){ @@ -669,7 +656,6 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis try { val item = adapter!!.getMediaItemFor(position) - if (item.recipient != null) item.recipient.removeListener(this@MediaPreviewActivity) } catch (e: CursorIndexOutOfBoundsException) { throw RuntimeException("position = $position leftIsRecent = $leftIsRecent", e) } catch (e: Exception){ @@ -691,8 +677,9 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis } } - private class CursorPagerAdapter( - context: Context, private val glideRequests: RequestManager, + private inner class CursorPagerAdapter( + context: Context, + private val glideRequests: RequestManager, private val window: Window, private val cursor: Cursor, private var autoPlayPosition: Int, private val leftIsRecent: Boolean ) : MediaItemAdapter() { @@ -754,7 +741,7 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis if (mediaRecord.attachment.dataUri == null) throw AssertionError() return MediaItem( - if (address != null) Recipient.from(context, address, true) else null, + address?.let(recipientRepository::getRecipientSync), mediaRecord.attachment, mediaRecord.attachment.dataUri!!, mediaRecord.contentType, @@ -821,7 +808,8 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis fun getPreviewIntent(context: Context?, args: MediaPreviewArgs): Intent? { return getPreviewIntent( context, args.slide, - args.mmsRecord, args.thread + args.mmsRecord, + args.thread ) } @@ -829,14 +817,14 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), RecipientModifiedLis context: Context?, slide: Slide, mms: MmsMessageRecord, - threadRecipient: Recipient + threadRecipient: Address ): Intent? { var previewIntent: Intent? = null if (isContentTypeSupported(slide.contentType) && slide.uri != null) { previewIntent = Intent(context, MediaPreviewActivity::class.java) previewIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .setDataAndType(slide.uri, slide.contentType) - .putExtra(ADDRESS_EXTRA, threadRecipient.address) + .putExtra(ADDRESS_EXTRA, threadRecipient) .putExtra(OUTGOING_EXTRA, mms.isOutgoing) .putExtra(DATE_EXTRA, mms.timestamp) .putExtra(SIZE_EXTRA, slide.asAttachment().size) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt index 3a812cbc6d..92d0ae537c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt @@ -1,11 +1,11 @@ package org.thoughtcrime.securesms -import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.Address import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.mms.Slide data class MediaPreviewArgs( val slide: Slide, val mmsRecord: MmsMessageRecord, - val thread: Recipient, + val thread: Address, ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.kt index 03126f64e8..ce0aa87a76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.kt @@ -22,28 +22,24 @@ import android.content.Intent import android.net.Uri import android.os.AsyncTask import android.os.Bundle -import android.os.Parcel import android.provider.OpenableColumns import android.view.MenuItem import android.view.View import android.widget.ImageView import android.widget.TextView +import androidx.core.content.IntentCompat import com.squareup.phrase.Phrase import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import org.session.libsession.utilities.Address -import org.session.libsession.utilities.Address.Companion.fromExternal -import org.session.libsession.utilities.DistributionTypes import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsession.utilities.ViewUtil -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.components.SearchToolbar import org.thoughtcrime.securesms.components.SearchToolbar.SearchListener import org.thoughtcrime.securesms.contacts.ShareContactListFragment import org.thoughtcrime.securesms.contacts.ShareContactListFragment.OnContactSelectedListener import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 -import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.providers.BlobUtils import org.thoughtcrime.securesms.util.MediaUtil @@ -58,9 +54,7 @@ class ShareActivity : ScreenLockActionBarActivity(), OnContactSelectedListener { private val TAG = ShareActivity::class.java.simpleName companion object { - const val EXTRA_THREAD_ID = "thread_id" - const val EXTRA_ADDRESS_MARSHALLED = "address_marshalled" - const val EXTRA_DISTRIBUTION_TYPE = "distribution_type" + const val EXTRA_ADDRESS = "address" } override val applyDefaultWindowInsets: Boolean @@ -186,20 +180,8 @@ class ShareActivity : ScreenLockActionBarActivity(), OnContactSelectedListener { } private fun handleResolvedMedia(intent: Intent, animate: Boolean) { - val threadId = intent.getLongExtra(EXTRA_THREAD_ID, -1) - val distributionType = intent.getIntExtra(EXTRA_DISTRIBUTION_TYPE, -1) - var address: Address? = null - - if (intent.hasExtra(EXTRA_ADDRESS_MARSHALLED)) { - val parcel = Parcel.obtain() - val marshalled = intent.getByteArrayExtra(EXTRA_ADDRESS_MARSHALLED) - parcel.unmarshall(marshalled!!, 0, marshalled.size) - parcel.setDataPosition(0) - address = parcel.readParcelable(classLoader) - parcel.recycle() - } - - val hasResolvedDestination = threadId != -1L && address != null && distributionType != -1 + val address = IntentCompat.getParcelableExtra
(intent, EXTRA_ADDRESS, Address::class.java) + val hasResolvedDestination = address != null if (!hasResolvedDestination && animate) { ViewUtil.fadeIn(contactsFragment.requireView(), 300) @@ -208,21 +190,12 @@ class ShareActivity : ScreenLockActionBarActivity(), OnContactSelectedListener { contactsFragment.requireView().visibility = View.VISIBLE progressWheel.visibility = View.GONE } else { - createConversation(threadId, address, distributionType) + createConversation(address) } } - private fun createConversation(threadId: Long, address: Address?, distributionType: Int) { - val intent = getBaseShareIntent(ConversationActivityV2::class.java) - intent.putExtra(ConversationActivityV2.ADDRESS, address) - intent.putExtra(ConversationActivityV2.THREAD_ID, threadId) - - isPassingAlongMedia = true - startActivity(intent) - } - - private fun getBaseShareIntent(target: Class<*>): Intent { - val intent = Intent(this, target) + private fun createConversation(address: Address) { + val intent = ConversationActivityV2.createIntent(this, address) if (resolvedExtra != null) { intent.setDataAndType(resolvedExtra, mimeType) @@ -231,7 +204,8 @@ class ShareActivity : ScreenLockActionBarActivity(), OnContactSelectedListener { intent.setType("text/plain") } - return intent + isPassingAlongMedia = true + startActivity(intent) } private fun getMimeType(uri: Uri?): String? { @@ -242,10 +216,8 @@ class ShareActivity : ScreenLockActionBarActivity(), OnContactSelectedListener { return MediaUtil.getJpegCorrectedMimeTypeIfRequired(intent.type) } - override fun onContactSelected(number: String?) { - val recipient = Recipient.from(this, fromExternal(this, number), true) - val existingThread = get(this).threadDatabase().getThreadIdIfExistsFor(recipient) - createConversation(existingThread, recipient.address, DistributionTypes.DEFAULT) + override fun onContactSelected(number: String) { + createConversation(Address.fromSerialized(number)) } override fun onContactDeselected(number: String?) { /* Nothing */ } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ShortcutLauncherActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/ShortcutLauncherActivity.kt index 7c078510d8..878cebad10 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ShortcutLauncherActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ShortcutLauncherActivity.kt @@ -13,9 +13,7 @@ import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized -import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 -import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.home.HomeActivity class ShortcutLauncherActivity : AppCompatActivity() { @@ -37,15 +35,8 @@ class ShortcutLauncherActivity : AppCompatActivity() { // start the appropriate conversation activity and finish this one lifecycleScope.launch(Dispatchers.Default) { - val context = this@ShortcutLauncherActivity - val address = fromSerialized(serializedAddress) - val recipient = Recipient.from(context, address, true) - val threadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient) - - val intent = Intent(context, ConversationActivityV2::class.java) - intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) - intent.putExtra(ConversationActivityV2.THREAD_ID, threadId) + val intent = ConversationActivityV2.createIntent(this@ShortcutLauncherActivity, address = address) backStack.addNextIntent(intent) backStack.startActivities() diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index 57188d6fe1..76a0ea1dcb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -16,7 +16,6 @@ import org.session.libsession.messaging.sending_receiving.attachments.SessionSer import org.session.libsession.utilities.Address import org.session.libsession.utilities.UploadResult import org.session.libsession.utilities.Util -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.messages.SignalServiceAttachment import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceAttachmentStream @@ -104,14 +103,6 @@ class DatabaseAttachmentProvider(context: Context, helper: Provider() private val resourcePadding by lazy { context.resources.getDimensionPixelSize(R.dimen.normal_padding).toFloat() } @@ -84,7 +83,7 @@ class ProfilePictureView @JvmOverloads constructor( address = address, profileViewDataType = when { isGroupV2Recipient -> ProfileViewDataType.GroupvV2( - customGroupImage = profileAvatar + customGroupImage = (avatar as? RemoteFile.Encrypted)?.url ) isLegacyGroupRecipient -> ProfileViewDataType.LegacyGroup isCommunityRecipient -> ProfileViewDataType.Community @@ -99,8 +98,8 @@ class ProfilePictureView @JvmOverloads constructor( address: Address, profileViewDataType: ProfileViewDataType = ProfileViewDataType.OneOnOne ) { - fun getUserDisplayName(publicKey: String): String = prefs.takeIf { userPublicKey == publicKey }?.getProfileName() - ?: usernameUtils.getContactNameWithAccountID(publicKey) + fun getUserDisplayName(publicKey: String): String = recipientRepository.getRecipientDisplayNameSync( + Address.fromSerialized(publicKey)) // group avatar if (profileViewDataType is ProfileViewDataType.GroupvV2 || profileViewDataType is ProfileViewDataType.LegacyGroup) { @@ -187,16 +186,11 @@ class ProfilePictureView @JvmOverloads constructor( this.recipient!! } else { - this.recipient = Recipient.from(context, Address.fromSerialized(publicKey), false) + val address = Address.fromSerialized(publicKey) + this.recipient = recipientRepository.getRecipientSync(address) ?: Recipient.empty(address) this.recipient!! } - if (profilePicturesCache[imageView] == recipient) return - // recipient is mutable so without cloning it the line above always returns true as the changes to the underlying recipient happens on both shared instances - profilePicturesCache[imageView] = recipient.clone() - val signalProfilePicture = recipient.contactPhoto - val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject - glide.clear(imageView) val placeholder = PlaceholderAvatarPhoto( @@ -205,9 +199,11 @@ class ProfilePictureView @JvmOverloads constructor( avatarUtils.generateTextBitmap(128, publicKey, displayName) ) - if (signalProfilePicture != null && avatar != "0" && avatar != "") { + val avatar = recipient.avatar + + if (avatar != null) { val maxSizePx = context.resources.getDimensionPixelSize(R.dimen.medium_profile_picture_size) - glide.load(signalProfilePicture) + glide.load(avatar) .avatarOptions(maxSizePx) .placeholder(createUnknownRecipientDrawable()) .error(glide.load(placeholder)) @@ -216,7 +212,7 @@ class ProfilePictureView @JvmOverloads constructor( glide.load(createUnknownRecipientDrawable(publicKey)) .centerCrop() .into(imageView) - } else if (recipient.isCommunityRecipient && recipient.groupAvatarId == null) { + } else if (recipient.isCommunityRecipient && avatar == null) { glide.load(unknownOpenGroupDrawable) .centerCrop() .into(imageView) @@ -235,7 +231,6 @@ class ProfilePictureView @JvmOverloads constructor( } fun recycle() { - profilePicturesCache.clear() } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java b/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java index d4c1ea98bb..285bfb8ae4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java @@ -1,13 +1,14 @@ package org.thoughtcrime.securesms.components; import android.annotation.SuppressLint; -import android.content.Context; import org.session.libsession.messaging.messages.control.TypingIndicator; import org.session.libsession.messaging.sending_receiving.MessageSender; +import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Util; import org.session.libsession.utilities.recipients.Recipient; +import org.thoughtcrime.securesms.database.RecipientRepository; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.util.SessionMetaProtocol; @@ -27,10 +28,12 @@ public class TypingStatusSender { private final Map selfTypingTimers; private final ThreadDatabase threadDatabase; + private final RecipientRepository recipientRepository; @Inject - public TypingStatusSender(ThreadDatabase threadDatabase) { + public TypingStatusSender(ThreadDatabase threadDatabase, RecipientRepository recipientRepository) { this.threadDatabase = threadDatabase; + this.recipientRepository = recipientRepository; this.selfTypingTimers = new HashMap<>(); } @@ -80,8 +83,11 @@ private synchronized void onTypingStopped(long threadId, boolean notify) { } private void sendTyping(long threadId, boolean typingStarted) { - Recipient recipient = threadDatabase.getRecipientForThreadId(threadId); + Address address = threadDatabase.getRecipientForThreadId(threadId); + if (address == null) { return; } + Recipient recipient = recipientRepository.getRecipientSync(address); if (recipient == null) { return; } + if (!SessionMetaProtocol.shouldSendTypingIndicator(recipient)) { return; } TypingIndicator typingIndicator; if (typingStarted) { 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 3f1ca68714..3c2fe7f1cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt @@ -7,7 +7,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN -import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED import network.loki.messenger.libsession_util.ReadableGroupInfoConfig import network.loki.messenger.libsession_util.ReadableUserGroupsConfig import network.loki.messenger.libsession_util.ReadableUserProfile @@ -17,11 +16,9 @@ import network.loki.messenger.libsession_util.util.Conversation import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupInfo import network.loki.messenger.libsession_util.util.UserPic -import network.loki.messenger.libsession_util.util.afterSend import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.jobs.BackgroundGroupAddJob import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1 import org.session.libsession.snode.OwnedSwarmAuth @@ -30,11 +27,9 @@ import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.GroupUtil -import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.UserConfigType import org.session.libsession.utilities.getGroup -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.crypto.ecc.DjbECPrivateKey import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.ECKeyPair @@ -46,7 +41,6 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.groups.ClosedGroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.repository.ConversationRepository -import org.thoughtcrime.securesms.sskenvironment.ProfileManager import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -63,7 +57,6 @@ class ConfigToDatabaseSync @Inject constructor( private val storage: StorageProtocol, private val threadDatabase: ThreadDatabase, private val clock: SnodeClock, - private val profileManager: ProfileManager, private val preferences: TextSecurePreferences, private val conversationRepository: ConversationRepository, private val mmsSmsDatabase: MmsSmsDatabase, @@ -100,7 +93,7 @@ class ConfigToDatabaseSync @Inject constructor( } when (configUpdate) { - is UpdateUserInfo -> updateUser(configUpdate, updateTimestamp) + is UpdateUserInfo -> updateUser(configUpdate) is UpdateUserGroupsInfo -> updateUserGroups(configUpdate, updateTimestamp) is UpdateContacts -> updateContacts(configUpdate, updateTimestamp) is UpdateConvoVolatile -> updateConvoVolatile(configUpdate) @@ -122,48 +115,16 @@ class ConfigToDatabaseSync @Inject constructor( ) } - private fun updateUser(userProfile: UpdateUserInfo, messageTimestamp: Long?) { + private fun updateUser(userProfile: UpdateUserInfo) { val userPublicKey = storage.getUserPublicKey() ?: return - // would love to get rid of recipient and context from this - val recipient = Recipient.from(context, fromSerialized(userPublicKey), false) + val address = fromSerialized(userPublicKey) - // Update profile name - userProfile.name?.takeUnless { it.isEmpty() }?.truncate(NAME_PADDED_LENGTH)?.let { - preferences.setProfileName(it) - profileManager.setName(context, recipient, it) - } - - // Update profile picture - if (userProfile.userPic == UserPic.DEFAULT) { - storage.clearUserPic(clearConfig = false) - } else if (userProfile.userPic.key.data.isNotEmpty() && userProfile.userPic.url.isNotEmpty() - && preferences.getProfilePictureURL() != userProfile.userPic.url - ) { - storage.setUserProfilePicture(userProfile.userPic.url, userProfile.userPic.key.data) - } - - if (userProfile.ntsPriority == PRIORITY_HIDDEN) { - // hide nts thread if needed - preferences.setHasHiddenNoteToSelf(true) - } else { + if (userProfile.ntsPriority != PRIORITY_HIDDEN) { // create note to self thread if needed (?) - val address = recipient.address val ourThread = storage.getThreadId(address) ?: storage.getOrCreateThreadIdFor(address).also { storage.setThreadCreationDate(it, 0) } threadDatabase.setHasSent(ourThread, true) - storage.setPinned(ourThread, userProfile.ntsPriority > 0) - preferences.setHasHiddenNoteToSelf(false) - } - - // Set or reset the shared library to use latest expiration config - if (messageTimestamp != null) { - storage.getThreadId(recipient)?.let { theadId -> - storage.setExpirationConfiguration( - storage.getExpirationConfiguration(theadId)?.takeIf { it.updatedTimestampMs > messageTimestamp } ?: - ExpirationConfiguration(theadId, userProfile.ntsExpiry, messageTimestamp) - ) - } } } @@ -186,14 +147,8 @@ class ConfigToDatabaseSync @Inject constructor( } private fun updateGroup(groupInfoConfig: UpdateGroupInfo) { - val threadId = storage.getThreadId(fromSerialized(groupInfoConfig.id.hexString)) ?: return - val recipient = storage.getRecipientForThread(threadId) ?: return - profileManager.setName(context, recipient, groupInfoConfig.name.orEmpty()) - profileManager.setProfilePicture( - context, recipient, - profilePictureURL = groupInfoConfig.profilePic?.url, - profileKey = groupInfoConfig.profilePic?.key?.data - ) + val address = fromSerialized(groupInfoConfig.id.hexString) + val threadId = storage.getThreadId(address) ?: return // Also update the name in the user groups config configFactory.withMutableUserConfigs { configs -> @@ -300,36 +255,17 @@ class ConfigToDatabaseSync @Inject constructor( } } - for (groupInfo in userGroups.communityInfo) { - val groupBaseCommunity = groupInfo.community - if (groupBaseCommunity.fullUrl() in existingJoinUrls) { - // add it - val (threadId, _) = existingCommunities.entries.first { (_, v) -> v.joinURL == groupInfo.community.fullUrl() } - threadDatabase.setPinned(threadId, groupInfo.priority == PRIORITY_PINNED) - } - } + val existingClosedGroupThreads: Map = threadDatabase.allThreads + .asSequence() + .filter { (address, _) -> address.isGroupV2 } + .associate { (address, threadId) -> AccountId(address.toString()) to threadId } - val existingClosedGroupThreads: Map = threadDatabase.readerFor(threadDatabase.conversationList).use { reader -> - buildMap(reader.count) { - var current = reader.next - while (current != null) { - if (current.recipient?.isGroupV2Recipient == true) { - put(AccountId(current.recipient.address.toString()), current.threadId) - } - - current = reader.next - } - } - } val groupThreadsToKeep = hashMapOf() for (closedGroup in userGroups.closedGroupInfo) { - val recipient = Recipient.from(context, fromSerialized(closedGroup.groupAccountId), false) - storage.setRecipientApprovedMe(recipient, true) - storage.setRecipientApproved(recipient, !closedGroup.invited) - profileManager.setName(context, recipient, closedGroup.name) - val threadId = storage.getOrCreateThreadIdFor(recipient.address) + val address = fromSerialized(closedGroup.groupAccountId) + val threadId = storage.getOrCreateThreadIdFor(address) // If we don't already have a date and the config has a date, use it if (closedGroup.joinedAtSecs > 0L && threadDatabase.getLastUpdated(threadId) <= 0L) { @@ -341,8 +277,6 @@ class ConfigToDatabaseSync @Inject constructor( groupThreadsToKeep[AccountId(closedGroup.groupAccountId)] = threadId - storage.setPinned(threadId, closedGroup.priority == PRIORITY_PINNED) - if (closedGroup.destroyed) { handleDestroyedGroup(threadId = threadId) } @@ -362,11 +296,6 @@ class ConfigToDatabaseSync @Inject constructor( if (group.priority == PRIORITY_HIDDEN && existingThread != null) { ClosedGroupManager.silentlyRemoveGroup(context,existingThread, GroupUtil.doubleDecodeGroupId(existingGroup.encodedId), existingGroup.encodedId, localUserPublicKey, delete = true) - } else if (existingThread == null) { - Log.w(TAG, "Existing group had no thread to hide") - } else { - Log.d(TAG, "Setting existing group pinned status to ${group.priority}") - threadDatabase.setPinned(existingThread, group.priority == PRIORITY_PINNED) } } else { val members = group.members.keys.map { fromSerialized(it) } @@ -374,7 +303,6 @@ class ConfigToDatabaseSync @Inject constructor( val title = group.name val formationTimestamp = (group.joinedAtSecs * 1000L) storage.createGroup(groupId, title, admins + members, null, null, admins, formationTimestamp) - storage.setProfileSharing(fromSerialized(groupId), true) // Add the group to the user's set of public keys to poll for storage.addClosedGroupPublicKey(group.accountId) // Store the encryption key pair @@ -390,15 +318,6 @@ class ConfigToDatabaseSync @Inject constructor( // which in turn allows us to show the `groupNoMessages` control message text. //insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp) } - - if (messageTimestamp != null) { - storage.getThreadId(fromSerialized(groupId))?.let { theadId -> - storage.setExpirationConfiguration( - storage.getExpirationConfiguration(theadId)?.takeIf { it.updatedTimestampMs > messageTimestamp } - ?: ExpirationConfiguration(theadId, afterSend(group.disappearingTimer), messageTimestamp) - ) - } - } } } @@ -428,11 +347,3 @@ class ConfigToDatabaseSync @Inject constructor( } } } - -/** - * Truncate a string to a specified number of bytes - * - * This could split multi-byte characters/emojis. - */ -private fun String.truncate(sizeInBytes: Int): String = - toByteArray().takeIf { it.size > sizeInBytes }?.take(sizeInBytes)?.toByteArray()?.let(::String) ?: this diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java index 8b0dd77064..bd3c7c1cc6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java @@ -29,6 +29,9 @@ import java.util.LinkedList; import java.util.List; +import javax.inject.Inject; +import javax.inject.Singleton; + import network.loki.messenger.R; /** @@ -43,19 +46,21 @@ * @author Moxie Marlinspike */ +@Singleton public class ContactAccessor { + private final GroupDatabase groupDatabase; - private static final ContactAccessor instance = new ContactAccessor(); - - public static synchronized ContactAccessor getInstance() { - return instance; + @Inject + public ContactAccessor(GroupDatabase groupDatabase) { + this.groupDatabase = groupDatabase; } - public List getNumbersForThreadSearchFilter(Context context, String constraint) { + + public List getNumbersForThreadSearchFilter(Context context, String constraint) { LinkedList numberList = new LinkedList<>(); GroupRecord record; - try (GroupDatabase.Reader reader = DatabaseComponent.get(context).groupDatabase().getGroupsFilteredByTitle(constraint)) { + try (GroupDatabase.Reader reader = groupDatabase.getGroupsFilteredByTitle(constraint)) { while ((record = reader.getNext()) != null) { numberList.add(record.getEncodedId()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt index 229e7b6cba..898c7d85d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt @@ -9,7 +9,7 @@ import org.session.libsession.utilities.recipients.Recipient class ContactSelectionListAdapter(private val context: Context, private val multiSelect: Boolean) : RecyclerView.Adapter() { lateinit var glide: RequestManager val selectedContacts = mutableSetOf() - var items = listOf() + var items = listOf() set(value) { field = value; notifyDataSetChanged() } var contactClickListener: ContactClickListener? = null @@ -41,11 +41,10 @@ class ContactSelectionListAdapter(private val context: Context, private val mult override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { val item = items[position] if (viewHolder is UserViewHolder) { - item as ContactSelectionListItem.Contact - viewHolder.view.setOnClickListener { contactClickListener?.onContactClick(item.recipient) } - val isSelected = selectedContacts.contains(item.recipient) + viewHolder.view.setOnClickListener { contactClickListener?.onContactClick(item) } + val isSelected = selectedContacts.contains(item) viewHolder.view.bind( - item.recipient, + item, if (multiSelect) UserView.ActionIndicator.Tick else UserView.ActionIndicator.None, isSelected, showCurrentUserAsNoteToSelf = true @@ -61,11 +60,7 @@ class ContactSelectionListAdapter(private val context: Context, private val mult selectedContacts.add(recipient) contactClickListener?.onContactSelected(recipient) } - val index = items.indexOfFirst { - when (it) { - is ContactSelectionListItem.Contact -> it.recipient == recipient - } - } + val index = items.indexOf(recipient) notifyItemChanged(index) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java deleted file mode 100644 index 801bee2376..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * 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.contacts; - -import android.content.Context; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.database.MergeCursor; -import android.provider.ContactsContract; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.loader.content.CursorLoader; - -import org.session.libsession.utilities.GroupRecord; -import org.thoughtcrime.securesms.database.GroupDatabase; -import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.database.model.ThreadRecord; -import org.thoughtcrime.securesms.dependencies.DatabaseComponent; - -import java.util.ArrayList; -import java.util.List; - -import network.loki.messenger.R; - -/** - * CursorLoader that initializes a ContactsDatabase instance - * - * @author Jake McGinty - */ -public class ContactsCursorLoader extends CursorLoader { - private static final String TAG = ContactsCursorLoader.class.getSimpleName(); - - static final int NORMAL_TYPE = 0; - static final int PUSH_TYPE = 1; - static final int NEW_TYPE = 2; - static final int RECENT_TYPE = 3; - static final int DIVIDER_TYPE = 4; - - static final String CONTACT_TYPE_COLUMN = "contact_type"; - static final String LABEL_COLUMN = "label"; - static final String NUMBER_TYPE_COLUMN = "number_type"; - static final String NUMBER_COLUMN = "number"; - static final String NAME_COLUMN = "name"; - - public static final class DisplayMode { - public static final int FLAG_PUSH = 1; - public static final int FLAG_SMS = 1 << 1; - public static final int FLAG_GROUPS = 1 << 2; - public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_GROUPS; - } - - private static final String[] CONTACT_PROJECTION = new String[]{NAME_COLUMN, - NUMBER_COLUMN, - NUMBER_TYPE_COLUMN, - LABEL_COLUMN, - CONTACT_TYPE_COLUMN}; - - private static final int RECENT_CONVERSATION_MAX = 25; - - private final String filter; - private final int mode; - private final boolean recents; - - public ContactsCursorLoader(@NonNull Context context, int mode, String filter, boolean recents) - { - super(context); - - this.filter = filter; - this.mode = mode; - this.recents = recents; - } - - @Override - public Cursor loadInBackground() { - List cursorList = TextUtils.isEmpty(filter) ? getUnfilteredResults() - : getFilteredResults(); - if (cursorList.size() > 0) { - return new MergeCursor(cursorList.toArray(new Cursor[0])); - } - return null; - } - - private List getUnfilteredResults() { - ArrayList cursorList = new ArrayList<>(); - - if (recents) { - Cursor recentConversations = getRecentConversationsCursor(); - if (recentConversations.getCount() > 0) { - cursorList.add(getRecentsHeaderCursor()); - cursorList.add(recentConversations); - cursorList.add(getContactsHeaderCursor()); - } - } - cursorList.addAll(getContactsCursors()); - return cursorList; - } - - private List getFilteredResults() { - ArrayList cursorList = new ArrayList<>(); - - if (groupsEnabled(mode)) { - Cursor groups = getGroupsCursor(); - if (groups.getCount() > 0) { - List contacts = getContactsCursors(); - if (!isCursorListEmpty(contacts)) { - cursorList.add(getContactsHeaderCursor()); - cursorList.addAll(contacts); - cursorList.add(getGroupsHeaderCursor()); - } - cursorList.add(groups); - } else { - cursorList.addAll(getContactsCursors()); - } - } else { - cursorList.addAll(getContactsCursors()); - } - - return cursorList; - } - - private Cursor getRecentsHeaderCursor() { - MatrixCursor recentsHeader = new MatrixCursor(CONTACT_PROJECTION); - /* - recentsHeader.addRow(new Object[]{ getContext().getString(R.string.ContactsCursorLoader_recent_chats), - "", - ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, - "", - ContactsDatabase.DIVIDER_TYPE }); - */ - return recentsHeader; - } - - private Cursor getContactsHeaderCursor() { - MatrixCursor contactsHeader = new MatrixCursor(CONTACT_PROJECTION, 1); - /* - contactsHeader.addRow(new Object[] { getContext().getString(R.string.ContactsCursorLoader_contacts), - "", - ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, - "", - ContactsDatabase.DIVIDER_TYPE }); - */ - return contactsHeader; - } - - private Cursor getGroupsHeaderCursor() { - MatrixCursor groupHeader = new MatrixCursor(CONTACT_PROJECTION, 1); - groupHeader.addRow(new Object[]{ getContext().getString(R.string.conversationsGroups), - "", - ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, - "", - DIVIDER_TYPE }); - return groupHeader; - } - - - private Cursor getRecentConversationsCursor() { - ThreadDatabase threadDatabase = DatabaseComponent.get(getContext()).threadDatabase(); - - MatrixCursor recentConversations = new MatrixCursor(CONTACT_PROJECTION, RECENT_CONVERSATION_MAX); - try (Cursor rawConversations = threadDatabase.getRecentConversationList(RECENT_CONVERSATION_MAX)) { - ThreadDatabase.Reader reader = threadDatabase.readerFor(rawConversations); - ThreadRecord threadRecord; - while ((threadRecord = reader.getNext()) != null) { - recentConversations.addRow(new Object[] { threadRecord.getRecipient().getName(), - threadRecord.getRecipient().getAddress().toString(), - ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, - "", - RECENT_TYPE }); - } - } - return recentConversations; - } - - private List getContactsCursors() { - return new ArrayList<>(2); - /* - if (!Permissions.hasAny(getContext(), Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) { - return cursorList; - } - - if (pushEnabled(mode)) { - cursorList.add(contactsDatabase.queryTextSecureContacts(filter)); - } - - if (pushEnabled(mode) && smsEnabled(mode)) { - cursorList.add(contactsDatabase.querySystemContacts(filter)); - } else if (smsEnabled(mode)) { - cursorList.add(filterNonPushContacts(contactsDatabase.querySystemContacts(filter))); - } - return cursorList; - */ - } - - private Cursor getGroupsCursor() { - MatrixCursor groupContacts = new MatrixCursor(CONTACT_PROJECTION); - try (GroupDatabase.Reader reader = DatabaseComponent.get(getContext()).groupDatabase().getGroupsFilteredByTitle(filter)) { - GroupRecord groupRecord; - while ((groupRecord = reader.getNext()) != null) { - groupContacts.addRow(new Object[] { groupRecord.getTitle(), - groupRecord.getEncodedId(), - ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM, - "", - NORMAL_TYPE }); - } - } - return groupContacts; - } - - private static boolean isCursorListEmpty(List list) { - int sum = 0; - for (Cursor cursor : list) { - sum += cursor.getCount(); - } - return sum == 0; - } - - private static boolean pushEnabled(int mode) { - return (mode & DisplayMode.FLAG_PUSH) > 0; - } - - private static boolean smsEnabled(int mode) { - return (mode & DisplayMode.FLAG_SMS) > 0; - } - - private static boolean groupsEnabled(int mode) { - return (mode & DisplayMode.FLAG_GROUPS) > 0; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ShareContactListFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ShareContactListFragment.kt index 1cccf0bb25..f16dcab10b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ShareContactListFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ShareContactListFragment.kt @@ -14,13 +14,15 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.bumptech.glide.Glide import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.databinding.ShareContactListFragmentBinding +import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.ThreadDatabase import javax.inject.Inject @AndroidEntryPoint -class ShareContactListFragment : Fragment(), LoaderManager.LoaderCallbacks>, ContactClickListener { +class ShareContactListFragment : Fragment(), LoaderManager.LoaderCallbacks>, ContactClickListener { private lateinit var binding: ShareContactListFragmentBinding private var cursorFilter: String? = null var onContactSelectedListener: OnContactSelectedListener? = null @@ -28,6 +30,12 @@ class ShareContactListFragment : Fragment(), LoaderManager.LoaderCallbacks> { + override fun onCreateLoader(id: Int, args: Bundle?): Loader> { return ShareContactListLoader( context = requireActivity(), - mode = ContactsCursorLoader.DisplayMode.FLAG_ALL, filter = cursorFilter, - deprecationManager = deprecationManager + deprecationManager = deprecationManager, + threadDatabase = threadDatabase, + storage = storage ) } - override fun onLoadFinished(loader: Loader>, items: List) { + override fun onLoadFinished(loader: Loader>, items: List) { update(items) } - override fun onLoaderReset(loader: Loader>) { + override fun onLoaderReset(loader: Loader>) { update(listOf()) } - private fun update(items: List) { + private fun update(items: List) { if (activity?.isDestroyed == true) { Log.e(ShareContactListFragment::class.java.name, "Received a loader callback after the fragment was detached from the activity.", diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ShareContactListLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ShareContactListLoader.kt index d3374b37bf..5f359b8733 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ShareContactListLoader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ShareContactListLoader.kt @@ -1,50 +1,46 @@ package org.thoughtcrime.securesms.contacts import android.content.Context -import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.util.AsyncLoader -import org.thoughtcrime.securesms.util.ContactUtilities -import org.thoughtcrime.securesms.util.LastMessageSentTimestamp -sealed class ContactSelectionListItem { - class Contact(val recipient: Recipient) : ContactSelectionListItem() -} class ShareContactListLoader( context: Context, - val mode: Int, val filter: String?, private val deprecationManager: LegacyGroupDeprecationManager, -) : AsyncLoader>(context) { + private val threadDatabase: ThreadDatabase, + private val storage: StorageProtocol, +) : AsyncLoader>(context) { - override fun loadInBackground(): List { - val contacts = ContactUtilities.getAllContacts(context).asSequence() - .filter { - if(it.first.isLegacyGroupRecipient && deprecationManager.isDeprecated) return@filter false // ignore legacy group when deprecated - if(it.first.isCommunityRecipient) { // ignore communities without write access - val storage = MessagingModuleConfiguration.shared.storage - val threadId = storage.getThreadId(it.first) ?: return@filter false + override fun loadInBackground(): List { + val threads = threadDatabase.approvedConversationList + .asSequence() + .filter { thread -> + val recipient = thread.recipient + if(recipient.isLegacyGroupRecipient && deprecationManager.isDeprecated) return@filter false // ignore legacy group when deprecated + if(recipient.isCommunityRecipient) { // ignore communities without write access + val threadId = storage.getThreadId(recipient.address) ?: return@filter false val openGroup = storage.getOpenGroup(threadId) ?: return@filter false return@filter openGroup.canWrite } if (filter.isNullOrEmpty()) return@filter true - it.first.name.contains(filter.trim(), true) || it.first.address.toString().contains(filter.trim(), true) - }.sortedWith( - compareBy> { !it.first.isLocalNumber } // NTS come first - .thenByDescending { it.second } // then order by last message time - ) - .map { it.first }.toList() + recipient.displayName.contains(filter.trim(), true) || recipient.address.toString().contains(filter.trim(), true) + } + .toMutableList() - return getItems(contacts) + threads.sortWith(COMPARATOR) + + return threads.map { it.recipient } } - private fun getItems(contacts: List): List { - val items = contacts.map { - ContactSelectionListItem.Contact(it) - } - if (items.isEmpty()) return listOf() - return items + companion object { + private val COMPARATOR = compareByDescending { it.recipient.isLocalNumber } // NTS come first + .thenByDescending { it.lastMessage?.timestamp ?: 0L } // then order by last message time + .thenBy { it.recipient.displayName } } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt index 0b60606a09..c8328ecc31 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt @@ -5,21 +5,15 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout -import com.bumptech.glide.RequestManager import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.ViewUserBinding -import org.session.libsession.utilities.UsernameUtils import org.session.libsession.utilities.recipients.Recipient -import javax.inject.Inject @AndroidEntryPoint class UserView : LinearLayout { private lateinit var binding: ViewUserBinding - @Inject - lateinit var usernameUtils: UsernameUtils - enum class ActionIndicator { None, Menu, @@ -52,18 +46,17 @@ class UserView : LinearLayout { fun bind(user: Recipient, actionIndicator: ActionIndicator, isSelected: Boolean = false, showCurrentUserAsNoteToSelf: Boolean = false) { val isLocalUser = user.isLocalNumber - fun getUserDisplayName(publicKey: String): String { + fun getUserDisplayName(): String { return when { isLocalUser && showCurrentUserAsNoteToSelf -> context.getString(R.string.noteToSelf) isLocalUser && !showCurrentUserAsNoteToSelf -> context.getString(R.string.you) - else -> usernameUtils.getContactNameWithAccountID(publicKey) + else -> user.displayName } } - val address = user.address.toString() binding.profilePictureView.update(user) binding.actionIndicatorImageView.setImageResource(R.drawable.ic_radio_unselected) - binding.nameTextView.text = if (user.isGroupOrCommunityRecipient) user.name else getUserDisplayName(address) + binding.nameTextView.text = if (user.isGroupOrCommunityRecipient) user.displayName else getUserDisplayName() when (actionIndicator) { ActionIndicator.None -> { binding.actionIndicatorImageView.visibility = View.GONE diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt index ec938d39e6..e4fedc8d57 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt @@ -1,18 +1,12 @@ package org.thoughtcrime.securesms.conversation.disappearingmessages import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import network.loki.messenger.R 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.control.ExpirationTimerUpdate 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.ExpirationUtil @@ -32,22 +26,21 @@ class DisappearingMessages @Inject constructor( private val textSecurePreferences: TextSecurePreferences, private val messageExpirationManager: MessageExpirationManagerProtocol, private val storage: StorageProtocol, + private val groupManagerV2: GroupManagerV2, private val clock: SnodeClock, - private val groupManagerV2: GroupManagerV2 ) { - fun set(threadId: Long, address: Address, mode: ExpiryMode, isGroup: Boolean) { - val expiryChangeTimestampMs = clock.currentTimeMills() - storage.setExpirationConfiguration(ExpirationConfiguration(threadId, mode, expiryChangeTimestampMs)) + fun set(address: Address, mode: ExpiryMode, isGroup: Boolean) { + storage.setExpirationConfiguration(address, mode) if (address.isGroupV2) { - groupManagerV2.setExpirationTimer(AccountId(address.toString()), mode, expiryChangeTimestampMs) + groupManagerV2.setExpirationTimer(AccountId(address.toString()), mode) } else { val message = ExpirationTimerUpdate(isGroup = isGroup).apply { expiryMode = mode sender = textSecurePreferences.getLocalNumber() isSenderSelf = true recipient = address.toString() - sentTimestamp = expiryChangeTimestampMs + sentTimestamp = clock.currentTimeMills() } messageExpirationManager.insertExpirationTimerMessage(message) @@ -69,7 +62,7 @@ class DisappearingMessages @Inject constructor( text = if (message.expiresIn == 0L) R.string.confirm else R.string.set, contentDescriptionRes = if (message.expiresIn == 0L) R.string.AccessibilityId_confirm else R.string.AccessibilityId_setButton ) { - set(message.threadId, message.recipient.address, message.expiryMode, message.recipient.isGroupRecipient) + set(message.recipient.address, message.expiryMode, message.recipient.address.isGroup) } cancelButton() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt index 3c86834ebe..67d3a773a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt @@ -1,26 +1,28 @@ package org.thoughtcrime.securesms.conversation.disappearingmessages import androidx.compose.runtime.Composable +import androidx.core.content.IntentCompat import androidx.hilt.navigation.compose.hiltViewModel import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.BuildConfig import org.session.libsession.messaging.messages.ExpirationConfiguration +import org.session.libsession.utilities.Address import org.thoughtcrime.securesms.FullComposeScreenLockActivity import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.DisappearingMessagesScreen @AndroidEntryPoint class DisappearingMessagesActivity: FullComposeScreenLockActivity() { - private val threadId: Long by lazy { - intent.getLongExtra(THREAD_ID, -1) - } - @Composable override fun ComposeContent() { val viewModel: DisappearingMessagesViewModel = hiltViewModel { factory -> factory.create( - threadId = threadId, + address = requireNotNull( + IntentCompat.getParcelableExtra(intent, ARG_ADDRESS, Address::class.java) + ) { + "DisappearingMessagesActivity requires an Address to be passed in via the intent." + }, isNewConfigEnabled = ExpirationConfiguration.isNewConfigEnabled, showDebugOptions = BuildConfig.DEBUG ) @@ -33,6 +35,6 @@ class DisappearingMessagesActivity: FullComposeScreenLockActivity() { } companion object { - const val THREAD_ID = "thread_id" + const val ARG_ADDRESS = "address" } } 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 3ebd1a938f..9ecc6dc9b9 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 @@ -18,26 +18,27 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.libsession_util.util.ExpiryMode +import org.session.libsession.utilities.Address import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.UiState import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.toUiState import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsNavigator import org.thoughtcrime.securesms.database.GroupDatabase +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.Storage -import org.thoughtcrime.securesms.database.ThreadDatabase @HiltViewModel(assistedFactory = DisappearingMessagesViewModel.Factory::class) class DisappearingMessagesViewModel @AssistedInject constructor( - @Assisted("threadId") private val threadId: Long, + @Assisted private val address: Address, @Assisted("isNewConfigEnabled") private val isNewConfigEnabled: Boolean, @Assisted("showDebugOptions") private val showDebugOptions: Boolean, - @ApplicationContext private val context: Context, + @param:ApplicationContext private val context: Context, private val textSecurePreferences: TextSecurePreferences, private val disappearingMessages: DisappearingMessages, - private val threadDb: ThreadDatabase, private val groupDb: GroupDatabase, private val storage: Storage, private val navigator: ConversationSettingsNavigator, + private val recipientRepository: RecipientRepository, ) : ViewModel() { private val _state = MutableStateFlow( @@ -54,27 +55,27 @@ class DisappearingMessagesViewModel @AssistedInject constructor( init { viewModelScope.launch { - val expiryMode = storage.getExpirationConfiguration(threadId)?.expiryMode ?: ExpiryMode.NONE - val recipient = threadDb.getRecipientForThreadId(threadId)?: return@launch + val expiryMode = recipientRepository.getRecipientOrEmpty(address).expiryMode val isAdmin = when { - recipient.isGroupV2Recipient -> { + address.isGroupV2 -> { // Handle the new closed group functionality - storage.getMembers(recipient.address.toString()).any { it.accountId() == textSecurePreferences.getLocalNumber() && it.admin } + storage.getMembers(address.toString()).any { it.accountId() == textSecurePreferences.getLocalNumber() && it.admin } } - recipient.isLegacyGroupRecipient -> { - val groupRecord = groupDb.getGroup(recipient.address.toGroupString()).orNull() + + address.isLegacyGroup -> { + val groupRecord = groupDb.getGroup(address.toGroupString()).orNull() // Handle as legacy group groupRecord?.admins?.any{ it.toString() == textSecurePreferences.getLocalNumber() } == true } - else -> !recipient.isGroupOrCommunityRecipient + else -> !address.isGroupOrCommunity } _state.update { it.copy( - address = recipient.address, - isGroup = recipient.isGroupRecipient, - isNoteToSelf = recipient.address.toString() == textSecurePreferences.getLocalNumber(), + address = address, + isGroup = address.isGroup, + isNoteToSelf = address.toString() == textSecurePreferences.getLocalNumber(), isSelfAdmin = isAdmin, expiryMode = expiryMode, persistedMode = expiryMode @@ -96,7 +97,7 @@ class DisappearingMessagesViewModel @AssistedInject constructor( return@launch } - disappearingMessages.set(threadId, address, mode, state.isGroup) + disappearingMessages.set(address, mode, state.isGroup) navigator.navigateUp() } @@ -104,7 +105,7 @@ class DisappearingMessagesViewModel @AssistedInject constructor( @AssistedFactory interface Factory { fun create( - @Assisted("threadId") threadId: Long, + address: Address, @Assisted("isNewConfigEnabled") isNewConfigEnabled: Boolean, @Assisted("showDebugOptions") showDebugOptions: Boolean ): DisappearingMessagesViewModel diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt index c6f3a5f809..7bd927e8d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.conversation.start import android.app.Dialog -import android.content.Intent import android.content.res.Resources import android.os.Build import android.os.Bundle @@ -100,9 +99,7 @@ class StartConversationFragment : BottomSheetDialogFragment(), StartConversation } override fun onContactSelected(address: String) { - val intent = Intent(requireContext(), ConversationActivityV2::class.java) - intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(address)) - requireContext().startActivity(intent) + startActivity(ConversationActivityV2.createIntent(requireContext(), Address.fromSerialized(address))) requireActivity().overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageFragment.kt index 8c383fe1a9..b6fc190915 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageFragment.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.conversation.start.newmessage -import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -12,10 +11,8 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch import org.session.libsession.utilities.Address -import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 -import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.openUrl import org.thoughtcrime.securesms.ui.createThemedComposeView @@ -50,11 +47,9 @@ class NewMessageFragment : Fragment() { } private fun createPrivateChat(hexEncodedPublicKey: String) { - val recipient = Recipient.from(requireContext(), Address.fromSerialized(hexEncodedPublicKey), false) - Intent(requireContext(), ConversationActivityV2::class.java).apply { - putExtra(ConversationActivityV2.ADDRESS, recipient.address) + val address = Address.fromSerialized(hexEncodedPublicKey) + ConversationActivityV2.createIntent(requireContext(), address).apply { setDataAndType(requireActivity().intent.data, requireActivity().intent.type) - putExtra(ConversationActivityV2.THREAD_ID, DatabaseComponent.get(requireContext()).threadDatabase().getThreadIdIfExistsFor(recipient)) }.let(requireContext()::startActivity) delegate.onDialogClosePressed() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHandler.kt index d1bcf6b542..8dff01f56b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHandler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHandler.kt @@ -17,6 +17,7 @@ import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.util.flatten import org.thoughtcrime.securesms.util.timedBuffer @@ -30,6 +31,7 @@ import org.thoughtcrime.securesms.util.timedBuffer class AttachmentDownloadHandler( private val storage: StorageProtocol, private val messageDataProvider: MessageDataProvider, + private val recipientRepository: RecipientRepository, jobQueue: JobQueue = JobQueue.shared, private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default) + SupervisorJob(), ) { @@ -96,7 +98,11 @@ class AttachmentDownloadHandler( val threadID = storage.getThreadIdForMms(attachment.mmsId) return AttachmentDownloadJob.eligibleForDownload( - threadID, storage, messageDataProvider, attachment.mmsId, + threadID = threadID, + storage = storage, + recipientRepository = recipientRepository, + messageDataProvider = messageDataProvider, + mmsId = attachment.mmsId, ) } 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 8e40df6853..9e513473a2 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 @@ -19,11 +19,11 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.provider.MediaStore +import android.provider.Settings import android.text.Spannable import android.text.SpannableStringBuilder import android.text.TextUtils import android.text.style.ImageSpan -import android.util.Pair import android.util.TypedValue import android.view.ActionMode import android.view.MotionEvent @@ -59,19 +59,23 @@ import com.annimon.stream.Stream import com.bumptech.glide.Glide import com.squareup.phrase.Phrase import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.lifecycle.withCreationCallback import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R @@ -86,6 +90,7 @@ import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.visible.Reaction import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.attachments.Attachment @@ -94,7 +99,6 @@ import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized -import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.MediaTypes import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY @@ -106,10 +110,7 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOT import org.session.libsession.utilities.concurrent.SimpleTask import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.recipients.Recipient -import org.session.libsession.utilities.recipients.RecipientModifiedListener import org.session.libsignal.crypto.MnemonicCodec -import org.session.libsignal.utilities.AccountId -import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.ListenableFuture import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.hexEncodedPrivateKey @@ -160,7 +161,6 @@ import org.thoughtcrime.securesms.database.LokiThreadDatabase import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.MmsSmsDatabase import org.thoughtcrime.securesms.database.ReactionDatabase -import org.thoughtcrime.securesms.database.SessionContactDatabase import org.thoughtcrime.securesms.database.SmsDatabase import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.GroupThreadStatus @@ -173,7 +173,7 @@ import org.thoughtcrime.securesms.giph.ui.GiphyActivity import org.thoughtcrime.securesms.groups.GroupMembersActivity import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.home.UserDetailsBottomSheet -import org.thoughtcrime.securesms.home.search.getSearchName +import org.thoughtcrime.securesms.home.search.searchName import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel @@ -216,7 +216,6 @@ import org.thoughtcrime.securesms.webrtc.WebRtcCallActivity.Companion.ACTION_STA import org.thoughtcrime.securesms.webrtc.WebRtcCallBridge.Companion.EXTRA_RECIPIENT_ADDRESS import java.io.File import java.util.LinkedList -import java.util.Locale import java.util.concurrent.ExecutionException import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicLong @@ -236,7 +235,7 @@ private const val TAG = "ConversationActivityV2" @AndroidEntryPoint class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher, - ConversationActionModeCallbackDelegate, VisibleMessageViewDelegate, RecipientModifiedListener, + ConversationActionModeCallbackDelegate, VisibleMessageViewDelegate, SearchBottomBar.EventListener, LoaderManager.LoaderCallbacks, OnReactionSelectedListener, ReactWithAnyEmojiDialogFragment.Callback, ReactionsDialogFragment.Callback, UserDetailsBottomSheet.UserDetailsBottomSheetCallback { @@ -247,15 +246,12 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, @Inject lateinit var threadDb: ThreadDatabase @Inject lateinit var mmsSmsDb: MmsSmsDatabase @Inject lateinit var lokiThreadDb: LokiThreadDatabase - @Inject lateinit var sessionContactDb: SessionContactDatabase @Inject lateinit var groupDb: GroupDatabase @Inject lateinit var smsDb: SmsDatabase @Inject lateinit var mmsDb: MmsDatabase @Inject lateinit var lokiMessageDb: LokiMessageDatabase @Inject lateinit var storage: StorageProtocol @Inject lateinit var reactionDb: ReactionDatabase - @Inject lateinit var viewModelFactory: ConversationViewModel.AssistedFactory - @Inject lateinit var mentionViewModelFactory: MentionViewModel.AssistedFactory @Inject lateinit var dateUtils: DateUtils @Inject lateinit var configFactory: ConfigFactory @Inject lateinit var groupManagerV2: GroupManagerV2 @@ -283,33 +279,18 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, .get(LinkPreviewViewModel::class.java) } - private val threadId: Long by lazy { - var threadId = intent.getLongExtra(THREAD_ID, -1L) - if (threadId == -1L) { - intent.getParcelableExtra
(ADDRESS)?.let { it -> - threadId = threadDb.getThreadIdIfExistsFor(it.toString()) - if (threadId == -1L) { - val accountId = AccountId(it.toString()) - val openGroup = lokiThreadDb.getOpenGroupChat(intent.getLongExtra(FROM_GROUP_THREAD_ID, -1)) - val address = if (accountId.prefix == IdPrefix.BLINDED && openGroup != null) { - storage.getOrCreateBlindedIdMapping(accountId.hexString, openGroup.server, openGroup.publicKey).accountId?.let { - fromSerialized(it) - } ?: GroupUtil.getEncodedOpenGroupInboxID(openGroup, accountId) - } else { - it - } - val recipient = Recipient.from(this, address, false) - threadId = storage.getOrCreateThreadIdFor(recipient.address) - } - } ?: finish() + private val address: Address by lazy { + requireNotNull(IntentCompat.getParcelableExtra
(intent, ADDRESS, Address::class.java)) { + "Address must be provided in the intent extras" } - - threadId } - private val viewModel: ConversationViewModel by viewModels { - viewModelFactory.create(threadId, storage.getUserED25519KeyPair()) - } + private val viewModel: ConversationViewModel by viewModels(extrasProducer = { + defaultViewModelCreationExtras.withCreationCallback { + it.create(address) + } + }) + private var actionMode: ActionMode? = null private var unreadCount = Int.MAX_VALUE // Attachments @@ -321,9 +302,12 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, private var isLockViewExpanded = false private var isShowingAttachmentOptions = false // Mentions - private val mentionViewModel: MentionViewModel by viewModels { - mentionViewModelFactory.create(threadId) - } + private val mentionViewModel: MentionViewModel by viewModels(extrasProducer = { + defaultViewModelCreationExtras.withCreationCallback { + it.create(address) + } + }) + private val mentionCandidateAdapter = MentionCandidateAdapter { mentionViewModel.onCandidateSelected(it.member.publicKey) @@ -347,6 +331,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, private var conversationLoadAnimationJob: Job? = null + private val layoutManager: LinearLayoutManager? get() { return binding.conversationRecyclerView.layoutManager as LinearLayoutManager? } @@ -363,13 +348,12 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, MnemonicCodec(loadFileContents).encode(hexEncodedSeed, MnemonicCodec.Language.Configuration.english) } - private var firstCursorLoad = false + private val firstCursorLoad = MutableStateFlow(false) private val adapter by lazy { val adapter = ConversationAdapter( this, null, - viewModel.recipient, storage.getLastSeen(viewModel.threadId), false, onItemPress = { message, position, view, event -> @@ -380,7 +364,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, }, onItemLongPress = { message, position, view -> // long pressing message for blocked users should show unblock dialog - if(viewModel.recipient?.isBlocked == true) unblock() + if (viewModel.recipient?.blocked == true) unblock() else { if (!viewModel.isMessageRequestThread) { showConversationReaction(message, view) @@ -397,7 +381,6 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, downloadPendingAttachment = viewModel::downloadPendingAttachment, retryFailedAttachments = viewModel::retryFailedAttachments, glide = glide, - lifecycleCoroutineScope = lifecycleScope ) adapter.visibleMessageViewDelegate = this @@ -497,17 +480,40 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, // region Settings companion object { // Extras - const val THREAD_ID = "thread_id" - const val ADDRESS = "address" - const val FROM_GROUP_THREAD_ID = "from_group_thread_id" - const val SCROLL_MESSAGE_ID = "scroll_message_id" - const val SCROLL_MESSAGE_AUTHOR = "scroll_message_author" + private const val ADDRESS = "address" + private const val FROM_GROUP_THREAD_ID = "from_group_thread_id" + private const val SCROLL_MESSAGE_ID = "scroll_message_id" + private const val SCROLL_MESSAGE_AUTHOR = "scroll_message_author" + + const val SHOW_SEARCH = "show_search" + // Request codes const val PICK_DOCUMENT = 2 const val TAKE_PHOTO = 7 const val PICK_GIF = 10 const val PICK_FROM_LIBRARY = 12 + + @JvmOverloads + fun createIntent( + context: Context, + address: Address, + fromGroupThreadId: Long? = null, + // If provided, this will scroll to the message with the given timestamp and author (TODO: use message id instead) + scrollToMessage: Pair? = null + ): Intent { + return Intent(context, ConversationActivityV2::class.java).apply { + putExtra(ADDRESS, address) + fromGroupThreadId?.let { + putExtra(FROM_GROUP_THREAD_ID, it) + } + + scrollToMessage?.let { (timestamp, author) -> + putExtra(SCROLL_MESSAGE_ID, timestamp) + putExtra(SCROLL_MESSAGE_AUTHOR, author) + } + } + } } // endregion @@ -544,13 +550,6 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, // messageIdToScroll messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1)) messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR)) - val recipient = viewModel.recipient - val openGroup = recipient.let { viewModel.openGroup } - if (recipient == null || (recipient.isCommunityRecipient && openGroup == null)) { - Toast.makeText(this, getString(R.string.conversationsDeleted), Toast.LENGTH_LONG).show() - return finish() - } - setUpToolBar() setUpInputBar() setUpLinkPreviewObserver() @@ -582,7 +581,6 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, setUpRecyclerView() setUpTypingObserver() - setUpRecipientObserver() setUpSearchResultObserver() setUpLegacyGroupUI() @@ -660,14 +658,14 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, when (event) { is ConversationUiEvent.NavigateToConversation -> { finish() - startActivity(Intent(this@ConversationActivityV2, ConversationActivityV2::class.java) - .putExtra(THREAD_ID, event.threadId) + startActivity( + createIntent(this@ConversationActivityV2, event.address) ) } is ConversationUiEvent.ShowDisappearingMessages -> { val intent = Intent(this@ConversationActivityV2, DisappearingMessagesActivity::class.java).apply { - putExtra(DisappearingMessagesActivity.THREAD_ID, event.threadId) + putExtra(DisappearingMessagesActivity.ARG_ADDRESS, event.address) } startActivity(intent) } @@ -681,7 +679,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, is ConversationUiEvent.ShowNotificationSettings -> { val intent = Intent(this@ConversationActivityV2, NotificationSettingsActivity::class.java).apply { - putExtra(NotificationSettingsActivity.THREAD_ID, event.threadId) + putExtra(NotificationSettingsActivity.ARG_ADDRESS, event.address) } startActivity(intent) } @@ -722,8 +720,6 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, true, screenshotObserver ) - - viewModel.onResume() } override fun onPause() { @@ -754,7 +750,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, adapter.changeCursor(cursor) if (cursor != null) { - firstCursorLoad = true + firstCursorLoad.value = true val messageTimestamp = messageToScrollTimestamp.getAndSet(-1) val author = messageToScrollAuthor.getAndSet(null) val initialUnreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId) @@ -774,8 +770,6 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, if (firstLoad.getAndSet(false)) scrollToFirstUnreadMessageOrBottom() handleRecyclerViewScrolled() } - - updatePlaceholder() } stopConversationLoader() @@ -844,12 +838,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, onSearchQueryClear = { onSearchQueryUpdated("") }, onSearchCanceled = ::onSearchClosed, onAvatarPressed = { - val intent = ConversationSettingsActivity.createIntent( - context = this, - threadId = viewModel.threadId, - threadAddress = viewModel.recipient?.address - ) - + val intent = ConversationSettingsActivity.createIntent(this, address) settingsLauncher.launch(intent) } ) @@ -889,7 +878,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, AttachmentManager.MediaType.VIDEO == mediaType) ) { val media = Media(mediaURI, filename, mimeType, 0, 0, 0, 0, null, null) - startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient!!, ""), PICK_FROM_LIBRARY) + startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient!!.address, ""), PICK_FROM_LIBRARY) return } else { prepMediaForSending(mediaURI, mediaType).addListener(object : ListenableFuture.Listener { @@ -931,9 +920,6 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } } - private fun setUpRecipientObserver() = viewModel.recipient?.addListener(this) - private fun tearDownRecipientObserver() = viewModel.recipient?.removeListener(this) - // called from onCreate private fun setUpExpiredGroupBanner() { lifecycleScope.launch { @@ -954,7 +940,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, if (shouldShowLegacy) { val txt = Phrase.from(this, R.string.disappearingMessagesLegacy) - .put(NAME_KEY, legacyRecipient!!.name) + .put(NAME_KEY, legacyRecipient!!.displayName) .format() binding.conversationHeader.outdatedDisappearingBannerTextView.text = txt } @@ -1035,9 +1021,8 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, // Observe toast messages lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.uiState - .mapNotNull { it.uiMessages.firstOrNull() } - .distinctUntilChanged() + viewModel.uiMessages + .mapNotNull { it.firstOrNull() } .collect { msg -> Toast.makeText(this@ConversationActivityV2, msg.message, Toast.LENGTH_LONG).show() viewModel.messageShown(msg.id) @@ -1049,29 +1034,88 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { // Wait for `shouldExit == true` then finish the activity - viewModel.uiState - .filter { it.shouldExit } - .first() + viewModel.shouldExit + .scan(null to null) { acc: Pair, current -> + acc.second to current + } + .mapNotNull { (prev, curr) -> + // If shouldExit(curr) is true, we will always finish the activity, + // but we will show a toast on the way out only if we used to have a conversation, + // this is a way to detect the deletion of the conversation. + val shouldShowToast = if (curr == true) { + prev == false + } else { + return@mapNotNull null + } - if (!isFinishing) { - finish() - } + shouldShowToast + } + .collect { shouldShowToast -> + if (shouldShowToast) { + Toast.makeText( + this@ConversationActivityV2, + getString(R.string.conversationsDeleted), + Toast.LENGTH_LONG + ).show() + } + + if (!isFinishing) { + finish() + } + } } } - // Observe the rest misc "simple" state change. They are bundled in one big - // state observing as these changes are relatively cheap to perform even redundantly. + // React to input bar state changes lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.uiState.collect { state -> - binding.inputBar.setState(state.inputBarState) + viewModel.inputBarState.collectLatest(binding.inputBar::setState) + } + } - binding.root.requestApplyInsets() + // React to input bar state changes + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.charLimitState.collectLatest(binding.inputBar::setCharLimitState) + } + } - // show or hide loading indicator - binding.loader.isVisible = state.showLoader - updatePlaceholder() + // React to loader visibility changes + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.showLoader + .collectLatest { show -> + binding.loader.isVisible = show + } + } + } + + // React to placeholder related changes + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + data class PlaceholderData( + val recipient: Recipient, + val openGroup: OpenGroup?, + val groupThreadStatus: GroupThreadStatus, + val firstLoad: Boolean + ) + + combine( + viewModel.recipientFlow.filterNotNull(), + viewModel.openGroupFlow, + viewModel.groupV2ThreadState, + firstCursorLoad, + ::PlaceholderData, + ).collectLatest { (r, og, groupState, firstLoad) -> + updatePlaceholder( + recipient = r, + blindedRecipient = viewModel.blindedRecipient, + openGroup = og, + groupThreadStatus = groupState, + firstLoad + + ) } } } @@ -1114,7 +1158,6 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, if(::binding.isInitialized) { viewModel.saveDraft(binding.inputBar.text.trim()) cancelVoiceMessage() - tearDownRecipientObserver() } // Delete any files we might have locally cached when sharing (which we need to do @@ -1126,14 +1169,15 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, // endregion // region Animation & Updating - override fun onModified(recipient: Recipient) { - viewModel.updateRecipient() - - runOnUiThread { - invalidateOptionsMenu() - updateSendAfterApprovalText() - } - } + //TODO test recipient update +// override fun onModified(recipient: Recipient) { +// viewModel.updateRecipient() +// +// runOnUiThread { +// invalidateOptionsMenu() +// updateSendAfterApprovalText() +// } +// } private fun updateSendAfterApprovalText() { binding.textSendAfterApproval.isVisible = viewModel.showSendAfterApprovalText @@ -1164,9 +1208,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.uiState - .map { it.messageRequestState } - .distinctUntilChanged() + viewModel.messageRequestState .collectLatest { state -> binding.messageRequestBar.root.isVisible = state is MessageRequestUiState.Visible @@ -1313,13 +1355,12 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } // Update placeholder / control messages in a conversation - private fun updatePlaceholder() { - if(!firstCursorLoad) return - val recipient = viewModel.recipient ?: return Log.w("Loki", "recipient was null in placeholder update") - val blindedRecipient = viewModel.blindedRecipient - val openGroup = viewModel.openGroup - - val groupThreadStatus = viewModel.groupV2ThreadState + private fun updatePlaceholder(recipient: Recipient, + blindedRecipient: Recipient?, + openGroup: OpenGroup?, + groupThreadStatus: GroupThreadStatus, + isFirstCursorLoad: Boolean) { + if(!isFirstCursorLoad) return // Special state handling for kicked/destroyed groups if (groupThreadStatus != GroupThreadStatus.None) { @@ -1327,12 +1368,12 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, binding.conversationRecyclerView.isVisible = false binding.placeholderText.text = when (groupThreadStatus) { GroupThreadStatus.Kicked -> Phrase.from(this, R.string.groupRemovedYou) - .put(GROUP_NAME_KEY, recipient.name) + .put(GROUP_NAME_KEY, recipient.displayName) .format() .toString() GroupThreadStatus.Destroyed -> Phrase.from(this, R.string.groupDeletedMemberDescription) - .put(GROUP_NAME_KEY, recipient.name) + .put(GROUP_NAME_KEY, recipient.displayName) .format() .toString() @@ -1354,9 +1395,9 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } // If we're trying to message someone who has blocked community message requests - blindedRecipient?.blocksCommunityMessageRequests == true -> { + blindedRecipient?.acceptsCommunityMessageRequests == false -> { Phrase.from(applicationContext, R.string.messageRequestsTurnedOff) - .put(NAME_KEY, recipient.name) + .put(NAME_KEY, recipient.displayName) .format() } @@ -1364,7 +1405,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, recipient.isCommunityInboxRecipient || recipient.isCommunityOutboxRecipient || recipient.is1on1 || recipient.isGroupOrCommunityRecipient -> { Phrase.from(applicationContext, R.string.groupNoMessages) - .put(GROUP_NAME_KEY, recipient.name) + .put(GROUP_NAME_KEY, recipient.displayName) .format() } @@ -1402,7 +1443,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, if(viewModel.recipient == null) return // if the user is blocked, show unblock modal - if(viewModel.recipient?.isBlocked == true){ + if(viewModel.recipient?.blocked == true){ unblock() return } @@ -1450,9 +1491,9 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val invitingAdmin = viewModel.invitingAdmin val name = if (recipient.isGroupV2Recipient && invitingAdmin != null) { - invitingAdmin.getSearchName() + invitingAdmin.searchName } else { - recipient.name + recipient.displayName } showSessionDialog { @@ -1489,7 +1530,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, title(R.string.blockUnblock) text( Phrase.from(context, R.string.blockUnblockName) - .put(NAME_KEY, recipient.name) + .put(NAME_KEY, recipient.displayName) .format() ) dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) { viewModel.unblock() } @@ -1925,8 +1966,8 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val recipient = viewModel.recipient ?: return // show the unblock dialog when trying to send a message to a blocked contact - if (recipient.isContactRecipient && recipient.isBlocked) { - BlockedDialog(recipient, viewModel.getUsername(recipient.address.toString())).show(supportFragmentManager, "Blocked Dialog") + if (recipient.isContactRecipient && recipient.blocked) { + BlockedDialog(recipient.address, viewModel.getUsername(recipient.address.toString())).show(supportFragmentManager, "Blocked Dialog") return } @@ -1951,7 +1992,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val mimeType = MediaUtil.getMimeType(this, contentUri)!! val filename = FilenameUtils.getFilenameFromUri(this, contentUri, mimeType) val media = Media(contentUri, filename, mimeType, 0, 0, 0, 0, null, null) - startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), recipient, getMessageBody()), PICK_FROM_LIBRARY) + startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), recipient.address, getMessageBody()), PICK_FROM_LIBRARY) } // If we previously approve this recipient, either implicitly or explicitly, we need to wait for @@ -1985,11 +2026,12 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val message = VisibleMessage().applyExpiryMode(viewModel.threadId) message.sentTimestamp = sentTimestamp message.text = text - val expiresInMillis = viewModel.expirationConfiguration?.expiryMode?.expiryMillis ?: 0 - val expireStartedAt = if (viewModel.expirationConfiguration?.expiryMode is ExpiryMode.AfterSend) { + val expiryMode = viewModel.recipient?.expiryMode + val expiresInMillis = expiryMode?.expiryMillis ?: 0 + val expireStartedAt = if (expiryMode is ExpiryMode.AfterSend) { message.sentTimestamp } else 0 - val outgoingTextMessage = OutgoingTextMessage.from(message, recipient, expiresInMillis, expireStartedAt!!) + val outgoingTextMessage = OutgoingTextMessage.from(message, recipient.address, expiresInMillis, expireStartedAt!!) // Clear the input bar binding.inputBar.text = "" @@ -2045,11 +2087,11 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, else it.individualRecipient.address quote?.copy(author = sender) } - val expiresInMs = viewModel.expirationConfiguration?.expiryMode?.expiryMillis ?: 0 - val expireStartedAtMs = if (viewModel.expirationConfiguration?.expiryMode is ExpiryMode.AfterSend) { + val expiresInMs = viewModel.recipient?.expiryMode?.expiryMillis ?: 0 + val expireStartedAtMs = if (viewModel.recipient?.expiryMode is ExpiryMode.AfterSend) { sentTimestamp } else 0 - val outgoingTextMessage = OutgoingMediaMessage.from(message, recipient, attachments, localQuote, linkPreview, expiresInMs, expireStartedAtMs) + val outgoingTextMessage = OutgoingMediaMessage.from(message, recipient.address, attachments, localQuote, linkPreview, expiresInMs, expireStartedAtMs) // Clear the input bar binding.inputBar.text = "" @@ -2125,11 +2167,11 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, private fun pickFromLibrary() { val recipient = viewModel.recipient ?: return binding.inputBar.text?.trim()?.let { text -> - AttachmentManager.selectGallery(this, PICK_FROM_LIBRARY, recipient, text) + AttachmentManager.selectGallery(this, PICK_FROM_LIBRARY, recipient.address, text) } } - private fun showCamera() { attachmentManager.capturePhoto(this, TAKE_PHOTO, viewModel.recipient) } + private fun showCamera() { attachmentManager.capturePhoto(this, TAKE_PHOTO, viewModel.recipient?.address) } override fun onAttachmentChanged() { /* Do nothing */ } @@ -2351,7 +2393,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, showSessionDialog { title(R.string.banUser) text(R.string.communityBanDescription) - dangerButton(R.string.theContinue) { viewModel.banUser(messages.first().individualRecipient); endActionMode() } + dangerButton(R.string.theContinue) { viewModel.banUser(messages.first().individualRecipient.address); endActionMode() } cancelButton(::endActionMode) } } @@ -2531,7 +2573,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, // initially denied it but then have a change of heart when they realise they can't // proceed without it. dangerButton(R.string.theContinue) { - val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val uri = Uri.fromParts("package", packageName, null) intent.setData(uri) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index c8b2fb1da6..31bb4a0450 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -2,28 +2,12 @@ package org.thoughtcrime.securesms.conversation.v2 import android.content.Context import android.database.Cursor -import android.util.SparseArray -import android.util.SparseBooleanArray import android.view.MotionEvent import android.view.View import android.view.ViewGroup -import androidx.annotation.WorkerThread -import androidx.core.util.getOrDefault -import androidx.core.util.set -import androidx.lifecycle.LifecycleCoroutineScope import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.bumptech.glide.RequestManager -import java.util.concurrent.atomic.AtomicLong -import kotlin.math.min -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment -import org.session.libsession.utilities.recipients.Recipient -import org.session.libsignal.utilities.AccountId import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate @@ -31,11 +15,12 @@ import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import java.util.concurrent.atomic.AtomicLong +import kotlin.math.min class ConversationAdapter( context: Context, cursor: Cursor?, - conversation: Recipient?, originalLastSeen: Long, private val isReversed: Boolean, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit, @@ -44,19 +29,14 @@ class ConversationAdapter( private val onDeselect: (MessageRecord, Int) -> Unit, private val downloadPendingAttachment: (DatabaseAttachment) -> Unit, private val retryFailedAttachments: (List) -> Unit, - private val glide: RequestManager, - lifecycleCoroutineScope: LifecycleCoroutineScope + private val glide: RequestManager ) : CursorRecyclerViewAdapter(context, cursor) { private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() } - private val contactDB by lazy { DatabaseComponent.get(context).sessionContactDatabase() } var selectedItems = mutableSetOf() var isAdmin: Boolean = false private var searchQuery: String? = null var visibleMessageViewDelegate: VisibleMessageViewDelegate? = null - private val updateQueue = Channel(1024, onBufferOverflow = BufferOverflow.DROP_OLDEST) - private val contactCache = SparseArray(100) - private val contactLoadedCache = SparseBooleanArray(100) private val lastSeen = AtomicLong(originalLastSeen) var lastSentMessageId: MessageId? = null @@ -67,25 +47,8 @@ class ConversationAdapter( } } - private val groupId = if(conversation?.isGroupV2Recipient == true) - AccountId(conversation.address.toString()) - else null - private val expandedMessageIds = mutableSetOf() - init { - lifecycleCoroutineScope.launch(IO) { - while (isActive) { - val item = updateQueue.receive() - val contact = getSenderInfo(item) ?: continue - contactCache[item.hashCode()] = contact - contactLoadedCache[item.hashCode()] = true - } - } - } - - @WorkerThread - private fun getSenderInfo(sender: String): Contact? = contactDB.getContactWithAccountID(sender) sealed class ViewType(val rawValue: Int) { object Visible : ViewType(0) @@ -129,19 +92,6 @@ class ConversationAdapter( val isSelected = selectedItems.contains(message) visibleMessageView.snIsSelected = isSelected visibleMessageView.indexInAdapter = position - val senderId = message.individualRecipient.address.toString() - val senderIdHash = senderId.hashCode() - updateQueue.trySend(senderId) - if (contactCache[senderIdHash] == null && !contactLoadedCache.getOrDefault( - senderIdHash, - false - ) - ) { - getSenderInfo(senderId)?.let { contact -> - contactCache[senderIdHash] = contact - } - } - val contact = contactCache[senderIdHash] val isExpanded = expandedMessageIds.contains(message.messageId) visibleMessageView.bind( @@ -150,10 +100,6 @@ class ConversationAdapter( next = getMessageAfter(position, cursor), glide = glide, searchQuery = searchQuery, - contact = contact, - // we pass in the groupId for groupV2 to use for determining the name of the members - groupId = groupId, - senderAccountID = senderId, lastSeen = lastSeen.get(), lastSentMessageId = lastSentMessageId, delegate = visibleMessageViewDelegate, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt index 09dbab7c2b..b3388fdb44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt @@ -39,12 +39,12 @@ import org.session.libsession.LocalisedTimeUtil.toShortTwoPartString import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.snode.SnodeAPI +import org.session.libsession.utilities.Address import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.getColorFromAttr -import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.components.emoji.EmojiImageView import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel import org.thoughtcrime.securesms.components.menu.ActionItem @@ -539,7 +539,7 @@ class ConversationReactionOverlay : FrameLayout { .firstOrNull() ?.let(ReactionRecord::emoji) - private fun getMenuActionItems(message: MessageRecord, recipient: Recipient): List { + private fun getMenuActionItems(message: MessageRecord, recipient: Address): List { val items: MutableList = ArrayList() // Prepare @@ -549,7 +549,7 @@ class ConversationReactionOverlay : FrameLayout { val openGroup = lokiThreadDatabase.getOpenGroupChat(message.threadId) val userPublicKey = textSecurePreferences.getLocalNumber()!! - val isDeprecatedLegacyGroup = recipient.isLegacyGroupRecipient && + val isDeprecatedLegacyGroup = recipient.isLegacyGroup && deprecationManager.isDeprecated // control messages and "marked as deleted" messages can only delete @@ -576,7 +576,7 @@ class ConversationReactionOverlay : FrameLayout { items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) }) } // Copy Account ID - if (!recipient.isCommunityRecipient && message.isIncoming && !isDeleteOnly) { + if (!recipient.isCommunity && message.isIncoming && !isDeleteOnly) { items += ActionItem(R.attr.menu_copy_icon, R.string.accountIDCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) }) } // Delete message diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 98eb080914..d4bb90307a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -5,13 +5,13 @@ import android.content.Context import android.widget.Toast import androidx.annotation.StringRes import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.bumptech.glide.Glide import com.squareup.phrase.Phrase import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -32,28 +33,25 @@ import kotlinx.coroutines.withContext import network.loki.messenger.R import network.loki.messenger.libsession_util.util.BlindKeyAPI import network.loki.messenger.libsession_util.util.ExpiryMode -import network.loki.messenger.libsession_util.util.KeyPair import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager -import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.ExpirationUtil -import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY import org.session.libsession.utilities.StringSubstitutionConstants.DATE_KEY import org.session.libsession.utilities.StringSubstitutionConstants.LIMIT_KEY import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.UsernameUtils import org.session.libsession.utilities.getGroup import org.session.libsession.utilities.recipients.MessageType import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.getType +import org.session.libsession.utilities.userConfigsChanged import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix @@ -62,8 +60,10 @@ import org.thoughtcrime.securesms.audio.AudioSlidePlayer import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase +import org.thoughtcrime.securesms.database.LokiThreadDatabase import org.thoughtcrime.securesms.database.ReactionDatabase import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.GroupThreadStatus import org.thoughtcrime.securesms.database.model.MessageId @@ -82,20 +82,20 @@ import org.thoughtcrime.securesms.ui.getSubbedString import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUtils import org.thoughtcrime.securesms.util.DateUtils -import org.thoughtcrime.securesms.util.RecipientChangeSource import org.thoughtcrime.securesms.util.avatarOptions +import org.thoughtcrime.securesms.util.mapStateFlow +import org.thoughtcrime.securesms.util.mapToStateFlow import org.thoughtcrime.securesms.webrtc.CallManager import org.thoughtcrime.securesms.webrtc.data.State import java.time.ZoneId import java.util.UUID - // the amount of character left at which point we should show an indicator private const val CHARACTER_LIMIT_THRESHOLD = 200 -class ConversationViewModel( - val threadId: Long, - val edKeyPair: KeyPair?, +@HiltViewModel(assistedFactory = ConversationViewModel.Factory::class) +class ConversationViewModel @AssistedInject constructor( + @Assisted val address: Address, private val application: Application, private val repository: ConversationRepository, private val storage: StorageProtocol, @@ -112,95 +112,90 @@ class ConversationViewModel( val legacyGroupDeprecationManager: LegacyGroupDeprecationManager, val dateUtils: DateUtils, private val expiredGroupManager: ExpiredGroupManager, - private val usernameUtils: UsernameUtils, private val avatarUtils: AvatarUtils, - private val recipientChangeSource: RecipientChangeSource, private val openGroupManager: OpenGroupManager, private val proStatusManager: ProStatusManager, + private val recipientRepository: RecipientRepository, + private val lokiThreadDatabase: LokiThreadDatabase, ) : ViewModel() { + val threadId: Long = threadDb.getThreadIdIfExistsFor(address) + + private val edKeyPair by lazy { + storage.getUserED25519KeyPair() + } + val showSendAfterApprovalText: Boolean get() = recipient?.run { // if the contact is a 1on1 or a blinded 1on1 that doesn't block requests - and is not the current user - and has not yet approved us - (getBlindedRecipient(recipient)?.blocksCommunityMessageRequests == false || isContactRecipient) && !isLocalNumber && !hasApprovedMe() + (getBlindedRecipient(recipient)?.acceptsCommunityMessageRequests == true || isContactRecipient) && !isLocalNumber && !approvedMe } ?: false - private val _uiState = MutableStateFlow(ConversationUiState()) - val uiState: StateFlow get() = _uiState - private val _uiEvents = MutableSharedFlow(extraBufferCapacity = 1) val uiEvents: SharedFlow get() = _uiEvents private val _dialogsState = MutableStateFlow(DialogsState()) val dialogsState: StateFlow = _dialogsState - private val _isAdmin = MutableStateFlow(false) - val isAdmin: StateFlow = _isAdmin - - // all the data we need for the conversation app bar - private val _appBarData = MutableStateFlow(ConversationAppBarData( - title = "", - pagerData = emptyList(), - showCall = false, - showAvatar = false, - avatarUIData = AvatarUIData( - elements = emptyList() - ) - )) - val appBarData: StateFlow = _appBarData - - private var _recipient: RetrieveOnce = RetrieveOnce { - val conversation = repository.maybeGetRecipientForThreadId(threadId) - - // set admin from current conversation - val conversationType = conversation?.getType() - // Determining is the current user is an admin will depend on the kind of conversation we are in - _isAdmin.value = when(conversationType) { - // for Groups V2 - MessageType.GROUPS_V2 -> { - configFactory.getGroup(AccountId(conversation.address.toString()))?.hasAdminKey() == true - } + val recipientFlow: StateFlow = recipientRepository.observeRecipient(address) + .filterNotNull() + .mapToStateFlow(viewModelScope, recipientRepository.getRecipientSync(address)) { it } - // for legacy groups, check if the user created the group - MessageType.LEGACY_GROUP -> { - // for legacy groups, we check if the current user is the one who created the group - run { - val localUserAddress = - textSecurePreferences.getLocalNumber() ?: return@run false - val group = storage.getGroup(conversation.address.toGroupString()) - group?.admins?.contains(fromSerialized(localUserAddress)) ?: false - } - } - - // for communities the the `isUserModerator` field - MessageType.COMMUNITY -> isUserCommunityManager() + val openGroupFlow: StateFlow = + lokiThreadDatabase.retrieveAndObserveOpenGroup(viewModelScope, threadId) ?: MutableStateFlow(null) - // false in other cases - else -> false - } + val openGroup: OpenGroup? + get() = openGroupFlow.value + + val isAdmin: StateFlow = when { + address.isCommunity -> openGroupFlow.mapStateFlow(viewModelScope) { og -> isUserCommunityManager(og) } + address.isGroupV2 -> configFactory.userConfigsChanged(500) + .onStart { emit(Unit) } + .mapToStateFlow(viewModelScope, initialData = null) { + configFactory.getGroup(AccountId(address.address))?.hasAdminKey() == true + } - updateAppBarData(conversation) + address.isLegacyGroup -> textSecurePreferences.watchLocalNumber() + .filterNotNull() + .mapToStateFlow(viewModelScope, initialData = textSecurePreferences.getLocalNumber()) { myAddress -> + myAddress != null && storage.getGroup(address.toGroupString()) + ?.admins?.contains(fromSerialized(myAddress)) == true + } + .stateIn(viewModelScope, SharingStarted.Eagerly, false) - conversation + else -> MutableStateFlow(false) } - val expirationConfiguration: ExpirationConfiguration? - get() = storage.getExpirationConfiguration(threadId) + private val _searchOpened = MutableStateFlow(false) + + val appBarData: StateFlow = combine( + recipientFlow.filterNotNull(), + openGroupFlow, + _searchOpened, + ::getAppBarData + ).stateIn(viewModelScope, SharingStarted.Eagerly, ConversationAppBarData( + title = "", + pagerData = emptyList(), + showCall = false, + showAvatar = false, + showSearch = false, + avatarUIData = AvatarUIData(emptyList()) + )) val recipient: Recipient? - get() = _recipient.value + get() = recipientFlow.value val blindedRecipient: Recipient? - get() = _recipient.value?.let { recipient -> + get() = recipient?.let { recipient -> getBlindedRecipient(recipient) } - private var currentAppBarNotificationState: String? = null - private fun getBlindedRecipient(recipient: Recipient?): Recipient? = when { recipient?.isCommunityOutboxRecipient == true -> recipient - recipient?.isCommunityInboxRecipient == true -> repository.maybeGetBlindedRecipient(recipient) + recipient?.isCommunityInboxRecipient == true -> + repository.maybeGetBlindedRecipient(recipient.address) + ?.let(recipientRepository::getRecipientSync) else -> null } @@ -214,37 +209,31 @@ class ConversationViewModel( val recipient = recipient ?: return null if (!recipient.isGroupV2Recipient) return null - return repository.getInvitingAdmin(threadId) + return repository.getInvitingAdmin(threadId)?.let(recipientRepository::getRecipientSync) } - val groupV2ThreadState: GroupThreadStatus - get() { - val recipient = recipient ?: return GroupThreadStatus.None - if (!recipient.isGroupV2Recipient) return GroupThreadStatus.None - - return configFactory.getGroup(AccountId(recipient.address.toString())).let { group -> - when { - group?.destroyed == true -> GroupThreadStatus.Destroyed - group?.kicked == true -> GroupThreadStatus.Kicked - else -> GroupThreadStatus.None + val groupV2ThreadState: Flow get() = when { + !address.isGroupV2 -> flowOf( GroupThreadStatus.None) + else -> configFactory.userConfigsChanged(500) + .onStart { emit(Unit) } + .map { + configFactory.getGroup(AccountId(address.toString())).let { group -> + when { + group?.destroyed == true -> GroupThreadStatus.Destroyed + group?.kicked == true -> GroupThreadStatus.Kicked + else -> GroupThreadStatus.None + } } } - } - - private val _openGroup: MutableStateFlow by lazy { - MutableStateFlow(storage.getOpenGroup(threadId)) } - val openGroup: OpenGroup? - get() = _openGroup.value - val serverCapabilities: List get() = openGroup?.let { storage.getServerCapabilities(it.server) } ?: listOf() val blindedPublicKey: String? get() = if (openGroup == null || edKeyPair == null || !serverCapabilities.contains(OpenGroupApi.Capability.BLIND.name.lowercase())) null else { BlindKeyAPI.blind15KeyPairOrNull( - ed25519SecretKey = edKeyPair.secretKey.data, + ed25519SecretKey = edKeyPair!!.secretKey.data, serverPubKey = Hex.fromStringCondensed(openGroup!!.publicKey), )?.pubKey?.data ?.let { AccountId(IdPrefix.BLINDED, it) }?.hexString @@ -253,16 +242,43 @@ class ConversationViewModel( val isMessageRequestThread : Boolean get() { val recipient = recipient ?: return false - return !recipient.isLocalNumber && !recipient.isLegacyGroupRecipient && !recipient.isCommunityRecipient && !recipient.isApproved + return !recipient.isLocalNumber && !recipient.isLegacyGroupRecipient && !recipient.isCommunityRecipient && !recipient.approved } + private val _showLoader = MutableStateFlow(false) + val showLoader: StateFlow get() = _showLoader + + val shouldExit: Flow get() = recipientFlow.map { it == null } + + val inputBarState: StateFlow = combine( + recipientFlow, openGroupFlow, legacyGroupDeprecationManager.deprecationState, + this::getInputBarState + ).stateIn(viewModelScope, SharingStarted.Eagerly, InputBarState()) + + + private val _acceptingMessageRequest = MutableStateFlow(false) + val messageRequestState: StateFlow = combine( + recipientFlow, + _acceptingMessageRequest, + ) { r, accepting -> + if (accepting) MessageRequestUiState.Pending + else buildMessageRequestState(r) + }.stateIn(viewModelScope, SharingStarted.Eagerly, MessageRequestUiState.Invisible) + + + private val _uiMessages = MutableStateFlow>(emptyList()) + val uiMessages: StateFlow> get() = _uiMessages + + private val _charLimitState = MutableStateFlow(null) + val charLimitState: StateFlow get() = _charLimitState + /** * returns true for outgoing message request, whether they are for 1 on 1 conversations or community outgoing MR */ val isOutgoingMessageRequest: Boolean get() { val recipient = recipient ?: return false - return (recipient.is1on1 || recipient.isCommunityInboxRecipient) && !recipient.hasApprovedMe() + return (recipient.is1on1 || recipient.isCommunityInboxRecipient) && !recipient.approvedMe } val showOptionsMenu: Boolean @@ -319,11 +335,12 @@ class ConversationViewModel( storage = storage, messageDataProvider = messageDataProvider, scope = viewModelScope, + recipientRepository = recipientRepository, ) val callBanner: StateFlow = callManager.currentConnectionStateFlow.map { // a call is in progress if it isn't idle nor disconnected and the recipient is the person on the call - if(it !is State.Idle && it !is State.Disconnected && callManager.recipient?.address == recipient?.address){ + if(it !is State.Idle && it !is State.Disconnected && callManager.recipient == recipient?.address){ // call is started, we need to differentiate between in progress vs incoming if(it is State.Connected) application.getString(R.string.callsInProgress) else application.getString(R.string.callsIncomingUnknown) @@ -333,55 +350,6 @@ class ConversationViewModel( val lastSeenMessageId: Flow get() = repository.getLastSentMessageID(threadId) - init { - viewModelScope.launch(Dispatchers.Default) { - combine( - repository.recipientUpdateFlow(threadId), - _openGroup, - legacyGroupDeprecationManager.deprecationState, - ::Triple - ).collect { (recipient, community, deprecationState) -> - _uiState.update { - it.copy( - shouldExit = recipient == null, - inputBarState = getInputBarState(recipient, community, deprecationState), - messageRequestState = buildMessageRequestState(recipient), - ) - } - } - } - - // update state on recipient changes - viewModelScope.launch(Dispatchers.Default) { - recipientChangeSource.changes().collect { - updateAppBarData(recipient) - _uiState.update { - it.copy( - shouldExit = recipient == null, - inputBarState = getInputBarState(recipient, _openGroup.value, legacyGroupDeprecationManager.deprecationState.value), - ) - } - } - } - - // Listen for changes in the open group's write access - viewModelScope.launch { - openGroupManager.getCommunitiesWriteAccessFlow() - .map { - withContext(Dispatchers.Default) { - if (openGroup?.groupId != null) - it[openGroup?.groupId] - else null - } - } - .filterNotNull() - .collect{ - // update our community object - _openGroup.value = openGroup?.copy(canWrite = it) - } - } - } - private fun getInputBarState( recipient: Recipient?, community: OpenGroup?, @@ -389,14 +357,14 @@ class ConversationViewModel( ): InputBarState { return when { // prioritise cases that demand the input to be hidden - !shouldShowInput(recipient, community, deprecationState) -> InputBarState( + !shouldShowInput(recipient, deprecationState) -> InputBarState( contentState = InputBarContentState.Hidden, enableAttachMediaControls = false ) // next are cases where the input is visible but disabled // when the recipient is blocked - recipient?.isBlocked == true -> InputBarState( + recipient?.blocked == true -> InputBarState( contentState = InputBarContentState.Disabled( text = application.getString(R.string.blockBlockedDescription), onClick = { @@ -407,7 +375,7 @@ class ConversationViewModel( ) // the user does not have write access in the community - openGroup?.canWrite == false -> InputBarState( + community?.canWrite == false -> InputBarState( contentState = InputBarContentState.Disabled( text = application.getString(R.string.permissionsWriteCommunity), ), @@ -417,84 +385,79 @@ class ConversationViewModel( // other cases the input is visible, and the buttons might be disabled based on some criteria else -> InputBarState( contentState = InputBarContentState.Visible, - enableAttachMediaControls = shouldEnableInputMediaControls(recipient) + enableAttachMediaControls = shouldEnableInputMediaControls(recipient, community) ) } } - private fun updateAppBarData(conversation: Recipient?) { - viewModelScope.launch { - // sort out the pager data, if any - val pagerData: MutableList = mutableListOf() - if (conversation != null) { - // Specify the disappearing messages subtitle if we should - val config = expirationConfiguration - if (config?.isEnabled == true) { - // Get the type of disappearing message and the abbreviated duration.. - val dmTypeString = when (config.expiryMode) { - is ExpiryMode.AfterRead -> R.string.disappearingMessagesDisappearAfterReadState - else -> R.string.disappearingMessagesDisappearAfterSendState - } - val durationAbbreviated = ExpirationUtil.getExpirationAbbreviatedDisplayValue(config.expiryMode.expirySeconds) + private suspend fun getAppBarData(conversation: Recipient, community: OpenGroup?, showSearch: Boolean): ConversationAppBarData { + // sort out the pager data, if any + val pagerData: MutableList = mutableListOf() + // Specify the disappearing messages subtitle if we should + val expiryMode = conversation.expiryMode + if (expiryMode.expiryMillis > 0) { + // Get the type of disappearing message and the abbreviated duration.. + val dmTypeString = when (expiryMode) { + is ExpiryMode.AfterRead -> R.string.disappearingMessagesDisappearAfterReadState + else -> R.string.disappearingMessagesDisappearAfterSendState + } + val durationAbbreviated = ExpirationUtil.getExpirationAbbreviatedDisplayValue(expiryMode.expirySeconds) - // ..then substitute into the string.. - val subtitleTxt = application.getSubbedString(dmTypeString, - TIME_KEY to durationAbbreviated - ) + // ..then substitute into the string.. + val subtitleTxt = application.getSubbedString(dmTypeString, + TIME_KEY to durationAbbreviated + ) - // .. and apply to the subtitle. - pagerData += ConversationAppBarPagerData( - title = subtitleTxt, - action = { - showDisappearingMessages() - }, - icon = R.drawable.ic_clock_11, - qaTag = application.resources.getString(R.string.AccessibilityId_disappearingMessagesDisappear) - ) - } + // .. and apply to the subtitle. + pagerData += ConversationAppBarPagerData( + title = subtitleTxt, + action = { + showDisappearingMessages() + }, + icon = R.drawable.ic_clock_11, + qaTag = application.resources.getString(R.string.AccessibilityId_disappearingMessagesDisappear) + ) + } - currentAppBarNotificationState = null - if (conversation.isMuted || conversation.notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS) { - currentAppBarNotificationState = getNotificationStatusTitle(conversation) - pagerData += ConversationAppBarPagerData( - title = currentAppBarNotificationState!!, - action = { - showNotificationSettings() - } - ) + if (conversation.isMuted() || conversation.notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS) { + pagerData += ConversationAppBarPagerData( + title = getNotificationStatusTitle(conversation), + action = { + showNotificationSettings() } + ) + } - if (conversation.isGroupOrCommunityRecipient && conversation.isApproved) { - val title = if (conversation.isCommunityRecipient) { - val userCount = openGroup?.let { lokiAPIDb.getUserCount(it.room, it.server) } ?: 0 - application.resources.getQuantityString(R.plurals.membersActive, userCount, userCount) - } else { - val userCount = if (conversation.isGroupV2Recipient) { - storage.getMembers(conversation.address.toString()).size - } else { // legacy closed groups - groupDb.getGroupMemberAddresses(conversation.address.toGroupString(), true).size - } - application.resources.getQuantityString(R.plurals.members, userCount, userCount) - } - pagerData += ConversationAppBarPagerData( - title = title, - action = { - showGroupMembers() - }, - ) + if (conversation.isGroupOrCommunityRecipient && conversation.approved) { + val title = if (conversation.isCommunityRecipient) { + val userCount = community?.let { lokiAPIDb.getUserCount(it.room, it.server) } ?: 0 + application.resources.getQuantityString(R.plurals.membersActive, userCount, userCount) + } else { + val userCount = if (conversation.isGroupV2Recipient) { + storage.getMembers(conversation.address.toString()).size + } else { // legacy closed groups + groupDb.getGroupMemberAddresses(conversation.address.toGroupString(), true).size } + application.resources.getQuantityString(R.plurals.members, userCount, userCount) } - - // calculate the main app bar data - val avatarData = avatarUtils.getUIDataFromRecipient(conversation) - _appBarData.value = ConversationAppBarData( - title = conversation.takeUnless { it?.isLocalNumber == true }?.name ?: application.getString(R.string.noteToSelf), - pagerData = pagerData, - showCall = conversation?.showCallMenu() ?: false, - showAvatar = showOptionsMenu, - showSearch = _appBarData.value.showSearch, - avatarUIData = avatarData + pagerData += ConversationAppBarPagerData( + title = title, + action = { + showGroupMembers() + }, ) + } + + // calculate the main app bar data + val avatarData = avatarUtils.getUIDataFromRecipient(conversation) + return ConversationAppBarData( + title = conversation.takeUnless { it.isLocalNumber }?.displayName ?: application.getString(R.string.noteToSelf), + pagerData = pagerData, + showCall = conversation.showCallMenu, + showAvatar = showOptionsMenu, + showSearch = showSearch, + avatarUIData = avatarData + ).also { // also preload the larger version of the avatar in case the user goes to the settings avatarData.elements.mapNotNull { it.contactPhoto }.forEach { val loadSize = application.resources.getDimensionPixelSize(R.dimen.large_profile_picture_size) @@ -505,8 +468,8 @@ class ConversationViewModel( } } - private fun getNotificationStatusTitle(conversation: Recipient): String{ - return if(conversation.isMuted) application.getString(R.string.notificationsHeaderMute) + private fun getNotificationStatusTitle(conversation: Recipient): String { + return if(conversation.isMuted()) application.getString(R.string.notificationsHeaderMute) else application.getString(R.string.notificationsHeaderMentionsOnly) } @@ -517,7 +480,7 @@ class ConversationViewModel( * 1. First time we send message to a person. * Since we haven't been approved by them, we can't send them any media, only text */ - private fun shouldEnableInputMediaControls(recipient: Recipient?): Boolean { + private fun shouldEnableInputMediaControls(recipient: Recipient?, openGroup: OpenGroup?): Boolean { // Specifically disallow multimedia if we don't have a recipient to send anything to if (recipient == null) { @@ -526,14 +489,14 @@ class ConversationViewModel( } // disable for blocked users - if (recipient.isBlocked) return false + if (recipient.blocked) return false // Specifically allow multimedia in our note-to-self if (recipient.isLocalNumber) return true // To send multimedia content to other people: // - For 1-on-1 conversations they must have approved us as a contact. - val allowedFor1on1 = recipient.is1on1 && recipient.hasApprovedMe() + val allowedFor1on1 = recipient.is1on1 && recipient.approvedMe // - For groups you just have to be a member of the group. Note: `isGroupRecipient` convers both legacy and V2 groups. val allowedForGroup = recipient.isGroupRecipient @@ -560,9 +523,9 @@ class ConversationViewModel( * 3. The legacy group is deprecated, OR * 4. Blinded recipient who have disabled message request from community members */ - private fun shouldShowInput(recipient: Recipient?, - community: OpenGroup?, - deprecationState: LegacyGroupDeprecationManager.DeprecationState + private fun shouldShowInput( + recipient: Recipient?, + deprecationState: LegacyGroupDeprecationManager.DeprecationState ): Boolean { return when { recipient?.isGroupV2Recipient == true -> !repository.isGroupReadOnly(recipient) @@ -570,7 +533,7 @@ class ConversationViewModel( groupDb.getGroup(recipient.address.toGroupString()).orNull()?.isActive == true && deprecationState != LegacyGroupDeprecationManager.DeprecationState.DEPRECATED } - getBlindedRecipient(recipient)?.blocksCommunityMessageRequests == true -> false + getBlindedRecipient(recipient)?.acceptsCommunityMessageRequests == false -> false else -> true } } @@ -586,7 +549,7 @@ class ConversationViewModel( recipient != null && // Req 1: we haven't approved the other party - (!recipient.isApproved && !recipient.isLocalNumber) && + (!recipient.approved && !recipient.isLocalNumber) && // Req 4: the type of conversation supports message request (recipient.is1on1 || recipient.isGroupV2Recipient) && @@ -644,7 +607,7 @@ class ConversationViewModel( val recipient = invitingAdmin ?: recipient ?: return Log.w("Loki", "Recipient was null for block action") if (recipient.isContactRecipient || recipient.isGroupV2Recipient) { viewModelScope.launch { - repository.setBlocked(recipient, true) + repository.setBlocked(recipient.address, true) } } @@ -659,7 +622,7 @@ class ConversationViewModel( val recipient = recipient ?: return Log.w("Loki", "Recipient was null for unblock action") if (recipient.isContactRecipient) { viewModelScope.launch { - repository.setBlocked(recipient, false) + repository.setBlocked(recipient.address, false) } } } @@ -811,11 +774,11 @@ class ConversationViewModel( viewModelScope.launch(Dispatchers.IO) { // show a loading indicator - _uiState.update { it.copy(showLoader = true) } + _showLoader.value = true // delete remotely try { - repository.deleteNoteToSelfMessagesRemotely(threadId, recipient!!, data.messages) + repository.deleteNoteToSelfMessagesRemotely(threadId, recipient!!.address, data.messages) // When this is done we simply need to remove the message locally (leave nothing behind) repository.deleteMessages(messages = data.messages, threadId = threadId) @@ -850,7 +813,7 @@ class ConversationViewModel( } // hide loading indicator - _uiState.update { it.copy(showLoader = false) } + _showLoader.value = false } } @@ -859,11 +822,11 @@ class ConversationViewModel( viewModelScope.launch(Dispatchers.IO) { // show a loading indicator - _uiState.update { it.copy(showLoader = true) } + _showLoader.value = true // delete remotely try { - repository.delete1on1MessagesRemotely(threadId, recipient!!, data.messages) + repository.delete1on1MessagesRemotely(threadId, recipient!!.address, data.messages) // When this is done we simply need to remove the message locally repository.markAsDeletedLocally( @@ -901,7 +864,7 @@ class ConversationViewModel( } // hide loading indicator - _uiState.update { it.copy(showLoader = false) } + _showLoader.value = false } } @@ -911,7 +874,7 @@ class ConversationViewModel( viewModelScope.launch(Dispatchers.IO) { // delete remotely try { - repository.deleteLegacyGroupMessagesRemotely(recipient!!, messages) + repository.deleteLegacyGroupMessagesRemotely(recipient!!.address, messages) // When this is done we simply need to remove the message locally repository.markAsDeletedLocally( @@ -951,10 +914,10 @@ class ConversationViewModel( private fun markAsDeletedForEveryoneGroupsV2(data: DeleteForEveryoneDialogData){ viewModelScope.launch(Dispatchers.Default) { // show a loading indicator - _uiState.update { it.copy(showLoader = true) } + _showLoader.value = true try { - repository.deleteGroupV2MessagesRemotely(recipient!!, data.messages) + repository.deleteGroupV2MessagesRemotely(recipient!!.address, data.messages) // the repo will handle the internal logic (calling `/delete` on the swarm // and sending 'GroupUpdateDeleteMemberContentMessage' @@ -993,14 +956,14 @@ class ConversationViewModel( } // hide loading indicator - _uiState.update { it.copy(showLoader = false) } + _showLoader.value = false } } private fun markAsDeletedForEveryoneCommunity(data: DeleteForEveryoneDialogData){ viewModelScope.launch(Dispatchers.IO) { // show a loading indicator - _uiState.update { it.copy(showLoader = true) } + _showLoader.value = true // delete remotely try { @@ -1039,11 +1002,11 @@ class ConversationViewModel( } // hide loading indicator - _uiState.update { it.copy(showLoader = false) } + _showLoader.value = false } } - private fun isUserCommunityManager() = openGroup?.let { openGroup -> + private fun isUserCommunityManager(openGroup: OpenGroup?) = openGroup?.let { openGroup -> val userPublicKey = textSecurePreferences.getLocalNumber() ?: return@let false openGroupManager.isUserModerator(openGroup.id, userPublicKey, blindedPublicKey) } ?: false @@ -1055,7 +1018,7 @@ class ConversationViewModel( AudioSlidePlayer.getInstance()?.takeIf { it.audioSlide == audioSlide }?.stop() } - fun banUser(recipient: Recipient) = viewModelScope.launch { + fun banUser(recipient: Address) = viewModelScope.launch { repository.banUser(threadId, recipient) .onSuccess { showMessage(application.getString(R.string.banUserBanned)) @@ -1067,7 +1030,7 @@ class ConversationViewModel( fun banAndDeleteAll(messageRecord: MessageRecord) = viewModelScope.launch { - repository.banAndDeleteAll(threadId, messageRecord.individualRecipient) + repository.banAndDeleteAll(threadId, messageRecord.individualRecipient.address) .onSuccess { // At this point the server side messages have been successfully deleted.. showMessage(application.getString(R.string.banUserBanned)) @@ -1082,45 +1045,31 @@ class ConversationViewModel( fun acceptMessageRequest(): Job = viewModelScope.launch { val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for accept message request action") - val currentState = _uiState.value.messageRequestState as? MessageRequestUiState.Visible + val currentState = messageRequestState.value as? MessageRequestUiState.Visible ?: return@launch Log.w("Loki", "Current state was not visible for accept message request action") - _uiState.update { - it.copy(messageRequestState = MessageRequestUiState.Pending(currentState)) - } + _acceptingMessageRequest.value = true - repository.acceptMessageRequest(threadId, recipient) - .onSuccess { - _uiState.update { - it.copy(messageRequestState = MessageRequestUiState.Invisible) - } - } + repository.acceptMessageRequest(threadId, recipient.address) .onFailure { Log.w("Loki", "Couldn't accept message request due to error", it) - - _uiState.update { state -> - state.copy(messageRequestState = currentState) - } + _acceptingMessageRequest.value = false } } fun declineMessageRequest() = viewModelScope.launch { - repository.declineMessageRequest(threadId, recipient!!) - .onSuccess { - _uiState.update { it.copy(shouldExit = true) } - } + repository.declineMessageRequest(threadId, recipient!!.address) .onFailure { Log.w("Loki", "Couldn't decline message request due to error", it) } } private fun showMessage(message: String) { - _uiState.update { currentUiState -> - val messages = currentUiState.uiMessages + UiMessage( + _uiMessages.update { messages -> + messages + UiMessage( id = UUID.randomUUID().mostSignificantBits, message = message ) - currentUiState.copy(uiMessages = messages) } } @@ -1131,19 +1080,13 @@ class ConversationViewModel( } fun messageShown(messageId: Long) { - _uiState.update { currentUiState -> - val messages = currentUiState.uiMessages.filterNot { it.id == messageId } - currentUiState.copy(uiMessages = messages) + _uiMessages.update { messages -> + messages.filterNot { it.id == messageId } } } - fun updateRecipient() { - _recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId)) - updateAppBarData(recipient) - } - fun legacyBannerRecipient(context: Context): Recipient? = recipient?.run { - storage.getLastLegacyRecipient(address.toString())?.let { Recipient.from(context, Address.fromSerialized(it), false) } + storage.getLastLegacyRecipient(address.toString())?.let { recipientRepository.getRecipientSync(fromSerialized(it)) } } fun downloadPendingAttachment(attachment: DatabaseAttachment) { @@ -1166,11 +1109,11 @@ class ConversationViewModel( fun implicitlyApproveRecipient(): Job? { val recipient = recipient - if (uiState.value.messageRequestState is MessageRequestUiState.Visible) { + if (messageRequestState.value is MessageRequestUiState.Visible) { return acceptMessageRequest() - } else if (recipient?.isApproved == false) { + } else if (recipient?.approved == false) { // edge case for new outgoing thread on new recipient without sending approval messages - repository.setApproved(recipient, true) + repository.setApproved(recipient.address, true) } return null } @@ -1250,7 +1193,7 @@ class ConversationViewModel( } is Commands.NavigateToConversation -> { - _uiEvents.tryEmit(ConversationUiEvent.NavigateToConversation(command.threadId)) + _uiEvents.tryEmit(ConversationUiEvent.NavigateToConversation(command.address)) } } } @@ -1267,7 +1210,7 @@ class ConversationViewModel( fun validateMessageLength(): Boolean { // the message is too long if we have a negative char left in the input state - val charsLeft = _uiState.value.inputBarState.charLimitState?.count ?: 0 + val charsLeft = charLimitState.value?.count ?: 0 return if(charsLeft < 0){ // the user is trying to send a message that is too long - we should display a dialog // we currently have different logic for PRE and POST Pro launch @@ -1285,7 +1228,7 @@ class ConversationViewModel( } private fun handleCharLimitTappedForProUser(){ - if((_uiState.value.inputBarState.charLimitState?.count ?: 0) < 0){ + if((charLimitState.value?.count ?: 0) < 0){ showMessageTooLongDialog() } else { showMessageLengthDialog() @@ -1298,7 +1241,7 @@ class ConversationViewModel( fun showMessageLengthDialog(){ _dialogsState.update { - val charsLeft = _uiState.value.inputBarState.charLimitState?.count ?: 0 + val charsLeft = charLimitState.value?.count ?: 0 it.copy( showSimpleDialog = SimpleDialogData( title = application.getString(R.string.modalMessageCharacterDisplayTitle), @@ -1379,14 +1322,15 @@ class ConversationViewModel( } } - fun getUsername(accountId: String) = usernameUtils.getContactNameWithAccountID(accountId) + fun getUsername(accountId: String) = recipientRepository + .getRecipientDisplayNameSync(fromSerialized(accountId)) - fun onSearchOpened(){ - _appBarData.update { _appBarData.value.copy(showSearch = true) } + fun onSearchOpened() { + _searchOpened.value = true } fun onSearchClosed(){ - _appBarData.update { _appBarData.value.copy(showSearch = false) } + _searchOpened.value = false } private fun showDisappearingMessages() { @@ -1397,7 +1341,7 @@ class ConversationViewModel( } } - _uiEvents.tryEmit(ConversationUiEvent.ShowDisappearingMessages(threadId)) + _uiEvents.tryEmit(ConversationUiEvent.ShowDisappearingMessages(address)) } } @@ -1410,18 +1354,9 @@ class ConversationViewModel( } private fun showNotificationSettings() { - _uiEvents.tryEmit(ConversationUiEvent.ShowNotificationSettings(threadId)) + _uiEvents.tryEmit(ConversationUiEvent.ShowNotificationSettings(address)) } - fun onResume() { - // when resuming we want to check if the app bar has notification status data, if so update it if it has changed - if(currentAppBarNotificationState != null && recipient!= null){ - val newAppBarNotificationState = getNotificationStatusTitle(recipient!!) - if(currentAppBarNotificationState != newAppBarNotificationState){ - updateAppBarData(recipient) - } - } - } fun onTextChanged(text: CharSequence) { // check the character limit @@ -1439,76 +1374,12 @@ class ConversationViewModel( null } - _uiState.update { - it.copy( - inputBarState = it.inputBarState.copy( - charLimitState = charLimitState - ) - ) - } + _charLimitState.value = charLimitState } - @dagger.assisted.AssistedFactory - interface AssistedFactory { - fun create(threadId: Long, edKeyPair: KeyPair?): Factory - } - - @Suppress("UNCHECKED_CAST") - class Factory @AssistedInject constructor( - @Assisted private val threadId: Long, - @Assisted private val edKeyPair: KeyPair?, - private val application: Application, - private val repository: ConversationRepository, - private val storage: StorageProtocol, - private val messageDataProvider: MessageDataProvider, - private val groupDb: GroupDatabase, - private val threadDb: ThreadDatabase, - private val reactionDb: ReactionDatabase, - @ApplicationContext - private val context: Context, - private val lokiMessageDb: LokiMessageDatabase, - private val lokiAPIDb: LokiAPIDatabase, - private val textSecurePreferences: TextSecurePreferences, - private val configFactory: ConfigFactory, - private val groupManagerV2: GroupManagerV2, - private val callManager: CallManager, - private val legacyGroupDeprecationManager: LegacyGroupDeprecationManager, - private val dateUtils: DateUtils, - private val expiredGroupManager: ExpiredGroupManager, - private val usernameUtils: UsernameUtils, - private val avatarUtils: AvatarUtils, - private val recipientChangeSource: RecipientChangeSource, - private val openGroupManager: OpenGroupManager, - private val proStatusManager: ProStatusManager, - ) : ViewModelProvider.Factory { - - override fun create(modelClass: Class): T { - return ConversationViewModel( - threadId = threadId, - edKeyPair = edKeyPair, - application = application, - repository = repository, - storage = storage, - messageDataProvider = messageDataProvider, - groupDb = groupDb, - threadDb = threadDb, - reactionDb = reactionDb, - lokiMessageDb = lokiMessageDb, - lokiAPIDb = lokiAPIDb, - textSecurePreferences = textSecurePreferences, - configFactory = configFactory, - groupManagerV2 = groupManagerV2, - callManager = callManager, - legacyGroupDeprecationManager = legacyGroupDeprecationManager, - dateUtils = dateUtils, - expiredGroupManager = expiredGroupManager, - usernameUtils = usernameUtils, - avatarUtils = avatarUtils, - recipientChangeSource = recipientChangeSource, - openGroupManager = openGroupManager, - proStatusManager = proStatusManager, - ) as T - } + @AssistedFactory + interface Factory { + fun create(address: Address): ConversationViewModel } data class DialogsState( @@ -1556,27 +1427,18 @@ class ConversationViewModel( data object HideRecreateGroupConfirm : Commands data object HideRecreateGroup : Commands data object HideSessionProCTA : Commands - data class NavigateToConversation(val threadId: Long) : Commands + data class NavigateToConversation(val address: Address) : Commands } } data class UiMessage(val id: Long, val message: String) -data class ConversationUiState( - val uiMessages: List = emptyList(), - val messageRequestState: MessageRequestUiState = MessageRequestUiState.Invisible, - val shouldExit: Boolean = false, - val inputBarState: InputBarState = InputBarState(), - val showLoader: Boolean = false, -) - data class InputBarState( val contentState: InputBarContentState = InputBarContentState.Visible, // Note: These input media controls are with regard to whether the user can attach multimedia files // or record voice messages to be sent to a recipient - they are NOT things like video or audio // playback controls. val enableAttachMediaControls: Boolean = true, - val charLimitState: InputBarCharLimitState? = null, ) data class InputBarCharLimitState( @@ -1593,9 +1455,9 @@ sealed interface InputBarContentState { sealed interface ConversationUiEvent { - data class NavigateToConversation(val threadId: Long) : ConversationUiEvent - data class ShowDisappearingMessages(val threadId: Long) : ConversationUiEvent - data class ShowNotificationSettings(val threadId: Long) : ConversationUiEvent + data class NavigateToConversation(val address: Address) : ConversationUiEvent + data class ShowDisappearingMessages(val address: Address) : ConversationUiEvent + data class ShowNotificationSettings(val address: Address) : ConversationUiEvent data class ShowGroupMembers(val groupId: String) : ConversationUiEvent data object ShowUnblockConfirmation : ConversationUiEvent } @@ -1603,33 +1465,11 @@ sealed interface ConversationUiEvent { sealed interface MessageRequestUiState { data object Invisible : MessageRequestUiState - data class Pending(val prevState: Visible) : MessageRequestUiState + data object Pending : MessageRequestUiState data class Visible( - @StringRes val acceptButtonText: Int, + @param:StringRes val acceptButtonText: Int, // If null, the block button shall not be shown val blockButtonText: String? = null ) : MessageRequestUiState } - -data class RetrieveOnce(val retrieval: () -> T?) { - private var triedToRetrieve: Boolean = false - private var _value: T? = null - - val value: T? - get() { - synchronized(this) { - if (triedToRetrieve) { - return _value - } - - triedToRetrieve = true - _value = retrieval() - return _value - } - } - - fun updateTo(value: T?) { - _value = value - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt index 8d1035cca2..135587a8e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.MmsSmsDatabase +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageId @@ -64,7 +65,8 @@ class MessageDetailsViewModel @AssistedInject constructor( private val avatarUtils: AvatarUtils, private val dateUtils: DateUtils, messageDataProvider: MessageDataProvider, - storage: Storage + storage: Storage, + private val recipientRepository: RecipientRepository, ) : ViewModel() { private val state = MutableStateFlow(MessageDetailsState()) val stateFlow = state.asStateFlow() @@ -76,6 +78,7 @@ class MessageDetailsViewModel @AssistedInject constructor( storage = storage, messageDataProvider = messageDataProvider, scope = viewModelScope, + recipientRepository = recipientRepository, ) init { @@ -114,9 +117,9 @@ class MessageDetailsViewModel @AssistedInject constructor( state.value = messageRecord.run { val slides = mmsRecord?.slideDeck?.slides ?: emptyList() - val conversation = threadDb.getRecipientForThreadId(threadId)!! - val isDeprecatedLegacyGroup = conversation.isLegacyGroupRecipient && - deprecationManager.isDeprecated + val conversationAddress = threadDb.getRecipientForThreadId(threadId)!! + val conversation = recipientRepository.getRecipient(conversationAddress) ?: Recipient.empty(conversationAddress) + val isDeprecatedLegacyGroup = conversationAddress.isLegacyGroup && deprecationManager.isDeprecated val errorString = lokiMessageDatabase.getErrorMessage(messageId) @@ -132,7 +135,7 @@ class MessageDetailsViewModel @AssistedInject constructor( } val sender = if(messageRecord.isOutgoing){ - Recipient.from(context, Address.fromSerialized(prefs.getLocalNumber() ?: ""), false) + recipientRepository.getRecipient(Address.fromSerialized(prefs.getLocalNumber()!!))!! } else individualRecipient val attachments = slides.map(::Attachment) @@ -168,7 +171,7 @@ class MessageDetailsViewModel @AssistedInject constructor( status = status, senderInfo = sender.run { TitledText( - if(messageRecord.isOutgoing) context.getString(R.string.you) else name, + if(messageRecord.isOutgoing) context.getString(R.string.you) else displayName, address.toString() ) }, @@ -223,7 +226,7 @@ class MessageDetailsViewModel @AssistedInject constructor( if(state.thread == null) return viewModelScope.launch { - MediaPreviewArgs(slide, state.mmsRecord, state.thread) + MediaPreviewArgs(slide, state.mmsRecord, state.thread.address) .let(Event::StartMediaPreview) .let { event.send(it) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt index b85f0f6d9c..a1d04a445e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt @@ -16,8 +16,8 @@ import com.squareup.phrase.Phrase import network.loki.messenger.R import network.loki.messenger.databinding.AlbumThumbnailViewBinding import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.session.libsession.utilities.Address import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY -import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView @@ -49,7 +49,7 @@ class AlbumThumbnailView : RelativeLayout { // region Interaction - fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient, downloadPendingAttachment: (DatabaseAttachment) -> Unit) { + fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Address, downloadPendingAttachment: (DatabaseAttachment) -> Unit) { val rawXInt = event.rawX.toInt() val rawYInt = event.rawY.toInt() val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt index 3077d227e3..f9a10c602d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt @@ -5,7 +5,7 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.widget.LinearLayout import network.loki.messenger.databinding.ViewConversationTypingContainerBinding -import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.Address class TypingIndicatorViewContainer : LinearLayout { private lateinit var binding: ViewConversationTypingContainerBinding @@ -18,7 +18,7 @@ class TypingIndicatorViewContainer : LinearLayout { binding = ViewConversationTypingContainerBinding.inflate(LayoutInflater.from(context), this, true) } - fun setTypists(typists: List) { + fun setTypists(typists: List
) { if (typists.isEmpty()) { binding.typingIndicator.root.stopAnimation(); return } binding.typingIndicator.root.startAnimation() } 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 2554431547..451dcc3031 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 @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.conversation.v2.dialogs import android.app.Dialog -import android.content.Context import android.graphics.Typeface import android.os.Bundle import android.text.Spannable @@ -10,13 +9,13 @@ import android.text.style.StyleSpan import androidx.fragment.app.DialogFragment import network.loki.messenger.R import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.utilities.Address import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY -import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.createSessionDialog import org.thoughtcrime.securesms.ui.getSubbedCharSequence /** Shown upon sending a message to a user that's blocked. */ -class BlockedDialog(private val recipient: Recipient, private val contactName: String) : DialogFragment() { +class BlockedDialog(private val recipient: Address, private val contactName: String) : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { val explanationCS = context.getSubbedCharSequence(R.string.blockUnblockName, NAME_KEY to contactName) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt index baba827140..2bcfa785d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt @@ -9,8 +9,8 @@ import network.loki.messenger.R import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment -import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY +import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.createSessionDialog import org.thoughtcrime.securesms.database.SessionContactDatabase import org.thoughtcrime.securesms.util.createAndStartAttachmentDownload @@ -20,7 +20,7 @@ import javax.inject.Inject * they are to be trusted and files sent by them are to be downloaded. */ @AndroidEntryPoint class AutoDownloadDialog(private val threadRecipient: Recipient, - private val databaseAttachment: DatabaseAttachment + private val databaseAttachment: DatabaseAttachment ) : DialogFragment() { @Inject lateinit var storage: StorageProtocol @@ -30,7 +30,7 @@ class AutoDownloadDialog(private val threadRecipient: Recipient, title(getString(R.string.attachmentsAutoDownloadModalTitle)) val explanation = Phrase.from(context, R.string.attachmentsAutoDownloadModalDescription) - .put(CONVERSATION_NAME_KEY, threadRecipient.name) + .put(CONVERSATION_NAME_KEY, threadRecipient.displayName) .format() text(explanation) @@ -42,7 +42,7 @@ class AutoDownloadDialog(private val threadRecipient: Recipient, } private fun setAutoDownload() { - storage.setAutoDownloadAttachments(threadRecipient, true) + storage.setAutoDownloadAttachments(threadRecipient.address, true) JobQueue.shared.createAndStartAttachmentDownload(databaseAttachment) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt index 95b87b5e02..bb6287394a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt @@ -22,6 +22,7 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.conversation.v2.InputBarCharLimitState import org.thoughtcrime.securesms.conversation.v2.InputBarContentState import org.thoughtcrime.securesms.conversation.v2.InputBarState import org.thoughtcrime.securesms.conversation.v2.components.LinkPreviewDraftView @@ -32,7 +33,6 @@ import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.util.addTextChangedListener import org.thoughtcrime.securesms.util.contains -import org.thoughtcrime.securesms.util.setSafeOnClickListener // TODO: A lot of the logic regarding voice messages is currently performed in the ConversationActivity // TODO: and here - it would likely be best to move this into the CA's ViewModel. @@ -316,16 +316,18 @@ class InputBar @JvmOverloads constructor( // handle buttons state allowAttachMultimediaButtons = state.enableAttachMediaControls + } + fun setCharLimitState(state: InputBarCharLimitState?) { // handle char limit - if(state.charLimitState != null){ - binding.characterLimitText.text = state.charLimitState.count.toString() - binding.characterLimitText.setTextColor(if(state.charLimitState.danger) dangerColor else textColor) + if(state != null){ + binding.characterLimitText.text = state.count.toString() + binding.characterLimitText.setTextColor(if(state.danger) dangerColor else textColor) binding.characterLimitContainer.setOnClickListener { delegate?.onCharLimitTapped() } - binding.badgePro.isVisible = state.charLimitState.showProBadge + binding.badgePro.isVisible = state.showProBadge binding.characterLimitContainer.isVisible = true } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt index 1dc06630f0..16bb83c446 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt @@ -9,10 +9,11 @@ import android.text.Spanned import android.text.style.StyleSpan import androidx.core.text.getSpans import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -29,18 +30,16 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import network.loki.messenger.R import network.loki.messenger.libsession_util.allWithStatus -import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.utilities.Address import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsignal.utilities.AccountId -import org.session.libsignal.utilities.IdPrefix import org.thoughtcrime.securesms.database.DatabaseContentProviders.Conversation import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.GroupMemberDatabase import org.thoughtcrime.securesms.database.MmsDatabase -import org.thoughtcrime.securesms.database.SessionContactDatabase +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.ThreadDatabase -import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.util.observeChanges /** @@ -50,19 +49,20 @@ import org.thoughtcrime.securesms.util.observeChanges * 1. Observe the [autoCompleteState] to get the mention search results. * 2. Set the EditText's editable factory to [editableFactory], via [android.widget.EditText.setEditableFactory] */ -class MentionViewModel( +@HiltViewModel(assistedFactory = MentionViewModel.Factory::class) +class MentionViewModel @AssistedInject constructor( application: Application, - threadID: Long, + @Assisted address: Address, contentResolver: ContentResolver, threadDatabase: ThreadDatabase, groupDatabase: GroupDatabase, mmsDatabase: MmsDatabase, - contactDatabase: SessionContactDatabase, memberDatabase: GroupMemberDatabase, storage: Storage, configFactory: ConfigFactoryProtocol, - dispatcher: CoroutineDispatcher = Dispatchers.IO, + recipientRepository: RecipientRepository, ) : ViewModel() { + private val dispatcher: CoroutineDispatcher = Dispatchers.Default private val editable = MentionEditable() /** @@ -85,35 +85,33 @@ class MentionViewModel( @Suppress("OPT_IN_USAGE") private val members: StateFlow?> = - (contentResolver.observeChanges(Conversation.getUriForThread(threadID)) as Flow) - .debounce(500L) - .onStart { emit(Unit) } + recipientRepository.observeRecipient(address) .mapLatest { - val recipient = checkNotNull(threadDatabase.getRecipientForThreadId(threadID)) { - "Recipient not found for thread ID: $threadID" + val threadID = withContext(Dispatchers.Default) { + threadDatabase.getThreadIdIfExistsFor(address) } val memberIDs = when { - recipient.isLegacyGroupRecipient -> { - groupDatabase.getGroupMemberAddresses(recipient.address.toGroupString(), false) + address.isLegacyGroup -> { + groupDatabase.getGroupMemberAddresses(address.toGroupString(), false) .map { it.toString() } } - recipient.isGroupV2Recipient -> { - storage.getMembers(recipient.address.toString()).map { it.accountId() } + address.isGroupV2 -> { + storage.getMembers(address.toString()).map { it.accountId() } } - recipient.isCommunityRecipient -> mmsDatabase.getRecentChatMemberIDs(threadID, 20) - recipient.isContactRecipient -> listOf(recipient.address.toString()) + address.isCommunity -> mmsDatabase.getRecentChatMemberIDs(threadID, 20) + address.isContact -> listOf(address.address) else -> listOf() } - val openGroup = if (recipient.isCommunityRecipient) { + val openGroup = if (address.isCommunity) { storage.getOpenGroup(threadID) } else { null } - val moderatorIDs = if (recipient.isCommunityRecipient) { + val moderatorIDs = if (address.isCommunity) { val groupId = openGroup?.id if (groupId.isNullOrBlank()) { emptySet() @@ -123,8 +121,8 @@ class MentionViewModel( memberId.takeIf { roles.any { it.isModerator } } } } - } else if (recipient.isGroupV2Recipient) { - configFactory.withGroupConfigs(AccountId(recipient.address.toString())) { + } else if (address.isGroupV2) { + configFactory.withGroupConfigs(AccountId(address.toString())) { it.groupMembers.allWithStatus() .filter { (member, status) -> member.isAdminOrBeingPromoted(status) } .mapTo(hashSetOf()) { (member, _) -> member.accountId() } @@ -133,12 +131,6 @@ class MentionViewModel( emptySet() } - val contactContext = if (recipient.isCommunityRecipient) { - Contact.ContactContext.OPEN_GROUP - } else { - Contact.ContactContext.REGULAR - } - val myId = if (openGroup != null) { requireNotNull(storage.getUserBlindedAccountId(openGroup.publicKey)).hexString } else { @@ -152,14 +144,16 @@ class MentionViewModel( isModerator = myId in moderatorIDs, isMe = true ) - ) + contactDatabase.getContacts(memberIDs) + ) + memberIDs .asSequence() - .filter { it.accountID != myId } + .filter { it != myId } + .mapNotNull { recipientRepository.getRecipientSync(Address.fromSerialized(it)) } + .filter { !it.isGroupOrCommunityRecipient } .map { contact -> Member( - publicKey = contact.accountID, - name = contact.displayName(contactContext), - isModerator = contact.accountID in moderatorIDs, + publicKey = contact.address.toString(), + name = contact.displayName, + isModerator = contact.address.address in moderatorIDs, isMe = false ) }) @@ -299,37 +293,8 @@ class MentionViewModel( object Error : AutoCompleteState } - @dagger.assisted.AssistedFactory - interface AssistedFactory { - fun create(threadId: Long): Factory - } - - class Factory @AssistedInject constructor( - @Assisted private val threadId: Long, - private val contentResolver: ContentResolver, - private val threadDatabase: ThreadDatabase, - private val groupDatabase: GroupDatabase, - private val mmsDatabase: MmsDatabase, - private val contactDatabase: SessionContactDatabase, - private val storage: Storage, - private val memberDatabase: GroupMemberDatabase, - private val configFactory: ConfigFactoryProtocol, - private val application: Application, - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return MentionViewModel( - threadID = threadId, - contentResolver = contentResolver, - threadDatabase = threadDatabase, - groupDatabase = groupDatabase, - mmsDatabase = mmsDatabase, - contactDatabase = contactDatabase, - memberDatabase = memberDatabase, - storage = storage, - configFactory = configFactory, - application = application, - ) as T - } + @AssistedFactory + interface Factory { + fun create(address: Address): MentionViewModel } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt index 61017dbff7..859e62e041 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt @@ -67,7 +67,7 @@ class ConversationActionModeCallback( )?.pubKey?.data } ?.let { AccountId(IdPrefix.BLINDED, it) }?.hexString - val isDeprecatedLegacyGroup = thread.isLegacyGroupRecipient && + val isDeprecatedLegacyGroup = thread.isLegacyGroup && deprecationManager.isDeprecated // Embedded function @@ -95,7 +95,7 @@ class ConversationActionModeCallback( menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText // Copy Account ID menu.findItem(R.id.menu_context_copy_public_key).isVisible = - (thread.isGroupOrCommunityRecipient && !thread.isCommunityRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey) + (thread.isGroupOrCommunity && !thread.isCommunity && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey) // Message detail menu.findItem(R.id.menu_message_details).isVisible = selectedItems.size == 1 && !isDeprecatedLegacyGroup // Resend diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/AttachmentControlView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/AttachmentControlView.kt index e7958fa984..bf8547af17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/AttachmentControlView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/AttachmentControlView.kt @@ -145,7 +145,7 @@ class AttachmentControlView: LinearLayout { // region Interaction fun showDownloadDialog(threadRecipient: Recipient, attachment: DatabaseAttachment) { - if (!storage.shouldAutoDownloadAttachments(threadRecipient)) { + if (threadRecipient.autoDownloadAttachments != true) { // just download (context.findActivity() as? ActivityDispatcher)?.showDialog(AutoDownloadDialog(threadRecipient, attachment)) } 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 d0f2b6d47f..304bfbc7ce 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 @@ -30,6 +30,7 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOT import org.session.libsession.utilities.getColorFromAttr import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessages import org.thoughtcrime.securesms.conversation.disappearingmessages.expiryMode +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.permissions.Permissions @@ -68,6 +69,7 @@ class ControlMessageView : LinearLayout { @Inject lateinit var disappearingMessages: DisappearingMessages @Inject lateinit var dateUtils: DateUtils + @Inject lateinit var recipientRepository: RecipientRepository val controlContentView: View get() = binding.controlContentView @@ -91,7 +93,7 @@ class ControlMessageView : LinearLayout { val threadRecipient = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(message.threadId) - if (threadRecipient?.isGroupRecipient == true) { + if (threadRecipient?.isGroup == true) { expirationTimerView.setTimerIcon() } else { expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn) @@ -99,8 +101,8 @@ class ControlMessageView : LinearLayout { followSetting.isVisible = ExpirationConfiguration.isNewConfigEnabled && !message.isOutgoing - && message.expiryMode != (MessagingModuleConfiguration.shared.storage.getExpirationConfiguration(message.threadId)?.expiryMode ?: ExpiryMode.NONE) - && threadRecipient?.isGroupOrCommunityRecipient != true + && message.expiryMode != MessagingModuleConfiguration.shared.storage.getExpirationConfiguration(message.threadId) + && threadRecipient?.isGroupOrCommunity != true if (followSetting.isVisible) { binding.controlContentView.setOnClickListener { disappearingMessages.showFollowSettingDialog(context, message) } @@ -130,9 +132,10 @@ class ControlMessageView : LinearLayout { val me = TextSecurePreferences.getLocalNumber(context) binding.textView.text = if(me == msgRecipient) { // you accepted the user's request val threadRecipient = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(message.threadId) + ?.let { recipientRepository.getRecipientSync(it) } context.getSubbedCharSequence( R.string.messageRequestYouHaveAccepted, - NAME_KEY to (threadRecipient?.name ?: "") + NAME_KEY to (threadRecipient?.displayName ?: "") ) } else { // they accepted your request context.getString(R.string.messageRequestsAccepted) @@ -187,13 +190,13 @@ class ControlMessageView : LinearLayout { context.showSessionDialog { val titleTxt = context.getSubbedString( R.string.callsMissedCallFrom, - NAME_KEY to message.individualRecipient.name + NAME_KEY to message.individualRecipient.displayName ) title(titleTxt) val bodyTxt = context.getSubbedCharSequence( R.string.callsYouMissedCallPermissions, - NAME_KEY to message.individualRecipient.name + NAME_KEY to message.individualRecipient.displayName ) text(bodyTxt) @@ -216,13 +219,13 @@ class ControlMessageView : LinearLayout { context.showSessionDialog { val titleTxt = context.getSubbedString( R.string.callsMissedCallFrom, - NAME_KEY to message.individualRecipient.name + NAME_KEY to message.individualRecipient.displayName ) title(titleTxt) val bodyTxt = context.getSubbedCharSequence( R.string.callsMicrophonePermissionsRequired, - NAME_KEY to message.individualRecipient.name + NAME_KEY to message.individualRecipient.displayName ) text(bodyTxt) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt index 07a7d05623..4fbb50f62b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt @@ -13,13 +13,12 @@ import com.bumptech.glide.RequestManager import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.ViewQuoteBinding -import org.session.libsession.messaging.contacts.Contact -import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.Address import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.recipients.Recipient -import org.session.libsession.utilities.truncateIdForDisplay +import org.session.libsession.utilities.recipients.displayNameOrFallback import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities -import org.thoughtcrime.securesms.database.SessionContactDatabase +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.toPx @@ -34,7 +33,7 @@ import javax.inject.Inject @AndroidEntryPoint class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ConstraintLayout(context, attrs) { - @Inject lateinit var contactDb: SessionContactDatabase + @Inject lateinit var recipientRepository: RecipientRepository private val binding: ViewQuoteBinding by lazy { ViewQuoteBinding.bind(this) } private val vPadding by lazy { toPx(6, resources) } @@ -67,16 +66,15 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? // region Updating fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient, - isOutgoingMessage: Boolean, isOpenGroupInvitation: Boolean, threadID: Long, - isOriginalMissing: Boolean, glide: RequestManager) { + isOutgoingMessage: Boolean, isOpenGroupInvitation: Boolean, threadID: Long, + isOriginalMissing: Boolean, glide: RequestManager) { // Author - val author = contactDb.getContactWithAccountID(authorPublicKey) - val localNumber = TextSecurePreferences.getLocalNumber(context) - val quoteIsLocalUser = localNumber != null && authorPublicKey == localNumber + val author = recipientRepository.getRecipientSyncOrEmpty(Address.fromSerialized(authorPublicKey)) val authorDisplayName = - if (quoteIsLocalUser) context.getString(R.string.you) - else author?.displayName(Contact.contextForRecipient(thread)) ?: truncateIdForDisplay(authorPublicKey) + if (author.isLocalNumber) context.getString(R.string.you) + else author.displayNameOrFallback(address = authorPublicKey) + binding.quoteViewAuthorTextView.text = authorDisplayName val textColor = getTextColor(isOutgoingMessage) binding.quoteViewAuthorTextView.setTextColor(textColor) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index fb3fb851f6..9f02060fd9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -309,7 +309,7 @@ class VisibleMessageContentView : ConstraintLayout { binding.albumThumbnailView.root.calculateHitObject( event, message, - thread, + thread.address, downloadPendingAttachment ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 734dbcbfc0..327a0fae1c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -33,18 +33,14 @@ import network.loki.messenger.databinding.ViewEmojiReactionsBinding import network.loki.messenger.databinding.ViewVisibleMessageBinding import network.loki.messenger.databinding.ViewstubVisibleMessageMarkerContainerBinding import network.loki.messenger.libsession_util.getOrNull -import org.session.libsession.messaging.contacts.Contact -import org.session.libsession.messaging.contacts.Contact.ContactContext import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ThemeUtil.getThemedColor -import org.session.libsession.utilities.UsernameUtils import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.modifyLayoutParams +import org.session.libsession.utilities.recipients.displayNameOrFallback import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.IdPrefix import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 @@ -53,6 +49,7 @@ import org.thoughtcrime.securesms.database.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase 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.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageId @@ -86,8 +83,8 @@ class VisibleMessageView : FrameLayout { @Inject lateinit var mmsDb: MmsDatabase @Inject lateinit var dateUtils: DateUtils @Inject lateinit var configFactory: ConfigFactoryProtocol - @Inject lateinit var usernameUtils: UsernameUtils @Inject lateinit var openGroupManager: OpenGroupManager + @Inject lateinit var recipientRepository: RecipientRepository private val binding = ViewVisibleMessageBinding.inflate(LayoutInflater.from(context), this, true) @@ -163,9 +160,6 @@ class VisibleMessageView : FrameLayout { next: MessageRecord? = null, glide: RequestManager = Glide.with(this), searchQuery: String? = null, - contact: Contact? = null, - groupId: AccountId? = null, - senderAccountID: String, lastSeen: Long, lastSentMessageId: MessageId?, delegate: VisibleMessageViewDelegate? = null, @@ -180,18 +174,20 @@ class VisibleMessageView : FrameLayout { isOutgoing = message.isOutgoing replyDisabled = message.isOpenGroupInvitation val threadID = message.threadId - val thread = threadDb.getRecipientForThreadId(threadID) ?: return - val isGroupThread = thread.isGroupOrCommunityRecipient + val threadRecipient = threadDb.getRecipientForThreadId(threadID)?.let(recipientRepository::getRecipientSync) ?: return + val isGroupThread = threadRecipient.isGroupOrCommunityRecipient val isStartOfMessageCluster = isStartOfMessageCluster(message, previous, isGroupThread) val isEndOfMessageCluster = isEndOfMessageCluster(message, next, isGroupThread) // Show profile picture and sender name if this is a group thread AND the message is incoming binding.moderatorIconImageView.isVisible = false binding.profilePictureView.visibility = when { - thread.isGroupOrCommunityRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE - thread.isGroupOrCommunityRecipient -> View.INVISIBLE + threadRecipient.isGroupOrCommunityRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE + threadRecipient.isGroupOrCommunityRecipient -> View.INVISIBLE else -> View.GONE } + val sender = message.individualRecipient + val bottomMargin = if (isEndOfMessageCluster) resources.getDimensionPixelSize(R.dimen.small_spacing) else ViewUtil.dpToPx(context,2) @@ -207,30 +203,32 @@ class VisibleMessageView : FrameLayout { if (isGroupThread && !message.isOutgoing) { if (isEndOfMessageCluster) { - binding.profilePictureView.publicKey = senderAccountID - binding.profilePictureView.update(message.individualRecipient) + binding.profilePictureView.update(sender) binding.profilePictureView.setOnClickListener { - if (thread.isCommunityRecipient) { - val openGroup = lokiThreadDb.getOpenGroupChat(threadID) - if (IdPrefix.fromValue(senderAccountID) == IdPrefix.BLINDED && openGroup?.canWrite == true) { + if (threadRecipient.isCommunityRecipient) { + val openGroup = lokiThreadDb.getOpenGroupChat(message.threadId) + if (IdPrefix.fromValue(sender.address.address) == IdPrefix.BLINDED && openGroup?.canWrite == true) { // TODO: support v2 soon - val intent = Intent(context, ConversationActivityV2::class.java) - intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID) - intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(senderAccountID)) - context.startActivity(intent) + context.startActivity( + ConversationActivityV2.createIntent( + context = context, + address = sender.address, + fromGroupThreadId = message.threadId + ) + ) } } else { - maybeShowUserDetails(senderAccountID, threadID) + maybeShowUserDetails(sender.address.address, message.threadId) } } - if (thread.isCommunityRecipient) { - val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return + if (threadRecipient.isCommunityRecipient) { + val openGroup = lokiThreadDb.getOpenGroupChat(message.threadId) ?: return var standardPublicKey = "" var blindedPublicKey: String? = null - if (IdPrefix.fromValue(senderAccountID)?.isBlinded() == true) { - blindedPublicKey = senderAccountID + if (IdPrefix.fromValue(sender.address.address)?.isBlinded() == true) { + blindedPublicKey = sender.address.address } else { - standardPublicKey = senderAccountID + standardPublicKey = sender.address.address } val isModerator = openGroupManager.isUserModerator( openGroup.groupId, @@ -239,15 +237,15 @@ class VisibleMessageView : FrameLayout { ) binding.moderatorIconImageView.isVisible = isModerator } - else if (thread.isLegacyGroupRecipient) { // legacy groups - val groupRecord = groupDb.getGroup(thread.address.toGroupString()).orNull() - val isAdmin: Boolean = groupRecord?.admins?.contains(fromSerialized(senderAccountID)) ?: false + else if (threadRecipient.isLegacyGroupRecipient) { // legacy groups + val groupRecord = groupDb.getGroup(threadRecipient.address.toGroupString()).orNull() + val isAdmin: Boolean = groupRecord?.admins?.contains(sender.address) ?: false binding.moderatorIconImageView.isVisible = isAdmin } - else if (thread.isGroupV2Recipient) { // groups v2 - val isAdmin = configFactory.withGroupConfigs(AccountId(thread.address.toString())) { - it.groupMembers.getOrNull(senderAccountID)?.admin == true + else if (threadRecipient.isGroupV2Recipient) { // groups v2 + val isAdmin = configFactory.withGroupConfigs(AccountId(threadRecipient.address.toString())) { + it.groupMembers.getOrNull(sender.address.address)?.admin == true } binding.moderatorIconImageView.isVisible = isAdmin @@ -255,14 +253,7 @@ class VisibleMessageView : FrameLayout { } } binding.senderNameTextView.isVisible = !message.isOutgoing && (isStartOfMessageCluster && (isGroupThread || snIsSelected)) - val contactContext = - if (thread.isCommunityRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR - binding.senderNameTextView.text = usernameUtils.getContactNameWithAccountID( - contact = contact, - accountID = senderAccountID, - contactContext = contactContext, - groupId = groupId - ) + binding.senderNameTextView.text = sender.displayNameOrFallback(address = sender.address.address) // Unread marker val shouldShowUnreadMarker = lastSeen != -1L && message.timestamp > lastSeen && (previous == null || previous.timestamp <= lastSeen) && !message.isOutgoing @@ -285,7 +276,7 @@ class VisibleMessageView : FrameLayout { // Emoji Reactions if (!message.isDeleted && message.reactions.isNotEmpty()) { - val capabilities = lokiThreadDb.getOpenGroupChat(threadID)?.server?.let { lokiApiDb.getServerCapabilities(it) } + val capabilities = lokiThreadDb.getOpenGroupChat(message.threadId)?.server?.let { lokiApiDb.getServerCapabilities(it) } if (capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase())) { emojiReactionsBinding.value.root.let { root -> root.setReactions(message.messageId, message.reactions, message.isOutgoing, delegate) @@ -309,7 +300,7 @@ class VisibleMessageView : FrameLayout { isStartOfMessageCluster, isEndOfMessageCluster, glide, - thread, + threadRecipient, searchQuery, downloadPendingAttachment = downloadPendingAttachment, retryFailedAttachments = retryFailedAttachments, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsActivity.kt index 4317090da8..c339446c3f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsActivity.kt @@ -13,13 +13,11 @@ import javax.inject.Inject class ConversationSettingsActivity: FullComposeScreenLockActivity() { companion object { - const val THREAD_ID = "conversation_settings_thread_id" const val THREAD_ADDRESS = "conversation_settings_thread_address" - fun createIntent(context: Context, threadId: Long, threadAddress: Address?): Intent { + fun createIntent(context: Context, address: Address): Intent { return Intent(context, ConversationSettingsActivity::class.java).apply { - putExtra(THREAD_ID, threadId) - putExtra(THREAD_ADDRESS, threadAddress) + putExtra(THREAD_ADDRESS, address) } } } @@ -30,8 +28,9 @@ class ConversationSettingsActivity: FullComposeScreenLockActivity() { @Composable override fun ComposeContent() { ConversationSettingsNavHost( - threadId = intent.getLongExtra(THREAD_ID, 0), - threadAddress = IntentCompat.getParcelableExtra(intent, THREAD_ADDRESS, Address::class.java), + address = requireNotNull(IntentCompat.getParcelableExtra(intent, THREAD_ADDRESS, Address::class.java)) { + "ConversationSettingsActivity requires an Address to be passed in the intent." + }, navigator = navigator, returnResult = { code, value -> setResult(RESULT_OK, Intent().putExtra(code, value)) 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 10b29b5ecc..77a42eccec 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 @@ -102,8 +102,7 @@ sealed interface ConversationSettingsDestination { @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun ConversationSettingsNavHost( - threadId: Long, - threadAddress: Address?, + address: Address, navigator: ConversationSettingsNavigator, returnResult: (String, Boolean) -> Unit, onBack: () -> Unit @@ -181,7 +180,7 @@ fun ConversationSettingsNavHost( ) { val viewModel = hiltViewModel { factory -> - factory.create(threadId) + factory.create(address) } val lifecycleOwner = LocalLifecycleOwner.current @@ -316,7 +315,7 @@ fun ConversationSettingsNavHost( val viewModel: DisappearingMessagesViewModel = hiltViewModel { factory -> factory.create( - threadId = threadId, + address = address, isNewConfigEnabled = ExpirationConfiguration.isNewConfigEnabled, showDebugOptions = BuildConfig.DEBUG ) @@ -332,14 +331,9 @@ fun ConversationSettingsNavHost( // All Media horizontalSlideComposable { - if (threadAddress == null) { - navController.popBackStack() - return@horizontalSlideComposable - } - val viewModel = hiltViewModel { factory -> - factory.create(threadAddress) + factory.create(address) } MediaOverviewScreen( @@ -354,7 +348,7 @@ fun ConversationSettingsNavHost( horizontalSlideComposable { val viewModel = hiltViewModel { factory -> - factory.create(threadId) + factory.create(address) } NotificationSettingsScreen( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt index b1e45f8178..ba01f7d2cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt @@ -11,7 +11,6 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity.CLIPBOARD_SERVICE import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.cash.copper.flow.observeQuery import com.bumptech.glide.Glide import com.squareup.phrase.Phrase import dagger.assisted.Assisted @@ -22,16 +21,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -42,12 +34,11 @@ import network.loki.messenger.libsession_util.util.BlindKeyAPI import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupInfo import org.session.libsession.database.StorageProtocol -import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.open_groups.OpenGroup +import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.ConfigFactoryProtocol -import org.session.libsession.utilities.ConfigUpdateNotification import org.session.libsession.utilities.ExpirationUtil import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY @@ -56,36 +47,34 @@ import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.getGroup import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.upsertContact import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.textSizeInBytes -import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.LokiThreadDatabase import org.thoughtcrime.securesms.database.RecipientDatabase -import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.dependencies.ConfigFactory.Companion.MAX_GROUP_DESCRIPTION_BYTES import org.thoughtcrime.securesms.dependencies.ConfigFactory.Companion.MAX_NAME_BYTES import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.home.HomeActivity import org.thoughtcrime.securesms.repository.ConversationRepository -import org.thoughtcrime.securesms.ui.DialogButtonData import org.thoughtcrime.securesms.ui.SimpleDialogData import org.thoughtcrime.securesms.ui.getSubbedString import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUtils import org.thoughtcrime.securesms.util.avatarOptions -import org.thoughtcrime.securesms.util.observeChanges import kotlin.math.min @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @HiltViewModel(assistedFactory = ConversationSettingsViewModel.Factory::class) class ConversationSettingsViewModel @AssistedInject constructor( - @Assisted private val threadId: Long, - @ApplicationContext private val context: Context, + @Assisted private val address: Address, + @param:ApplicationContext private val context: Context, private val avatarUtils: AvatarUtils, private val repository: ConversationRepository, private val configFactory: ConfigFactoryProtocol, @@ -93,14 +82,20 @@ class ConversationSettingsViewModel @AssistedInject constructor( private val conversationRepository: ConversationRepository, private val textSecurePreferences: TextSecurePreferences, private val navigator: ConversationSettingsNavigator, - private val threadDb: ThreadDatabase, private val groupManagerV2: GroupManagerV2, private val prefs: TextSecurePreferences, private val lokiThreadDatabase: LokiThreadDatabase, private val groupManager: GroupManagerV2, private val openGroupManager: OpenGroupManager, + private val recipientRepository: RecipientRepository, ) : ViewModel() { + private val threadId by lazy { + requireNotNull(storage.getThreadId(address)) { + "Thread doesn't exist for this conversation" + } + } + private val _uiState: MutableStateFlow = MutableStateFlow( UIState( avatarUIData = AvatarUIData(emptyList()) @@ -335,26 +330,12 @@ class ConversationSettingsViewModel @AssistedInject constructor( init { // update data when we have a recipient and update when there are changes from the thread or recipient - viewModelScope.launch(Dispatchers.Default) { - repository.recipientUpdateFlow(threadId) // get the recipient - .flatMapLatest { recipient -> // get updates from the thread or recipient - merge( - context.contentResolver - .observeQuery(DatabaseContentProviders.Recipient.CONTENT_URI), // recipient updates - (context.contentResolver.observeChanges( - DatabaseContentProviders.Conversation.getUriForThread(threadId) - ) as Flow<*>), // thread updates - configFactory.configUpdateNotifications.filterIsInstance() - .filter { it.groupId.hexString == recipient?.address?.toString() } - ).map { - recipient // return the recipient - } - .debounce(200L) - .onStart { emit(recipient) } // make sure there's a value straight away - } + viewModelScope.launch { + recipientRepository.observeRecipient(address) + .filterNotNull() .collect { recipient = it - getStateFromRecipient() + getStateFromRecipient(it) } } } @@ -400,8 +381,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( } } - private suspend fun getStateFromRecipient(){ - val conversation = recipient ?: return + private suspend fun getStateFromRecipient(conversation: Recipient){ val configContact = configFactory.withUserConfigs { configs -> configs.contacts.get(conversation.address.toString()) } @@ -466,10 +446,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( // name val name = when { conversation.isLocalNumber -> context.getString(R.string.noteToSelf) - - conversation.isGroupV2Recipient -> getGroupName() - - else -> conversation.name + else -> conversation.displayName } // account ID @@ -479,15 +456,15 @@ class ConversationSettingsViewModel @AssistedInject constructor( } // disappearing message type - val expiration = storage.getExpirationConfiguration(threadId) - val disappearingSubtitle = if(expiration?.isEnabled == true) { + val expiryMode = recipient?.expiryMode + val disappearingSubtitle = if(expiryMode != null && expiryMode != ExpiryMode.NONE) { // Get the type of disappearing message and the abbreviated duration.. - val dmTypeString = when (expiration.expiryMode) { + val dmTypeString = when (expiryMode) { is ExpiryMode.AfterRead -> R.string.disappearingMessagesDisappearAfterReadState else -> R.string.disappearingMessagesDisappearAfterSendState } val durationAbbreviated = - ExpirationUtil.getExpirationAbbreviatedDisplayValue(expiration.expiryMode.expirySeconds) + ExpirationUtil.getExpirationAbbreviatedDisplayValue(expiryMode.expirySeconds) // ..then substitute into the string.. context.getSubbedString( @@ -496,7 +473,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( ) } else context.getString(R.string.off) - val pinned = threadDb.isPinned(threadId) + val pinned = recipient?.isPinned == true val (notificationIconRes, notificationSubtitle) = getNotificationsData(conversation) @@ -506,7 +483,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( val mainOptions = mutableListOf() val dangerOptions = mutableListOf() - val ntsHidden = prefs.hasHiddenNoteToSelf() + val ntsHidden = conversation.priority == PRIORITY_HIDDEN mainOptions.addAll(listOf( optionCopyAccountId, @@ -546,7 +523,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( )) // these options are only for users who aren't blocked - if(!conversation.isBlocked) { + if(!conversation.blocked) { mainOptions.addAll(listOf( optionDisappearingMessage(disappearingSubtitle), if(pinned) optionUnpin else optionPin, @@ -558,7 +535,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( mainOptions.add(optionAttachments) dangerOptions.addAll(listOf( - if(recipient?.isBlocked == true) optionUnblock else optionBlock, + if(recipient?.blocked == true) optionUnblock else optionBlock, optionClearMessages, optionDeleteConversation, optionDeleteContact @@ -735,7 +712,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( private fun getNotificationsData(conversation: Recipient): Pair { return when{ - conversation.isMuted -> R.drawable.ic_volume_off to context.getString(R.string.notificationsMuted) + conversation.isMuted() -> R.drawable.ic_volume_off to context.getString(R.string.notificationsMuted) conversation.notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS -> R.drawable.ic_at_sign to context.getString(R.string.notificationsMentionsOnly) else -> R.drawable.ic_volume_2 to context.getString(R.string.notificationsAllMessages) @@ -774,15 +751,11 @@ class ConversationSettingsViewModel @AssistedInject constructor( } private fun pinConversation(){ - viewModelScope.launch { - storage.setPinned(threadId, true) - } + storage.setPinned(address, true) } private fun unpinConversation(){ - viewModelScope.launch { - storage.setPinned(threadId, false) - } + storage.setPinned(address, false) } private fun confirmBlockUser(){ @@ -791,7 +764,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( showSimpleDialog = SimpleDialogData( title = context.getString(R.string.block), message = Phrase.from(context, R.string.blockDescription) - .put(NAME_KEY, recipient?.name ?: "") + .put(NAME_KEY, recipient?.displayName ?: "") .format(), positiveText = context.getString(R.string.block), negativeText = context.getString(R.string.cancel), @@ -810,7 +783,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( showSimpleDialog = SimpleDialogData( title = context.getString(R.string.blockUnblock), message = Phrase.from(context, R.string.blockUnblockName) - .put(NAME_KEY, recipient?.name ?: "") + .put(NAME_KEY, recipient?.displayName ?: "") .format(), positiveText = context.getString(R.string.blockUnblock), negativeText = context.getString(R.string.cancel), @@ -827,7 +800,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( val conversation = recipient ?: return viewModelScope.launch { if (conversation.isContactRecipient || conversation.isGroupV2Recipient) { - repository.setBlocked(conversation, true) + repository.setBlocked(conversation.address, true) } if (conversation.isGroupV2Recipient) { @@ -839,7 +812,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( private fun unblockUser() { if(recipient == null) return viewModelScope.launch { - repository.setBlocked(recipient!!, false) + repository.setBlocked(recipient!!.address, false) } } @@ -879,25 +852,15 @@ class ConversationSettingsViewModel @AssistedInject constructor( } private fun hideNoteToSelf() { - prefs.setHasHiddenNoteToSelf(true) configFactory.withMutableUserConfigs { it.userProfile.setNtsPriority(PRIORITY_HIDDEN) } - // update state to reflect the change - viewModelScope.launch { - getStateFromRecipient() - } } fun showNoteToSelf() { - prefs.setHasHiddenNoteToSelf(false) configFactory.withMutableUserConfigs { it.userProfile.setNtsPriority(PRIORITY_VISIBLE) } - // update state to reflect the change - viewModelScope.launch { - getStateFromRecipient() - } } private fun confirmDeleteContact(){ @@ -906,8 +869,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( showSimpleDialog = SimpleDialogData( title = context.getString(R.string.contactDelete), message = Phrase.from(context, R.string.deleteContactDescription) - .put(NAME_KEY, recipient?.name ?: "") - .put(NAME_KEY, recipient?.name ?: "") + .put(NAME_KEY, recipient?.displayName ?: "") .format(), positiveText = context.getString(R.string.delete), negativeText = context.getString(R.string.cancel), @@ -939,7 +901,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( showSimpleDialog = SimpleDialogData( title = context.getString(R.string.conversationsDelete), message = Phrase.from(context, R.string.deleteConversationDescription) - .put(NAME_KEY, recipient?.name ?: "") + .put(NAME_KEY, recipient?.displayName ?: "") .format(), positiveText = context.getString(R.string.delete), negativeText = context.getString(R.string.cancel), @@ -970,7 +932,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( showSimpleDialog = SimpleDialogData( title = context.getString(R.string.communityLeave), message = Phrase.from(context, R.string.groupLeaveDescription) - .put(GROUP_NAME_KEY, recipient?.name ?: "") + .put(GROUP_NAME_KEY, recipient?.displayName ?: "") .format(), positiveText = context.getString(R.string.leave), negativeText = context.getString(R.string.cancel), @@ -1003,26 +965,26 @@ class ConversationSettingsViewModel @AssistedInject constructor( // default to 1on1 var message: CharSequence = Phrase.from(context, R.string.clearMessagesChatDescriptionUpdated) - .put(NAME_KEY,conversation.name) + .put(NAME_KEY,conversation.displayName) .format() when{ conversation.isGroupV2Recipient -> { if(groupV2?.hasAdminKey() == true){ // group admin clearing messages have a dedicated custom dialog - _dialogState.update { it.copy(groupAdminClearMessagesDialog = GroupAdminClearMessageDialog(getGroupName())) } + _dialogState.update { it.copy(groupAdminClearMessagesDialog = GroupAdminClearMessageDialog(conversation.displayName)) } return } else { message = Phrase.from(context, R.string.clearMessagesGroupDescriptionUpdated) - .put(GROUP_NAME_KEY, getGroupName()) + .put(GROUP_NAME_KEY, conversation.displayName) .format() } } conversation.isCommunityRecipient -> { message = Phrase.from(context, R.string.clearMessagesCommunityUpdated) - .put(COMMUNITY_NAME_KEY, conversation.name) + .put(COMMUNITY_NAME_KEY, conversation.displayName) .format() } @@ -1075,15 +1037,6 @@ class ConversationSettingsViewModel @AssistedInject constructor( } } - - private fun getGroupName(): String { - val conversation = recipient ?: return "" - val accountId = AccountId(conversation.address.toString()) - return configFactory.withGroupConfigs(accountId) { - it.groupInfo.getName() - } ?: groupV2?.name ?: "" - } - private fun confirmLeaveGroup(){ val groupData = groupV2 ?: return _dialogState.update { state -> @@ -1122,7 +1075,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( hideLoading() val txt = Phrase.from(context, R.string.groupLeaveErrorFailed) - .put(GROUP_NAME_KEY, getGroupName()) + .put(GROUP_NAME_KEY, conversation.displayName) .format().toString() Toast.makeText(context, txt, Toast.LENGTH_LONG).show() } @@ -1303,9 +1256,13 @@ class ConversationSettingsViewModel @AssistedInject constructor( viewModelScope.launch(Dispatchers.Default) { val publicKey = conversation.address.toString() - val contact = storage.getContactWithAccountID(publicKey) ?: Contact(publicKey) - contact.nickname = nickname - storage.setContact(contact) + if (AccountId.fromStringOrNull(publicKey)?.prefix == IdPrefix.STANDARD) { + configFactory.withMutableUserConfigs { configs -> + configs.contacts.upsertContact(publicKey) { + this.nickname = nickname.orEmpty() + } + } + } } } @@ -1384,7 +1341,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( try { withContext(Dispatchers.Default) { val recipients = contacts.map { contact -> - Recipient.from(context, fromSerialized(contact.hexString), true) + fromSerialized(contact.hexString) } repository.inviteContactsToCommunity(threadId, recipients) @@ -1429,7 +1386,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(threadId: Long): ConversationSettingsViewModel + fun create(address: Address): ConversationSettingsViewModel } data class UIState( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/notification/NotificationSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/notification/NotificationSettingsActivity.kt index 713af5a710..7c0bad06ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/notification/NotificationSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/notification/NotificationSettingsActivity.kt @@ -1,10 +1,11 @@ package org.thoughtcrime.securesms.conversation.v2.settings.notification import androidx.compose.runtime.Composable +import androidx.core.content.IntentCompat import androidx.hilt.navigation.compose.hiltViewModel import dagger.hilt.android.AndroidEntryPoint +import org.session.libsession.utilities.Address import org.thoughtcrime.securesms.FullComposeScreenLockActivity -import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity /** * Forced to add an activity entry point for this screen @@ -14,15 +15,15 @@ import org.thoughtcrime.securesms.conversation.disappearingmessages.Disappearing @AndroidEntryPoint class NotificationSettingsActivity: FullComposeScreenLockActivity() { - private val threadId: Long by lazy { - intent.getLongExtra(DisappearingMessagesActivity.THREAD_ID, -1) - } - @Composable override fun ComposeContent() { val viewModel = hiltViewModel { factory -> - factory.create(threadId) + factory.create(requireNotNull( + IntentCompat.getParcelableExtra(intent, ARG_ADDRESS, Address::class.java) + ) { + "NotificationSettingsActivity requires an Address to be passed in via the intent." + }) } NotificationSettingsScreen( @@ -32,6 +33,6 @@ class NotificationSettingsActivity: FullComposeScreenLockActivity() { } companion object { - const val THREAD_ID = "thread_id" + const val ARG_ADDRESS = "address" } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/notification/NotificationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/notification/NotificationSettingsViewModel.kt index 6cfb6b0fe7..d45c5e739b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/notification/NotificationSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/notification/NotificationSettingsViewModel.kt @@ -13,12 +13,14 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.BuildConfig import network.loki.messenger.R import org.session.libsession.LocalisedTimeUtil +import org.session.libsession.utilities.Address import org.session.libsession.utilities.StringSubstitutionConstants.DATE_TIME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY import org.session.libsession.utilities.recipients.Recipient @@ -26,25 +28,24 @@ import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_ALL import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_MENTIONS import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_NONE +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.OptionsCardData import org.thoughtcrime.securesms.ui.RadioOption import org.thoughtcrime.securesms.ui.getSubbedString import org.thoughtcrime.securesms.util.DateUtils -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.milliseconds @HiltViewModel(assistedFactory = NotificationSettingsViewModel.Factory::class) class NotificationSettingsViewModel @AssistedInject constructor( - @Assisted private val threadId: Long, + @Assisted private val address: Address, @ApplicationContext private val context: Context, private val recipientDatabase: RecipientDatabase, private val repository: ConversationRepository, private val dateUtils: DateUtils, + private val recipientRepository: RecipientRepository, ) : ViewModel() { private var thread: Recipient? = null @@ -64,11 +65,11 @@ class NotificationSettingsViewModel @AssistedInject constructor( init { // update data when we have a recipient and update when there are changes from the thread or recipient viewModelScope.launch(Dispatchers.Default) { - repository.recipientUpdateFlow(threadId).collect { + recipientRepository.observeRecipient(address).collectLatest { thread = it // update the user's current choice of notification - currentMutedUntil = if(it?.isMuted == true) it.mutedUntil else null + currentMutedUntil = if(it?.isMuted() == true) it.mutedUntilMills else null val hasMutedUntil = currentMutedUntil != null && currentMutedUntil!! > 0L currentOption = when{ @@ -250,21 +251,21 @@ class NotificationSettingsViewModel @AssistedInject constructor( private suspend fun unmute() { val conversation = thread ?: return withContext(Dispatchers.Default) { - recipientDatabase.setMuted(conversation, 0) + recipientDatabase.setMuted(conversation.address, 0) } } private suspend fun mute(until: Long) { val conversation = thread ?: return withContext(Dispatchers.Default) { - recipientDatabase.setMuted(conversation, until) + recipientDatabase.setMuted(conversation.address, until) } } private suspend fun setNotifyType(notifyType: Int) { val conversation = thread ?: return withContext(Dispatchers.Default) { - recipientDatabase.setNotifyType(conversation, notifyType) + recipientDatabase.setNotifyType(conversation.address, notifyType) } } @@ -295,6 +296,6 @@ class NotificationSettingsViewModel @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(threadId: Long): NotificationSettingsViewModel + fun create(address: Address): NotificationSettingsViewModel } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java index 9f54f9d9c9..ef80f320e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java @@ -41,7 +41,8 @@ import java.util.LinkedList; import java.util.List; import network.loki.messenger.R; -import org.session.libsession.utilities.recipients.Recipient; + +import org.session.libsession.utilities.Address; import org.session.libsignal.utilities.ListenableFuture; import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.SettableFuture; @@ -264,7 +265,7 @@ public static void selectDocument(Activity activity, int requestCode) { .execute(); } - public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) { + public static void selectGallery(Activity activity, int requestCode, @NonNull Address recipient, @NonNull String body) { Context c = activity.getApplicationContext(); @@ -318,7 +319,7 @@ public static void selectGif(Activity activity, int requestCode) { return captureUri; } - public void capturePhoto(Activity activity, int requestCode, Recipient recipient) { + public void capturePhoto(Activity activity, int requestCode, Address recipient) { String cameraPermissionDeniedTxt = Phrase.from(context, R.string.permissionsCameraDenied) .put(APP_NAME_KEY, context.getString(R.string.app_name)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt index 5950c44386..86b6e47be2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt @@ -10,12 +10,12 @@ import android.util.Range import network.loki.messenger.R import network.loki.messenger.libsession_util.util.BlindKeyAPI import nl.komponents.kovenant.combine.Tuple2 -import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.OpenGroup +import org.session.libsession.utilities.Address import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.getColorFromAttr -import org.session.libsession.utilities.truncateIdForDisplay import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.util.RoundedBackgroundSpan import org.thoughtcrime.securesms.util.getAccentColor @@ -60,22 +60,20 @@ object MentionUtilities { val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @ val isYou = isYou(publicKey, userPublicKey, openGroup) - val userDisplayName: String? = if (isYou) { + val userDisplayName: String = if (isYou) { context.getString(R.string.you) } else { - val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey) - @Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR - contact?.displayName(context) - } - if (userDisplayName != null) { - val mention = "@$userDisplayName" - text = text.subSequence(0, matcher.start()).toString() + mention + text.subSequence(matcher.end(), text.length) - val endIndex = matcher.start() + 1 + userDisplayName.length - startIndex = endIndex - mentions.add(Tuple2(Range.create(matcher.start(), endIndex), publicKey)) - } else { - startIndex = matcher.end() + MessagingModuleConfiguration.shared.recipientRepository.getRecipientDisplayNameSync( + Address.fromSerialized(publicKey) + ) } + + val mention = "@$userDisplayName" + text = text.subSequence(0, matcher.start()).toString() + mention + text.subSequence(matcher.end(), text.length) + val endIndex = matcher.start() + 1 + userDisplayName.length + startIndex = endIndex + mentions.add(Tuple2(Range.create(matcher.start(), endIndex), publicKey)) + matcher = pattern.matcher(text) if (!matcher.find(startIndex)) { break } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt index 3fb6a77bac..2c24c19d4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt @@ -10,14 +10,13 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord object ResendMessageUtilities { fun resend(context: Context, messageRecord: MessageRecord, userBlindedKey: String?, isResync: Boolean = false) { - val recipient: Recipient = messageRecord.recipient + val recipient = messageRecord.recipient.address val message = VisibleMessage() message.id = messageRecord.messageId if (messageRecord.isOpenGroupInvitation) { @@ -34,8 +33,8 @@ object ResendMessageUtilities { message.text = messageRecord.body } message.sentTimestamp = messageRecord.timestamp - if (recipient.isGroupOrCommunityRecipient) { - message.groupPublicKey = recipient.address.toGroupString() + if (recipient.isGroupOrCommunity) { + message.groupPublicKey = recipient.toGroupString() } else { message.recipient = messageRecord.recipient.address.toString() } @@ -56,10 +55,10 @@ object ResendMessageUtilities { if (sentTimestamp != null && sender != null) { if (isResync) { MessagingModuleConfiguration.shared.storage.markAsResyncing(messageRecord.messageId) - MessageSender.sendNonDurably(message, Destination.from(recipient.address), isSyncMessage = true) + MessageSender.sendNonDurably(message, Destination.from(recipient), isSyncMessage = true) } else { MessagingModuleConfiguration.shared.storage.markAsSending(messageRecord.messageId) - MessageSender.send(message, recipient.address) + MessageSender.send(message, recipient) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ExpirationConfigurationDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ExpirationConfigurationDatabase.kt index a8ff610ac6..cea284f9b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ExpirationConfigurationDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ExpirationConfigurationDatabase.kt @@ -11,6 +11,7 @@ import org.session.libsession.utilities.GroupUtil.COMMUNITY_PREFIX import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import javax.inject.Provider +@Deprecated("We no longer store the expiration configuration in the database. ") class ExpirationConfigurationDatabase(context: Context, helper: Provider) : Database(context, helper) { companion object { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 867f29e17d..0dfb98e028 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -15,12 +15,10 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase; -import org.jetbrains.annotations.NotNull; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.GroupRecord; import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.database.LokiOpenGroupDatabaseProtocol; import org.session.libsignal.messages.SignalServiceAttachmentPointer; import org.session.libsignal.utilities.guava.Optional; @@ -28,19 +26,24 @@ import org.thoughtcrime.securesms.util.BitmapUtil; import java.io.Closeable; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import javax.inject.Provider; +import kotlinx.coroutines.channels.BufferOverflow; +import kotlinx.coroutines.flow.MutableSharedFlow; +import kotlinx.coroutines.flow.SharedFlow; +import kotlinx.coroutines.flow.SharedFlowKt; + /** * @deprecated This database table management is only used for * legacy group and community management. It is not used in groupv2. For group v2 data, you generally need * to query config system directly. The Storage class may also be more up-to-date. * */ -@Deprecated public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProtocol { @SuppressWarnings("unused") @@ -102,10 +105,17 @@ public static String getCreateUpdatedTimestampCommand() { "ADD COLUMN " + UPDATED + " INTEGER DEFAULT 0;"; } + private final MutableSharedFlow updateNotification = SharedFlowKt.MutableSharedFlow(0, 128, BufferOverflow.DROP_OLDEST); + public GroupDatabase(Context context, Provider databaseHelper) { super(context, databaseHelper); } + @NonNull + public SharedFlow getUpdateNotification() { + return updateNotification; + } + public Optional getGroup(String groupId) { try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, null, GROUP_ID + " = ?", new String[] {groupId}, @@ -154,20 +164,20 @@ public List getAllGroups(boolean includeInactive) { return groups; } - public @NonNull List getGroupMembers(String groupId, boolean includeSelf) { + public @NonNull List
getGroupMembers(String groupId, boolean includeSelf) { List
members = getCurrentMembers(groupId, false); - List recipients = new LinkedList<>(); + List
filtered = new ArrayList<>(); for (Address member : members) { if (!includeSelf && Util.isOwnNumber(context, member.toString())) continue; if (member.isContact()) { - recipients.add(Recipient.from(context, member, false)); + filtered.add(member); } } - return recipients; + return filtered; } public @NonNull List
getGroupMemberAddresses(String groupId, boolean includeSelf) { @@ -184,17 +194,6 @@ public List getAllGroups(boolean includeInactive) { return members; } - public @NonNull List getGroupZombieMembers(String groupId) { - List
members = getCurrentZombieMembers(groupId); - List recipients = new LinkedList<>(); - - for (Address member : members) { - recipients.add(Recipient.from(context, member, false)); - } - - return recipients; - } - public long create(@NonNull String groupId, @Nullable String title, @NonNull List
members, @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List
admins, @NonNull Long formationTimestamp) { @@ -224,14 +223,11 @@ public long create(@NonNull String groupId, @Nullable String title, @NonNull Lis long threadId = getWritableDatabase().insert(TABLE_NAME, null, contentValues); - Recipient.applyCached(Address.fromSerialized(groupId), recipient -> { - recipient.setName(title); - recipient.setGroupAvatarId(avatar != null ? avatar.getId() : null); - recipient.setParticipants(Stream.of(members).map(memberAddress -> Recipient.from(context, memberAddress, true)).toList()); - }); - notifyConversationListeners(threadId); notifyConversationListListeners(); + + updateNotification.tryEmit(groupId); + return threadId; } @@ -239,8 +235,8 @@ public boolean delete(@NonNull String groupId) { int result = getWritableDatabase().delete(TABLE_NAME, GROUP_ID + " = ?", new String[]{groupId}); if (result > 0) { - Recipient.removeCached(Address.fromSerialized(groupId)); notifyConversationListListeners(); + updateNotification.tryEmit(groupId); return true; } else { return false; @@ -263,11 +259,7 @@ public void update(String groupId, String title, SignalServiceAttachmentPointer GROUP_ID + " = ?", new String[] {groupId}); - Recipient.applyCached(Address.fromSerialized(groupId), recipient -> { - recipient.setName(title); - recipient.setGroupAvatarId(avatar != null ? avatar.getId() : null); - }); - + updateNotification.tryEmit(groupId); notifyConversationListListeners(); } @@ -275,14 +267,12 @@ public void update(String groupId, String title, SignalServiceAttachmentPointer public void updateTitle(String groupID, String newValue) { ContentValues contentValues = new ContentValues(); contentValues.put(TITLE, newValue); - getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", - new String[] {groupID}); - Recipient recipient = Recipient.from(context, Address.fromSerialized(groupID), false); - final boolean nameChanged = !newValue.equals(recipient.getName()); - recipient.setName(newValue); - - if (nameChanged) { + // Only notify if the title is actually changed. This is more a performance optimization rather + // than functional. + if (getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ? AND " + TITLE + " != ?", + new String[] {groupID, newValue}) > 0) { + updateNotification.tryEmit(groupID); notifyConversationListListeners(); } } @@ -306,8 +296,8 @@ public void updateProfilePicture(String groupID, byte[] newValue) { getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", new String[] {groupID}); - Recipient.applyCached(Address.fromSerialized(groupID), recipient -> recipient.setGroupAvatarId(avatarId == 0 ? null : avatarId)); notifyConversationListListeners(); + updateNotification.tryEmit(groupID); } @Override @@ -325,8 +315,8 @@ public void removeProfilePicture(String groupID) { GROUP_ID + " = ?", new String[] {groupID}); - Recipient.applyCached(Address.fromSerialized(groupID), recipient -> recipient.setGroupAvatarId(null)); notifyConversationListListeners(); + updateNotification.tryEmit(groupID); } public boolean hasDownloadedProfilePicture(String groupId) { @@ -352,28 +342,7 @@ public void updateMembers(String groupId, List
members) { getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", new String[] {groupId}); - Recipient.applyCached(Address.fromSerialized(groupId), recipient -> { - recipient.setParticipants(Stream.of(members).map(a -> Recipient.from(context, a, false)).toList()); - }); - } - - public void updateZombieMembers(String groupId, List
members) { - Collections.sort(members); - - ContentValues contents = new ContentValues(); - contents.put(ZOMBIE_MEMBERS, Address.toSerializedList(members, ',')); - getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", - new String[] {groupId}); - } - - public void updateAdmins(String groupId, List
admins) { - Collections.sort(admins); - - ContentValues contents = new ContentValues(); - contents.put(ADMINS, Address.toSerializedList(admins, ',')); - contents.put(ACTIVE, 1); - - getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", new String[] {groupId}); + updateNotification.tryEmit(groupId); } public void updateFormationTimestamp(String groupId, Long formationTimestamp) { @@ -381,6 +350,8 @@ public void updateFormationTimestamp(String groupId, Long formationTimestamp) { contents.put(TIMESTAMP, formationTimestamp); getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", new String[] {groupId}); + + updateNotification.tryEmit(groupId); } public void updateTimestampUpdated(String groupId, Long updatedTimestamp) { @@ -388,6 +359,7 @@ public void updateTimestampUpdated(String groupId, Long updatedTimestamp) { contents.put(UPDATED, updatedTimestamp); getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", new String[] {groupId}); + updateNotification.tryEmit(groupId); } public void removeMember(String groupId, Address source) { @@ -400,13 +372,7 @@ public void removeMember(String groupId, Address source) { getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", new String[] {groupId}); - Recipient.applyCached(Address.fromSerialized(groupId), recipient -> { - List current = recipient.getParticipants(); - Recipient removal = Recipient.from(context, source, false); - - current.remove(removal); - recipient.setParticipants(current); - }); + updateNotification.tryEmit(groupId); } private List
getCurrentMembers(String groupId, boolean zombieMembers) { @@ -448,26 +414,11 @@ public void setActive(String groupId, boolean active) { ContentValues values = new ContentValues(); values.put(ACTIVE, active ? 1 : 0); database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId}); - } - - public boolean hasGroup(@NonNull String groupId) { - try (Cursor cursor = getReadableDatabase().rawQuery( - "SELECT 1 FROM " + TABLE_NAME + " WHERE " + GROUP_ID + " = ? LIMIT 1", - new String[]{groupId} - )) { - return cursor.getCount() > 0; - } - } - public void migrateEncodedGroup(@NotNull String legacyEncodedGroupId, @NotNull String newEncodedGroupId) { - String query = GROUP_ID+" = ?"; - ContentValues contentValues = new ContentValues(1); - contentValues.put(GROUP_ID, newEncodedGroupId); - SQLiteDatabase db = getWritableDatabase(); - db.update(TABLE_NAME, contentValues, query, new String[]{legacyEncodedGroupId}); + updateNotification.tryEmit(groupId); } - public static class Reader implements Closeable { + public static class Reader implements Closeable { private final Cursor cursor; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt index aad47f0635..28f048df25 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.crypto.ecc.DjbECPrivateKey import org.session.libsignal.crypto.ecc.DjbECPublicKey @@ -17,7 +18,9 @@ import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import java.util.Date +import javax.inject.Inject import javax.inject.Provider +import javax.inject.Singleton class LokiAPIDatabase(context: Context, helper: Provider) : Database(context, helper), LokiAPIDatabaseProtocol { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt index 492462c79f..571b36c7c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt @@ -3,16 +3,35 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context import android.database.Cursor +import androidx.collection.LruCache +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.takeWhile import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsignal.utilities.JsonUtil import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import javax.inject.Inject import javax.inject.Provider +import javax.inject.Singleton -class LokiThreadDatabase(context: Context, helper: Provider) : Database(context, helper) { +@Singleton +class LokiThreadDatabase @Inject constructor( + @ApplicationContext context: Context, + helper: Provider +) : Database(context, helper) { companion object { private val sessionResetTable = "loki_thread_session_reset_database" - val publicChatTable = "loki_public_chat_database" + private val publicChatTable = "loki_public_chat_database" val threadID = "thread_id" private val sessionResetStatus = "session_reset_status" val publicChat = "public_chat" @@ -22,6 +41,12 @@ class LokiThreadDatabase(context: Context, helper: Provider val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);" } + private val mutableChangeNotification = MutableSharedFlow() + + val changeNotification: SharedFlow get() = mutableChangeNotification + + private val cacheByThreadId = LruCache(32) + fun getAllOpenGroups(): Map { val database = readableDatabase var cursor: Cursor? = null @@ -39,6 +64,12 @@ class LokiThreadDatabase(context: Context, helper: Provider } finally { cursor?.close() } + + // Update the cache with the results + for ((id, group) in result) { + cacheByThreadId.put(id, group) + } + return result } @@ -46,6 +77,12 @@ class LokiThreadDatabase(context: Context, helper: Provider if (threadID < 0) { return null } + + // Check the cache first + cacheByThreadId[threadID]?.let { + return it + } + val database = readableDatabase return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor -> val json = cursor.getString(publicChat) @@ -53,22 +90,25 @@ class LokiThreadDatabase(context: Context, helper: Provider } } - fun getThreadId(openGroup: OpenGroup): Long? { - val database = readableDatabase - return database.get(publicChatTable, "$publicChat = ?", arrayOf(JsonUtil.toJson(openGroup.toJson()))) { cursor -> - cursor.getLong(threadID) - } - } - fun setOpenGroupChat(openGroup: OpenGroup, threadID: Long) { if (threadID < 0) { return } + + // Check if the group has really changed + val cache = cacheByThreadId[threadID] + if (cache == openGroup) { + return + } else { + cacheByThreadId.put(threadID, openGroup) + } + val database = writableDatabase val contentValues = ContentValues(2) contentValues.put(Companion.threadID, threadID) contentValues.put(publicChat, JsonUtil.toJson(openGroup.toJson())) database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString())) + mutableChangeNotification.tryEmit(threadID) } fun removeOpenGroupChat(threadID: Long) { @@ -76,6 +116,26 @@ class LokiThreadDatabase(context: Context, helper: Provider val database = writableDatabase database.delete(publicChatTable,"${Companion.threadID} = ?", arrayOf(threadID.toString())) + + cacheByThreadId.remove(threadID) + mutableChangeNotification.tryEmit(threadID) + } + + /** + * Retrieves the OpenGroup for the given threadID and observes changes to it. + * + * If in the beginning the OpenGroup is not available, it returns null. + * If in the middle of the flow the OpenGroup is deleted, it will stop emitting updates. + */ + fun retrieveAndObserveOpenGroup(scope: CoroutineScope, threadID: Long): StateFlow? { + val initialOpenGroup = getOpenGroupChat(threadID) ?: return null + + return mutableChangeNotification + .filter { it == threadID } + .map { getOpenGroupChat(threadID) } + .takeWhile { it != null } + .filterNotNull() + .stateIn(scope, SharingStarted.Eagerly, initialOpenGroup) } } \ 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 f22769de73..b8bccf365e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -20,6 +20,7 @@ import android.content.ContentValues import android.content.Context import android.database.Cursor import com.annimon.stream.Stream +import dagger.hilt.android.qualifiers.ApplicationContext import org.apache.commons.lang3.StringUtils import org.json.JSONArray import org.json.JSONException @@ -37,7 +38,6 @@ import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.UNKNOWN -import org.session.libsession.utilities.Address.Companion.fromExternal import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Contact import org.session.libsession.utilities.IdentityKeyMismatch @@ -49,7 +49,6 @@ import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.ThreadUtils.queue -import org.session.libsignal.utilities.Util.SECURE_RANDOM import org.session.libsignal.utilities.guava.Optional import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord @@ -64,9 +63,16 @@ import org.thoughtcrime.securesms.util.asSequence import java.io.Closeable import java.io.IOException import java.util.LinkedList +import javax.inject.Inject import javax.inject.Provider - -class MmsDatabase(context: Context, databaseHelper: Provider) : MessagingDatabase(context, databaseHelper) { +import javax.inject.Singleton + +@Singleton +class MmsDatabase @Inject constructor( + @ApplicationContext context: Context, + databaseHelper: Provider, + private val recipientRepository: RecipientRepository, +) : MessagingDatabase(context, databaseHelper) { private val earlyDeliveryReceiptCache = EarlyReceiptCache() private val earlyReadReceiptCache = EarlyReceiptCache() override fun getTableName() = TABLE_NAME @@ -459,7 +465,6 @@ class MmsDatabase(context: Context, databaseHelper: Provider obj.isQuote || contactAttachments.contains(obj) || previewAttachments.contains(obj) } .toList() - val recipient = Recipient.from(context, fromSerialized(address), false) var networkFailures: List? = LinkedList() var mismatches: List? = LinkedList() var quote: QuoteModel? = null @@ -491,7 +496,7 @@ class MmsDatabase(context: Context, databaseHelper: Provider obj } @@ -769,12 +774,11 @@ class MmsDatabase(context: Context, databaseHelper: Provider obj.address } - .toList(), + receiptDatabase.insert(members, messageId, GroupReceiptDatabase.STATUS_UNDELIVERED, message.sentTimeMillis ) for (address in earlyDeliveryReceipts.keys) receiptDatabase.update( @@ -1256,8 +1260,6 @@ class MmsDatabase(context: Context, databaseHelper: Provider? { @@ -1432,7 +1404,7 @@ class MmsDatabase(context: Context, databaseHelper: Provider updateNotifications = SharedFlowKt.MutableSharedFlow(0, 256, BufferOverflow.DROP_OLDEST); + + @NonNull + private final LruCache recipientSettingsCache = new LruCache<>(512); + public RecipientDatabase(Context context, Provider databaseHelper) { super(context, databaseHelper); } - public RecipientReader getRecipientsWithNotificationChannels() { - SQLiteDatabase database = getReadableDatabase(); - Cursor cursor = database.query(TABLE_NAME, new String[] {ID, ADDRESS}, NOTIFICATION_CHANNEL + " NOT NULL", - null, null, null, null, null); - - return new RecipientReader(context, cursor); + @NonNull + public SharedFlow
getUpdateNotifications() { + return updateNotifications; } - public Optional getRecipientSettings(@NonNull Address address) { + @Nullable + public RecipientSettings getRecipientSettings(@NonNull Address address) { + final RecipientSettings cachedSettings = recipientSettingsCache.get(address); + if (cachedSettings != null) { + return cachedSettings; + } + SQLiteDatabase database = getReadableDatabase(); try (Cursor cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?", new String[]{address.toString()}, null, null, null)) { if (cursor != null && cursor.moveToNext()) { - return getRecipientSettings(cursor); + RecipientSettings settings = getRecipientSettings(cursor); + recipientSettingsCache.put(address, settings); + return settings; } - return Optional.absent(); + return null; } } - Optional getRecipientSettings(@NonNull Cursor cursor) { + @NonNull + private RecipientSettings getRecipientSettings(@NonNull Cursor cursor) { boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCK)) == 1; boolean approved = cursor.getInt(cursor.getColumnIndexOrThrow(APPROVED)) == 1; boolean approvedMe = cursor.getInt(cursor.getColumnIndexOrThrow(APPROVED_ME)) == 1; - String messageRingtone = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION)); - String callRingtone = cursor.getString(cursor.getColumnIndexOrThrow(CALL_RINGTONE)); - int disappearingState = cursor.getInt(cursor.getColumnIndexOrThrow(DISAPPEARING_STATE)); - int messageVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(VIBRATE)); - int callVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(CALL_VIBRATE)); long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL)); int notifyType = cursor.getInt(cursor.getColumnIndexOrThrow(NOTIFY_TYPE)); - boolean autoDownloadAttachments = cursor.getInt(cursor.getColumnIndexOrThrow(AUTO_DOWNLOAD)) == 1; - String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR)); - int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID)); + Boolean autoDownloadAttachments = switch (cursor.getInt(cursor.getColumnIndexOrThrow(AUTO_DOWNLOAD))) { + case 1 -> true; + case -1 -> null; + default -> false; + }; + int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(EXPIRE_MESSAGES)); - int registeredState = cursor.getInt(cursor.getColumnIndexOrThrow(REGISTERED)); String profileKeyString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY)); String systemDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_DISPLAY_NAME)); - String systemContactPhoto = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHOTO_URI)); - String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL)); - String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI)); String signalProfileName = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_NAME)); String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SESSION_PROFILE_AVATAR)); - boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1; - String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL)); - boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1; boolean blocksCommunityMessageRequests = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKS_COMMUNITY_MESSAGE_REQUESTS)) == 1; - MaterialColor color; byte[] profileKey = null; - try { - color = serializedColor == null ? null : MaterialColor.fromSerialized(serializedColor); - } catch (MaterialColor.UnknownColorException e) { - Log.w(TAG, e); - color = null; - } - if (profileKeyString != null) { try { profileKey = Base64.decode(profileKeyString); @@ -240,133 +265,95 @@ Optional getRecipientSettings(@NonNull Cursor cursor) { } } - return Optional.of(new RecipientSettings(blocked, approved, approvedMe, muteUntil, - notifyType, autoDownloadAttachments, - Recipient.DisappearingState.fromId(disappearingState), - Recipient.VibrateState.fromId(messageVibrateState), - Recipient.VibrateState.fromId(callVibrateState), - Util.uri(messageRingtone), Util.uri(callRingtone), - color, defaultSubscriptionId, expireMessages, - Recipient.RegisteredState.fromId(registeredState), - profileKey, systemDisplayName, systemContactPhoto, - systemPhoneLabel, systemContactUri, - signalProfileName, signalProfileAvatar, profileSharing, - notificationChannel, - forceSmsSelection, blocksCommunityMessageRequests)); - } - - public boolean isAutoDownloadFlagSet(Recipient recipient) { - SQLiteDatabase db = getReadableDatabase(); - Cursor cursor = db.query(TABLE_NAME, new String[]{ AUTO_DOWNLOAD }, ADDRESS+" = ?", new String[]{ recipient.getAddress().toString() }, null, null, null); - boolean flagUnset = false; - try { - if (cursor.moveToFirst()) { - // flag isn't set if it is -1 - flagUnset = cursor.getInt(cursor.getColumnIndexOrThrow(AUTO_DOWNLOAD)) == -1; - } - } finally { - cursor.close(); - } - // negate result (is flag set) - return !flagUnset; - } - - public void setColor(@NonNull Recipient recipient, @NonNull MaterialColor color) { - ContentValues values = new ContentValues(); - values.put(COLOR, color.serialize()); - updateOrInsert(recipient.getAddress(), values); - recipient.resolve().setColor(color); - notifyRecipientListeners(); - } - - public void setDefaultSubscriptionId(@NonNull Recipient recipient, int defaultSubscriptionId) { - ContentValues values = new ContentValues(); - values.put(DEFAULT_SUBSCRIPTION_ID, defaultSubscriptionId); - updateOrInsert(recipient.getAddress(), values); - recipient.resolve().setDefaultSubscriptionId(Optional.of(defaultSubscriptionId)); - notifyRecipientListeners(); - } - - public void setForceSmsSelection(@NonNull Recipient recipient, boolean forceSmsSelection) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(FORCE_SMS_SELECTION, forceSmsSelection ? 1 : 0); - updateOrInsert(recipient.getAddress(), contentValues); - recipient.resolve().setForceSmsSelection(forceSmsSelection); - notifyRecipientListeners(); + return new RecipientSettings(blocked, approved, approvedMe, muteUntil, + notifyType, autoDownloadAttachments, + expireMessages, + profileKey, systemDisplayName, + signalProfileName, signalProfileAvatar, + blocksCommunityMessageRequests); } - public boolean getApproved(@NonNull Address address) { - SQLiteDatabase db = getReadableDatabase(); - try (Cursor cursor = db.query(TABLE_NAME, new String[]{APPROVED}, ADDRESS + " = ?", new String[]{address.toString()}, null, null, null)) { - if (cursor != null && cursor.moveToNext()) { - return cursor.getInt(cursor.getColumnIndexOrThrow(APPROVED)) == 1; - } - } - return false; + private void invalidateCache(@NonNull Address recipient) { + recipientSettingsCache.remove(recipient); } - public void setApproved(@NonNull Recipient recipient, boolean approved) { + public void setApproved(@NonNull Address recipient, boolean approved) { ContentValues values = new ContentValues(); values.put(APPROVED, approved ? 1 : 0); - updateOrInsert(recipient.getAddress(), values); - recipient.resolve().setApproved(approved); + updateOrInsert(recipient, values); notifyRecipientListeners(); + + invalidateCache(recipient); + updateNotifications.tryEmit(recipient); } - public void setApprovedMe(@NonNull Recipient recipient, boolean approvedMe) { + public void setApprovedMe(@NonNull Address recipient, boolean approvedMe) { ContentValues values = new ContentValues(); values.put(APPROVED_ME, approvedMe ? 1 : 0); - updateOrInsert(recipient.getAddress(), values); - recipient.resolve().setHasApprovedMe(approvedMe); + updateOrInsert(recipient, values); notifyRecipientListeners(); + + invalidateCache(recipient); + updateNotifications.tryEmit(recipient); } - public void setBlocked(@NonNull Iterable recipients, boolean blocked) { + public void setBlocked(@NonNull Iterable
recipients, boolean blocked) { SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); try { ContentValues values = new ContentValues(); values.put(BLOCK, blocked ? 1 : 0); - for (Recipient recipient : recipients) { - db.update(TABLE_NAME, values, ADDRESS + " = ?", new String[]{recipient.getAddress().toString()}); - recipient.resolve().setBlocked(blocked); + for (Address recipient : recipients) { + db.update(TABLE_NAME, values, ADDRESS + " = ?", new String[]{recipient.toString()}); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } + + for (Address recipient : recipients) { + invalidateCache(recipient); + updateNotifications.tryEmit(recipient); + } + notifyRecipientListeners(); } // Delete a recipient with the given address from the database public void deleteRecipient(@NonNull String recipientAddress) { + Address address = Address.fromSerialized(recipientAddress); SQLiteDatabase db = getWritableDatabase(); int rowCount = db.delete(TABLE_NAME, ADDRESS + " = ?", new String[] { recipientAddress }); if (rowCount == 0) { Log.w(TAG, "Could not find to delete recipient with address: " + recipientAddress); } + + invalidateCache(address); notifyRecipientListeners(); + updateNotifications.tryEmit(address); } - public void setAutoDownloadAttachments(@NonNull Recipient recipient, boolean shouldAutoDownloadAttachments) { + public void setAutoDownloadAttachments(@NonNull Address recipient, boolean shouldAutoDownloadAttachments) { SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); try { ContentValues values = new ContentValues(); values.put(AUTO_DOWNLOAD, shouldAutoDownloadAttachments ? 1 : 0); - db.update(TABLE_NAME, values, ADDRESS+ " = ?", new String[]{recipient.getAddress().toString()}); - recipient.resolve().setAutoDownloadAttachments(shouldAutoDownloadAttachments); + db.update(TABLE_NAME, values, ADDRESS+ " = ?", new String[]{recipient.toString()}); db.setTransactionSuccessful(); } finally { db.endTransaction(); } + + invalidateCache(recipient); notifyRecipientListeners(); + updateNotifications.tryEmit(recipient); } - public void setMuted(@NonNull Recipient recipient, long until) { + public void setMuted(@NonNull Address recipient, long until) { ContentValues values = new ContentValues(); values.put(MUTE_UNTIL, until); - updateOrInsert(recipient.getAddress(), values); - recipient.resolve().setMuted(until); + updateOrInsert(recipient, values); notifyRecipientListeners(); + updateNotifications.tryEmit(recipient); } /** @@ -374,70 +361,71 @@ public void setMuted(@NonNull Recipient recipient, long until) { * @param recipient to modify notifications for * @param notifyType the new notification type {@link #NOTIFY_TYPE_ALL}, {@link #NOTIFY_TYPE_MENTIONS} or {@link #NOTIFY_TYPE_NONE} */ - public void setNotifyType(@NonNull Recipient recipient, int notifyType) { + public void setNotifyType(@NonNull Address recipient, int notifyType) { ContentValues values = new ContentValues(); values.put(NOTIFY_TYPE, notifyType); - updateOrInsert(recipient.getAddress(), values); - recipient.resolve().setNotifyType(notifyType); + updateOrInsert(recipient, values); + + invalidateCache(recipient); notifyConversationListListeners(); notifyRecipientListeners(); + updateNotifications.tryEmit(recipient); } - public void setProfileKey(@NonNull Recipient recipient, @Nullable byte[] profileKey) { - ContentValues values = new ContentValues(1); - values.put(PROFILE_KEY, profileKey == null ? null : Base64.encodeBytes(profileKey)); - updateOrInsert(recipient.getAddress(), values); - recipient.resolve().setProfileKey(profileKey); - notifyRecipientListeners(); - } + public void updateProfile(@NonNull Address recipient, + @Nullable String newName, + @Nullable UserPic profilePic, + @Nullable Boolean acceptsCommunityRequests) { + if (newName == null && profilePic == null) { + return; // nothing to update + } - public void setProfileAvatar(@NonNull Recipient recipient, @Nullable String profileAvatar) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(SESSION_PROFILE_AVATAR, profileAvatar); - updateOrInsert(recipient.getAddress(), contentValues); - recipient.resolve().setProfileAvatar(profileAvatar); - notifyRecipientListeners(); - } + // This call could be called with a lot of same data so it's worth checking if we + // actually need to update the database. + final RecipientSettings cached = recipientSettingsCache.get(recipient); + if (cached != null && + Objects.equals(cached.getSystemDisplayName(), newName) && + Objects.equals(cached.getProfilePic(), profilePic) && + (acceptsCommunityRequests == null || !cached.getBlocksCommunityMessagesRequests() == acceptsCommunityRequests) + ) { + Log.w(TAG, "No changes to update for recipient: " + recipient); + return; + } - public void setProfileName(@NonNull Recipient recipient, @Nullable String profileName) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(SYSTEM_DISPLAY_NAME, profileName); - updateOrInsert(recipient.getAddress(), contentValues); - recipient.resolve().setName(profileName); - recipient.resolve().setProfileName(profileName); - notifyRecipientListeners(); - } + ContentValues contentValues = new ContentValues(4); + if (newName != null) { + contentValues.put(SYSTEM_DISPLAY_NAME, newName); + } + if (profilePic != null) { + contentValues.put(SESSION_PROFILE_AVATAR, profilePic.getUrl()); + contentValues.put(PROFILE_KEY, Base64.encodeBytes(profilePic.getKeyAsByteArray())); + } - public void setProfileSharing(@NonNull Recipient recipient, boolean enabled) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(PROFILE_SHARING, enabled ? 1 : 0); - updateOrInsert(recipient.getAddress(), contentValues); - recipient.setProfileSharing(enabled); - notifyRecipientListeners(); - } + if (acceptsCommunityRequests != null) { + contentValues.put(BLOCKS_COMMUNITY_MESSAGE_REQUESTS, acceptsCommunityRequests ? 0 : 1); + } - public void setNotificationChannel(@NonNull Recipient recipient, @Nullable String notificationChannel) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(NOTIFICATION_CHANNEL, notificationChannel); - updateOrInsert(recipient.getAddress(), contentValues); - recipient.setNotificationChannel(notificationChannel); - notifyRecipientListeners(); + updateOrInsert(recipient, contentValues); + invalidateCache(recipient); + updateNotifications.tryEmit(recipient); } - public void setRegistered(@NonNull Recipient recipient, RegisteredState registeredState) { + public void setNotificationChannel(@NonNull Address recipient, @Nullable String notificationChannel) { ContentValues contentValues = new ContentValues(1); - contentValues.put(REGISTERED, registeredState.getId()); - updateOrInsert(recipient.getAddress(), contentValues); - recipient.setRegistered(registeredState); + contentValues.put(NOTIFICATION_CHANNEL, notificationChannel); + updateOrInsert(recipient, contentValues); + invalidateCache(recipient); notifyRecipientListeners(); + updateNotifications.tryEmit(recipient); } - public void setBlocksCommunityMessageRequests(@NonNull Recipient recipient, boolean isBlocked) { + public void setBlocksCommunityMessageRequests(@NonNull Address recipient, boolean isBlocked) { ContentValues contentValues = new ContentValues(1); contentValues.put(BLOCKS_COMMUNITY_MESSAGE_REQUESTS, isBlocked ? 1 : 0); - updateOrInsert(recipient.getAddress(), contentValues); - recipient.resolve().setBlocksCommunityMessageRequests(isBlocked); + updateOrInsert(recipient, contentValues); + invalidateCache(recipient); notifyRecipientListeners(); + updateNotifications.tryEmit(recipient); } private void updateOrInsert(Address address, ContentValues contentValues) { @@ -457,20 +445,18 @@ private void updateOrInsert(Address address, ContentValues contentValues) { database.endTransaction(); } - public List getBlockedContacts() { + public List
getBlockedContacts() { SQLiteDatabase database = getReadableDatabase(); - Cursor cursor = database.query(TABLE_NAME, new String[] {ID, ADDRESS}, BLOCK + " = 1", - null, null, null, null, null); - - RecipientReader reader = new RecipientReader(context, cursor); - List returnList = new ArrayList<>(); - Recipient current; - while ((current = reader.getNext()) != null) { - returnList.add(current); + try (Cursor cursor = database.query(TABLE_NAME, new String[] {ID, ADDRESS}, BLOCK + " = 1", + null, null, null, null, null)) { + List
blockedContacts = new ArrayList<>(cursor.getCount()); + while (cursor.moveToNext()) { + String serialized = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)); + blockedContacts.add(Address.fromSerialized(serialized)); + } + return blockedContacts; } - reader.close(); - return returnList; } /** @@ -478,57 +464,17 @@ public List getBlockedContacts() { * * @return A list of all recipients */ - public List getAllRecipients() { + public List
getAllRecipients() { SQLiteDatabase database = getReadableDatabase(); - Cursor cursor = database.query(TABLE_NAME, new String[] {ID, ADDRESS}, null, - null, null, null, null, null); - - RecipientReader reader = new RecipientReader(context, cursor); - List returnList = new ArrayList<>(); - Recipient current; - - while ((current = reader.getNext()) != null) { - returnList.add(current); - } - - reader.close(); - return returnList; - } - - public void setDisappearingState(@NonNull Recipient recipient, @NonNull Recipient.DisappearingState disappearingState) { - ContentValues values = new ContentValues(); - values.put(DISAPPEARING_STATE, disappearingState.getId()); - updateOrInsert(recipient.getAddress(), values); - recipient.resolve().setDisappearingState(disappearingState); - notifyRecipientListeners(); - } - - public static class RecipientReader implements Closeable { - - private final Context context; - private final Cursor cursor; - - RecipientReader(Context context, Cursor cursor) { - this.context = context; - this.cursor = cursor; - } - - public @NonNull Recipient getCurrent() { - String serialized = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)); - return Recipient.from(context, Address.fromSerialized(serialized), false); - } - - public @Nullable Recipient getNext() { - if (cursor != null && !cursor.moveToNext()) { - return null; + try(final Cursor cursor = database.query(TABLE_NAME, new String[] {ADDRESS}, null, + null, null, null, null, null)) { + final List
recipients = new ArrayList<>(cursor.getCount()); + while (cursor.moveToNext()) { + String serialized = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)); + recipients.add(Address.fromSerialized(serialized)); } - - return getCurrent(); - } - - public void close() { - cursor.close(); + return recipients; } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt new file mode 100644 index 0000000000..e813b4e40f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt @@ -0,0 +1,567 @@ +package org.thoughtcrime.securesms.database + +import dagger.Lazy +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.withContext +import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE +import network.loki.messenger.libsession_util.ReadableGroupInfoConfig +import network.loki.messenger.libsession_util.ReadableUserProfile +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.session.libsession.database.StorageProtocol +import org.session.libsession.messaging.open_groups.OpenGroup +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsession.utilities.ConfigUpdateNotification +import org.session.libsession.utilities.GroupRecord +import org.session.libsession.utilities.GroupUtil +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.getGroup +import org.session.libsession.utilities.recipients.BasicRecipient +import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.recipients.RecipientSettings +import org.session.libsession.utilities.recipients.RemoteFile +import org.session.libsession.utilities.recipients.RemoteFile.Companion.toRecipientAvatar +import org.session.libsession.utilities.recipients.displayNameOrFallback +import org.session.libsession.utilities.userConfigsChanged +import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.IdPrefix +import org.session.libsignal.utilities.Log +import java.lang.ref.WeakReference +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This repository is responsible for observing and retrieving recipient data from different sources. + * + * Not to be confused with [RecipientDatabase], where it manages the actual database storage of + * some recipient data. Note that not all recipient data is stored in the database, as we've moved + * them to the config system. Details in the [RecipientDatabase]. + * + * This class will source the correct recipient data from different sources based on their types. + */ +@Singleton +class RecipientRepository @Inject constructor( + private val configFactory: ConfigFactoryProtocol, + private val groupDatabase: GroupDatabase, + private val recipientDatabase: RecipientDatabase, + private val preferences: TextSecurePreferences, + private val lokiThreadDatabase: LokiThreadDatabase, + private val storage: Lazy, +) { + private val recipientCache = HashMap>>() + + fun observeRecipient(address: Address): Flow { + synchronized(recipientCache) { + var cached = recipientCache[address]?.get() + if (cached == null) { + cached = createRecipientFlow(address) + recipientCache[address] = WeakReference(cached) + } + + return cached + } + } + + // This function creates a flow that emits the recipient information for the given address, + // the function itself must be fast, not directly access db and lock free, as it is called from a locked context. + @OptIn(FlowPreview::class) + private fun createRecipientFlow(address: Address): SharedFlow { + return flow { + while (true) { + val (value, changeSource) = fetchRecipient( + address = address, + settingsFetcher = { + withContext(Dispatchers.Default) { recipientDatabase.getRecipientSettings(it) } + }, + openGroupFetcher = { + withContext(Dispatchers.Default) { storage.get().getOpenGroup(it) } + } + ) ?: run { + // If we don't have a recipient for this address, emit null and terminate the flow. + emit(null) + return@flow + } + + emit(value) + val evt = changeSource + .debounce(200) // Debounce to avoid too frequent updates + .first() + Log.d(TAG, "Recipient changed for ${address.debugString}, triggering event: $evt") + } + + }.shareIn( + GlobalScope, + // replay must be cleared one when no one is subscribed, so that if no one is subscribed, + // we will always fetch the latest data. The cache is only valid while there is at least one subscriber. + SharingStarted.WhileSubscribed(replayExpirationMillis = 0L), replay = 1 + ) + } + + private inline fun fetchRecipient( + address: Address, + settingsFetcher: (address: Address) -> RecipientSettings?, + openGroupFetcher: (address: Address) -> OpenGroup? + ): Pair>? { + val basicRecipient = getBasicRecipientFast(address) + + val changeSource: Flow<*> + val value: Recipient? + + when (basicRecipient) { + is BasicRecipient.Self -> { + value = createLocalRecipient(basicRecipient) + changeSource = configFactory.userConfigsChanged() + } + + is BasicRecipient.Contact -> { + value = createContactRecipient( + basic = basicRecipient, + fallbackSettings = settingsFetcher(address) + ) + + changeSource = merge( + configFactory.userConfigsChanged(), + recipientDatabase.updateNotifications.filter { it == address } + ) + } + + is BasicRecipient.Group -> { + value = createGroupV2Recipient( + basic = basicRecipient, + settings = settingsFetcher(address) + ) + + changeSource = merge( + configFactory.userConfigsChanged(), + configFactory.configUpdateNotifications + .filterIsInstance() + .filter { it.groupId.hexString == address.address }, + recipientDatabase.updateNotifications.filter { it == address } + ) + } + + null -> { + // Given address is not backed by the config system so we'll get them from + // local database. + + val settings = settingsFetcher(address) + + when { + address.isLegacyGroup -> { + changeSource = merge( + groupDatabase.updateNotification, + recipientDatabase.updateNotifications.filter { it == address }, + configFactory.userConfigsChanged(), + ) + + val group: GroupRecord? = + groupDatabase.getGroup(address.toGroupString()).orNull() + + val groupConfig = configFactory.withUserConfigs { + it.userGroups.getLegacyGroupInfo(GroupUtil.doubleDecodeGroupId(address.address)) + } + + value = group?.let { createLegacyGroupRecipient(address, groupConfig, it, settings) } + } + + address.isCommunity -> { + value = openGroupFetcher(address) + ?.let { openGroup -> + val groupConfig = configFactory.withUserConfigs { + it.userGroups.getCommunityInfo(openGroup.server, openGroup.room) + } + + createCommunityRecipient( + address, + groupConfig, + openGroup, + settings + ) + } + + changeSource = merge( + lokiThreadDatabase.changeNotification, + recipientDatabase.updateNotifications.filter { it == address }, + configFactory.userConfigsChanged(), + ) + } + + settings != null -> { + value = createGenericRecipient(address, settings) + changeSource = + recipientDatabase.updateNotifications.filter { it == address } + } + + else -> return null // No recipient found for this address + } + } + } + + return value to changeSource + } + + suspend fun getRecipient(address: Address): Recipient? { + return observeRecipient(address).first() + } + + /** + * Returns a [Recipient] for the given address, or null if not found. + * + * Note that this method might be querying database directly so use with caution. + */ + @DelicateCoroutinesApi + fun getRecipientSync(address: Address): Recipient? { + val flow = observeRecipient(address) + + // If the flow is a SharedFlow, we might be able to access its last cached value directly. + if (flow is SharedFlow) { + val lastCacheValue = flow.replayCache.lastOrNull() + if (lastCacheValue != null) { + return lastCacheValue + } + } + + // Otherwise, we might have to go to the database to get the recipient.. + return fetchRecipient( + address = address, + settingsFetcher = recipientDatabase::getRecipientSettings, + openGroupFetcher = storage.get()::getOpenGroup + )?.first + } + + /** + * Returns the recipient name for the given address. This will try to get the information + * as efficiently as possible, but if it fails to do so a blocking call to the database + * might be made. + * + * If you know the recipient is backed by the config system, it's better to use + * [getBasicRecipientFast] instead. + */ + @DelicateCoroutinesApi + inline fun getRecipientDisplayNameSync( + address: Address, + fallbackName: () -> String? = { null } + ): String { + val basic = getBasicRecipientFast(address) + if (basic != null) { + return basic.displayName + } + + return getRecipientSync(address).displayNameOrFallback( + fallbackName = fallbackName, + address = address.address, + ) + } + + /** + * Returns a [BasicRecipient] for the given address, without going into the database. + * If it's impossible, this will return null. When it does, it doesn't mean the recipient + * doesn't exist, it just means we don't have a fast way to get it. You will need to call + * [RecipientRepository.getRecipient] to get the full recipient data. + */ + fun getBasicRecipientFast(address: Address): BasicRecipient.ConfigBasedRecipient? { + return when { + // Is this our own address? + address.address.equals(preferences.getLocalNumber(), ignoreCase = true) -> { + configFactory.withUserConfigs { configs -> + BasicRecipient.Self( + address = address, + name = configs.userProfile.getName().orEmpty(), + avatar = configs.userProfile.getPic().toRecipientAvatar(), + expiryMode = configs.userProfile.getNtsExpiry(), + acceptsCommunityMessageRequests = configs.userProfile.getCommunityMessageRequests(), + priority = configs.userProfile.getNtsPriority(), + ) + } + } + + // Is this in our contact? + !address.isGroupOrCommunity && + IdPrefix.fromValue(address.address) == IdPrefix.STANDARD -> { + configFactory.withUserConfigs { configs -> + configs.contacts.get(address.address) + }?.let { contact -> + BasicRecipient.Contact( + address = address, + name = contact.name, + nickname = contact.nickname.takeIf { it.isNotBlank() }, + avatar = contact.profilePicture.toRecipientAvatar(), + approved = contact.approved, + approvedMe = contact.approvedMe, + blocked = contact.blocked, + expiryMode = contact.expiryMode, + priority = contact.priority, + ) + } + } + + // Is this a group? + address.isGroupV2 -> { + val groupId = AccountId(address.address) + val groupInfo = configFactory.getGroup(groupId) ?: return null + configFactory.withGroupConfigs(groupId) { configs -> + BasicRecipient.Group( + address = address, + avatar = configs.groupInfo.getProfilePic().toRecipientAvatar(), + expiryMode = configs.groupInfo.expiryMode, + name = configs.groupInfo.getName() ?: groupInfo.name, + approved = !groupInfo.invited, + priority = groupInfo.priority + ) + } + } + + // Otherwise, there's no fast way to get a basic recipient + else -> null + } + } + + /** + * Returns a recipient for the given address, or an empty recipient if not found. + * This is useful to avoid null checks in the UI. + */ + @DelicateCoroutinesApi + fun getRecipientSyncOrEmpty(address: Address): Recipient { + return getRecipientSync(address) ?: empty(address) + } + + suspend fun getRecipientOrEmpty(address: Address): Recipient { + return getRecipient(address) ?: empty(address) + } + + fun getAllConfigBasedUnapprovedConversations(): List
{ + return getConfigBasedConversations( + nts = { false }, + contactFilter = { !it.approved && it.approvedMe && !it.blocked }, + groupFilter = { it.invited }, + communityFilter = { false }, + legacyFilter = { false }, + ) + } + + fun getAllConfigBasedApprovedConversations(): List
{ + return getConfigBasedConversations( + contactFilter = { it.approved && !it.blocked }, + groupFilter = { !it.invited } + ) + } + + fun getConfigBasedConversations( + nts: (ReadableUserProfile) -> Boolean = { true }, + contactFilter: (Contact) -> Boolean = { true }, + groupFilter: (GroupInfo.ClosedGroupInfo) -> Boolean = { true }, + legacyFilter: (GroupInfo.LegacyGroupInfo) -> Boolean = { true }, + communityFilter: (GroupInfo.CommunityGroupInfo) -> Boolean = { true } + ): List
{ + val (shouldHaveNts, contacts, groups) = configFactory.withUserConfigs { configs -> + Triple( + configs.userProfile.getNtsPriority() >= 0 && nts(configs.userProfile), + configs.contacts.all(), + configs.userGroups.all(), + ) + } + + val ntsSequence = sequenceOf( + preferences.getLocalNumber() + ?.takeIf { shouldHaveNts } + ?.let(Address::fromSerialized)) + .filterNotNull() + + val contactsSequence = contacts.asSequence() + .filter { it.priority >= 0 && contactFilter(it) } + .map { Address.fromSerialized(it.id) } + + val groupsSequence = groups.asSequence() + .filterIsInstance() + .filter { it.priority >= 0 && groupFilter(it) } + .map { Address.fromSerialized(it.groupAccountId) } + + val legacyGroupsSequence = groups.asSequence() + .filterIsInstance() + .filter { it.priority >= 0 && legacyFilter(it) } + .map { Address.fromSerialized(GroupUtil.doubleEncodeGroupID(it.accountId)) } + + val communityGroupsSequence = groups.asSequence() + .filterIsInstance() + .filter(communityFilter) + .map { Address.fromSerialized(GroupUtil.getEncodedOpenGroupID(it.groupId.toByteArray())) } + + return (ntsSequence + contactsSequence + groupsSequence + legacyGroupsSequence + communityGroupsSequence).toList() + } + + private val GroupInfo.CommunityGroupInfo.groupId: String + get() = "${community.baseUrl}.${community.room}" + + companion object { + private const val TAG = "RecipientRepository" + + private fun createLocalRecipient(basic: BasicRecipient.Self): Recipient { + return Recipient( + basic = basic, + mutedUntil = null, + autoDownloadAttachments = true, + notifyType = RecipientDatabase.NOTIFY_TYPE_ALL, + acceptsCommunityMessageRequests = basic.acceptsCommunityMessageRequests, + ) + } + + private val RecipientSettings.muteUntilDate: ZonedDateTime? + get() = if (muteUntil > 0) { + Instant.ofEpochMilli(muteUntil).atZone(ZoneId.of("UTC")) + } else { + null + } + + private val ReadableGroupInfoConfig.expiryMode: ExpiryMode + get() { + val timer = getExpiryTimer() + return when { + timer > 0 -> ExpiryMode.AfterSend(timer) + else -> ExpiryMode.NONE + } + } + + private fun createGroupV2Recipient( + basic: BasicRecipient.Group, + settings: RecipientSettings? + ): Recipient { + return Recipient( + basic = basic, + mutedUntil = settings?.muteUntilDate, + notifyType = settings?.notifyType ?: RecipientDatabase.NOTIFY_TYPE_ALL, + autoDownloadAttachments = settings?.autoDownloadAttachments, + acceptsCommunityMessageRequests = false, + ) + + } + + /** + * Creates a RecipientV2 instance from the provided Contact config and optional fallback settings. + */ + private fun createContactRecipient( + basic: BasicRecipient.Contact, + fallbackSettings: RecipientSettings?, // Local db data + ): Recipient { + return Recipient( + basic = basic, + mutedUntil = fallbackSettings?.muteUntilDate, + autoDownloadAttachments = fallbackSettings?.autoDownloadAttachments, + notifyType = fallbackSettings?.notifyType ?: RecipientDatabase.NOTIFY_TYPE_ALL, + acceptsCommunityMessageRequests = fallbackSettings?.blocksCommunityMessagesRequests == false, + ) + } + + private fun createCommunityRecipient( + address: Address, + config: GroupInfo.CommunityGroupInfo?, + community: OpenGroup, + settings: RecipientSettings?, + ): Recipient { + return Recipient( + basic = BasicRecipient.Generic( + address = address, + displayName = community.name, + avatar = community.imageId?.let { + RemoteFile.Community( + communityServerBaseUrl = community.server, + roomId = community.room, + fileId = it, + ) + }, + priority = config?.priority ?: PRIORITY_VISIBLE, + ), + mutedUntil = settings?.muteUntilDate, + autoDownloadAttachments = settings?.autoDownloadAttachments, + notifyType = settings?.notifyType ?: RecipientDatabase.NOTIFY_TYPE_ALL, + acceptsCommunityMessageRequests = false, + notificationChannel = null, + ) + } + + private fun createLegacyGroupRecipient( + address: Address, + config: GroupInfo.LegacyGroupInfo?, + group: GroupRecord, // Local db data + settings: RecipientSettings?, // Local db data + ): Recipient { + return Recipient( + basic = BasicRecipient.Generic( + address = address, + displayName = group.title, + avatar = if (group.url != null && group.avatarId != null) { + RemoteFile.Community( + communityServerBaseUrl = group.url, + roomId = "", + fileId = group.avatarId.toString() + ) + } else { + null + }, + priority = config?.priority ?: PRIORITY_VISIBLE, + ), + mutedUntil = settings?.muteUntilDate, + autoDownloadAttachments = settings?.autoDownloadAttachments, + notifyType = settings?.notifyType ?: RecipientDatabase.NOTIFY_TYPE_ALL, + acceptsCommunityMessageRequests = false, + ) + } + + /** + * Creates a RecipientV2 instance from the provided Address and RecipientSettings. + * Note that this method assumes the recipient is not ourselves. + */ + private fun createGenericRecipient( + address: Address, + settings: RecipientSettings + ): Recipient { + return Recipient( + basic = BasicRecipient.Generic( + address = address, + displayName = settings.systemDisplayName?.takeIf { it.isNotBlank() } + ?: settings.profileName.orEmpty(), + avatar = settings.profileAvatar?.let { + RemoteFile.from( + it, + settings.profileKey + ) + }, + isLocalNumber = false, + blocked = settings.blocked + ), + mutedUntil = settings.muteUntil.takeIf { it > 0 } + ?.let { ZonedDateTime.from(Instant.ofEpochMilli(it)) }, + autoDownloadAttachments = settings.autoDownloadAttachments, + notifyType = settings.notifyType, + acceptsCommunityMessageRequests = !settings.blocksCommunityMessagesRequests + ) + } + + fun empty(address: Address): Recipient { + return Recipient( + basic = BasicRecipient.Generic(address = address), + mutedUntil = null, + autoDownloadAttachments = null, + notifyType = RecipientDatabase.NOTIFY_TYPE_ALL, + acceptsCommunityMessageRequests = false, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt index df8bf98be0..59767a3f57 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt @@ -1,29 +1,24 @@ package org.thoughtcrime.securesms.database -import android.content.ContentValues import android.content.Context -import android.database.Cursor -import org.json.JSONArray -import org.session.libsession.messaging.contacts.Contact -import org.session.libsignal.utilities.AccountId -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.utilities.IdPrefix -import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import javax.inject.Provider +@Deprecated("We no longer store contacts in the database, use the one from config instead") class SessionContactDatabase(context: Context, helper: Provider) : Database(context, helper) { companion object { const val sessionContactTable = "session_contact_database" + const val accountID = "session_id" - const val name = "name" - const val nickname = "nickname" - const val profilePictureURL = "profile_picture_url" - const val profilePictureFileName = "profile_picture_file_name" - const val profilePictureEncryptionKey = "profile_picture_encryption_key" - const val threadID = "thread_id" + private const val name = "name" + private const val nickname = "nickname" + private const val profilePictureURL = "profile_picture_url" + private const val profilePictureFileName = "profile_picture_file_name" + private const val profilePictureEncryptionKey = "profile_picture_encryption_key" + private const val threadID = "thread_id" const val isTrusted = "is_trusted" + @JvmStatic val createSessionContactTableCommand = "CREATE TABLE $sessionContactTable " + "($accountID STRING PRIMARY KEY, " + @@ -35,97 +30,4 @@ class SessionContactDatabase(context: Context, helper: Provider - contactFromCursor(cursor) - } - } - - fun getContacts(accountIDs: Collection): List { - val database = readableDatabase - return database.getAll( - sessionContactTable, - "$accountID IN (SELECT value FROM json_each(?))", - arrayOf(JSONArray(accountIDs).toString()) - ) { cursor -> contactFromCursor(cursor) } - } - - fun getAllContacts(): Set { - val database = readableDatabase - return database.getAll(sessionContactTable, null, null) { cursor -> - contactFromCursor(cursor) - }.toSet() - } - - fun setContactIsTrusted(contact: Contact, isTrusted: Boolean, threadID: Long) { - val database = writableDatabase - val contentValues = ContentValues(1) - contentValues.put(Companion.isTrusted, if (isTrusted) 1 else 0) - database.update(sessionContactTable, contentValues, "$accountID = ?", arrayOf( contact.accountID )) - if (threadID >= 0) { - notifyConversationListeners(threadID) - } - notifyConversationListListeners() - } - - fun setContact(contact: Contact) { - val database = writableDatabase - val contentValues = ContentValues(8) - contentValues.put(accountID, contact.accountID) - contentValues.put(name, contact.name) - contentValues.put(nickname, contact.nickname) - contentValues.put(profilePictureURL, contact.profilePictureURL) - contentValues.put(profilePictureFileName, contact.profilePictureFileName) - contact.profilePictureEncryptionKey?.let { - contentValues.put(profilePictureEncryptionKey, Base64.encodeBytes(it)) - } - contentValues.put(threadID, contact.threadID) - database.insertOrUpdate(sessionContactTable, contentValues, "$accountID = ?", arrayOf( contact.accountID )) - notifyConversationListListeners() - } - - fun deleteContact(accountId: String) { - val database = writableDatabase - val rowsAffected = database.delete(sessionContactTable, "$accountID = ?", arrayOf( accountId )) - if (rowsAffected == 0) { - Log.w("SessionContactDatabase", "Failed to delete contact with id: $accountId") - } - notifyConversationListListeners() - } - - fun contactFromCursor(cursor: Cursor): Contact { - val contact = Contact(cursor.getString(accountID)) - contact.name = cursor.getStringOrNull(name) - contact.nickname = cursor.getStringOrNull(nickname) - contact.profilePictureURL = cursor.getStringOrNull(profilePictureURL) - contact.profilePictureFileName = cursor.getStringOrNull(profilePictureFileName) - cursor.getStringOrNull(profilePictureEncryptionKey)?.let { - contact.profilePictureEncryptionKey = Base64.decode(it) - } - contact.threadID = cursor.getLong(threadID) - return contact - } - - fun queryContactsByName(constraint: String, excludeUserAddresses: Set = emptySet()): Cursor { - val whereClause = StringBuilder("($name LIKE ? OR $nickname LIKE ?)") - val whereArgs = ArrayList() - whereArgs.add("%$constraint%") - whereArgs.add("%$constraint%") - - // filter out users is the list isn't empty - if (excludeUserAddresses.isNotEmpty()) { - whereClause.append(" AND $accountID NOT IN (") - whereClause.append(excludeUserAddresses.joinToString(", ") { "?" }) - whereClause.append(")") - - whereArgs.addAll(excludeUserAddresses) - } - - return readableDatabase.query( - sessionContactTable, null, whereClause.toString(), whereArgs.toTypedArray(), - null, null, null - ) - } } \ No newline at end of file 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 e977f9f978..acd41f422c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -17,7 +17,6 @@ */ package org.thoughtcrime.securesms.database; -import static org.session.libsignal.utilities.Util.SECURE_RANDOM; import static org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_UPDATE_MESSAGE_BIT; import android.content.ContentValues; @@ -54,19 +53,23 @@ import java.io.Closeable; import java.io.IOException; import java.util.Arrays; -import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import javax.inject.Inject; import javax.inject.Provider; +import javax.inject.Singleton; + +import dagger.hilt.android.qualifiers.ApplicationContext; /** * Database for storage of SMS messages. * * @author Moxie Marlinspike */ +@Singleton public class SmsDatabase extends MessagingDatabase { private static final String TAG = SmsDatabase.class.getSimpleName(); @@ -150,8 +153,14 @@ public class SmsDatabase extends MessagingDatabase { private static final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache(); private static final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache(); - public SmsDatabase(Context context, Provider databaseHelper) { + private final RecipientRepository recipientRepository; + + @Inject + public SmsDatabase(@ApplicationContext Context context, + Provider databaseHelper, + RecipientRepository recipientRepository) { super(context, databaseHelper); + this.recipientRepository = recipientRepository; } protected String getTableName() { @@ -443,14 +452,14 @@ private Pair updateMessageBodyAndType(long messageId, String body, l } protected Optional insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runThreadUpdate) { - Recipient recipient = Recipient.from(context, message.getSender(), true); + Address recipient = message.getSender(); - Recipient groupRecipient; + Address groupRecipient; if (message.getGroupId() == null) { groupRecipient = null; } else { - groupRecipient = Recipient.from(context, message.getGroupId(), true); + groupRecipient = message.getGroupId(); } boolean unread = (message.isSecureMessage() || message.isGroup() || message.isUnreadCallMessage()); @@ -512,10 +521,6 @@ protected Optional insertMessageInbox(IncomingTextMessage message, DatabaseComponent.get(context).threadDatabase().update(threadId, true); } - if (message.getSubscriptionId() != -1) { - DatabaseComponent.get(context).recipientDatabase().setDefaultSubscriptionId(recipient, message.getSubscriptionId()); - } - notifyConversationListeners(threadId); return Optional.of(new InsertResult(messageId, threadId)); @@ -571,7 +576,7 @@ public long insertMessageOutbox(long threadId, OutgoingTextMessage message, if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT; if (message.isOpenGroupInvitation()) type |= Types.OPEN_GROUP_INVITATION_BIT; - Address address = message.getRecipient().getAddress(); + Address address = message.getRecipient(); Map earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(date); Map earlyReadReceipts = earlyReadReceiptCache.remove(date); @@ -712,7 +717,7 @@ private boolean isDuplicate(IncomingTextMessage message, long threadId) { private boolean isDuplicate(OutgoingTextMessage 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.getRecipient().getAddress().toString(), String.valueOf(threadId)}, + new String[]{String.valueOf(message.getSentTimestampMillis()), message.getRecipient().toString(), String.valueOf(threadId)}, null, null, null, "1"); try { @@ -786,33 +791,6 @@ public Reader readerFor(Cursor cursor) { return new Reader(cursor); } - public OutgoingMessageReader readerFor(OutgoingTextMessage message, long threadId) { - return new OutgoingMessageReader(message, threadId); - } - - public class OutgoingMessageReader { - - private final OutgoingTextMessage message; - private final long id; - private final long threadId; - - public OutgoingMessageReader(OutgoingTextMessage message, long threadId) { - this.message = message; - this.threadId = threadId; - this.id = SECURE_RANDOM.nextLong(); - } - - public MessageRecord getCurrent() { - return new SmsMessageRecord(id, message.getMessageBody(), - message.getRecipient(), message.getRecipient(), - SnodeAPI.getNowWithOffset(), SnodeAPI.getNowWithOffset(), - 0, message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(), - threadId, 0, new LinkedList(), - message.getExpiresIn(), - SnodeAPI.getNowWithOffset(), 0, Collections.emptyList(), false); - } - } - public class Reader implements Closeable { private final Cursor cursor; @@ -856,7 +834,7 @@ public SmsMessageRecord getCurrent() { } List mismatches = getMismatches(mismatchDocument); - Recipient recipient = Recipient.from(context, address, true); + Recipient recipient = recipientRepository.getRecipientSyncOrEmpty(address); List reactions = DatabaseComponent.get(context).reactionDatabase().getReactions(cursor); return new SmsMessageRecord(messageId, body, recipient, 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 8bc32a1211..8e60e9caee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -13,22 +13,18 @@ import network.loki.messenger.libsession_util.util.Bytes import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupInfo import network.loki.messenger.libsession_util.util.KeyPair -import network.loki.messenger.libsession_util.util.UserPic -import org.session.libsession.avatars.AvatarHelper import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.BlindedIdMapping import org.session.libsession.messaging.calls.CallMessageType -import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageSendJob -import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob -import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.messages.ProfileUpdateHandler import org.session.libsession.messaging.messages.control.GroupUpdated import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage @@ -58,20 +54,17 @@ import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.GroupDisplayInfo import org.session.libsession.utilities.GroupRecord import org.session.libsession.utilities.GroupUtil -import org.session.libsession.utilities.ProfileKeyUtil import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.UsernameUtils import org.session.libsession.utilities.getGroup +import org.session.libsession.utilities.recipients.RecipientSettings import org.session.libsession.utilities.recipients.Recipient -import org.session.libsession.utilities.recipients.Recipient.DisappearingState import org.session.libsession.utilities.upsertContact import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.utilities.AccountId -import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.KeyHelper @@ -91,7 +84,6 @@ import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.util.FilenameUtils import org.thoughtcrime.securesms.util.SessionMetaProtocol import org.thoughtcrime.securesms.util.SessionMetaProtocol.clearReceivedMessages -import java.security.MessageDigest import javax.inject.Inject import javax.inject.Provider import javax.inject.Singleton @@ -120,16 +112,14 @@ open class Storage @Inject constructor( private val groupMemberDatabase: GroupMemberDatabase, private val reactionDatabase: ReactionDatabase, private val lokiThreadDatabase: LokiThreadDatabase, - private val sessionContactDatabase: SessionContactDatabase, - private val expirationConfigurationDatabase: ExpirationConfigurationDatabase, - private val profileManager: SSKEnvironment.ProfileManagerProtocol, private val notificationManager: MessageNotifier, private val messageDataProvider: MessageDataProvider, private val messageExpirationManager: SSKEnvironment.MessageExpirationManagerProtocol, private val clock: SnodeClock, private val preferences: TextSecurePreferences, - private val usernameUtils: UsernameUtils, private val openGroupManager: Lazy, + private val recipientRepository: RecipientRepository, + private val profileUpdateHandler: ProfileUpdateHandler, ) : Database(context, helper), StorageProtocol, ThreadDatabase.ConversationThreadUpdateListener { init { @@ -138,7 +128,8 @@ open class Storage @Inject constructor( override fun threadCreated(address: Address, threadId: Long) { val localUserAddress = getUserPublicKey() ?: return - if (!getRecipientApproved(address) && localUserAddress != address.toString()) return // don't store unapproved / message requests + val approved = recipientRepository.getRecipientSyncOrEmpty(address).approved + if (!approved && localUserAddress != address.toString()) return // don't store unapproved / message requests when { address.isLegacyGroup -> { @@ -214,36 +205,21 @@ open class Storage @Inject constructor( } override fun getUserProfile(): Profile { - val displayName = usernameUtils.getCurrentUsername() - val profileKey = ProfileKeyUtil.getProfileKey(context) - val profilePictureUrl = TextSecurePreferences.getProfilePictureURL(context) - return Profile(displayName, profileKey, profilePictureUrl) - } - - override fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?) { - val db = recipientDatabase - db.setProfileAvatar(recipient, newProfilePicture) - db.setProfileKey(recipient, newProfileKey) + return configFactory.withUserConfigs { configs -> + val pic = configs.userProfile.getPic() + Profile( + displayName = configs.userProfile.getName(), + profilePictureURL = pic.url.takeIf { it.isNotBlank() }, + profileKey = pic.key.data.takeIf { pic.url.isNotBlank() }, + ) + } } - override fun setBlocksCommunityMessageRequests(recipient: Recipient, blocksMessageRequests: Boolean) { + override fun setBlocksCommunityMessageRequests(recipient: Address, blocksMessageRequests: Boolean) { val db = recipientDatabase db.setBlocksCommunityMessageRequests(recipient, blocksMessageRequests) } - override fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) { - val ourRecipient = fromSerialized(getUserPublicKey()!!).let { - Recipient.from(context, it, false) - } - ourRecipient.resolve().profileKey = newProfileKey - preferences.setProfileKey(newProfileKey?.let { Base64.encodeBytes(it) }) - preferences.setProfilePictureURL(newProfilePicture) - - if (newProfileKey != null) { - JobQueue.shared.add(RetrieveProfileAvatarJob(newProfilePicture, ourRecipient.address, newProfileKey)) - } - } - override fun getOrGenerateRegistrationID(): Int { var registrationID = TextSecurePreferences.getLocalRegistrationId(context) if (registrationID == 0) { @@ -312,31 +288,31 @@ open class Storage @Inject constructor( getRecipientForThread(threadId)?.let { recipient -> val currentLastRead = threadDb.getLastSeenAndHasSent(threadId).first() // don't set the last read in the volatile if we didn't set it in the DB - if (!threadDb.markAllAsRead(threadId, recipient.isGroupOrCommunityRecipient, lastSeenTime, force) && !force) return + if (!threadDb.markAllAsRead(threadId, recipient.isGroupOrCommunity, lastSeenTime, force) && !force) return // don't process configs for inbox recipients - if (recipient.isCommunityInboxRecipient) return + if (recipient.isCommunityInbox) return configFactory.withMutableUserConfigs { configs -> val config = configs.convoInfoVolatile val convo = when { // recipient closed group - recipient.isLegacyGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.toString())) - recipient.isGroupV2Recipient -> config.getOrConstructClosedGroup(recipient.address.toString()) + recipient.isLegacyGroup -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.toString())) + recipient.isGroupV2 -> config.getOrConstructClosedGroup(recipient.toString()) // recipient is open group - recipient.isCommunityRecipient -> { + recipient.isCommunity -> { val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return@withMutableUserConfigs BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) -> config.getOrConstructCommunity(base, room, pubKey) } ?: return@withMutableUserConfigs } // otherwise recipient is one to one - recipient.isContactRecipient -> { + recipient.isContact -> { // don't process non-standard account IDs though - if (IdPrefix.fromValue(recipient.address.toString()) != IdPrefix.STANDARD) return@withMutableUserConfigs - config.getOrConstructOneToOne(recipient.address.toString()) + if (IdPrefix.fromValue(recipient.toString()) != IdPrefix.STANDARD) return@withMutableUserConfigs + config.getOrConstructOneToOne(recipient.toString()) } - else -> throw NullPointerException("Weren't expecting to have a convo with address ${recipient.address.toString()}") + else -> throw NullPointerException("Weren't expecting to have a convo with address ${recipient.toString()}") } convo.lastRead = lastSeenTime if (convo.unread) { @@ -398,15 +374,18 @@ open class Storage @Inject constructor( } else { senderAddress } - val targetRecipient = Recipient.from(context, targetAddress, false) - if (!targetRecipient.isGroupOrCommunityRecipient) { - if (isUserSender || isUserBlindedSender) { - setRecipientApproved(targetRecipient, true) - } else { - setRecipientApprovedMe(targetRecipient, true) + if (!targetAddress.isGroupOrCommunity && IdPrefix.fromValue(targetAddress.address) == IdPrefix.STANDARD) { + configFactory.withMutableUserConfigs { configs -> + configs.contacts.upsertContact(targetAddress.address) { + if (isUserSender || isUserBlindedSender) { + approved = true + } else { + approvedMe = true + } + } } } - if (message.threadID == null && !targetRecipient.isCommunityRecipient) { + if (message.threadID == null && !targetAddress.isCommunity) { // open group recipients should explicitly create threads message.threadID = getOrCreateThreadIdFor(targetAddress) } @@ -436,7 +415,7 @@ open class Storage @Inject constructor( val mediaMessage = OutgoingMediaMessage.from( message, - targetRecipient, + targetAddress, pointers, quote.orNull(), linkPreviews.orNull()?.firstOrNull(), @@ -459,8 +438,8 @@ open class Storage @Inject constructor( val isOpenGroupInvitation = (message.openGroupInvitation != null) val insertResult = if (isUserSender || isUserBlindedSender) { - val textMessage = if (isOpenGroupInvitation) OutgoingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, targetRecipient, message.sentTimestamp, expiresInMillis, expireStartedAt) - else OutgoingTextMessage.from(message, targetRecipient, expiresInMillis, expireStartedAt) + val textMessage = if (isOpenGroupInvitation) OutgoingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, targetAddress, message.sentTimestamp, expiresInMillis, expireStartedAt) + else OutgoingTextMessage.from(message, targetAddress, expiresInMillis, expireStartedAt) smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!, runThreadUpdate) } else { val textMessage = if (isOpenGroupInvitation) IncomingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, senderAddress, message.sentTimestamp, expiresInMillis, expireStartedAt) @@ -542,25 +521,6 @@ open class Storage @Inject constructor( return configFactory.withUserConfigs { it.userProfile.getCommunityMessageRequests() } } - override fun clearUserPic(clearConfig: Boolean) { - val userPublicKey = getUserPublicKey() ?: return Log.w(TAG, "No user public key when trying to clear user pic") - val recipient = Recipient.from(context, fromSerialized(userPublicKey), false) - - // Clear details related to the user's profile picture - preferences.setProfileKey(null) - ProfileKeyUtil.setEncodedProfileKey(context, null) - recipientDatabase.setProfileAvatar(recipient, null) - preferences.setProfileAvatarId(0) - preferences.setProfilePictureURL(null) - - Recipient.removeCached(fromSerialized(userPublicKey)) // ACL HERE?!?!?! - if (clearConfig) { - configFactory.withMutableUserConfigs { - it.userProfile.setPic(UserPic.DEFAULT) - } - } - } - override fun setAuthToken(room: String, server: String, newValue: String) { val id = "$server.$room" lokiAPIDatabase.setAuthToken(id, newValue) @@ -572,12 +532,11 @@ open class Storage @Inject constructor( } override fun getOpenGroup(threadId: Long): OpenGroup? { - if (threadId.toInt() < 0) { return null } - val database = readableDatabase - return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf( threadId.toString() )) { cursor -> - val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat) - OpenGroup.fromJSON(publicChatAsJson) - } + return lokiThreadDatabase.getOpenGroupChat(threadId) + } + + override fun getOpenGroup(address: Address): OpenGroup? { + return getThreadId(address)?.let(lokiThreadDatabase::getOpenGroupChat) } override fun getOpenGroupPublicKey(server: String): String? { @@ -783,38 +742,6 @@ open class Storage @Inject constructor( } } - override fun updateGroupConfig(groupPublicKey: String) { - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val groupAddress = fromSerialized(groupID) - val existingGroup = getGroup(groupID) - ?: return Log.w("Loki-DBG", "No existing group for ${groupPublicKey.take(4)}} when updating group config") - configFactory.withMutableUserConfigs { - val userGroups = it.userGroups - if (!existingGroup.isActive) { - userGroups.eraseLegacyGroup(groupPublicKey) - return@withMutableUserConfigs - } - val name = existingGroup.title - val admins = existingGroup.admins.map { it.toString() } - val members = existingGroup.members.map { it.toString() } - val membersMap = GroupUtil.createConfigMemberMap(admins = admins, members = members) - val latestKeyPair = getLatestClosedGroupEncryptionKeyPair(groupPublicKey) - ?: return@withMutableUserConfigs Log.w("Loki-DBG", "No latest closed group encryption key pair for ${groupPublicKey.take(4)}} when updating group config") - - val threadID = getThreadId(groupAddress) ?: return@withMutableUserConfigs - val groupInfo = userGroups.getOrConstructLegacyGroupInfo(groupPublicKey).copy( - name = name, - members = membersMap, - encPubKey = Bytes((latestKeyPair.publicKey as DjbECPublicKey).publicKey), // 'serialize()' inserts an extra byte - encSecKey = Bytes(latestKeyPair.privateKey.serialize()), - priority = if (isPinned(threadID)) PRIORITY_PINNED else PRIORITY_VISIBLE, - disappearingTimer = getExpirationConfiguration(threadID)?.expiryMode?.expirySeconds ?: 0L, - joinedAtSecs = (existingGroup.formationTimestamp / 1000L) - ) - userGroups.set(groupInfo) - } - } - override fun isGroupActive(groupPublicKey: String): Boolean { return groupDatabase.getGroup(GroupUtil.doubleEncodeGroupID(groupPublicKey)).orNull()?.isActive == true } @@ -823,10 +750,6 @@ open class Storage @Inject constructor( groupDatabase.setActive(groupID, value) } - override fun getZombieMembers(groupID: String): Set { - return groupDatabase.getGroupZombieMembers(groupID).map { it.address.toString() }.toHashSet() - } - override fun removeMember(groupID: String, member: Address) { groupDatabase.removeMember(groupID, member) } @@ -835,10 +758,6 @@ open class Storage @Inject constructor( groupDatabase.updateMembers(groupID, members) } - override fun setZombieMembers(groupID: String, members: List
) { - groupDatabase.updateZombieMembers(groupID, members) - } - override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, sentTimestamp: Long): Long? { val group = SignalServiceGroup(type, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList()) val m = IncomingTextMessage(fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, 0, true, false) @@ -856,7 +775,7 @@ open class Storage @Inject constructor( override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, threadID: Long, sentTimestamp: Long): Long? { val userPublicKey = getUserPublicKey()!! - val recipient = Recipient.from(context, fromSerialized(groupID), false) + val recipient = fromSerialized(groupID) val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() ?: "" val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, 0, true, null, listOf(), listOf()) val mmsDB = mmsDatabase @@ -1000,11 +919,11 @@ open class Storage @Inject constructor( private fun insertUpdateControlMessage(updateData: UpdateMessageData, sentTimestamp: Long, senderPublicKey: String?, closedGroup: AccountId): MessageId? { val userPublicKey = getUserPublicKey()!! - val recipient = Recipient.from(context, fromSerialized(closedGroup.hexString), false) + val address = fromSerialized(closedGroup.hexString) + val recipient = recipientRepository.getRecipientSync(address) val threadDb = threadDatabase - val threadID = threadDb.getThreadIdIfExistsFor(recipient) - val expirationConfig = getExpirationConfiguration(threadID) - val expiryMode = expirationConfig?.expiryMode + val threadID = threadDb.getThreadIdIfExistsFor(address) + val expiryMode = recipient?.expiryMode val expiresInMillis = expiryMode?.expiryMillis ?: 0 val expireStartedAt = if (expiryMode is ExpiryMode.AfterSend) sentTimestamp else 0 val inviteJson = updateData.toJSON() @@ -1012,7 +931,7 @@ open class Storage @Inject constructor( if (senderPublicKey == null || senderPublicKey == userPublicKey) { val infoMessage = OutgoingGroupMediaMessage( - recipient, + address, inviteJson, closedGroup.hexString, null, @@ -1097,31 +1016,25 @@ open class Storage @Inject constructor( return jobDatabase.hasBackgroundGroupAddJob(groupJoinUrl) } - override fun setProfileSharing(address: Address, value: Boolean) { - val recipient = Recipient.from(context, address, false) - recipientDatabase.setProfileSharing(recipient, value) - } - override fun getOrCreateThreadIdFor(address: Address): Long { - val recipient = Recipient.from(context, address, false) - return threadDatabase.getOrCreateThreadIdFor(recipient) + return threadDatabase.getOrCreateThreadIdFor(address) } override fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long? { val database = threadDatabase return if (!openGroupID.isNullOrEmpty()) { - val recipient = Recipient.from(context, fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false) + val recipient = fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())) database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } else if (!groupPublicKey.isNullOrEmpty() && !groupPublicKey.startsWith(IdPrefix.GROUP.value)) { - val recipient = Recipient.from(context, fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false) + val recipient = fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)) if (createThread) database.getOrCreateThreadIdFor(recipient) else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } else if (!groupPublicKey.isNullOrEmpty()) { - val recipient = Recipient.from(context, fromSerialized(groupPublicKey), false) + val recipient = fromSerialized(groupPublicKey) if (createThread) database.getOrCreateThreadIdFor(recipient) else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } else { - val recipient = Recipient.from(context, fromSerialized(publicKey), false) + val recipient = fromSerialized(publicKey) if (createThread) database.getOrCreateThreadIdFor(recipient) else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } @@ -1137,12 +1050,7 @@ open class Storage @Inject constructor( } override fun getThreadId(address: Address): Long? { - val recipient = Recipient.from(context, address, false) - return getThreadId(recipient) - } - - override fun getThreadId(recipient: Recipient): Long? { - val threadID = threadDatabase.getThreadIdIfExistsFor(recipient) + val threadID = threadDatabase.getThreadIdIfExistsFor(address) return if (threadID < 0) null else threadID } @@ -1155,21 +1063,6 @@ open class Storage @Inject constructor( return threadId ?: -1 } - override fun getContactWithAccountID(accountID: String): Contact? { - return sessionContactDatabase.getContactWithAccountID(accountID) - } - - override fun getAllContacts(): Set { - return sessionContactDatabase.getAllContacts() - } - - override fun setContact(contact: Contact) { - sessionContactDatabase.setContact(contact) - val address = fromSerialized(contact.accountID) - if (!getRecipientApproved(address)) return - profileManager.contactUpdatedInternal(contact) - } - override fun deleteContactAndSyncConfig(accountId: String) { deleteContact(accountId) // also handle the contact removal from the config's point of view @@ -1177,8 +1070,6 @@ open class Storage @Inject constructor( } private fun deleteContact(accountId: String){ - sessionContactDatabase.deleteContact(accountId) - Recipient.removeCached(fromSerialized(accountId)) recipientDatabase.deleteRecipient(accountId) val threadId: Long = threadDatabase.getThreadIdIfExistsFor(accountId) @@ -1187,16 +1078,12 @@ open class Storage @Inject constructor( notifyRecipientListeners() } - override fun getRecipientForThread(threadId: Long): Recipient? { + override fun getRecipientForThread(threadId: Long): Address? { return threadDatabase.getRecipientForThreadId(threadId) } - override fun getRecipientSettings(address: Address): Recipient.RecipientSettings? { - return recipientDatabase.getRecipientSettings(address).orNull() - } - - override fun hasAutoDownloadFlagBeenSet(recipient: Recipient): Boolean { - return recipientDatabase.isAutoDownloadFlagSet(recipient) + override fun getRecipientSettings(address: Address): RecipientSettings? { + return recipientDatabase.getRecipientSettings(address) } override fun syncLibSessionContacts(contacts: List, timestamp: Long?) { @@ -1207,43 +1094,12 @@ open class Storage @Inject constructor( } moreContacts.forEach { contact -> val address = fromSerialized(contact.id) - val recipient = Recipient.from(context, address, false) - setBlocked(listOf(recipient), contact.blocked, fromConfigUpdate = true) - setRecipientApproved(recipient, contact.approved) - setRecipientApprovedMe(recipient, contact.approvedMe) - if (contact.name.isNotEmpty()) { - profileManager.setName(context, recipient, contact.name) - } else { - profileManager.setName(context, recipient, null) - } - if (contact.nickname.isNotEmpty()) { - profileManager.setNickname(context, recipient, contact.nickname) - } else { - profileManager.setNickname(context, recipient, null) - } - if (contact.profilePicture != UserPic.DEFAULT) { - val (url, key) = contact.profilePicture - if (key.data.size != ProfileKeyUtil.PROFILE_KEY_BYTES) return@forEach - profileManager.setProfilePicture(context, recipient, url, key.data) - } else { - profileManager.setProfilePicture(context, recipient, null, null) - } if (contact.priority == PRIORITY_HIDDEN) { - getThreadId(fromSerialized(contact.id))?.let(::deleteConversation) + getThreadId(address)?.let(::deleteConversation) } else { - ( - getThreadId(address) ?: getOrCreateThreadIdFor(address).also { - setThreadCreationDate(it, 0) - } - ).also { setPinned(it, contact.priority == PRIORITY_PINNED) } - } - if (timestamp != null) { - getThreadId(recipient)?.let { - setExpirationConfiguration( - getExpirationConfiguration(it)?.takeIf { it.updatedTimestampMs > timestamp } - ?: ExpirationConfiguration(it, contact.expiryMode, timestamp) - ) + getOrCreateThreadIdFor(address).also { + setThreadCreationDate(it, 0) } } } @@ -1255,22 +1111,18 @@ open class Storage @Inject constructor( // which in the case of contacts we are messaging for the first time and who haven't yet approved us, it won't be the case // But that person is saved in the Recipient db. We might need to investigate how to clean the relationship between Recipients, Contacts and config Contacts. val removedContacts = recipientDatabase.allRecipients.filter { localContact -> - IdPrefix.fromValue(localContact.address.toString()) == IdPrefix.STANDARD && // only want standard address - localContact.is1on1 && // only for conversations - localContact.address.toString() != currentUserKey && // we don't want to remove ourselves (ie, our Note to Self) - moreContacts.none { it.id == localContact.address.toString() } // we don't want to remove contacts that are present in the config + IdPrefix.fromValue(localContact.toString()) == IdPrefix.STANDARD && // only want standard address + localContact.isContact && // only for conversations + localContact.address != currentUserKey && // we don't want to remove ourselves (ie, our Note to Self) + moreContacts.none { it.id == localContact.address } // we don't want to remove contacts that are present in the config } removedContacts.forEach { - deleteContact(it.address.toString()) + deleteContact(it.address) } } - - override fun shouldAutoDownloadAttachments(recipient: Recipient): Boolean { - return recipient.autoDownloadAttachments - } override fun setAutoDownloadAttachments( - recipient: Recipient, + recipient: Address, shouldAutoDownloadAttachments: Boolean ) { val recipientDb = recipientDatabase @@ -1282,11 +1134,6 @@ open class Storage @Inject constructor( return threadDB.getLastUpdated(threadID) } - override fun trimThread(threadID: Long, threadLimit: Int) { - val threadDB = threadDatabase - threadDB.trimThread(threadID, threadLimit) - } - override fun trimThreadBefore(threadID: Long, timestamp: Long) { val threadDB = threadDatabase threadDB.trimThreadBefore(threadID, timestamp) @@ -1297,36 +1144,34 @@ open class Storage @Inject constructor( return mmsSmsDb.getConversationCount(threadID) } - override fun setPinned(threadID: Long, isPinned: Boolean) { - val threadDB = threadDatabase - threadDB.setPinned(threadID, isPinned) - val threadRecipient = getRecipientForThread(threadID) ?: return + override fun setPinned(address: Address, isPinned: Boolean) { + val isLocalNumber = address == getUserPublicKey()?.let { fromSerialized(it) } configFactory.withMutableUserConfigs { configs -> - if (threadRecipient.isLocalNumber) { + if (isLocalNumber) { configs.userProfile.setNtsPriority(if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE) - } else if (threadRecipient.isContactRecipient) { - configs.contacts.upsertContact(threadRecipient.address.toString()) { + } else if (address.isContact) { + configs.contacts.upsertContact(address.toString()) { priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE } - } else if (threadRecipient.isGroupOrCommunityRecipient) { + } else if (address.isGroupOrCommunity) { when { - threadRecipient.isLegacyGroupRecipient -> { - threadRecipient.address.toString() + address.isLegacyGroup -> { + address.toString() .let(GroupUtil::doubleDecodeGroupId) .let(configs.userGroups::getOrConstructLegacyGroupInfo) .copy(priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE) .let(configs.userGroups::set) } - threadRecipient.isGroupV2Recipient -> { + address.isGroupV2 -> { val newGroupInfo = configs.userGroups - .getOrConstructClosedGroup(threadRecipient.address.toString()) + .getOrConstructClosedGroup(address.toString()) .copy(priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE) configs.userGroups.set(newGroupInfo) } - threadRecipient.isCommunityRecipient -> { - val openGroup = getOpenGroup(threadID) ?: return@withMutableUserConfigs + address.isCommunity -> { + val openGroup = getOpenGroup(address) ?: return@withMutableUserConfigs val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return@withMutableUserConfigs val newGroupInfo = configs.userGroups.getOrConstructCommunityInfo( @@ -1341,11 +1186,6 @@ open class Storage @Inject constructor( } } - override fun isPinned(threadID: Long): Boolean { - val threadDB = threadDatabase - return threadDB.isPinned(threadID) - } - override fun setThreadCreationDate(threadId: Long, newDate: Long) { val threadDb = threadDatabase threadDb.setCreationDate(threadId, newDate) @@ -1362,7 +1202,7 @@ open class Storage @Inject constructor( val threadDB = threadDatabase val groupDB = groupDatabase - val recipientAddress = getRecipientForThread(threadID)?.address + val recipientAddress = getRecipientForThread(threadID) // Delete the conversation and its messages smsDatabase.deleteThread(threadID) @@ -1443,14 +1283,12 @@ open class Storage @Inject constructor( override fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) { val address = fromSerialized(senderPublicKey) - val recipient = Recipient.from(context, address, false) + val recipient = recipientRepository.getRecipientSync(address) - if (recipient.isBlocked) return - val threadId = getThreadId(recipient) ?: return - val expirationConfig = getExpirationConfiguration(threadId) - val expiryMode = expirationConfig?.expiryMode ?: ExpiryMode.NONE - val expiresInMillis = expiryMode.expiryMillis - val expireStartedAt = if (expiryMode is ExpiryMode.AfterSend) sentTimestamp else 0 + if (recipient?.blocked == true) return + val threadId = getThreadId(address) ?: return + val expiresInMillis = recipient?.expiryMode?.expiryMillis ?: 0 + val expireStartedAt = if (recipient?.expiryMode is ExpiryMode.AfterSend) sentTimestamp else 0 val mediaMessage = IncomingMediaMessage( address, sentTimestamp, @@ -1492,46 +1330,41 @@ open class Storage @Inject constructor( ) return if (userPublicKey == senderPublicKey) { - val requestRecipient = Recipient.from(context, fromSerialized(recipientPublicKey), false) + val requestRecipient = fromSerialized(recipientPublicKey) recipientDatabase.setApproved(requestRecipient, true) val threadId = threadDatabase.getOrCreateThreadIdFor(requestRecipient) threadDatabase.setHasSent(threadId, true) } else { - val sender = Recipient.from(context, fromSerialized(senderPublicKey), false) - val threadId = getOrCreateThreadIdFor(sender.address) + val sender = fromSerialized(senderPublicKey) + val threadId = getOrCreateThreadIdFor(sender) val profile = response.profile if (profile != null) { - val name = profile.displayName!! - if (name.isNotEmpty()) { - profileManager.setName(context, sender, name) - } - val newProfileKey = profile.profileKey - - val needsProfilePicture = !AvatarHelper.avatarFileExists(context, sender.address) - val profileKeyValid = newProfileKey?.isNotEmpty() == true && (newProfileKey.size == 16 || newProfileKey.size == 32) && profile.profilePictureURL?.isNotEmpty() == true - val profileKeyChanged = (sender.profileKey == null || !MessageDigest.isEqual(sender.profileKey, newProfileKey)) - - if ((profileKeyValid && profileKeyChanged) || (profileKeyValid && needsProfilePicture)) { - profileManager.setProfilePicture(context, sender, profile.profilePictureURL!!, newProfileKey!!) - } + profileUpdateHandler.handleProfileUpdate( + sender, + ProfileUpdateHandler.Updates( + name = profile.displayName, + picUrl = profile.profilePictureURL, + picKey = profile.profileKey, + acceptsCommunityRequests = null + ), + communityServerPubKey = null) } val mappingDb = blindedIdMappingDatabase val mappings = mutableMapOf() - threadDatabase.readerFor(threadDatabase.conversationList).use { reader -> - while (reader.next != null) { - val recipient = reader.current.recipient - val address = recipient.address.toString() - val blindedId = when { - recipient.isGroupOrCommunityRecipient -> null - recipient.isCommunityInboxRecipient -> GroupUtil.getDecodedOpenGroupInboxAccountId(address) - else -> address.takeIf { AccountId(it).prefix == IdPrefix.BLINDED } - } ?: continue - mappingDb.getBlindedIdMapping(blindedId).firstOrNull()?.let { - mappings[address] = it - } + + for ((address, _) in threadDatabase.allThreads) { + val blindedId = when { + address.isGroupOrCommunity -> null + address.isCommunityInbox -> GroupUtil.getDecodedOpenGroupInboxAccountId(address.toString()) + else -> address.address.takeIf { AccountId.fromStringOrNull(it)?.prefix == IdPrefix.BLINDED } + } ?: continue + + mappingDb.getBlindedIdMapping(blindedId).firstOrNull()?.let { + mappings[address.address] = it } } + for (mapping in mappings) { if (!BlindKeyAPI.sessionIdMatchesBlindedId( sessionId = senderPublicKey, @@ -1543,24 +1376,26 @@ open class Storage @Inject constructor( } mappingDb.addBlindedIdMapping(mapping.value.copy(accountId = senderPublicKey)) - val blindedThreadId = threadDatabase.getOrCreateThreadIdFor(Recipient.from(context, fromSerialized(mapping.key), false)) + val blindedThreadId = threadDatabase.getOrCreateThreadIdFor(fromSerialized(mapping.key)) mmsDatabase.updateThreadId(blindedThreadId, threadId) smsDatabase.updateThreadId(blindedThreadId, threadId) deleteConversation(blindedThreadId) } - var alreadyApprovedMe: Boolean = false - configFactory.withUserConfigs { - // check is the person had not yet approvedMe - alreadyApprovedMe = it.contacts.get(sender.address.toString())?.approvedMe ?: false - } + var alreadyApprovedMe = false - setRecipientApprovedMe(sender, true) + // Update the contact's approval status + configFactory.withMutableUserConfigs { configs -> + configs.contacts.upsertContact(sender.toString()) { + alreadyApprovedMe = approvedMe + approvedMe = true + } + } // only show the message if wasn't already approvedMe before if(!alreadyApprovedMe) { val message = IncomingMediaMessage( - sender.address, + sender, response.sentTimestamp!!, -1, 0, @@ -1611,40 +1446,10 @@ open class Storage @Inject constructor( mmsDatabase.insertSecureDecryptedMessageInbox(message, threadId, runThreadUpdate = false) } - override fun getRecipientApproved(address: Address): Boolean { - return address.isGroupV2 || recipientDatabase.getApproved(address) - } - - override fun setRecipientApproved(recipient: Recipient, approved: Boolean) { - recipientDatabase.setApproved(recipient, approved) - if (recipient.isLocalNumber || !recipient.isContactRecipient) return - configFactory.withMutableUserConfigs { - it.contacts.upsertContact(recipient.address.toString()) { - // if the contact wasn't approved before but is approved now, make sure it's visible - if(approved && !this.approved) this.priority = PRIORITY_VISIBLE - - // update approval - this.approved = approved - } - } - } - - override fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean) { - recipientDatabase.setApprovedMe(recipient, approvedMe) - if (recipient.isLocalNumber || !recipient.isContactRecipient) return - configFactory.withMutableUserConfigs { - it.contacts.upsertContact(recipient.address.toString()) { - this.approvedMe = approvedMe - } - } - } - override fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) { val address = fromSerialized(senderPublicKey) - val recipient = Recipient.from(context, address, false) - val threadId = threadDatabase.getOrCreateThreadIdFor(recipient) - val expirationConfig = getExpirationConfiguration(threadId) - val expiryMode = expirationConfig?.expiryMode?.coerceSendToRead() ?: ExpiryMode.NONE + val recipient = recipientRepository.getRecipientSync(address) + val expiryMode = recipient?.expiryMode?.coerceSendToRead() ?: ExpiryMode.NONE val expiresInMillis = expiryMode.expiryMillis val expireStartedAt = if (expiryMode is ExpiryMode.AfterSend) sentTimestamp else 0 val callMessage = IncomingTextMessage.fromCallInfo(callMessageType, address, Optional.absent(), sentTimestamp, expiresInMillis, expireStartedAt) @@ -1699,19 +1504,21 @@ open class Storage @Inject constructor( if (mapping.accountId != null) { return mapping } - getAllContacts().forEach { contact -> - val accountId = AccountId(contact.accountID) - if (accountId.prefix == IdPrefix.STANDARD && BlindKeyAPI.sessionIdMatchesBlindedId( - sessionId = accountId.hexString, - blindedId = blindedId, - serverPubKey = serverPublicKey - ) - ) { - val contactMapping = mapping.copy(accountId = accountId.hexString) - db.addBlindedIdMapping(contactMapping) - return contactMapping + + configFactory.withUserConfigs { it.contacts.all() } + .forEach { contact -> + val accountId = AccountId(contact.id) + if (accountId.prefix == IdPrefix.STANDARD && BlindKeyAPI.sessionIdMatchesBlindedId( + sessionId = accountId.hexString, + blindedId = blindedId, + serverPubKey = serverPublicKey + ) + ) { + val contactMapping = mapping.copy(accountId = accountId.hexString) + db.addBlindedIdMapping(contactMapping) + return contactMapping + } } - } db.getBlindedIdMappingsExceptFor(server).forEach { if (BlindKeyAPI.sessionIdMatchesBlindedId( sessionId = it.accountId!!, @@ -1813,15 +1620,16 @@ open class Storage @Inject constructor( ) } - override fun setBlocked(recipients: Iterable, isBlocked: Boolean, fromConfigUpdate: Boolean) { + override fun setBlocked(recipients: Iterable
, isBlocked: Boolean, fromConfigUpdate: Boolean) { val recipientDb = recipientDatabase recipientDb.setBlocked(recipients, isBlocked) if (!fromConfigUpdate) { + val currentUserKey = getUserPublicKey() configFactory.withMutableUserConfigs { configs -> - recipients.filter { it.isContactRecipient && !it.isLocalNumber } + recipients.filter { it.isContact && (it.toString() != currentUserKey) } .forEach { recipient -> - configs.contacts.upsertContact(recipient.address.toString()) { + configs.contacts.upsertContact(recipient.toString()) { this.blocked = isBlocked } } @@ -1830,82 +1638,60 @@ open class Storage @Inject constructor( } override fun blockedContacts(): List { - val recipientDb = recipientDatabase - return recipientDb.blockedContacts - } - - override fun getExpirationConfiguration(threadId: Long): ExpirationConfiguration? { - val recipient = getRecipientForThread(threadId) ?: return null - val dbExpirationMetadata = expirationConfigurationDatabase.getExpirationConfiguration(threadId) - return when { - recipient.isLocalNumber -> configFactory.withUserConfigs { it.userProfile.getNtsExpiry() } - recipient.isContactRecipient -> { - // read it from contacts config if exists - recipient.address.toString().takeIf { it.startsWith(IdPrefix.STANDARD.value) } - ?.let { configFactory.withUserConfigs { configs -> configs.contacts.get(it)?.expiryMode } } - } - recipient.isGroupV2Recipient -> { - configFactory.withGroupConfigs(AccountId(recipient.address.toString())) { configs -> - configs.groupInfo.getExpiryTimer() - }.let { - if (it == 0L) ExpiryMode.NONE else ExpiryMode.AfterSend(it) - } - } - recipient.isLegacyGroupRecipient -> { - // read it from group config if exists - GroupUtil.doubleDecodeGroupId(recipient.address.toString()) - .let { id -> configFactory.withUserConfigs { it.userGroups.getLegacyGroupInfo(id) } } - ?.run { disappearingTimer.takeIf { it != 0L }?.let(ExpiryMode::AfterSend) ?: ExpiryMode.NONE } - } - else -> null - }?.let { ExpirationConfiguration( - threadId, - it, - // This will be 0L for new closed groups, apparently we don't need this anymore? - dbExpirationMetadata?.updatedTimestampMs ?: 0L - ) } + val allBlockedContacts = hashSetOf
() + + // Source data from config first + configFactory.withUserConfigs { + it.contacts.all() + }.asSequence() + .filter { it.blocked } + .mapTo(allBlockedContacts) { Address.fromSerialized(it.id) } + + // Source data from the local database. This might contain something that is not synced + // to the config system.e + allBlockedContacts.addAll(recipientDatabase.blockedContacts) + + return allBlockedContacts.map { + recipientRepository.getRecipientSync(it) ?: Recipient.empty(it) + } } - override fun setExpirationConfiguration(config: ExpirationConfiguration) { - val recipient = getRecipientForThread(config.threadId) ?: return + override fun getExpirationConfiguration(threadId: Long): ExpiryMode { + val recipient = getRecipientForThread(threadId) ?: return ExpiryMode.NONE - val expirationDb = expirationConfigurationDatabase - val currentConfig = expirationDb.getExpirationConfiguration(config.threadId) - if (currentConfig != null && currentConfig.updatedTimestampMs >= config.updatedTimestampMs) return - val expiryMode = config.expiryMode + return recipientRepository.getRecipientSync(recipient)?.expiryMode ?: ExpiryMode.NONE + } + override fun setExpirationConfiguration(address: Address, expiryMode: ExpiryMode) { if (expiryMode == ExpiryMode.NONE) { // Clear the legacy recipients on updating config to be none - lokiAPIDatabase.setLastLegacySenderAddress(recipient.address.toString(), null) + lokiAPIDatabase.setLastLegacySenderAddress(address.toString(), null) } - if (recipient.isLegacyGroupRecipient) { - val groupPublicKey = GroupUtil.addressToGroupAccountId(recipient.address) + if (address.isLegacyGroup) { + val groupPublicKey = GroupUtil.addressToGroupAccountId(address) configFactory.withMutableUserConfigs { val groupInfo = it.userGroups.getLegacyGroupInfo(groupPublicKey) ?.copy(disappearingTimer = expiryMode.expirySeconds) ?: return@withMutableUserConfigs it.userGroups.set(groupInfo) } - } else if (recipient.isGroupV2Recipient) { - val groupSessionId = AccountId(recipient.address.toString()) + } else if (address.isGroupV2) { + val groupSessionId = AccountId(address.toString()) configFactory.withMutableGroupConfigs(groupSessionId) { configs -> configs.groupInfo.setExpiryTimer(expiryMode.expirySeconds) } - } else if (recipient.isLocalNumber) { + } else if (address.address == getUserPublicKey()) { configFactory.withMutableUserConfigs { it.userProfile.setNtsExpiry(expiryMode) } - } else if (recipient.isContactRecipient) { + } else if (address.isContact) { configFactory.withMutableUserConfigs { - val contact = it.contacts.get(recipient.address.toString())?.copy(expiryMode = expiryMode) ?: return@withMutableUserConfigs + val contact = it.contacts.get(address.toString())?.copy(expiryMode = expiryMode) ?: return@withMutableUserConfigs it.contacts.set(contact) } } - expirationDb.setExpirationConfiguration( - config.run { copy(expiryMode = expiryMode) } - ) } override fun getExpiringMessages(messageIds: List): List> { @@ -1928,26 +1714,4 @@ open class Storage @Inject constructor( } return expiringMessages } - - override fun updateDisappearingState( - messageSender: String, - threadID: Long, - disappearingState: Recipient.DisappearingState - ) { - val threadDb = threadDatabase - val lokiDb = lokiAPIDatabase - val recipient = threadDb.getRecipientForThreadId(threadID) ?: return - val recipientAddress = recipient.address.toString() - recipientDatabase - .setDisappearingState(recipient, disappearingState); - val currentLegacyRecipient = lokiDb.getLastLegacySenderAddress(recipientAddress) - val currentExpiry = getExpirationConfiguration(threadID) - if (disappearingState == DisappearingState.LEGACY - && currentExpiry?.isEnabled == true - && ExpirationConfiguration.isNewConfigEnabled) { // only set "this person is legacy" if new config enabled - lokiDb.setLastLegacySenderAddress(recipientAddress, messageSender) - } else if (messageSender == currentLegacyRecipient) { - lokiDb.setLastLegacySenderAddress(recipientAddress, null) - } - } } 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 ee31899f99..74ad3292cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -17,15 +17,12 @@ */ package org.thoughtcrime.securesms.database; -import static org.session.libsession.utilities.GroupUtil.COMMUNITY_PREFIX; -import static org.session.libsession.utilities.GroupUtil.LEGACY_CLOSED_GROUP_PREFIX; import static org.thoughtcrime.securesms.database.GroupDatabase.GROUP_ID; import static org.thoughtcrime.securesms.database.UtilKt.generatePlaceholders; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; -import android.database.MergeCursor; import android.net.Uri; import androidx.annotation.NonNull; @@ -35,22 +32,19 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase; +import org.json.JSONArray; import org.session.libsession.messaging.MessagingModuleConfiguration; import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.ConfigFactoryProtocolKt; -import org.session.libsession.utilities.DelimiterUtil; import org.session.libsession.utilities.DistributionTypes; -import org.session.libsession.utilities.GroupRecord; import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.recipients.Recipient.RecipientSettings; import org.session.libsignal.utilities.AccountId; import org.session.libsignal.utilities.IdPrefix; import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Pair; -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.GroupThreadStatus; @@ -66,16 +60,19 @@ import java.io.Closeable; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Set; +import javax.inject.Inject; import javax.inject.Provider; +import javax.inject.Singleton; +import dagger.Lazy; +import kotlin.collections.CollectionsKt; import network.loki.messenger.libsession_util.util.GroupInfo; +@Singleton public class ThreadDatabase extends Database { public interface ConversationThreadUpdateListener { @@ -85,7 +82,6 @@ public interface ConversationThreadUpdateListener { private static final String TAG = ThreadDatabase.class.getSimpleName(); // Map of threadID -> Address - private final Map addressCache = new HashMap<>(); public static final String TABLE_NAME = "thread"; public static final String ID = "_id"; @@ -108,6 +104,8 @@ public interface ConversationThreadUpdateListener { public static final String EXPIRES_IN = "expires_in"; public static final String LAST_SEEN = "last_seen"; public static final String HAS_SENT = "has_sent"; + + @Deprecated(forRemoval = true) public static final String IS_PINNED = "is_pinned"; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + @@ -157,8 +155,14 @@ public static String getUnreadMentionCountCommand() { private ConversationThreadUpdateListener updateListener; - public ThreadDatabase(Context context, Provider databaseHelper) { + private final Lazy recipientRepository; + + @Inject + public ThreadDatabase(@dagger.hilt.android.qualifiers.ApplicationContext Context context, + Provider databaseHelper, + Lazy recipientRepository) { super(context, databaseHelper); + this.recipientRepository = recipientRepository; } public void setUpdateListener(ConversationThreadUpdateListener updateListener) { @@ -212,104 +216,12 @@ public void clearSnippet(long threadId){ notifyConversationListListeners(); } - public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type, boolean unarchive) { - ContentValues contentValues = new ContentValues(4); - - contentValues.put(THREAD_CREATION_DATE, date - date % 1000); - if (!snippet.isEmpty()) { - contentValues.put(SNIPPET, snippet); - } - contentValues.put(SNIPPET_TYPE, type); - contentValues.put(SNIPPET_URI, attachment == null ? null : attachment.toString()); - - if (unarchive) { - contentValues.put(ARCHIVED, 0); - } - - SQLiteDatabase db = getWritableDatabase(); - db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""}); - notifyConversationListListeners(); - } - public void deleteThread(long threadId) { SQLiteDatabase db = getWritableDatabase(); db.delete(TABLE_NAME, ID_WHERE, new String[] {threadId + ""}); - addressCache.remove(threadId); notifyConversationListListeners(); } - private void deleteThreads(Set threadIds) { - SQLiteDatabase db = getWritableDatabase(); - String where = ""; - - for (long threadId : threadIds) { where += ID + " = '" + threadId + "' OR "; } - - where = where.substring(0, where.length() - 4); - - db.delete(TABLE_NAME, where, null); - for (long threadId: threadIds) { - addressCache.remove(threadId); - } - notifyConversationListListeners(); - } - - private void deleteAllThreads() { - SQLiteDatabase db = getWritableDatabase(); - db.delete(TABLE_NAME, null, null); - addressCache.clear(); - notifyConversationListListeners(); - } - - public void trimAllThreads(int length, ProgressListener listener) { - Cursor cursor = null; - int threadCount = 0; - int complete = 0; - - try { - cursor = this.getConversationList(); - - if (cursor != null) - threadCount = cursor.getCount(); - - while (cursor != null && cursor.moveToNext()) { - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); - trimThread(threadId, length); - - listener.onProgress(++complete, threadCount); - } - } finally { - if (cursor != null) - cursor.close(); - } - } - - public void trimThread(long threadId, int length) { - Log.i("ThreadDatabase", "Trimming thread: " + threadId + " to: " + length); - Cursor cursor = null; - - try { - cursor = DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadId, true); - - if (cursor != null && length > 0 && cursor.getCount() > length) { - Log.w("ThreadDatabase", "Cursor count is greater than length!"); - cursor.moveToPosition(length - 1); - - long lastTweetDate = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED)); - - Log.i("ThreadDatabase", "Cut off tweet date: " + lastTweetDate); - - DatabaseComponent.get(context).smsDatabase().deleteMessagesInThreadBeforeDate(threadId, lastTweetDate); - DatabaseComponent.get(context).mmsDatabase().deleteMessagesInThreadBeforeDate(threadId, lastTweetDate, false); - - update(threadId, false); - notifyConversationListeners(threadId); - } - } finally { - if (cursor != null) - cursor.close(); - } - } - public void trimThreadBefore(long threadId, long timestamp) { Log.i("ThreadDatabase", "Trimming thread: " + threadId + " before :"+timestamp); DatabaseComponent.get(context).smsDatabase().deleteMessagesInThreadBeforeDate(threadId, timestamp); @@ -422,88 +334,108 @@ public Cursor searchConversationAddresses(String addressQuery, Set exclu selectionArgs.addAll(excludeAddresses); } - String query = createQuery(selection.toString(), 0); + String query = createQuery(selection.toString()); return db.rawQuery(query, selectionArgs.toArray(new String[0])); } + @NonNull + private ArrayList getThreadRecords(@NonNull final Cursor cursor) { + final ArrayList threads = new ArrayList<>(cursor.getCount()); + Reader reader = new Reader(cursor); + ThreadRecord thread; + while ((thread = reader.getNext()) != null) { + threads.add(thread); + } - public Cursor getFilteredConversationList(@Nullable List
filter) { - if (filter == null || filter.size() == 0) - return null; + return threads; + } - SQLiteDatabase db = getReadableDatabase(); - List> partitionedAddresses = Util.partition(filter, 900); - List cursors = new LinkedList<>(); + @NonNull + public ArrayList getFilteredConversationList(@Nullable List
filter) { + if (filter == null || filter.isEmpty()) + return new ArrayList<>(0); - for (List
addresses : partitionedAddresses) { - StringBuilder selection = new StringBuilder(TABLE_NAME + "." + ADDRESS + " = ?"); - String[] selectionArgs = new String[addresses.size()]; + final String query = createQuery( + TABLE_NAME + "." + ADDRESS + " IN (SELECT value FROM json_each(?))" + ); - for (int i = 0; i < addresses.size() - 1; i++) { - selection.append(" OR " + TABLE_NAME + "." + ADDRESS + " = ?"); - } + final String[] selectionArgs = new String[] { + new JSONArray(CollectionsKt.map(filter, Address::toString)).toString() + }; - int i= 0; - for (Address address : addresses) { - selectionArgs[i++] = DelimiterUtil.escape(address.toString(), ' '); - } - - String query = createQuery(selection.toString(), 0); - cursors.add(db.rawQuery(query, selectionArgs)); + try(final Cursor cursor = getReadableDatabase().rawQuery(query, selectionArgs)) { + return getThreadRecords(cursor); } - - Cursor cursor = cursors.size() > 1 ? new MergeCursor(cursors.toArray(new Cursor[0])) : cursors.get(0); - setNotifyConversationListListeners(cursor); - return cursor; } - public Cursor getRecentConversationList(int limit) { - SQLiteDatabase db = getReadableDatabase(); - String query = createQuery("", limit); + private List
getBlindedConversations(@Nullable Boolean approved, @Nullable Boolean blocked) { + String query = "SELECT " + TABLE_NAME + "." + ADDRESS + " FROM " + TABLE_NAME + + " INNER JOIN " + RecipientDatabase.TABLE_NAME + " ON " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS + " = " + TABLE_NAME + "." + ADDRESS + + " WHERE " + TABLE_NAME + "." + ADDRESS + " LIKE '" + IdPrefix.BLINDED.getValue() + "%'"; - return db.rawQuery(query, null); - } + if (approved != null) { + query += " AND " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = " + (approved ? 1 : 0); + } - public Cursor getConversationList() { - return getConversationList(ARCHIVED + " = 0 "); - } + if (blocked != null) { + query += " AND " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = " + (blocked ? 1 : 0); + } - public Cursor getBlindedConversationList() { - String where = TABLE_NAME + "." + ADDRESS + " LIKE '" + IdPrefix.BLINDED.getValue() + "%' "; - return getConversationList(where); - } + try(final Cursor cursor = getReadableDatabase().rawQuery(query)) { + final ArrayList
addresses = new ArrayList<>(cursor.getCount()); + while (cursor.moveToNext()) { + String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)); + if (address != null && !address.isEmpty()) { + addresses.add(Address.fromSerialized(address)); + } + } - public Cursor getApprovedConversationList() { - String where = "((" + HAS_SENT + " = 1 OR " + RecipientDatabase.APPROVED + " = 1 OR "+ GroupDatabase.TABLE_NAME +"."+GROUP_ID+" LIKE '"+ LEGACY_CLOSED_GROUP_PREFIX +"%') " + - "OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + COMMUNITY_PREFIX + "%') " + - "AND " + ARCHIVED + " = 0 "; - return getConversationList(where); + return addresses; + } } - public Cursor getUnapprovedConversationList() { - String where = "("+MESSAGE_COUNT + " != 0 OR "+ThreadDatabase.TABLE_NAME+"."+ThreadDatabase.ADDRESS+" LIKE '"+IdPrefix.GROUP.getValue()+"%')" + - " AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " + - RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = 0 AND " + - RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " + - GroupDatabase.TABLE_NAME + "." + GROUP_ID + " IS NULL"; - return getConversationList(where); + /** + * @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() { + SQLiteDatabase db = getReadableDatabase(); + 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; + } } - private Cursor getConversationList(String where) { - SQLiteDatabase db = getReadableDatabase(); - String query = createQuery(where, 0); - Cursor cursor = db.rawQuery(query, null); - - setNotifyConversationListListeners(cursor); + @NonNull + public ArrayList getApprovedConversationList() { + // Approved conversations will come from two different sources: + // 1. Config based conversations + // 2. Blinded conversations stored in the database + final List
blindedConversations = getBlindedConversations(true, false); + final List
configBasedConversations = recipientRepository.get().getAllConfigBasedApprovedConversations(); - return cursor; + return getFilteredConversationList(CollectionsKt.plus(blindedConversations, configBasedConversations)); } - public Cursor getDirectShareList() { - SQLiteDatabase db = getReadableDatabase(); - String query = createQuery("", 0); + @NonNull + public ArrayList getUnapprovedConversationList() { + // Unapproved conversations will come from two different sources: + // 1. Config based conversations + // 2. Blinded conversations stored in the database + final List
blindedConversations = getBlindedConversations(false, false); + final List
configBasedConversations = recipientRepository.get().getAllConfigBasedUnapprovedConversations(); - return db.rawQuery(query, null); + return getFilteredConversationList(CollectionsKt.plus(blindedConversations, configBasedConversations)); } /** @@ -514,8 +446,8 @@ public Cursor getDirectShareList() { 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) MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase(); - Recipient forThreadId = getRecipientForThreadId(threadId); - if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && forThreadId != null && forThreadId.isCommunityRecipient()) return false; + Address forThreadId = getRecipientForThreadId(threadId); + if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && forThreadId != null && forThreadId.isCommunity()) return false; SQLiteDatabase db = getWritableDatabase(); @@ -614,12 +546,12 @@ public long getThreadIdIfExistsFor(String address) { } } - public long getThreadIdIfExistsFor(Recipient recipient) { - return getThreadIdIfExistsFor(recipient.getAddress().toString()); + public long getThreadIdIfExistsFor(Address address) { + return getThreadIdIfExistsFor(address.toString()); } - public long getOrCreateThreadIdFor(Recipient recipient) { - return getOrCreateThreadIdFor(recipient, DistributionTypes.DEFAULT); + public long getOrCreateThreadIdFor(Address address) { + return getOrCreateThreadIdFor(address, DistributionTypes.DEFAULT); } public void setThreadArchived(long threadId) { @@ -633,10 +565,10 @@ public void setThreadArchived(long threadId) { notifyConversationListeners(threadId); } - public long getOrCreateThreadIdFor(Recipient recipient, int distributionType) { + public long getOrCreateThreadIdFor(Address address, int distributionType) { SQLiteDatabase db = getReadableDatabase(); String where = ADDRESS + " = ?"; - String[] recipientsArg = new String[]{recipient.getAddress().toString()}; + String[] recipientsArg = new String[]{address.toString()}; Cursor cursor = null; boolean created = false; @@ -650,16 +582,14 @@ public long getOrCreateThreadIdFor(Recipient recipient, int distributionType) { if (cursor != null && cursor.moveToFirst()) { threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); } else { - threadId = createThreadForRecipient(recipient.getAddress(), recipient.isGroupOrCommunityRecipient(), distributionType); + threadId = createThreadForRecipient(address, address.isGroupOrCommunity(), distributionType); created = true; } } if (created) { - DatabaseComponent.get(context).recipientDatabase().setProfileSharing(recipient, true); - if (updateListener != null) { - updateListener.threadCreated(recipient.getAddress(), threadId); + updateListener.threadCreated(address, threadId); } } @@ -670,25 +600,13 @@ public long getOrCreateThreadIdFor(Recipient recipient, int distributionType) { } } - public @Nullable Recipient getRecipientForThreadId(long threadId) { - if (addressCache.containsKey(threadId) && addressCache.get(threadId) != null) { - return Recipient.from(context, addressCache.get(threadId), false); - } - + public @Nullable Address getRecipientForThreadId(long threadId) { SQLiteDatabase db = getReadableDatabase(); - Cursor cursor = null; - - try { - cursor = db.query(TABLE_NAME, null, ID + " = ?", new String[] {threadId+""}, null, null, null); + try(final Cursor cursor = db.query(TABLE_NAME, null, ID + " = ?", new String[] {threadId+""}, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { - Address address = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))); - addressCache.put(threadId, address); - return Recipient.from(context, address, false); + return Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))); } - } finally { - if (cursor != null) - cursor.close(); } return null; @@ -733,30 +651,6 @@ record = reader.getNext(); } } - public void setPinned(long threadId, boolean pinned) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(IS_PINNED, pinned ? 1 : 0); - - getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE, - new String[] {String.valueOf(threadId)}); - - notifyConversationListeners(threadId); - notifyConversationListListeners(); - } - - public boolean isPinned(long threadId) { - SQLiteDatabase db = getReadableDatabase(); - Cursor cursor = db.query(TABLE_NAME, new String[]{IS_PINNED}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null); - try { - if (cursor != null && cursor.moveToFirst()) { - return cursor.getInt(0) == 1; - } - return false; - } finally { - if (cursor != null) cursor.close(); - } - } - /** * @param threadId * @param isGroupRecipient @@ -799,24 +693,16 @@ public boolean markAllAsRead(long threadId, boolean isGroupRecipient, long lastS return null; } - private @NonNull String createQuery(@NonNull String where, int limit) { + private @NonNull String createQuery(@NonNull String where) { String projection = Util.join(COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION, ","); - String query = - "SELECT " + projection + " FROM " + TABLE_NAME + + return "SELECT " + projection + " FROM " + TABLE_NAME + " LEFT OUTER JOIN " + RecipientDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.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 + - " ORDER BY " + TABLE_NAME + "." + IS_PINNED + " DESC, " + TABLE_NAME + "." + THREAD_CREATION_DATE + " DESC"; - - if (limit > 0) { - query += " LIMIT " + limit; - } - - return query; + " WHERE " + where; } public void notifyThreadUpdated(long threadId) { @@ -827,29 +713,12 @@ public interface ProgressListener { void onProgress(int complete, int total); } - public Reader readerFor(Cursor cursor) { - return readerFor(cursor, true); - } - - /** - * Create a reader to conveniently access the thread cursor - * - * @param retrieveGroupStatus Whether group status should be calculated based on the config data. - * Normally you always want it, but if you don't want the reader - * to access the config system, this is the flag to turn it off. - */ - public Reader readerFor(Cursor cursor, boolean retrieveGroupStatus) { - return new Reader(cursor, retrieveGroupStatus); - } - - public class Reader implements Closeable { + private class Reader implements Closeable { private final Cursor cursor; - private final boolean retrieveGroupStatus; - public Reader(Cursor cursor, boolean retrieveGroupStatus) { + public Reader(Cursor cursor) { this.cursor = cursor; - this.retrieveGroupStatus = retrieveGroupStatus; } public int getCount() { @@ -868,18 +737,7 @@ public ThreadRecord getCurrent() { int distributionType = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.DISTRIBUTION_TYPE)); Address address = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.ADDRESS))); - Optional settings; - Optional groupRecord; - - if (distributionType != DistributionTypes.ARCHIVE && distributionType != DistributionTypes.INBOX_ZERO) { - settings = DatabaseComponent.get(context).recipientDatabase().getRecipientSettings(cursor); - groupRecord = DatabaseComponent.get(context).groupDatabase().getGroup(cursor); - } else { - settings = Optional.absent(); - groupRecord = Optional.absent(); - } - - Recipient recipient = Recipient.from(context, address, settings, groupRecord, true); + Recipient recipient = recipientRepository.get().getRecipientSyncOrEmpty(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)); @@ -908,7 +766,7 @@ public ThreadRecord getCurrent() { } final GroupThreadStatus groupThreadStatus; - if (recipient.isGroupV2Recipient() && retrieveGroupStatus) { + if (recipient.isGroupV2Recipient()) { GroupInfo.ClosedGroupInfo group = ConfigFactoryProtocolKt.getGroup( MessagingModuleConfiguration.getShared().getConfigFactory(), new AccountId(recipient.getAddress().toString()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java index 6e861dad0d..50188e100d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java @@ -10,7 +10,7 @@ import androidx.core.util.Pair; import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId; -import org.session.libsession.utilities.recipients.Recipient; +import org.session.libsession.utilities.Address; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.mms.PartAuthority; @@ -21,11 +21,11 @@ public class PagingMediaLoader extends AsyncLoader> { @SuppressWarnings("unused") private static final String TAG = PagingMediaLoader.class.getSimpleName(); - private final Recipient recipient; + private final Address recipient; private final Uri uri; private final boolean leftIsRecent; - public PagingMediaLoader(@NonNull Context context, @NonNull Recipient recipient, @NonNull Uri uri, boolean leftIsRecent) { + public PagingMediaLoader(@NonNull Context context, @NonNull Address recipient, @NonNull Uri uri, boolean leftIsRecent) { super(context); this.recipient = recipient; this.uri = uri; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index 7eeec84d07..c1b56629b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -34,7 +34,7 @@ public abstract class DisplayRecord { protected final long type; - private final Recipient recipient; + private final Recipient recipient; private final long dateSent; private final long dateReceived; private final long threadId; @@ -44,8 +44,8 @@ public abstract class DisplayRecord { private final int readReceiptCount; DisplayRecord(String body, Recipient recipient, long dateSent, - long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount, - long type, int readReceiptCount) + long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount, + long type, int readReceiptCount) { this.threadId = threadId; this.recipient = recipient; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index e5e375dac9..24ae1a1248 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -32,6 +32,7 @@ import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage; import org.session.libsession.messaging.utilities.UpdateMessageBuilder; import org.session.libsession.messaging.utilities.UpdateMessageData; +import org.session.libsession.utilities.Address; import org.session.libsession.utilities.IdentityKeyMismatch; import org.session.libsession.utilities.NetworkFailure; import org.session.libsession.utilities.ThemeUtil; @@ -50,7 +51,7 @@ * */ public abstract class MessageRecord extends DisplayRecord { - private final Recipient individualRecipient; + private final Recipient individualRecipient; private final List mismatches; private final List networkFailures; private final long expiresIn; @@ -74,13 +75,13 @@ public final MessageId getMessageId() { } MessageRecord(long id, String body, Recipient conversationRecipient, - Recipient individualRecipient, - long dateSent, long dateReceived, long threadId, - int deliveryStatus, int deliveryReceiptCount, long type, - List mismatches, - List networkFailures, - long expiresIn, long expireStarted, - int readReceiptCount, List reactions, boolean hasMention) + Recipient individualRecipient, + long dateSent, long dateReceived, long threadId, + int deliveryStatus, int deliveryReceiptCount, long type, + List mismatches, + List networkFailures, + long expiresIn, long expireStarted, + int readReceiptCount, List reactions, boolean hasMention) { super(body, conversationRecipient, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount); @@ -136,7 +137,7 @@ public UpdateMessageData getGroupUpdateMessage() { public CharSequence getDisplayBody(@NonNull Context context) { if (isGroupUpdateMessage()) { UpdateMessageData updateMessageData = getGroupUpdateMessage(); - Recipient groupRecipient = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(getThreadId()); + Address groupRecipient = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(getThreadId()); if (updateMessageData == null || groupRecipient == null) { return ""; @@ -144,7 +145,7 @@ public CharSequence getDisplayBody(@NonNull Context context) { SpannableString text = new SpannableString(UpdateMessageBuilder.buildGroupUpdateMessage( context, - groupRecipient.isGroupV2Recipient() ? new AccountId(groupRecipient.getAddress().toString()) : null, // accountId is only used for GroupsV2 + groupRecipient.isGroupV2() ? new AccountId(groupRecipient.toString()) : null, // accountId is only used for GroupsV2 updateMessageData, MessagingModuleConfiguration.getShared().getConfigFactory(), isOutgoing(), @@ -161,7 +162,7 @@ public CharSequence getDisplayBody(@NonNull Context context) { return text; } else if (isExpirationTimerUpdate()) { int seconds = (int) (getExpiresIn() / 1000); - boolean isGroup = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(getThreadId()).isGroupOrCommunityRecipient(); + boolean isGroup = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(getThreadId()).isGroupOrCommunity(); return new SpannableString(UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, isGroup, getIndividualRecipient().getAddress().toString(), isOutgoing(), getTimestamp(), expireStarted)); } else if (isDataExtractionNotification()) { if (isScreenshotNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().toString()))); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java index 320eb4e739..c1cd3e6158 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java @@ -11,7 +11,6 @@ import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; -import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -22,13 +21,13 @@ public abstract class MmsMessageRecord extends MessageRecord { private final @NonNull List linkPreviews = new LinkedList<>(); MmsMessageRecord(long id, String body, Recipient conversationRecipient, - Recipient individualRecipient, long dateSent, - long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount, - long type, List mismatches, - List networkFailures, long expiresIn, - long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount, - @Nullable Quote quote, @NonNull List contacts, - @NonNull List linkPreviews, List reactions, boolean hasMention) + Recipient individualRecipient, long dateSent, + long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount, + long type, List mismatches, + List networkFailures, long expiresIn, + long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount, + @Nullable Quote quote, @NonNull List contacts, + @NonNull List linkPreviews, List reactions, boolean hasMention) { super(id, body, conversationRecipient, individualRecipient, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, expiresIn, expireStarted, readReceiptCount, reactions, hasMention); this.slideDeck = slideDeck; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java index 4a746d23a2..2432a475a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -35,14 +35,14 @@ public class SmsMessageRecord extends MessageRecord { public SmsMessageRecord(long id, - String body, Recipient recipient, - Recipient individualRecipient, - long dateSent, long dateReceived, - int deliveryReceiptCount, - long type, long threadId, - int status, List mismatches, - long expiresIn, long expireStarted, - int readReceiptCount, List reactions, boolean hasMention) + String body, Recipient recipient, + Recipient individualRecipient, + long dateSent, long dateReceived, + int deliveryReceiptCount, + long type, long threadId, + int status, List mismatches, + long expiresIn, long expireStarted, + int readReceiptCount, List reactions, boolean hasMention) { super(id, body, recipient, individualRecipient, dateSent, dateReceived, threadId, status, deliveryReceiptCount, type, 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 index 4a4b8ae462..3b658d69b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -67,7 +67,7 @@ public class ThreadRecord extends DisplayRecord { public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri, @Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount, int unreadMentionCount, long threadId, int deliveryReceiptCount, int status, - long snippetType, int distributionType, boolean archived, long expiresIn, + long snippetType, int distributionType, boolean archived, long expiresIn, long lastSeen, int readReceiptCount, boolean pinned, String invitingAdminId, @NonNull GroupThreadStatus groupThreadStatus) { @@ -89,10 +89,9 @@ public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri, } private String getName() { - return getRecipient().getName(); + return getRecipient().getDisplayName(); } - @Override public CharSequence getDisplayBody(@NonNull Context context) { if (groupThreadStatus == GroupThreadStatus.Kicked) { @@ -198,7 +197,7 @@ public CharSequence getNonControlMessageDisplayBody(@NonNull Context context) { // The logic will differ depending on the type. // 1-1, note to self and control messages (we shouldn't have any in here, but leaving the // logic to be safe) do not need author details - if (recipient.isLocalNumber() || recipient.is1on1() || + if (recipient.isLocalNumber() || recipient.getAddress().isContact() || (lastMessage != null && lastMessage.isControlMessage()) ) { return getBody(); @@ -208,7 +207,7 @@ public CharSequence getNonControlMessageDisplayBody(@NonNull Context context) { prefix = context.getString(R.string.you); } else if(lastMessage != null){ - prefix = lastMessage.getIndividualRecipient().getName(); + prefix = lastMessage.getIndividualRecipient().getDisplayName(); } return Phrase.from(context.getString(R.string.messageSnippetGroup)) @@ -231,15 +230,13 @@ public boolean isGroupUpdateMessage() { public long getDate() { return getDateReceived(); } - public boolean isArchived() { return archived; } - public int getDistributionType() { return distributionType; } public long getExpiresIn() { return expiresIn; } public long getLastSeen() { return lastSeen; } - public boolean isPinned() { return pinned; } + public boolean isPinned() { return getRecipient().isPinned(); } public int getInitialRecipientHash() { return initialRecipientHash; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt index aaddbf527c..d7eb78a13a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -42,7 +42,7 @@ import javax.inject.Inject @HiltViewModel class DebugMenuViewModel @Inject constructor( - @ApplicationContext private val context: Context, + @param:ApplicationContext private val context: Context, private val textSecurePreferences: TextSecurePreferences, private val tokenPageNotificationManager: TokenPageNotificationManager, private val configFactory: ConfigFactory, @@ -64,7 +64,7 @@ class DebugMenuViewModel @Inject constructor( showLoadingDialog = false, showDeprecatedStateWarningDialog = false, hideMessageRequests = textSecurePreferences.hasHiddenMessageRequests(), - hideNoteToSelf = textSecurePreferences.hasHiddenNoteToSelf(), + hideNoteToSelf = configFactory.withUserConfigs { it.userProfile.getNtsPriority() == PRIORITY_HIDDEN }, forceDeprecationState = deprecationManager.deprecationStateOverride.value, availableDeprecationState = listOf(null) + LegacyGroupDeprecationManager.DeprecationState.entries.toList(), deprecatedTime = deprecationManager.deprecatedTime.value, @@ -135,7 +135,6 @@ class DebugMenuViewModel @Inject constructor( } is Commands.HideNoteToSelf -> { - textSecurePreferences.setHasHiddenNoteToSelf(command.hide) configFactory.withMutableUserConfigs { it.userProfile.setNtsPriority(if(command.hide) PRIORITY_HIDDEN else PRIORITY_VISIBLE) } @@ -283,12 +282,10 @@ class DebugMenuViewModel @Inject constructor( // clear trusted downloads for all recipients viewModelScope.launch { - val conversations: List = threadDb.approvedConversationList.use { openCursor -> - threadDb.readerFor(openCursor).run { generateSequence { next }.toList() } - } + val conversations: List = threadDb.approvedConversationList conversations.filter { !it.recipient.isLocalNumber }.forEach { - recipientDatabase.setAutoDownloadAttachments(it.recipient, false) + recipientDatabase.setAutoDownloadAttachments(it.recipient.address, false) } // set all attachments back to pending diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt index 5bf323d5f6..14ef65b94e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt @@ -12,13 +12,11 @@ import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier import org.session.libsession.utilities.AppTextSecurePreferences import org.session.libsession.utilities.ConfigFactoryProtocol -import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.groups.GroupManagerV2Impl import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.repository.DefaultConversationRepository -import org.thoughtcrime.securesms.sskenvironment.ProfileManager import org.thoughtcrime.securesms.tokenpage.TokenRepository import org.thoughtcrime.securesms.tokenpage.TokenRepositoryImpl import javax.inject.Singleton @@ -44,9 +42,6 @@ abstract class AppBindings { @Binds abstract fun bindGroupManager(groupManager: GroupManagerV2Impl): GroupManagerV2 - @Binds - abstract fun bindProfileManager(profileManager: ProfileManager): SSKEnvironment.ProfileManagerProtocol - @Binds abstract fun bindConfigFactory(configFactory: ConfigFactory): ConfigFactoryProtocol diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/CallModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/CallModule.kt index 8e0c735770..ec32b9625c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/CallModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/CallModule.kt @@ -19,9 +19,4 @@ object CallModule { @Singleton fun provideAudioManagerCompat(@ApplicationContext context: Context) = AudioManagerCompat.create(context) - @Provides - @Singleton - fun provideCallManager(@ApplicationContext context: Context, audioManagerCompat: AudioManagerCompat, storage: StorageProtocol) = - CallManager(context, audioManagerCompat, storage) - } \ No newline at end of file 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 3351cf7b71..a853e944b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt @@ -14,44 +14,27 @@ import network.loki.messenger.libsession_util.Curve25519 import network.loki.messenger.libsession_util.GroupInfoConfig import network.loki.messenger.libsession_util.GroupKeysConfig import network.loki.messenger.libsession_util.GroupMembersConfig -import network.loki.messenger.libsession_util.MutableContacts -import network.loki.messenger.libsession_util.MutableConversationVolatileConfig -import network.loki.messenger.libsession_util.MutableUserGroupsConfig -import network.loki.messenger.libsession_util.MutableUserProfile import network.loki.messenger.libsession_util.UserGroupsConfig import network.loki.messenger.libsession_util.UserProfile -import network.loki.messenger.libsession_util.util.BaseCommunityInfo -import network.loki.messenger.libsession_util.util.Bytes import network.loki.messenger.libsession_util.util.ConfigPush -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 network.loki.messenger.libsession_util.util.MultiEncrypt -import network.loki.messenger.libsession_util.util.UserPic -import okio.ByteString.Companion.decodeBase64 import org.session.libsession.database.StorageProtocol import org.session.libsession.snode.OwnedSwarmAuth import org.session.libsession.snode.SnodeClock import org.session.libsession.snode.SwarmAuth -import org.session.libsession.utilities.Address import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigMessage import org.session.libsession.utilities.ConfigPushResult import org.session.libsession.utilities.ConfigUpdateNotification import org.session.libsession.utilities.GroupConfigs -import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.MutableGroupConfigs import org.session.libsession.utilities.MutableUserConfigs import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.UserConfigType import org.session.libsession.utilities.UserConfigs -import org.session.libsession.utilities.UsernameUtils import org.session.libsession.utilities.getGroup -import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.utilities.AccountId -import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix -import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.configs.ConfigToDatabaseSync import org.thoughtcrime.securesms.database.ConfigDatabase import org.thoughtcrime.securesms.database.ConfigVariant @@ -75,7 +58,6 @@ class ConfigFactory @Inject constructor( private val textSecurePreferences: TextSecurePreferences, private val clock: SnodeClock, private val configToDatabaseSync: Lazy, - private val usernameUtils: Lazy ) : ConfigFactoryProtocol { companion object { // This is a buffer period within which we will process messages which would result in a @@ -129,11 +111,9 @@ class ConfigFactory @Inject constructor( val instance = ReentrantReadWriteLock() to UserConfigsImpl( userEd25519SecKey = requiresCurrentUserED25519SecKey(), userAccountId = userAccountId, - threadDb = threadDb, configDatabase = configDatabase, storage = storage.get(), - textSecurePreferences = textSecurePreferences, - usernameUtils = usernameUtils.get() + threadDb = threadDb ) return synchronized(userConfigs) { @@ -574,155 +554,11 @@ private val UserConfigType.configVariant: ConfigVariant UserConfigType.USER_GROUPS -> ConfigDatabase.USER_GROUPS_VARIANT } -/** - * Sync group data from our local database - */ -private fun MutableUserGroupsConfig.initFrom(storage: StorageProtocol) { - storage - .getAllOpenGroups() - .values - .asSequence() - .mapNotNull { openGroup -> - val (baseUrl, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return@mapNotNull null - val pubKeyHex = Hex.toStringCondensed(pubKey) - val baseInfo = BaseCommunityInfo(baseUrl, room, pubKeyHex) - val threadId = storage.getThreadId(openGroup) ?: return@mapNotNull null - val isPinned = storage.isPinned(threadId) - GroupInfo.CommunityGroupInfo(baseInfo, if (isPinned) 1 else 0) - } - .forEach(this::set) - - storage - .getAllGroups(includeInactive = false) - .asSequence().filter { it.isLegacyGroup && it.isActive && it.members.size > 1 } - .mapNotNull { group -> - val groupAddress = Address.fromSerialized(group.encodedId) - val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupAddress.toString()).toHexString() - val recipient = storage.getRecipientSettings(groupAddress) ?: return@mapNotNull null - val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return@mapNotNull null - val threadId = storage.getThreadId(group.encodedId) - val isPinned = threadId?.let { storage.isPinned(threadId) } ?: false - val admins = group.admins.associate { it.toString() to true } - val members = group.members.filterNot { it.toString() !in admins.keys }.associate { it.toString() to false } - GroupInfo.LegacyGroupInfo( - accountId = groupPublicKey, - name = group.title, - members = admins + members, - priority = if (isPinned) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE, - encPubKey = Bytes((encryptionKeyPair.publicKey as DjbECPublicKey).publicKey), // 'serialize()' inserts an extra byte - encSecKey = Bytes(encryptionKeyPair.privateKey.serialize()), - disappearingTimer = recipient.expireMessages.toLong(), - joinedAtSecs = (group.formationTimestamp / 1000L) - ) - } - .forEach(this::set) -} - -private fun MutableConversationVolatileConfig.initFrom(storage: StorageProtocol, threadDb: ThreadDatabase) { - threadDb.approvedConversationList.use { cursor -> - val reader = threadDb.readerFor(cursor, false) - var current = reader.next - while (current != null) { - val recipient = current.recipient - val contact = when { - recipient.isCommunityRecipient -> { - val openGroup = storage.getOpenGroup(current.threadId) ?: continue - val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue - getOrConstructCommunity(base, room, pubKey) - } - recipient.isGroupV2Recipient -> { - // It's probably safe to assume there will never be a case where new closed groups will ever be there before a dump is created... - // but just in case... - getOrConstructClosedGroup(recipient.address.toString()) - } - recipient.isLegacyGroupRecipient -> { - val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.toString()) - getOrConstructLegacyGroup(groupPublicKey) - } - recipient.isContactRecipient -> { - if (recipient.isLocalNumber) null // this is handled by the user profile NTS data - else if (recipient.isCommunityInboxRecipient) null // specifically exclude - else if (!recipient.address.toString().startsWith(IdPrefix.STANDARD.value)) null - else getOrConstructOneToOne(recipient.address.toString()) - } - else -> null - } - if (contact == null) { - current = reader.next - continue - } - contact.lastRead = current.lastSeen - contact.unread = false - set(contact) - current = reader.next - } - } -} - -private fun MutableUserProfile.initFrom(storage: StorageProtocol, - usernameUtils: UsernameUtils, - textSecurePreferences: TextSecurePreferences -) { - val ownPublicKey = storage.getUserPublicKey() ?: return - val displayName = usernameUtils.getCurrentUsername() ?: return - val picUrl = textSecurePreferences.getProfilePictureURL() - val picKey = textSecurePreferences.getProfileKey()?.decodeBase64()?.toByteArray() - setName(displayName) - if (!picUrl.isNullOrEmpty() && picKey != null && picKey.isNotEmpty()) { - setPic(UserPic(picUrl, picKey)) - } - val ownThreadId = storage.getThreadId(Address.fromSerialized(ownPublicKey)) - setNtsPriority( - if (ownThreadId != null) - if (storage.isPinned(ownThreadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE - else ConfigBase.PRIORITY_HIDDEN - ) -} - -private fun MutableContacts.initFrom(storage: StorageProtocol) { - val localUserKey = storage.getUserPublicKey() ?: return - val contactsWithSettings = storage.getAllContacts().filter { recipient -> - recipient.accountID != localUserKey && recipient.accountID.startsWith(IdPrefix.STANDARD.value) - && storage.getThreadId(recipient.accountID) != null - }.map { contact -> - val address = Address.fromSerialized(contact.accountID) - val thread = storage.getThreadId(address) - val isPinned = if (thread != null) { - storage.isPinned(thread) - } else false - - Triple(contact, storage.getRecipientSettings(address)!!, isPinned) - } - for ((contact, settings, isPinned) in contactsWithSettings) { - val url = contact.profilePictureURL - val key = contact.profilePictureEncryptionKey - val userPic = if (url.isNullOrEmpty() || key?.isNotEmpty() != true) { - null - } else { - UserPic(url, key) - } - - val contactInfo = Contact( - id = contact.accountID, - name = contact.name.orEmpty(), - nickname = contact.nickname.orEmpty(), - blocked = settings.isBlocked, - approved = settings.isApproved, - approvedMe = settings.hasApprovedMe(), - profilePicture = userPic ?: UserPic.DEFAULT, - priority = if (isPinned) 1 else 0, - expiryMode = if (settings.expireMessages == 0) ExpiryMode.NONE else ExpiryMode.AfterRead(settings.expireMessages.toLong()) - ) - set(contactInfo) - } -} private class UserConfigsImpl( userEd25519SecKey: ByteArray, private val userAccountId: AccountId, private val configDatabase: ConfigDatabase, - textSecurePreferences: TextSecurePreferences, - usernameUtils: UsernameUtils, storage: StorageProtocol, threadDb: ThreadDatabase, contactsDump: ByteArray? = configDatabase.retrieveConfigAndHashes( @@ -759,24 +595,6 @@ private class UserConfigsImpl( ed25519SecretKey = userEd25519SecKey, initialDump = convoInfoDump, ) - - init { - if (contactsDump == null) { - contacts.initFrom(storage) - } - - if (userGroupsDump == null) { - userGroups.initFrom(storage) - } - - if (userProfileDump == null) { - userProfile.initFrom(storage, usernameUtils, textSecurePreferences) - } - - if (convoInfoDump == null) { - convoInfoVolatile.initFrom(storage, threadDb) - } - } } private class GroupConfigsImpl( diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt index ddad988b59..2e9312ba98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt @@ -61,14 +61,6 @@ object DatabaseModule { return manager.openHelper } - @Provides - @Singleton - fun provideSmsDatabase(@ApplicationContext context: Context, openHelper: Provider) = SmsDatabase(context, openHelper) - - @Provides - @Singleton - fun provideMmsDatabase(@ApplicationContext context: Context, openHelper: Provider) = MmsDatabase(context, openHelper) - @Provides @Singleton fun provideAttachmentDatabase(@ApplicationContext context: Context, @@ -78,9 +70,6 @@ object DatabaseModule { @Singleton fun provideMediaDatbase(@ApplicationContext context: Context, openHelper: Provider) = MediaDatabase(context, openHelper) - @Provides - @Singleton - fun provideThread(@ApplicationContext context: Context, openHelper: Provider) = ThreadDatabase(context,openHelper) @Provides @Singleton @@ -118,10 +107,6 @@ object DatabaseModule { @Singleton fun provideLokiMessageDatabase(@ApplicationContext context: Context, openHelper: Provider) = LokiMessageDatabase(context,openHelper) - @Provides - @Singleton - fun provideLokiThreadDatabase(@ApplicationContext context: Context, openHelper: Provider) = LokiThreadDatabase(context,openHelper) - @Provides @Singleton fun provideLokiUserDatabase(@ApplicationContext context: Context, openHelper: Provider) = LokiUserDatabase(context,openHelper) diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt index 03fbb56c24..9c94e15a4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt @@ -8,15 +8,11 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope import org.session.libsession.messaging.groups.GroupScope import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.UsernameUtils -import org.thoughtcrime.securesms.database.SessionContactDatabase -import org.thoughtcrime.securesms.util.UsernameUtilsImpl import javax.inject.Named import javax.inject.Singleton @@ -31,7 +27,6 @@ object SessionUtilModule { @Named(POLLER_SCOPE) fun providePollerScope(): CoroutineScope = GlobalScope - @OptIn(ExperimentalCoroutinesApi::class) @Provides @Named(POLLER_SCOPE) fun provideExecutor(): CoroutineDispatcher = Dispatchers.IO.limitedParallelism(1) @@ -51,15 +46,4 @@ object SessionUtilModule { return LegacyGroupDeprecationManager(prefs) } - @Provides - @Singleton - fun provideUsernameUtils( - prefs: TextSecurePreferences, - configFactory: ConfigFactory, - sessionContactDatabase: SessionContactDatabase, - ): UsernameUtils = UsernameUtilsImpl( - prefs = prefs, - configFactory = configFactory, - sessionContactDatabase = sessionContactDatabase, - ) } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/CommunityFileDownloadWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/CommunityFileDownloadWorker.kt new file mode 100644 index 0000000000..1bfd283869 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/CommunityFileDownloadWorker.kt @@ -0,0 +1,149 @@ +package org.thoughtcrime.securesms.glide + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.Operation +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import org.session.libsession.messaging.open_groups.OpenGroupApi +import org.session.libsession.snode.utilities.await +import org.session.libsession.utilities.AESGCM +import org.session.libsession.utilities.recipients.RemoteFile +import org.session.libsignal.utilities.ByteArraySlice +import org.session.libsignal.utilities.toHexString +import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider +import java.io.File +import java.security.MessageDigest +import java.time.Duration + +@HiltWorker +class CommunityFileDownloadWorker @AssistedInject constructor( + @Assisted val context: Context, + @Assisted val params: WorkerParameters +) : RemoteFileDownloadWorker(context, params) { + private val communityServer: String + get() = requireNotNull( + inputData.getString(ARG_COMMUNITY_SERVER) + ) { + "CommunityFileDownloadWorker requires a community server URL" + } + + private val roomId: String + get() = requireNotNull(inputData.getString(ARG_ROOM_ID)) { + "CommunityFileDownloadWorker requires a room ID" + } + + private val fileId: String + get() = requireNotNull(inputData.getString(ARG_FILE_ID)) { + "CommunityFileDownloadWorker requires a file ID" + } + + override suspend fun downloadFile(): ByteArraySlice { + return OpenGroupApi.download(fileId = fileId, room = roomId, server = communityServer).await() + } + + override fun getFilesFromInputData(): DownloadedFiles { + return getFileForUrl( + context, + RemoteFile.Community( + communityServerBaseUrl = communityServer, + roomId = roomId, + fileId = fileId + ) + ) + } + + override fun saveDownloadedFile(from: ByteArraySlice, out: File) { + val encrypted = AESGCM.encrypt( + plaintext = from, + symmetricKey = AttachmentSecretProvider.getInstance(context) + .orCreateAttachmentSecret.modernKey + ) + + // Write the encrypted bytes to a temporary file then move it to the final location. + val tmpOut = File.createTempFile("download-community-", null, context.cacheDir) + tmpOut.writeBytes(encrypted) + require(tmpOut.renameTo(out)) { + "Failed to rename temporary file ${tmpOut.absolutePath} to ${out.absolutePath}" + } + } + + override val debugName: String + get() = "CommunityFile(server=${communityServer.take(8)}, roomId=${roomId.take(3)}, fileId=$fileId)" + + companion object { + private const val TAG = "CommunityFileDownloadWorker" + + private const val ARG_COMMUNITY_SERVER = "community_server" + private const val ARG_ROOM_ID = "room_id" + private const val ARG_FILE_ID = "file_id" + + fun getFileForUrl( + context: Context, + avatar: RemoteFile.Community, + ): DownloadedFiles { + val digest = MessageDigest.getInstance("SHA-256") + + digest.update(avatar.communityServerBaseUrl.lowercase().toByteArray()) + digest.update(avatar.roomId.lowercase().toByteArray()) + digest.update(avatar.fileId.lowercase().toByteArray()) + + val hashed = digest.digest().toHexString() + + return DownloadedFiles( + completedFile = File(context.cacheDir, "community_files/$hashed"), + ) + } + + private fun uniqueWorkName(file: RemoteFile.Community): String { + return "download-community-${file.communityServerBaseUrl}-${file.roomId}-${file.fileId}" + } + + fun cancelAll(context: Context) { + WorkManager.getInstance(context).cancelAllWorkByTag(TAG) + } + + fun enqueue( + context: Context, + file: RemoteFile.Community + ): Flow { + val inputData = Data.Builder() + .putString(ARG_COMMUNITY_SERVER, file.communityServerBaseUrl) + .putString(ARG_ROOM_ID, file.roomId) + .putString(ARG_FILE_ID, file.fileId) + .build() + + val request = OneTimeWorkRequestBuilder() + .setConstraints(Constraints(requiredNetworkType = NetworkType.CONNECTED)) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, Duration.ofSeconds(5)) + .setInputData(inputData) + .addTag(TAG) + .build() + + val workName = uniqueWorkName(file) + WorkManager.getInstance(context) + .enqueueUniqueWork( + workName, + ExistingWorkPolicy.REPLACE, + request + ) + + return WorkManager.getInstance(context) + .getWorkInfoByIdFlow(request.id) + } + + fun cancel(context: Context, avatar: RemoteFile.Community): Operation { + return WorkManager.getInstance(context).cancelUniqueWork(uniqueWorkName(avatar)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoFetcher.java b/app/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoFetcher.java deleted file mode 100644 index 6ab528b785..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoFetcher.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.thoughtcrime.securesms.glide; - - -import android.content.Context; -import androidx.annotation.NonNull; - -import com.bumptech.glide.Priority; -import com.bumptech.glide.load.DataSource; -import com.bumptech.glide.load.data.DataFetcher; - -import org.session.libsession.avatars.ContactPhoto; - -import java.io.IOException; -import java.io.InputStream; - -class ContactPhotoFetcher implements DataFetcher { - - private final Context context; - private final ContactPhoto contactPhoto; - - private InputStream inputStream; - - ContactPhotoFetcher(@NonNull Context context, @NonNull ContactPhoto contactPhoto) { - this.context = context.getApplicationContext(); - this.contactPhoto = contactPhoto; - } - - @Override - public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { - try { - inputStream = contactPhoto.openInputStream(context); - callback.onDataReady(inputStream); - } catch (IOException e) { - callback.onLoadFailed(e); - } - } - - @Override - public void cleanup() { - try { - if (inputStream != null) inputStream.close(); - } catch (IOException e) {} - } - - @Override - public void cancel() { - - } - - @Override - public @NonNull Class getDataClass() { - return InputStream.class; - } - - @Override - public @NonNull DataSource getDataSource() { - return DataSource.LOCAL; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoLoader.java b/app/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoLoader.java deleted file mode 100644 index 61a89c0bd5..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoLoader.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.thoughtcrime.securesms.glide; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.bumptech.glide.load.Options; -import com.bumptech.glide.load.model.ModelLoader; -import com.bumptech.glide.load.model.ModelLoaderFactory; -import com.bumptech.glide.load.model.MultiModelLoaderFactory; - -import org.session.libsession.avatars.ContactPhoto; - -import java.io.InputStream; - -public class ContactPhotoLoader implements ModelLoader { - - private final Context context; - - private ContactPhotoLoader(Context context) { - this.context = context; - } - - @Override - public @Nullable LoadData buildLoadData(@NonNull ContactPhoto contactPhoto, int width, int height, @NonNull Options options) { - return new LoadData<>(contactPhoto, new ContactPhotoFetcher(context, contactPhoto)); - } - - @Override - public boolean handles(@NonNull ContactPhoto contactPhoto) { - return true; - } - - public static class Factory implements ModelLoaderFactory { - - private final Context context; - - public Factory(Context context) { - this.context = context.getApplicationContext(); - } - - @Override - public @NonNull ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { - return new ContactPhotoLoader(context); - } - - @Override - public void teardown() {} - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/EncryptedFileDownloadWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/EncryptedFileDownloadWorker.kt new file mode 100644 index 0000000000..0c0b07b47f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/EncryptedFileDownloadWorker.kt @@ -0,0 +1,120 @@ +package org.thoughtcrime.securesms.glide + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.Operation +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import org.session.libsession.messaging.file_server.FileServerApi +import org.session.libsession.snode.utilities.await +import org.session.libsignal.utilities.ByteArraySlice +import org.session.libsignal.utilities.ByteArraySlice.Companion.write +import org.session.libsignal.utilities.toHexString +import java.io.File +import java.io.FileOutputStream +import java.security.MessageDigest +import java.time.Duration + +@HiltWorker +class EncryptedFileDownloadWorker @AssistedInject constructor( + @Assisted private val context: Context, + @Assisted params: WorkerParameters +) : RemoteFileDownloadWorker(context, params) { + private val fileId: String + get() = requireNotNull(inputData.getString(ARG_FILE_ID)) { + "EncryptedFileDownloadWorker requires a file ID to download" + } + + private val folderName: String + get() = requireNotNull(inputData.getString(ARG_FOLDER)) { + "EncryptedFileDownloadWorker requires a cache folder name" + } + + override suspend fun downloadFile(): ByteArraySlice { + return FileServerApi.download(fileId).await() + } + + override fun getFilesFromInputData(): DownloadedFiles = getFileForUrl(context, folderName, fileId) + + override fun saveDownloadedFile(from: ByteArraySlice, out: File) { + // Write the downloaded bytes to a temporary file then move it to the final location. + // This is done to ensure that the file is fully written before being used. + val tmpOut = File.createTempFile("downloaded-", null, out.parentFile) + FileOutputStream(tmpOut).use { it.write(from) } + + require(tmpOut.renameTo(out)) { + "Failed to rename temporary file ${tmpOut.absolutePath} to ${out.absolutePath}" + } + } + + override val debugName: String + get() = "EncryptedFile(id=$fileId)" + + companion object { + private const val TAG = "EncryptedFileDownloadWorker" + + private const val ARG_FILE_ID = "file_id" + private const val ARG_FOLDER = "folder" + + // Deterministically get the file path for the given URL, using SHA-256 hash for the + // filename to ensure uniqueness and avoid collisions. + fun getFileForUrl(context: Context, folderName: String, url: String): DownloadedFiles { + val hash = MessageDigest.getInstance("SHA-256") + .digest(url.lowercase().trim().toByteArray()) + .toHexString() + + return DownloadedFiles( + completedFile = File(context.cacheDir, "$folderName/$hash") + ) + } + + fun cancelAll(context: Context) { + WorkManager.getInstance(context).cancelAllWorkByTag(TAG) + } + + private fun uniqueWorkName(fileId: String, cacheFolderName: String): String { + return "download-$cacheFolderName-$fileId" + } + + fun cancel(context: Context, fileId: String, cacheFolderName: String): Operation { + return WorkManager.getInstance(context).cancelUniqueWork( + uniqueWorkName(fileId, cacheFolderName) + ) + } + + fun enqueue(context: Context, fileId: String, cacheFolderName: String): Flow { + val request = OneTimeWorkRequestBuilder() + .setConstraints(Constraints(requiredNetworkType = NetworkType.CONNECTED)) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, Duration.ofSeconds(5)) + .addTag(TAG) + .setInputData( + Data.Builder() + .putString(ARG_FILE_ID, fileId) + .putString(ARG_FOLDER, cacheFolderName) + .build() + ) + .build() + + val workName = uniqueWorkName(fileId, cacheFolderName) + WorkManager.getInstance(context) + .enqueueUniqueWork( + workName, + ExistingWorkPolicy.KEEP, + request + ) + + return WorkManager.getInstance(context) + .getWorkInfoByIdFlow(request.id) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/RecipientAvatarDownloadManager.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/RecipientAvatarDownloadManager.kt new file mode 100644 index 0000000000..3f72cf1c42 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/RecipientAvatarDownloadManager.kt @@ -0,0 +1,162 @@ +package org.thoughtcrime.securesms.glide + +import android.app.Application +import androidx.work.await +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +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.GroupInfo +import org.session.libsession.messaging.file_server.FileServerApi +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.recipients.RemoteFile +import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.GroupDatabase +import org.thoughtcrime.securesms.dependencies.ConfigFactory +import javax.inject.Inject +import javax.inject.Singleton + +@Suppress("OPT_IN_USAGE") +@OptIn(FlowPreview::class) +@Singleton +class RecipientAvatarDownloadManager @Inject constructor( + private val application: Application, + private val prefs: TextSecurePreferences, + private val configFactory: ConfigFactory, + private val groupDatabase: GroupDatabase, +) { + init { + GlobalScope.launch { + prefs.watchLocalNumber() + .map { it != null } + .flatMapLatest { isLoggedIn -> + if (isLoggedIn) { + (configFactory.configUpdateNotifications as Flow<*>) + .debounce(500) + .onStart { emit(Unit) } + .map { getAllAvatars() } + } else { + flowOf(emptySet()) + } + } + .scan(State(emptySet())) { acc, newSet -> + val toDownload = newSet - acc.downloadedAvatar + for (file in toDownload) { + Log.d(TAG, "Downloading $file") + when (file) { + is AvatarFile.FileServer -> { + EncryptedFileDownloadWorker.enqueue( + context = application, + fileId = file.fileId, + cacheFolderName = CACHE_FOLDER_NAME, + ) + } + is AvatarFile.Community -> { + CommunityFileDownloadWorker.enqueue( + context = application, + file = file.avatar, + ) + } + } + } + + val toRemove = acc.downloadedAvatar - newSet + for (file in toRemove) { + Log.d(TAG, "Cancelling downloading of $file") + when (file) { + is AvatarFile.FileServer -> { + EncryptedFileDownloadWorker.cancel( + context = application, + fileId = file.fileId, + cacheFolderName = CACHE_FOLDER_NAME, + ).await() + } + + is AvatarFile.Community -> { + CommunityFileDownloadWorker.cancel( + context = application, + avatar = file.avatar, + ) + } + } + } + + acc.copy(downloadedAvatar = newSet) + } + .collect() + // Look at all the avatar URLs stored in the config and download them if necessary + } + } + + fun getAllAvatars(): Set { + val (contacts, groups) = configFactory.withUserConfigs { configs -> + configs.contacts.all() to configs.userGroups.all() + } + + val contactAvatars = contacts.asSequence() + .map { it.profilePicture.url } + + val groupsAvatars = groups.asSequence() + .filterIsInstance() + .flatMap { it.getGroupAvatarUrls() } + + val out = mutableSetOf() + + // Note that for contacts + groups avatars, contacts ones take precedence over groups, + // so their order in the set is important. + (groupsAvatars + contactAvatars) + .mapNotNull { url -> FileServerApi.getFileIdFromUrl(url) } + .mapTo(out) { AvatarFile.FileServer(it) } + + + groups.asSequence() + .filterIsInstance() + .flatMap { it.getCommunityAvatarFile() } + .mapTo(out) { it } + + return out + } + + private fun GroupInfo.ClosedGroupInfo.getGroupAvatarUrls(): List { + if (destroyed) { + return emptyList() + } + + return configFactory.withGroupConfigs(AccountId(groupAccountId)) { + buildList { + add(it.groupInfo.getProfilePic().url) + it.groupMembers.all().forEach { m -> + m.profilePic()?.url?.let(::add) + } + } + } + } + + private fun GroupInfo.CommunityGroupInfo.getCommunityAvatarFile(): Sequence { + // Don't download avatars from community servers yet, future improvement + return emptySequence() + } + + sealed interface AvatarFile { + data class FileServer(val fileId: String) : AvatarFile + data class Community(val avatar: RemoteFile.Community) : AvatarFile + } + + private data class State( + val downloadedAvatar: Set + ) + + companion object { + const val CACHE_FOLDER_NAME = "recipient_avatars" + + private const val TAG = "RecipientAvatarDownloadManager" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/RemoteFileDownloadWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/RemoteFileDownloadWorker.kt new file mode 100644 index 0000000000..ee8a274cae --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/RemoteFileDownloadWorker.kt @@ -0,0 +1,80 @@ +package org.thoughtcrime.securesms.glide + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.session.libsession.snode.OnionRequestAPI +import org.session.libsignal.exceptions.NonRetryableException +import org.session.libsignal.utilities.ByteArraySlice +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.util.getRootCause +import java.io.File + +/** + * A worker that downloads files from Session's file server. + */ +abstract class RemoteFileDownloadWorker( + context: Context, + params: WorkerParameters +) : CoroutineWorker(context, params) { + abstract suspend fun downloadFile(): ByteArraySlice + abstract fun getFilesFromInputData(): DownloadedFiles + abstract fun saveDownloadedFile(from: ByteArraySlice, out: File) + + abstract val debugName: String + + override suspend fun doWork(): Result = withContext(Dispatchers.Default) { + val files = getFilesFromInputData() + + if (files.completedFile.exists()) { + Log.i(TAG, "File already downloaded: ${files.completedFile}") + return@withContext Result.success() + } + + if (files.permanentErrorMarkerFile.exists()) { + Log.w(TAG, "Skipping downloading $debugName due to it being marked as a permanent error") + return@withContext Result.failure() + } + + Log.d(TAG, "Start downloading file from $debugName onto ${files.completedFile}") + + // Make sure the parent directory exists for the completed file. + files.completedFile.parentFile?.mkdirs() + + try { + val bytes = downloadFile() + Log.d(TAG, "Downloaded ${bytes.len} bytes from file server: $debugName") + + saveDownloadedFile(bytes, files.completedFile) + + Result.success() + } catch (e: CancellationException) { + Log.i(TAG, "Download cancelled for file $debugName") + throw e + } catch (e: Exception) { + Log.e(TAG, "Failed to download file $debugName", e) + if (e is NonRetryableException || (e.getRootCause())?.statusCode == 404) { + files.permanentErrorMarkerFile.parentFile?.mkdirs() + if (!files.permanentErrorMarkerFile.createNewFile()) { + Log.w(TAG, "Failed to create permanent error marker file: ${files.permanentErrorMarkerFile}") + } + + Result.failure() + } else { + Result.retry() + } + } + } + + data class DownloadedFiles( + val completedFile: File, + val permanentErrorMarkerFile: File = File(completedFile.parentFile, "${completedFile.name}.error") + ) + + companion object { + private const val TAG = "RemoteFileDownloadWorker" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/RemoteFileLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/RemoteFileLoader.kt new file mode 100644 index 0000000000..8aa41f430f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/RemoteFileLoader.kt @@ -0,0 +1,165 @@ +package org.thoughtcrime.securesms.glide + +import android.content.Context +import androidx.work.WorkInfo +import com.bumptech.glide.Priority +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Key +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.session.libsession.messaging.file_server.FileServerApi +import org.session.libsession.utilities.AESGCM +import org.session.libsession.utilities.recipients.RemoteFile +import org.session.libsignal.exceptions.NonRetryableException +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.security.MessageDigest + +class RemoteFileLoader( + private val context: Context, +) : ModelLoader { + override fun buildLoadData( + model: RemoteFile, + width: Int, + height: Int, + options: Options + ): ModelLoader.LoadData { + return ModelLoader.LoadData( + RemoteFileKey(model), + RemoteFileDataFetcher(model) + ) + } + + private inner class RemoteFileDataFetcher(private val file: RemoteFile) : + DataFetcher { + private var job: Job? = null + + override fun loadData( + priority: Priority, + callback: DataFetcher.DataCallback + ) { + job = GlobalScope.launch { + try { + val files: RemoteFileDownloadWorker.DownloadedFiles + val encryptionKey: ByteArray + + when (file) { + is RemoteFile.Encrypted -> { + val fileId = requireNotNull(FileServerApi.getFileIdFromUrl(file.url)) { + "Target URL is not supported, must be a session file server url but got: ${file.url}" + } + + files = EncryptedFileDownloadWorker.getFileForUrl( + context, + RecipientAvatarDownloadManager.CACHE_FOLDER_NAME, + fileId + ) + + if (!files.permanentErrorMarkerFile.exists() && !files.completedFile.exists()) { + // Files not exists, enqueue a download + EncryptedFileDownloadWorker.enqueue( + context = context, + fileId = fileId, + cacheFolderName = RecipientAvatarDownloadManager.CACHE_FOLDER_NAME + ).first { it?.state == WorkInfo.State.FAILED || it?.state == WorkInfo.State.SUCCEEDED } + } + + encryptionKey = file.key.data + } + + is RemoteFile.Community -> { + files = CommunityFileDownloadWorker.getFileForUrl(context, file) + + if (!files.permanentErrorMarkerFile.exists() && !files.completedFile.exists()) { + // Files not exists, enqueue a download + CommunityFileDownloadWorker.enqueue(context, file) + .first { it?.state == WorkInfo.State.FAILED || it?.state == WorkInfo.State.SUCCEEDED } + } + + encryptionKey = AttachmentSecretProvider.getInstance(context) + .orCreateAttachmentSecret.modernKey + } + } + + + if (files.permanentErrorMarkerFile.exists()) { + throw NonRetryableException("Requested file is marked as a permanent error:") + } + + check(files.completedFile.exists()) { + "File not downloaded but no reason is given. Most likely a bug in the download worker." + } + + val encrypted = files.completedFile.readBytes() + Log.d(TAG, "About to decrypt file with size: ${encrypted.size} bytes") + + callback.onDataReady( + ByteArrayInputStream( + AESGCM.decrypt(encrypted, symmetricKey = encryptionKey) + ) + ) + + } catch (e: CancellationException) { + Log.i(TAG, "Download cancelled for file: $file") + throw e + } catch (e: Exception) { + Log.e(TAG, "Error downloading file: $file", e) + callback.onLoadFailed(e) + } + } + } + + override fun cleanup() { + job?.cancel() + job = null + } + + override fun cancel() { + cleanup() + } + + override fun getDataClass(): Class = InputStream::class.java + override fun getDataSource(): DataSource = DataSource.REMOTE + } + + private data class RemoteFileKey(val file: RemoteFile) : Key { + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + when (file) { + is RemoteFile.Community -> { + messageDigest.update(file.communityServerBaseUrl.toByteArray()) + messageDigest.update(file.roomId.toByteArray()) + messageDigest.update(file.fileId.toByteArray()) + } + + is RemoteFile.Encrypted -> { + messageDigest.update(file.url.toByteArray()) + messageDigest.update(file.key.data) + } + } + } + } + + override fun handles(model: RemoteFile): Boolean = true + + class Factory(private val context: Context) : ModelLoaderFactory { + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { + return RemoteFileLoader(context) + } + + override fun teardown() {} + } + + companion object { + private const val TAG = "RemoteFileLoader" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/BaseGroupMembersViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/BaseGroupMembersViewModel.kt index 3e1fe9d78b..94130b445a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/BaseGroupMembersViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/BaseGroupMembersViewModel.kt @@ -21,22 +21,24 @@ import network.loki.messenger.R import network.loki.messenger.libsession_util.allWithStatus import network.loki.messenger.libsession_util.util.GroupMember import org.session.libsession.database.StorageProtocol +import org.session.libsession.utilities.Address import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigUpdateNotification import org.session.libsession.utilities.GroupDisplayInfo -import org.session.libsession.utilities.UsernameUtils +import org.session.libsession.utilities.recipients.displayNameOrFallback import org.session.libsignal.utilities.AccountId +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUtils import java.util.EnumSet -abstract class BaseGroupMembersViewModel ( +abstract class BaseGroupMembersViewModel( private val groupId: AccountId, @ApplicationContext private val context: Context, private val storage: StorageProtocol, - private val usernameUtils: UsernameUtils, private val configFactory: ConfigFactoryProtocol, - private val avatarUtils: AvatarUtils + private val avatarUtils: AvatarUtils, + private val recipientRepository: RecipientRepository, ) : ViewModel() { // Output: the source-of-truth group information. Other states are derived from this. protected val groupInfo: StateFlow>?> = @@ -100,7 +102,8 @@ abstract class BaseGroupMembersViewModel ( val name = if (isMyself) { context.getString(R.string.you) } else { - usernameUtils.getContactNameWithAccountID(memberAccountId.hexString, groupId) + recipientRepository.getRecipient(Address.fromSerialized(memberAccountId.hexString)) + .displayNameOrFallback(fallbackName = { member.name }, address = memberAccountId.hexString) } val highlightStatus = status in EnumSet.of( diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt index 9d5a7dbbc5..41cf84bcd4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt @@ -1,16 +1,10 @@ package org.thoughtcrime.securesms.groups import android.content.Context -import network.loki.messenger.libsession_util.ConfigBase -import network.loki.messenger.libsession_util.util.Bytes import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1 import org.session.libsession.utilities.Address -import org.session.libsession.utilities.GroupRecord -import org.session.libsession.utilities.GroupUtil -import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.dependencies.ConfigFactory object ClosedGroupManager { @@ -32,26 +26,4 @@ object ClosedGroupManager { } } - fun ConfigFactory.updateLegacyGroup(group: GroupRecord) { - if (!group.isLegacyGroup) return - val storage = MessagingModuleConfiguration.shared.storage - val threadId = storage.getThreadId(group.encodedId) ?: return - val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId()) - val latestKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return - - withMutableUserConfigs { - val groups = it.userGroups - - val legacyInfo = groups.getOrConstructLegacyGroupInfo(groupPublicKey) - val latestMemberMap = GroupUtil.createConfigMemberMap(group.members.map(Address::toString), group.admins.map(Address::toString)) - val toSet = legacyInfo.copy( - members = latestMemberMap, - name = group.title, - priority = if (storage.isPinned(threadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE, - encPubKey = Bytes((latestKeyPair.publicKey as DjbECPublicKey).publicKey), // 'serialize()' inserts an extra byte - encSecKey = Bytes(latestKeyPair.privateKey.serialize()) - ) - groups.set(toSet) - } - } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt index 6a27b213a6..8d857e8c56 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt @@ -26,10 +26,9 @@ class CreateGroupFragment : Fragment() { setContent { SessionMaterialTheme { CreateGroupScreen( - onNavigateToConversationScreen = { threadID -> + onNavigateToConversationScreen = { address -> startActivity( - Intent(requireContext(), ConversationActivityV2::class.java) - .putExtra(ConversationActivityV2.THREAD_ID, threadID) + ConversationActivityV2.createIntent(requireContext(), address) ) }, onBack = delegate::onDialogBackPressed, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupViewModel.kt index b7108adc4b..d9268aa386 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupViewModel.kt @@ -18,9 +18,11 @@ import kotlinx.coroutines.withContext import network.loki.messenger.R import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.GroupManagerV2 +import org.session.libsession.utilities.Address import org.session.libsignal.utilities.AccountId import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.textSizeInBytes import org.thoughtcrime.securesms.database.GroupDatabase +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.util.AvatarUtils @@ -34,6 +36,7 @@ class CreateGroupViewModel @AssistedInject constructor( private val avatarUtils: AvatarUtils, groupDatabase: GroupDatabase, @Assisted createFromLegacyGroupId: String?, + recipientRepository: RecipientRepository, ): ViewModel() { // Child view model to handle contact selection logic //todo we should probably extend this VM instead of instantiating it here @@ -42,8 +45,8 @@ class CreateGroupViewModel @AssistedInject constructor( excludingAccountIDs = emptySet(), applyDefaultFiltering = true, scope = viewModelScope, - appContext = appContext, - avatarUtils = avatarUtils + avatarUtils = avatarUtils, + recipientRepository = recipientRepository, ) // Input: group name @@ -126,8 +129,7 @@ class CreateGroupViewModel @AssistedInject constructor( } else -> { - val threadId = withContext(Dispatchers.Default) { storage.getOrCreateThreadIdFor(recipient.address) } - mutableEvents.emit(CreateGroupEvent.NavigateToConversation(threadId)) + mutableEvents.emit(CreateGroupEvent.NavigateToConversation(recipient.address)) } } @@ -148,7 +150,7 @@ class CreateGroupViewModel @AssistedInject constructor( } sealed interface CreateGroupEvent { - data class NavigateToConversation(val threadID: Long): CreateGroupEvent + data class NavigateToConversation(val address: Address): CreateGroupEvent data class Error(val message: String): CreateGroupEvent } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt index 6718bed70e..5be4b98480 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt @@ -21,8 +21,8 @@ import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.GroupInviteException import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.utilities.ConfigFactoryProtocol -import org.session.libsession.utilities.UsernameUtils import org.session.libsignal.utilities.AccountId +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.util.AvatarUtils @@ -34,9 +34,9 @@ class EditGroupViewModel @AssistedInject constructor( storage: StorageProtocol, private val configFactory: ConfigFactoryProtocol, private val groupManager: GroupManagerV2, - private val usernameUtils: UsernameUtils, - private val avatarUtils: AvatarUtils -) : BaseGroupMembersViewModel(groupId, context, storage, usernameUtils, configFactory, avatarUtils) { + avatarUtils: AvatarUtils, + private val recipientRepository: RecipientRepository, +) : BaseGroupMembersViewModel(groupId, context, storage, configFactory, avatarUtils, recipientRepository) { // Output: The name of the group. This is the current name of the group, not the name being edited. val groupName: StateFlow = groupInfo @@ -69,7 +69,7 @@ class EditGroupViewModel @AssistedInject constructor( showLoading = false, errorMessage = { err -> if (err is GroupInviteException) { - err.format(context, usernameUtils).toString() + err.format(context, recipientRepository).toString() } else { null } @@ -89,7 +89,7 @@ class EditGroupViewModel @AssistedInject constructor( showLoading = false, errorMessage = { err -> if (err is GroupInviteException) { - err.format(context, usernameUtils).toString() + err.format(context, recipientRepository).toString() } else { null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index b1a0c5d9fa..3e294f572f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -6,25 +6,20 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.session.libsession.messaging.MessagingModuleConfiguration; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.DistributionTypes; import org.session.libsession.utilities.GroupUtil; import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.util.BitmapUtil; -import java.io.IOException; import java.util.HashSet; import java.util.LinkedList; import java.util.Objects; import java.util.Set; -import network.loki.messenger.libsession_util.UserGroupsConfig; - public class GroupManager { public static long getOpenGroupThreadID(String id, @NonNull Context context) { @@ -33,8 +28,7 @@ public static long getOpenGroupThreadID(String id, @NonNull Context context) { } public static long getThreadIDFromGroupID(String groupID, @NonNull Context context) { - final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupID), true); - return DatabaseComponent.get(context).threadDatabase().getThreadIdIfExistsFor(groupRecipient); + return DatabaseComponent.get(context).threadDatabase().getThreadIdIfExistsFor(Address.fromSerialized(groupID)); } public static @NonNull GroupActionResult createOpenGroup(@NonNull String id, @@ -53,7 +47,6 @@ public static long getThreadIDFromGroupID(String groupID, @NonNull Context cont { final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); final GroupDatabase groupDatabase = DatabaseComponent.get(context).groupDatabase(); - final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false); final Set
memberAddresses = new HashSet<>(); memberAddresses.add(Address.fromSerialized(Objects.requireNonNull(TextSecurePreferences.getLocalNumber(context)))); @@ -61,6 +54,7 @@ public static long getThreadIDFromGroupID(String groupID, @NonNull Context cont groupDatabase.updateProfilePicture(groupId, avatarBytes); + Address groupRecipient = Address.fromSerialized(groupId); long threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor( groupRecipient, DistributionTypes.CONVERSATION); return new GroupActionResult(groupRecipient, threadID); @@ -71,7 +65,7 @@ public static boolean deleteGroup(@NonNull String groupId, { final GroupDatabase groupDatabase = DatabaseComponent.get(context).groupDatabase(); final ThreadDatabase threadDatabase = DatabaseComponent.get(context).threadDatabase(); - final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false); + final Address groupRecipient = Address.fromSerialized(groupId); long threadId = threadDatabase.getThreadIdIfExistsFor(groupRecipient); if (threadId != -1L) { @@ -82,15 +76,15 @@ public static boolean deleteGroup(@NonNull String groupId, } public static class GroupActionResult { - private Recipient groupRecipient; + private Address groupRecipient; private long threadId; - public GroupActionResult(Recipient groupRecipient, long threadId) { + public GroupActionResult(Address groupRecipient, long threadId) { this.groupRecipient = groupRecipient; this.threadId = threadId; } - public Recipient getGroupRecipient() { + public Address getGroupRecipient() { return groupRecipient; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt index 5eb6fa1602..4161859b48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt @@ -44,10 +44,11 @@ import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.model.BatchResponse import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.Address -import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY import org.session.libsession.utilities.getGroup +import org.session.libsession.utilities.recipients.BasicRecipient import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.recipients.toUserPic import org.session.libsession.utilities.waitUntilGroupConfigsPushed import org.session.libsignal.protos.SignalServiceProtos.DataMessage import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage @@ -62,6 +63,7 @@ import org.thoughtcrime.securesms.configs.ConfigUploader import org.thoughtcrime.securesms.database.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.MmsSmsDatabase +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.util.SessionMetaProtocol @@ -78,7 +80,6 @@ class GroupManagerV2Impl @Inject constructor( private val mmsSmsDatabase: MmsSmsDatabase, private val lokiDatabase: LokiMessageDatabase, private val threadDatabase: ThreadDatabase, - private val profileManager: SSKEnvironment.ProfileManagerProtocol, @ApplicationContext val application: Context, private val clock: SnodeClock, private val messageDataProvider: MessageDataProvider, @@ -86,6 +87,7 @@ class GroupManagerV2Impl @Inject constructor( private val configUploader: ConfigUploader, private val scope: GroupScope, private val groupPollerManager: GroupPollerManager, + private val recipientRepository: RecipientRepository, ) : GroupManagerV2 { private val dispatcher = Dispatchers.Default @@ -131,8 +133,8 @@ class GroupManagerV2Impl @Inject constructor( val adminKey = checkNotNull(group.adminKey?.data) { "Admin key is null for new group creation." } val groupId = AccountId(group.groupAccountId) - val memberAsRecipients = members.map { - Recipient.from(application, Address.fromSerialized(it.hexString), false) + val memberAsRecipients = members.mapNotNull { + recipientRepository.getRecipient(Address.fromSerialized(it.hexString)) } try { @@ -146,10 +148,10 @@ class GroupManagerV2Impl @Inject constructor( for (member in memberAsRecipients) { newGroupConfigs.groupMembers.set( newGroupConfigs.groupMembers.getOrConstruct(member.address.toString()).apply { - setName(member.name) - setProfilePic(member.profileAvatar?.let { url -> - member.profileKey?.let { key -> UserPic(url, key) } - } ?: UserPic.DEFAULT) + // Must use the contact's original name because we are setting this info + // for other gorup members to see. + setName((member.basic as? BasicRecipient.Contact)?.name.orEmpty()) + setProfilePic(member.avatar?.toUserPic() ?: UserPic.DEFAULT) } ) } @@ -195,13 +197,7 @@ class GroupManagerV2Impl @Inject constructor( "Failed to create a thread for the group" } - val recipient = - Recipient.from(application, Address.fromSerialized(groupId.hexString), false) - - // Apply various data locally - profileManager.setName(application, recipient, groupName) - storage.setRecipientApprovedMe(recipient, true) - storage.setRecipientApproved(recipient, true) + val recipient = recipientRepository.getRecipient(Address.fromSerialized(groupId.hexString))!! // Invite members JobQueue.shared.add( @@ -793,11 +789,10 @@ class GroupManagerV2Impl @Inject constructor( inviteMessageTimestamp: Long, inviteMessageHash: String, ) { - val recipient = - Recipient.from(application, Address.fromSerialized(groupId.hexString), false) + val address = Address.fromSerialized(groupId.hexString) + val inviterRecipient = recipientRepository.getBasicRecipientFast(Address.fromSerialized(inviter.hexString)) - val shouldAutoApprove = - storage.getRecipientApproved(Address.fromSerialized(inviter.hexString)) + val shouldAutoApprove = (inviterRecipient as? BasicRecipient.Contact)?.approved == true val closedGroupInfo = GroupInfo.ClosedGroupInfo( groupAccountId = groupId.hexString, adminKey = authDataOrAdminSeed.takeIf { fromPromotion }?.let { GroupInfo.ClosedGroupInfo.adminKeyFromSeed(it) }?.toBytes(), @@ -814,10 +809,7 @@ class GroupManagerV2Impl @Inject constructor( it.userGroups.set(closedGroupInfo) } - profileManager.setName(application, recipient, groupName) - val groupThreadId = storage.getOrCreateThreadIdFor(recipient.address) - storage.setRecipientApprovedMe(recipient, true) - storage.setRecipientApproved(recipient, shouldAutoApprove) + val groupThreadId = storage.getOrCreateThreadIdFor(address) if (shouldAutoApprove) { approveGroupInvite(closedGroupInfo, inviteMessageHash) @@ -1133,8 +1125,7 @@ class GroupManagerV2Impl @Inject constructor( override fun setExpirationTimer( groupId: AccountId, - mode: ExpiryMode, - expiryChangeTimestampMs: Long + mode: ExpiryMode ) { val adminKey = requireAdminAccess(groupId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMembersViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMembersViewModel.kt index abd8296c6b..c7dbb8c86a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMembersViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMembersViewModel.kt @@ -12,25 +12,24 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.session.libsession.database.StorageProtocol import org.session.libsession.utilities.Address import org.session.libsession.utilities.ConfigFactoryProtocol -import org.session.libsession.utilities.UsernameUtils import org.session.libsignal.utilities.AccountId import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.util.AvatarUtils @HiltViewModel(assistedFactory = GroupMembersViewModel.Factory::class) class GroupMembersViewModel @AssistedInject constructor( @Assisted private val groupId: AccountId, - @ApplicationContext private val context: Context, - private val storage: StorageProtocol, + @param:ApplicationContext private val context: Context, + storage: StorageProtocol, configFactory: ConfigFactoryProtocol, - usernameUtils: UsernameUtils, - avatarUtils: AvatarUtils -) : BaseGroupMembersViewModel(groupId, context, storage, usernameUtils, configFactory, avatarUtils) { + avatarUtils: AvatarUtils, + recipientRepository: RecipientRepository, +) : BaseGroupMembersViewModel(groupId, context, storage, configFactory, avatarUtils, recipientRepository) { private val _navigationActions = Channel() val navigationActions get() = _navigationActions.receiveAsFlow() @@ -43,16 +42,10 @@ class GroupMembersViewModel @AssistedInject constructor( fun onMemberClicked(accountId: AccountId) { viewModelScope.launch(Dispatchers.Default) { val address = Address.fromSerialized(accountId.hexString) - val threadId = storage.getThreadId(address) - val intent = Intent( - context, - ConversationActivityV2::class.java - ) - intent.putExtra(ConversationActivityV2.ADDRESS, address) - intent.putExtra(ConversationActivityV2.THREAD_ID, threadId) - - _navigationActions.send(intent) + _navigationActions.send(ConversationActivityV2.createIntent( + context, address + )) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt index 86c3ccf372..c4daacb706 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.groups import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.content.Context -import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -23,7 +22,6 @@ import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 @@ -114,17 +112,13 @@ class JoinCommunityFragment : Fragment() { ) val storage = MessagingModuleConfiguration.shared.storage storage.onOpenGroupAdded(sanitizedServer, openGroup.room) - val threadID = - GroupManager.getOpenGroupThreadID(openGroupID, requireContext()) val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray()) withContext(Dispatchers.Main) { - val recipient = Recipient.from( + openConversationActivity( requireContext(), - Address.fromSerialized(groupID), - false + Address.fromSerialized(groupID) ) - openConversationActivity(requireContext(), threadID, recipient) delegate.onDialogClosePressed() } } catch (e: Exception) { @@ -155,11 +149,10 @@ class JoinCommunityFragment : Fragment() { mediator.attach() } - private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) { - val intent = Intent(context, ConversationActivityV2::class.java) - intent.putExtra(ConversationActivityV2.THREAD_ID, threadId) - intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) - context.startActivity(intent) + private fun openConversationActivity(context: Context, address: Address) { + context.startActivity( + ConversationActivityV2.createIntent(context, address) + ) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt index f46216678f..e18708cade 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt @@ -3,8 +3,6 @@ package org.thoughtcrime.securesms.groups import android.content.Context import android.widget.Toast import com.squareup.phrase.Phrase -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import network.loki.messenger.R import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.session.libsession.database.StorageProtocol @@ -34,13 +32,6 @@ class OpenGroupManager @Inject constructor( private val groupMemberDatabase: GroupMemberDatabase, private val pollerManager: OpenGroupPollerManager, ) { - - // flow holding information on write access for our current communities - private val _communityWriteAccess: MutableStateFlow> = MutableStateFlow(emptyMap()) - - - fun getCommunitiesWriteAccessFlow() = _communityWriteAccess.asStateFlow() - suspend fun add(server: String, room: String, publicKey: String, context: Context) { val openGroupID = "$server.$room" val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context) @@ -117,11 +108,6 @@ class OpenGroupManager @Inject constructor( fun updateOpenGroup(openGroup: OpenGroup, context: Context) { val threadID = GroupManager.getOpenGroupThreadID(openGroup.groupId, context) lokiThreadDB.setOpenGroupChat(openGroup, threadID) - - // update write access for this community - val writeAccesses = _communityWriteAccess.value.toMutableMap() - writeAccesses[openGroup.groupId] = openGroup.canWrite - _communityWriteAccess.value = writeAccesses } fun isUserModerator( diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt index d59d76609f..e60b209bf0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt @@ -1,13 +1,11 @@ package org.thoughtcrime.securesms.groups -import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -28,8 +26,9 @@ import kotlinx.coroutines.withContext import org.session.libsession.utilities.Address import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.AccountId +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.dependencies.ConfigFactory -import org.thoughtcrime.securesms.home.search.getSearchName +import org.thoughtcrime.securesms.home.search.searchName import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUtils @@ -38,10 +37,10 @@ import org.thoughtcrime.securesms.util.AvatarUtils open class SelectContactsViewModel @AssistedInject constructor( private val configFactory: ConfigFactory, private val avatarUtils: AvatarUtils, - @ApplicationContext private val appContext: Context, @Assisted private val excludingAccountIDs: Set, @Assisted private val applyDefaultFiltering: Boolean, // true by default - If true will filter out blocked and unapproved contacts @Assisted private val scope: CoroutineScope, + private val recipientRepository: RecipientRepository, ) : ViewModel() { // Input: The search query private val mutableSearchQuery = MutableStateFlow("") @@ -91,15 +90,12 @@ open class SelectContactsViewModel @AssistedInject constructor( } else { allContacts.filterNotTo(mutableSetOf()) { it in excludingAccountIDs } }.map { - Recipient.from( - appContext, - Address.fromSerialized(it.hexString), - false - ) + val address = Address.fromSerialized(it.hexString) + recipientRepository.getRecipient(address) ?: Recipient.empty(address) } if(applyDefaultFiltering){ - recipientContacts.filter { !it.isBlocked && it.isApproved } // filter out blocked contacts and unapproved contacts + recipientContacts.filter { !it.blocked && it.approved } // filter out blocked contacts and unapproved contacts } else recipientContacts } } @@ -113,12 +109,12 @@ open class SelectContactsViewModel @AssistedInject constructor( ): List { val items = mutableListOf() for (contact in contacts) { - if (query.isBlank() || contact.getSearchName().contains(query, ignoreCase = true)) { + if (query.isBlank() || contact.searchName.contains(query, ignoreCase = true)) { val accountId = AccountId(contact.address.toString()) val avatarData = avatarUtils.getUIDataFromRecipient(contact) items.add( ContactItem( - name = contact.getSearchName(), + name = contact.searchName, accountID = accountId, avatarUIData = avatarData, selected = selectedAccountIDs.contains(accountId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroupScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroupScreen.kt index 396d19d834..4944b79dc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroupScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroupScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import network.loki.messenger.R +import org.session.libsession.utilities.Address import org.session.libsignal.utilities.AccountId import org.thoughtcrime.securesms.groups.ContactItem import org.thoughtcrime.securesms.groups.CreateGroupEvent @@ -50,7 +51,7 @@ import org.thoughtcrime.securesms.util.AvatarUIElement @Composable fun CreateGroupScreen( fromLegacyGroupId: String?, - onNavigateToConversationScreen: (threadID: Long) -> Unit, + onNavigateToConversationScreen: (address: Address) -> Unit, onBack: () -> Unit, onClose: () -> Unit, ) { @@ -65,7 +66,7 @@ fun CreateGroupScreen( when (event) { is CreateGroupEvent.NavigateToConversation -> { onClose() - onNavigateToConversationScreen(event.threadID) + onNavigateToConversationScreen(event.address) } is CreateGroupEvent.Error -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt index e5058fd9af..064530bf3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt @@ -13,9 +13,6 @@ import network.loki.messenger.R import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager import org.session.libsession.utilities.GroupRecord -import org.session.libsession.utilities.getGroup -import org.session.libsession.utilities.isGroupDestroyed -import org.session.libsession.utilities.wasKickedFromGroupV2 import org.session.libsignal.utilities.AccountId import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord @@ -78,8 +75,8 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto if (!recipient.isGroupOrCommunityRecipient && !recipient.isLocalNumber) { binding.detailsTextView.visibility = View.VISIBLE - binding.unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE - binding.blockTextView.visibility = if (recipient.isBlocked) View.GONE else View.VISIBLE + binding.unblockTextView.visibility = if (recipient.blocked) View.VISIBLE else View.GONE + binding.blockTextView.visibility = if (recipient.blocked) View.GONE else View.VISIBLE binding.detailsTextView.setOnClickListener(this) binding.blockTextView.setOnClickListener(this) binding.unblockTextView.setOnClickListener(this) @@ -99,7 +96,7 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto binding.copyCommunityUrl.setOnClickListener(this) val notificationIconRes = when{ - recipient.isMuted -> R.drawable.ic_volume_off + recipient.isMuted() -> R.drawable.ic_volume_off recipient.notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS -> R.drawable.ic_at_sign else -> R.drawable.ic_volume_2 diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index 2af1728aa0..4339f6539e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -52,11 +52,7 @@ class ConversationView : LinearLayout { // region Updating fun bind(thread: ThreadRecord, isTyping: Boolean) { this.thread = thread - if (thread.isPinned) { - binding.iconPinned.isVisible = true - } else { - binding.iconPinned.isVisible = false - } + binding.iconPinned.isVisible = thread.isPinned binding.root.background = if (thread.unreadCount > 0) { ContextCompat.getDrawable(context, R.drawable.conversation_unread_background) @@ -65,7 +61,7 @@ class ConversationView : LinearLayout { } val unreadCount = thread.unreadCount - if (thread.recipient.isBlocked) { + if (thread.recipient.blocked) { binding.accentView.setBackgroundColor(ThemeUtil.getThemedColor(context, R.attr.danger)) binding.accentView.visibility = View.VISIBLE } else { @@ -97,9 +93,9 @@ class ConversationView : LinearLayout { ) } val recipient = thread.recipient - binding.muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != NOTIFY_TYPE_ALL + binding.muteIndicatorImageView.isVisible = recipient.isMuted() || recipient.notifyType != NOTIFY_TYPE_ALL - val drawableRes = if (recipient.isMuted || recipient.notifyType == NOTIFY_TYPE_NONE) { + val drawableRes = if (recipient.isMuted() || recipient.notifyType == NOTIFY_TYPE_NONE) { R.drawable.ic_volume_off } else { R.drawable.ic_at_sign @@ -146,7 +142,7 @@ class ConversationView : LinearLayout { private fun getTitle(recipient: Recipient): String = when { recipient.isLocalNumber -> context.getString(R.string.noteToSelf) - else -> recipient.name // Internally uses the Contact API + else -> recipient.displayName // Internally uses the Contact API } // endregion } 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 214d81eec8..cb73911327 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -12,8 +12,6 @@ import android.view.ViewGroup.MarginLayoutParams import android.widget.Toast import androidx.activity.viewModels import androidx.core.os.bundleOf -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding @@ -36,9 +34,6 @@ import kotlinx.coroutines.withContext import network.loki.messenger.BuildConfig import network.loki.messenger.R import network.loki.messenger.databinding.ActivityHomeBinding -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager @@ -46,12 +41,9 @@ import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.Address -import org.session.libsession.utilities.ProfilePictureModifiedEvent import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.recipients.Recipient -import org.session.libsession.utilities.wasKickedFromGroupV2 import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.ApplicationContext @@ -138,47 +130,35 @@ class HomeActivity : ScreenLockActionBarActivity(), GlobalSearchAdapter( dateUtils = dateUtils, onContactClicked = { model -> - when (model) { - is GlobalSearchAdapter.Model.Message -> push { - model.messageResult.run { - putExtra(ConversationActivityV2.THREAD_ID, threadId) - putExtra(ConversationActivityV2.SCROLL_MESSAGE_ID, sentTimestampMs) - putExtra( - ConversationActivityV2.SCROLL_MESSAGE_AUTHOR, - messageRecipient.address - ) - } - } + val intent = when (model) { + is GlobalSearchAdapter.Model.Message -> ConversationActivityV2 + .createIntent(this, + address = model.messageResult.conversationRecipient.address, + scrollToMessage = model.messageResult.sentTimestampMs to model.messageResult.messageRecipient.address + ) - is GlobalSearchAdapter.Model.SavedMessages -> push { - putExtra( - ConversationActivityV2.ADDRESS, - Address.fromSerialized(model.currentUserPublicKey) + is GlobalSearchAdapter.Model.SavedMessages -> ConversationActivityV2 + .createIntent(this, + address = Address.fromSerialized(model.currentUserPublicKey) ) - } - is GlobalSearchAdapter.Model.Contact -> push { - putExtra( - ConversationActivityV2.ADDRESS, - model.contact.hexString.let(Address::fromSerialized) + is GlobalSearchAdapter.Model.Contact -> ConversationActivityV2 + .createIntent(this, + address = Address.fromSerialized(model.contact.hexString) ) - } - is GlobalSearchAdapter.Model.GroupConversation -> model.groupId - .let { Recipient.from(this, Address.fromSerialized(it), false) } - .let(threadDb::getThreadIdIfExistsFor) - .takeIf { it >= 0 } - ?.let { - push { - putExtra( - ConversationActivityV2.THREAD_ID, - it - ) - } - } + is GlobalSearchAdapter.Model.GroupConversation -> ConversationActivityV2 + .createIntent(this, + address = Address.fromSerialized(model.groupId) + ) - else -> Log.d("Loki", "callback with model: $model") + else -> { + Log.d("Loki", "callback with model: $model") + return@GlobalSearchAdapter + } } + + push(intent) }, onContactLongPressed = { model -> onSearchContactLongPress(model.contact.hexString, model.name) @@ -326,7 +306,6 @@ class HomeActivity : ScreenLockActionBarActivity(), }.collectLatest(globalSearchAdapter::setNewData) } } - EventBus.getDefault().register(this@HomeActivity) if (isFromOnboarding) { if (Build.VERSION.SDK_INT >= 33 && (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled().not()) { @@ -401,11 +380,10 @@ class HomeActivity : ScreenLockActionBarActivity(), return contacts // Remove ourself, we're shown above. - .filter { it.accountID != publicKey } + .filter { it.address.address != publicKey } // Get the name that we will display and sort by, and uppercase it to // help with sorting and we need the char uppercased later. - .map { (it.nickname?.takeIf(String::isNotEmpty) ?: it.name?.takeIf(String::isNotEmpty)) - .let { name -> NamedValue(name?.uppercase(), it) } } + .map { NamedValue(it.displayName.uppercase(), it) } // Digits are all grouped under a #, the rest are grouped by their first character.uppercased() // If there is no name, they go under Unknown .groupBy { it.name?.run { first().takeUnless(Char::isDigit)?.toString() ?: numbersTitle } ?: unknownTitle } @@ -421,14 +399,20 @@ class HomeActivity : ScreenLockActionBarActivity(), .flatMap { (key, contacts) -> listOf( GlobalSearchAdapter.Model.SubHeader(key) - ) + contacts.sortedBy { it.name ?: it.value.accountID }.map { it.value }.map { GlobalSearchAdapter.Model.Contact(it, it.accountID == publicKey) } + ) + contacts.sortedBy { it.name ?: it.value.address.address } + .map { + GlobalSearchAdapter.Model.Contact( + contact = it.value, + isSelf = it.value.address.address == publicKey + ) + } } } private val GlobalSearchResult.contactAndGroupList: List get() = - contacts.map { GlobalSearchAdapter.Model.Contact(it, it.accountID == publicKey) } + + contacts.map { GlobalSearchAdapter.Model.Contact(it, it.address.address == publicKey) } + threads.map { - GlobalSearchAdapter.Model.GroupConversation(this@HomeActivity, it) + GlobalSearchAdapter.Model.GroupConversation(it) } private val GlobalSearchResult.messageResults: List get() { @@ -488,11 +472,6 @@ class HomeActivity : ScreenLockActionBarActivity(), ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(false) } - override fun onDestroy() { - super.onDestroy() - EventBus.getDefault().unregister(this) - } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) @@ -500,15 +479,6 @@ class HomeActivity : ScreenLockActionBarActivity(), // endregion // region Updating - @Subscribe(threadMode = ThreadMode.MAIN) - fun onUpdateProfileEvent(event: ProfilePictureModifiedEvent) { - if (event.recipient.isLocalNumber) { - updateProfileButton() - } else { - homeViewModel.tryReload() - } - } - private fun updateProfileButton() { binding.profileButton.publicKey = publicKey binding.profileButton.displayName = homeViewModel.getCurrentUsername() @@ -530,9 +500,7 @@ class HomeActivity : ScreenLockActionBarActivity(), } override fun onConversationClick(thread: ThreadRecord) { - val intent = Intent(this, ConversationActivityV2::class.java) - intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId) - push(intent) + push(ConversationActivityV2.createIntent(this, address = thread.recipient.address)) } override fun onLongConversationClick(thread: ThreadRecord) { @@ -559,7 +527,7 @@ class HomeActivity : ScreenLockActionBarActivity(), Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show() } else if (thread.recipient.isCommunityRecipient) { - val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient) + val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient.address) val openGroup = lokiThreadDatabase.getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit val clip = ClipData.newPlainText("Community URL", openGroup.joinURL) @@ -570,13 +538,13 @@ class HomeActivity : ScreenLockActionBarActivity(), } bottomSheet.onBlockTapped = { bottomSheet.dismiss() - if (!thread.recipient.isBlocked) { + if (!thread.recipient.blocked) { blockConversation(thread) } } bottomSheet.onUnblockTapped = { bottomSheet.dismiss() - if (thread.recipient.isBlocked) { + if (thread.recipient.blocked) { unblockConversation(thread) } } @@ -588,17 +556,17 @@ class HomeActivity : ScreenLockActionBarActivity(), bottomSheet.dismiss() // go to the notification settings val intent = Intent(this, NotificationSettingsActivity::class.java).apply { - putExtra(NotificationSettingsActivity.THREAD_ID, thread.threadId) + putExtra(NotificationSettingsActivity.ARG_ADDRESS, thread.recipient.address) } startActivity(intent) } bottomSheet.onPinTapped = { bottomSheet.dismiss() - setConversationPinned(thread.threadId, true) + setConversationPinned(thread.recipient.address, true) } bottomSheet.onUnpinTapped = { bottomSheet.dismiss() - setConversationPinned(thread.threadId, false) + setConversationPinned(thread.recipient.address, false) } bottomSheet.onMarkAllAsReadTapped = { bottomSheet.dismiss() @@ -615,18 +583,18 @@ class HomeActivity : ScreenLockActionBarActivity(), showSessionDialog { title(R.string.block) text(Phrase.from(context, R.string.blockDescription) - .put(NAME_KEY, thread.recipient.name) + .put(NAME_KEY, thread.recipient.displayName) .format()) dangerButton(R.string.block, R.string.AccessibilityId_blockConfirm) { lifecycleScope.launch(Dispatchers.Default) { - storage.setBlocked(listOf(thread.recipient), true) + storage.setBlocked(listOf(thread.recipient.address), true) withContext(Dispatchers.Main) { binding.conversationsRecyclerView.adapter!!.notifyDataSetChanged() } } // Block confirmation toast added as per SS-64 - val txt = Phrase.from(context, R.string.blockBlockedUser).put(NAME_KEY, thread.recipient.name).format().toString() + val txt = Phrase.from(context, R.string.blockBlockedUser).put(NAME_KEY, thread.recipient.displayName).format().toString() Toast.makeText(context, txt, Toast.LENGTH_LONG).show() } cancelButton() @@ -636,10 +604,10 @@ class HomeActivity : ScreenLockActionBarActivity(), private fun unblockConversation(thread: ThreadRecord) { showSessionDialog { title(R.string.blockUnblock) - text(Phrase.from(context, R.string.blockUnblockName).put(NAME_KEY, thread.recipient.name).format()) + text(Phrase.from(context, R.string.blockUnblockName).put(NAME_KEY, thread.recipient.displayName).format()) dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) { lifecycleScope.launch(Dispatchers.Default) { - storage.setBlocked(listOf(thread.recipient), false) + storage.setBlocked(listOf(thread.recipient.address), false) withContext(Dispatchers.Main) { binding.conversationsRecyclerView.adapter!!.notifyDataSetChanged() } @@ -654,8 +622,7 @@ class HomeActivity : ScreenLockActionBarActivity(), title(R.string.contactDelete) text( Phrase.from(context, R.string.deleteContactDescription) - .put(NAME_KEY, thread.recipient?.name ?: "") - .put(NAME_KEY, thread.recipient?.name ?: "") + .put(NAME_KEY, thread.recipient?.displayName ?: "") .format() ) dangerButton(R.string.delete, R.string.qa_conversation_settings_dialog_delete_contact_confirm) { @@ -665,11 +632,8 @@ class HomeActivity : ScreenLockActionBarActivity(), } } - private fun setConversationPinned(threadId: Long, pinned: Boolean) { - lifecycleScope.launch(Dispatchers.Default) { - storage.setPinned(threadId, pinned) - homeViewModel.tryReload() - } + private fun setConversationPinned(address: Address, pinned: Boolean) { + storage.setPinned(address, pinned) } private fun markAllAsRead(thread: ThreadRecord) { @@ -771,7 +735,7 @@ class HomeActivity : ScreenLockActionBarActivity(), else { // If this is a 1-on-1 conversation title = getString(R.string.conversationsDelete) message = Phrase.from(this, R.string.conversationsDeleteDescription) - .put(NAME_KEY, recipient.name) + .put(NAME_KEY, recipient.displayName) .format() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt index 3ce691e367..e24f1cda7d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt @@ -2,12 +2,9 @@ package org.thoughtcrime.securesms.home import android.content.ContentResolver import android.content.Context -import android.widget.Toast -import androidx.annotation.AttrRes import androidx.lifecycle.ViewModel import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope -import com.squareup.phrase.Phrase import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -28,7 +25,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onErrorResume import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -39,12 +35,11 @@ import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.utilities.Address import org.session.libsession.utilities.ConfigUpdateNotification -import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.utilities.Log -import org.session.libsession.utilities.UsernameUtils -import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.currentUserName +import org.session.libsession.utilities.userConfigsChanged import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord @@ -57,15 +52,13 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( - @ApplicationContext - private val context: Context, + @param:ApplicationContext private val context: Context, private val threadDb: ThreadDatabase, private val contentResolver: ContentResolver, private val prefs: TextSecurePreferences, private val typingStatusRepository: TypingStatusRepository, private val configFactory: ConfigFactory, - private val callManager: CallManager, - private val usernameUtils: UsernameUtils, + callManager: CallManager, private val storage: StorageProtocol, private val groupManager: GroupManagerV2 ) : ViewModel() { @@ -108,7 +101,7 @@ class HomeViewModel @Inject constructor( // or if the contact is blocked, do not add it if ( thread.recipient.isLocalNumber && hideNoteToSelf || - thread.recipient.isBlocked + thread.recipient.blocked ) { return@mapNotNullTo null } @@ -130,10 +123,12 @@ class HomeViewModel @Inject constructor( .map { prefs.hasHiddenMessageRequests() } .onStart { emit(prefs.hasHiddenMessageRequests()) } - private fun hasHiddenNoteToSelf() = TextSecurePreferences.events - .filter { it == TextSecurePreferences.HAS_HIDDEN_NOTE_TO_SELF } - .map { prefs.hasHiddenNoteToSelf() } - .onStart { emit(prefs.hasHiddenNoteToSelf()) } + private fun hasHiddenNoteToSelf(): Flow = + configFactory.userConfigsChanged() + .debounce(1000L) + .onStart { emit(Unit) } + .map { configFactory.withUserConfigs { it.userProfile.getNtsPriority() == PRIORITY_HIDDEN } } + private fun observeTypingStatus(): Flow> = typingStatusRepository .typingThreads @@ -148,16 +143,26 @@ class HomeViewModel @Inject constructor( ).flowOn(Dispatchers.Default) private fun unapprovedConversationCount() = reloadTriggersAndContentChanges() - .map { threadDb.unapprovedConversationList.use { cursor -> cursor.count } } + .map { threadDb.unapprovedConversationList.size } @Suppress("OPT_IN_USAGE") private fun observeConversationList(): Flow> = reloadTriggersAndContentChanges() .mapLatest { _ -> - threadDb.approvedConversationList.use { openCursor -> - threadDb.readerFor(openCursor).run { generateSequence { next }.toList() } + withContext(Dispatchers.Default) { + val records = threadDb.approvedConversationList + + // Sort the threads by priority and last message timestamp + records.sortWith(threadRecordComparator) + + records } } - .flowOn(Dispatchers.IO) + + private val threadRecordComparator = compareByDescending { it.recipient.isPinned } + .thenByDescending { it.recipient.priority } + .thenByDescending { it.lastMessage?.timestamp ?: 0L } + .thenByDescending { it.date } + .thenBy { it.recipient.displayName } @OptIn(FlowPreview::class) private fun reloadTriggersAndContentChanges(): Flow<*> = merge( @@ -191,11 +196,6 @@ class HomeViewModel @Inject constructor( val items: List, ) - data class MessageSnippetOverride( - val text: CharSequence, - @AttrRes val colorAttr: Int, - ) - sealed interface Item { data class Thread( val thread: ThreadRecord, @@ -212,18 +212,16 @@ class HomeViewModel @Inject constructor( fun hideNoteToSelf() { - prefs.setHasHiddenNoteToSelf(true) configFactory.withMutableUserConfigs { it.userProfile.setNtsPriority(PRIORITY_HIDDEN) } } - fun getCurrentUsername() = usernameUtils.getCurrentUsernameWithAccountIdFallback() + fun getCurrentUsername() = configFactory.currentUserName fun blockContact(accountId: String) { viewModelScope.launch(Dispatchers.Default) { - val recipient = Recipient.from(context, Address.fromSerialized(accountId), false) - storage.setBlocked(listOf(recipient), isBlocked = true) + storage.setBlocked(listOf(Address.fromSerialized(accountId)), isBlocked = true) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt index f4ceb84c86..3ab9b3c43a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import android.content.Intent import android.os.Bundle import android.view.ContextThemeWrapper import android.view.LayoutInflater @@ -18,20 +17,24 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.FragmentUserDetailsBottomSheetBinding -import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.Address +import org.session.libsession.utilities.recipients.BasicRecipient import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.upsertContact +import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.IdPrefix import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 -import org.thoughtcrime.securesms.database.DatabaseContentProviders +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.dependencies.ConfigFactory import javax.inject.Inject @AndroidEntryPoint class UserDetailsBottomSheet: BottomSheetDialogFragment() { @Inject lateinit var threadDb: ThreadDatabase + @Inject lateinit var recipientRepository: RecipientRepository + @Inject lateinit var configFactory: ConfigFactory private lateinit var binding: FragmentUserDetailsBottomSheetBinding @@ -53,8 +56,8 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() { super.onViewCreated(view, savedInstanceState) val publicKey = arguments?.getString(ARGUMENT_PUBLIC_KEY) ?: return dismiss() val threadID = arguments?.getLong(ARGUMENT_THREAD_ID) ?: return dismiss() - val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false) - val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss() + val threadRecipient = threadDb.getRecipientForThreadId(threadID)?.let(recipientRepository::getRecipientSync) ?: return dismiss() + val recipient = recipientRepository.getRecipientSync(Address.fromSerialized(publicKey)) ?: return dismiss() with(binding) { profilePictureView.publicKey = publicKey profilePictureView.update(recipient) @@ -85,11 +88,9 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() { else -> return@setOnEditorActionListener false } } - nameTextView.text = recipient.name + nameTextView.text = recipient.displayName - nameEditIcon.isVisible = recipient.isContactRecipient - && !threadRecipient.isCommunityInboxRecipient - && !threadRecipient.isCommunityOutboxRecipient + nameEditIcon.isVisible = recipient.basic is BasicRecipient.Contact publicKeyTextView.isVisible = !threadRecipient.isCommunityRecipient && !threadRecipient.isCommunityInboxRecipient @@ -106,15 +107,13 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() { true } messageButton.setOnClickListener { - val threadId = MessagingModuleConfiguration.shared.storage.getThreadId(recipient) - val intent = Intent( - context, - ConversationActivityV2::class.java + startActivity( + ConversationActivityV2.createIntent( + requireContext(), + address = recipient.address, + fromGroupThreadId = threadID + ) ) - intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) - intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1) - intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID) - startActivity(intent) dismiss() } } @@ -131,17 +130,21 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() { hideSoftKeyboard() nameTextViewContainer.visibility = View.VISIBLE nameEditTextContainer.visibility = View.INVISIBLE - var newNickName: String? = null + val newNickName: String? if (nicknameEditText.text.isNotEmpty() && nicknameEditText.text.trim().length != 0) { newNickName = nicknameEditText.text.toString() } else { newNickName = previousContactNickname } - val publicKey = recipient.address.toString() - val storage = MessagingModuleConfiguration.shared.storage - val contact = storage.getContactWithAccountID(publicKey) ?: Contact(publicKey) - contact.nickname = newNickName - storage.setContact(contact) - nameTextView.text = recipient.name + + if (AccountId.fromStringOrNull(recipient.address.address)?.prefix == IdPrefix.STANDARD) { + configFactory.withMutableUserConfigs { configs -> + configs.contacts.upsertContact(recipient.address.address) { + this.nickname = newNickName + } + } + } + + nameTextView.text = newNickName (parentFragment as? UserDetailsBottomSheetCallback) ?: (requireActivity() as? UserDetailsBottomSheetCallback)?.onNicknameSaved() diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt index 437751e66b..0da0df9f33 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.home.search -import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -11,8 +10,9 @@ import network.loki.messenger.R import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding import network.loki.messenger.databinding.ViewGlobalSearchResultBinding import network.loki.messenger.databinding.ViewGlobalSearchSubheaderBinding +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.utilities.GroupRecord -import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.recipients.BasicRecipient import org.session.libsignal.utilities.AccountId import org.thoughtcrime.securesms.search.model.MessageResult import org.thoughtcrime.securesms.ui.GetString @@ -165,8 +165,8 @@ class GlobalSearchAdapter( data class SavedMessages(val currentUserPublicKey: String): Model // Note: "Note to Self" counts as SavedMessages rather than a Contact where `isSelf` is true. data class Contact(val contact: AccountId, val name: String, val isSelf: Boolean) : Model { - constructor(contact: org.session.libsession.messaging.contacts.Contact, isSelf: Boolean): - this(AccountId(contact.accountID), contact.getSearchName(), isSelf) + constructor(contact: BasicRecipient.Contact, isSelf: Boolean): + this(AccountId(contact.address.address), contact.searchName, isSelf) } data class GroupConversation( val isLegacy: Boolean, @@ -174,14 +174,16 @@ class GlobalSearchAdapter( val title: String, val legacyMembersString: String?, ) : Model { - constructor(context: Context, groupRecord: GroupRecord): + constructor(groupRecord: GroupRecord): this( isLegacy = groupRecord.isLegacyGroup, groupId = groupRecord.encodedId, title = groupRecord.title, legacyMembersString = if (groupRecord.isLegacyGroup) { - val recipients = groupRecord.members.map { Recipient.from(context, it, false) } - recipients.joinToString(transform = Recipient::getSearchName) + val recipients = groupRecord.members.map { + MessagingModuleConfiguration.shared.recipientRepository.getRecipientSyncOrEmpty(it) + } + recipients.joinToString(transform = { it.searchName }) } else { null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt index 2f4d0fcd91..fca51b3e14 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt @@ -7,8 +7,9 @@ import android.text.style.StyleSpan import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import network.loki.messenger.R -import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.utilities.Address +import org.session.libsession.utilities.recipients.BasicRecipient import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.truncateIdForDisplay import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView @@ -56,7 +57,7 @@ fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) { val textSpannable = SpannableStringBuilder() if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) { // group chat, bind - val text = "${model.messageResult.messageRecipient.getSearchName()}: " + val text = "${model.messageResult.messageRecipient.searchName}: " textSpannable.append(text) } textSpannable.append(getHighlight( @@ -65,7 +66,7 @@ fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) { )) binding.searchResultSubtitle.text = textSpannable binding.searchResultSubtitle.isVisible = true - binding.searchResultTitle.text = model.messageResult.conversationRecipient.getSearchName() + binding.searchResultTitle.text = model.messageResult.conversationRecipient.searchName } is GroupConversation -> { binding.searchResultTitle.text = getHighlight( @@ -89,7 +90,9 @@ fun ContentView.bindModel(query: String?, model: GroupConversation) { binding.searchResultProfilePicture.isVisible = true binding.searchResultSubtitle.isVisible = model.isLegacy binding.searchResultTimestamp.isVisible = false - val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupId), false) + val threadRecipient = MessagingModuleConfiguration.shared.recipientRepository.getRecipientSyncOrEmpty( + Address.fromSerialized(model.groupId) + ) binding.searchResultProfilePicture.update(threadRecipient) val nameString = model.title binding.searchResultTitle.text = getHighlight(query, nameString) @@ -104,7 +107,9 @@ fun ContentView.bindModel(query: String?, model: ContactModel) = binding.run { searchResultSubtitle.isVisible = false searchResultTimestamp.isVisible = false searchResultSubtitle.text = null - val recipient = Recipient.from(root.context, Address.fromSerialized(model.contact.hexString), false) + val recipient = MessagingModuleConfiguration.shared.recipientRepository.getRecipientSyncOrEmpty( + Address.fromSerialized(model.contact.hexString) + ) searchResultProfilePicture.update(recipient) val nameString = if (model.isSelf) root.context.getString(R.string.noteToSelf) else model.name @@ -131,7 +136,7 @@ fun ContentView.bindModel(query: String?, model: Message, dateUtils: DateUtils) val textSpannable = SpannableStringBuilder() if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) { // group chat, bind - val text = "${model.messageResult.messageRecipient.name}: " + val text = "${model.messageResult.messageRecipient.displayName}: " textSpannable.append(text) } textSpannable.append(getHighlight( @@ -140,17 +145,14 @@ fun ContentView.bindModel(query: String?, model: Message, dateUtils: DateUtils) )) searchResultSubtitle.text = textSpannable searchResultTitle.text = if (model.isSelf) root.context.getString(R.string.noteToSelf) - else model.messageResult.conversationRecipient.getSearchName() + else model.messageResult.conversationRecipient.searchName searchResultSubtitle.isVisible = true } -fun Recipient.getSearchName(): String = - name.takeIf { it.isNotEmpty() && !it.looksLikeAccountId } - ?: address.toString().let(::truncateIdForDisplay) +val BasicRecipient.searchName: String + get() = displayName.takeIf { it.isNotBlank() && !it.looksLikeAccountId } + ?: address.toString().let(::truncateIdForDisplay) -fun Contact.getSearchName(): String = - nickname?.takeIf { it.isNotEmpty() && !it.looksLikeAccountId } - ?: name?.takeIf { it.isNotEmpty() && !it.looksLikeAccountId } - ?: truncateIdForDisplay(accountID) +val Recipient.searchName: String get() = basic.searchName private val String.looksLikeAccountId: Boolean get() = length > 60 && all { it.isDigit() || it.isLetter() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt index c2c5f01a20..9a7fe259cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt @@ -1,13 +1,14 @@ package org.thoughtcrime.securesms.home.search -import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.GroupRecord +import org.session.libsession.utilities.recipients.BasicRecipient +import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.search.model.MessageResult import org.thoughtcrime.securesms.search.model.SearchResult data class GlobalSearchResult( val query: String, - val contacts: List = emptyList(), + val contacts: List = emptyList(), val threads: List = emptyList(), val messages: List = emptyList(), val showNoteToSelf: Boolean = false diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt index d986996501..4d2141f1bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt @@ -60,7 +60,7 @@ class GlobalSearchViewModel @Inject constructor( // without a nickname/name who haven't approved us. GlobalSearchResult( query, - searchRepository.queryContacts("05").toList() + searchRepository.queryContacts().toList() ) } } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchRepository.kt index 6d888389aa..106fabf286 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchRepository.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.keyboard.emoji.search import android.content.Context import android.net.Uri +import dagger.hilt.android.qualifiers.ApplicationContext import io.reactivex.Single import io.reactivex.schedulers.Schedulers import org.session.libsession.utilities.TextSecurePreferences @@ -13,6 +14,7 @@ import org.thoughtcrime.securesms.database.EmojiSearchDatabase import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.emoji.EmojiSource import java.util.function.Consumer +import javax.inject.Inject private const val MINIMUM_QUERY_THRESHOLD = 1 private const val MINIMUM_INLINE_QUERY_THRESHOLD = 2 @@ -20,9 +22,9 @@ private const val EMOJI_SEARCH_LIMIT = 20 private val NOT_PUNCTUATION = "[A-Za-z0-9 ]".toRegex() -class EmojiSearchRepository(private val context: Context) { - - private val emojiSearchDatabase: EmojiSearchDatabase = DatabaseComponent.get(context).emojiSearchDatabase() +class EmojiSearchRepository @Inject constructor( + private val emojiSearchDatabase: EmojiSearchDatabase +) { fun submitQuery(query: String, limit: Int = EMOJI_SEARCH_LIMIT): Single> { val result = if (query.length >= MINIMUM_INLINE_QUERY_THRESHOLD && NOT_PUNCTUATION.matches(query.substring(query.lastIndex))) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt index 3951c44763..b4de5ec712 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt @@ -21,6 +21,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn @@ -32,11 +34,11 @@ import org.session.libsession.messaging.messages.control.DataExtractionNotificat import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address -import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.MediaDatabase import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.mms.Slide @@ -45,7 +47,6 @@ import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.SaveAttachmentTask import org.thoughtcrime.securesms.util.asSequence -import org.thoughtcrime.securesms.util.observeChanges @HiltViewModel(assistedFactory = MediaOverviewViewModel.Factory::class) class MediaOverviewViewModel @AssistedInject constructor( @@ -53,22 +54,20 @@ class MediaOverviewViewModel @AssistedInject constructor( private val application: Application, private val threadDatabase: ThreadDatabase, private val mediaDatabase: MediaDatabase, - private val dateUtils: DateUtils + private val dateUtils: DateUtils, + private val recipientRepository: RecipientRepository, ) : AndroidViewModel(application) { private val timeBuckets by lazy { FixedTimeBuckets() } private val monthTimeBucketFormatter = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.getDefault()) - private val recipient: SharedFlow = application.contentResolver - .observeChanges(DatabaseContentProviders.Attachment.CONTENT_URI) - .onStart { emit(DatabaseContentProviders.Attachment.CONTENT_URI) } - .map { Recipient.from(application, address, false) } - .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) + private val recipient = recipientRepository.observeRecipient(address) val mediaListState: StateFlow = recipient - .map { recipient -> + .distinctUntilChanged() + .map { withContext(Dispatchers.Default) { - val threadId = threadDatabase.getOrCreateThreadIdFor(recipient) + val threadId = threadDatabase.getOrCreateThreadIdFor(address) val mediaItems = mediaDatabase.getGalleryMediaForThread(threadId) .use { cursor -> cursor.asSequence() @@ -92,10 +91,11 @@ class MediaOverviewViewModel @AssistedInject constructor( .stateIn(viewModelScope, SharingStarted.Eagerly, null) val conversationName: StateFlow = recipient + .filterNotNull() .map { recipient -> when { recipient.isLocalNumber -> application.getString(R.string.noteToSelf) - else -> recipient.name + else -> recipient.displayName } } .stateIn(viewModelScope, SharingStarted.Eagerly, "") diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java index f04b69e56d..aa9214fadb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java @@ -19,14 +19,12 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.WindowManager; import com.bumptech.glide.Glide; import com.squareup.phrase.Phrase; import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.Log; -import org.session.libsignal.utilities.guava.Optional; import org.thoughtcrime.securesms.util.ViewUtilitiesKt; import dagger.hilt.android.AndroidEntryPoint; @@ -46,12 +44,8 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo private GridLayoutManager layoutManager; public static @NonNull MediaPickerFolderFragment newInstance(@NonNull Recipient recipient) { - String name = Optional.fromNullable(recipient.getName()) - .or(Optional.fromNullable(recipient.getProfileName())) - .or(recipient.getName()); - Bundle args = new Bundle(); - args.putString(KEY_RECIPIENT_NAME, name); + args.putString(KEY_RECIPIENT_NAME, recipient.getDisplayName()); MediaPickerFolderFragment fragment = new MediaPickerFolderFragment(); fragment.setArguments(args); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt index 42d04ebae1..4fa027e673 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt @@ -23,6 +23,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.MediasendActivityBinding +import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.MediaTypes import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY @@ -31,6 +32,7 @@ import org.session.libsession.utilities.concurrent.SimpleTask import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.ScreenLockActionBarActivity +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.CountButtonState import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.scribbles.ImageEditorFragment @@ -53,6 +55,8 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme private lateinit var binding: MediasendActivityBinding + lateinit var recipientRepository: RecipientRepository + override val applyDefaultWindowInsets: Boolean get() = false // we want to handle window insets manually here for fullscreen fragments like the camera screen @@ -74,11 +78,9 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme // Apply windowInsets for our own UI (not the fragment ones because they will want to do their own things) binding.mediasendBottomBar.applySafeInsetsPaddings() - recipient = Recipient.from( - this, fromSerialized( - intent.getStringExtra(KEY_ADDRESS)!! - ), true - ) + recipient = recipientRepository.getRecipientSync(fromSerialized( + intent.getStringExtra(KEY_ADDRESS)!! + )) viewModel.onBodyChanged(intent.getStringExtra(KEY_BODY)!!) @@ -93,7 +95,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme } else if (!isEmpty(media)) { viewModel.onSelectedMediaChanged(this, media!!) - val fragment: Fragment = MediaSendFragment.newInstance(recipient!!) + val fragment: Fragment = MediaSendFragment.newInstance(recipient!!.address) supportFragmentManager.beginTransaction() .replace(R.id.mediasend_fragment_container, fragment, TAG_SEND) .commit() @@ -168,7 +170,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme override fun onMediaSelected(media: Media) { try { viewModel.onSingleMediaSelected(this, media) - navigateToMediaSend(recipient!!) + navigateToMediaSend(recipient!!.address) } catch (e: Exception){ Log.e(TAG, "Error selecting media", e) Toast.makeText(this, R.string.errorUnknown, Toast.LENGTH_LONG).show() @@ -263,7 +265,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme Log.i(TAG, "Camera capture stored: " + media.uri.toString()) viewModel.onImageCaptured(media) - navigateToMediaSend(recipient!!) + navigateToMediaSend(recipient!!.address) }) } @@ -282,7 +284,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme if (buttonState.count > 0) { binding.mediasendCountButton.setOnClickListener { v: View? -> navigateToMediaSend( - recipient!! + recipient!!.address ) } if (buttonState.isVisible) { @@ -330,7 +332,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme } } - private fun navigateToMediaSend(recipient: Recipient) { + private fun navigateToMediaSend(recipient: Address) { val fragment = MediaSendFragment.newInstance(recipient) var backstackTag: String? = null @@ -511,9 +513,9 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme * Get an intent to launch the media send flow starting with the picker. */ @JvmStatic - fun buildGalleryIntent(context: Context, recipient: Recipient, body: String): Intent { + fun buildGalleryIntent(context: Context, recipient: Address, body: String): Intent { val intent = Intent(context, MediaSendActivity::class.java) - intent.putExtra(KEY_ADDRESS, recipient.address.toString()) + intent.putExtra(KEY_ADDRESS, recipient.toString()) intent.putExtra(KEY_BODY, body) return intent } @@ -522,7 +524,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme * Get an intent to launch the media send flow starting with the camera. */ @JvmStatic - fun buildCameraIntent(context: Context, recipient: Recipient): Intent { + fun buildCameraIntent(context: Context, recipient: Address): Intent { val intent = buildGalleryIntent(context, recipient, "") intent.putExtra(KEY_IS_CAMERA, true) return intent @@ -535,7 +537,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme fun buildEditorIntent( context: Context, media: List, - recipient: Recipient, + recipient: Address, body: String ): Intent { val intent = buildGalleryIntent(context, recipient, body) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt index 76bb5ef406..538e6337a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt @@ -31,9 +31,9 @@ import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext import network.loki.messenger.R import network.loki.messenger.databinding.MediasendFragmentBinding +import org.session.libsession.utilities.Address import org.session.libsession.utilities.MediaTypes import org.session.libsession.utilities.TextSecurePreferences.Companion.isEnterSendsEnabled -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardHiddenListener import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener @@ -435,9 +435,9 @@ class MediaSendFragment : Fragment(), RailItemListener, private const val KEY_ADDRESS = "address" - fun newInstance(recipient: Recipient): MediaSendFragment { + fun newInstance(address: Address): MediaSendFragment { val args = Bundle() - args.putParcelable(KEY_ADDRESS, recipient.address) + args.putParcelable(KEY_ADDRESS, address) val fragment = MediaSendFragment() fragment.arguments = args diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt index e43aeabeca..fa1632b522 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt @@ -8,13 +8,11 @@ import android.widget.LinearLayout import androidx.recyclerview.widget.RecyclerView import network.loki.messenger.R import network.loki.messenger.databinding.ViewMessageRequestBinding -import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions import org.thoughtcrime.securesms.database.model.ThreadRecord -import com.bumptech.glide.RequestManager +import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.util.DateUtils import java.util.Locale -import javax.inject.Inject class MessageRequestView : LinearLayout { private lateinit var binding: ViewMessageRequestBinding @@ -64,7 +62,7 @@ class MessageRequestView : LinearLayout { return if (recipient.isLocalNumber) { context.getString(R.string.noteToSelf) } else { - recipient.name // Internally uses the Contact API + recipient.displayName // Internally uses the Contact API } } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt index 004c8a7050..3ecd05b523 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt @@ -1,25 +1,21 @@ package org.thoughtcrime.securesms.messagerequests -import android.content.Intent -import android.database.Cursor import android.os.Bundle -import android.view.ViewGroup.MarginLayoutParams import androidx.activity.viewModels import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import androidx.core.view.updatePadding -import androidx.loader.app.LoaderManager -import androidx.loader.content.Loader +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager import com.squareup.phrase.Phrase import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.ActivityMessageRequestsBinding import org.session.libsession.utilities.Address import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY -import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.ScreenLockActionBarActivity import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.database.ThreadDatabase @@ -28,9 +24,10 @@ import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.applySafeInsetsPaddings import org.thoughtcrime.securesms.util.push +import javax.inject.Inject @AndroidEntryPoint -class MessageRequestsActivity : ScreenLockActionBarActivity(), ConversationClickListener, LoaderManager.LoaderCallbacks { +class MessageRequestsActivity : ScreenLockActionBarActivity(), ConversationClickListener { private lateinit var binding: ActivityMessageRequestsBinding private lateinit var glide: RequestManager @@ -41,7 +38,7 @@ class MessageRequestsActivity : ScreenLockActionBarActivity(), ConversationClick private val viewModel: MessageRequestsViewModel by viewModels() private val adapter: MessageRequestsAdapter by lazy { - MessageRequestsAdapter(context = this, cursor = null, dateUtils = dateUtils, listener = this) + MessageRequestsAdapter(dateUtils = dateUtils, listener = this) } override val applyDefaultWindowInsets: Boolean @@ -55,7 +52,6 @@ class MessageRequestsActivity : ScreenLockActionBarActivity(), ConversationClick glide = Glide.with(this) adapter.setHasStableIds(true) - adapter.glide = glide binding.recyclerView.adapter = adapter binding.clearAllMessageRequestsButton.setOnClickListener { deleteAll() } @@ -63,45 +59,34 @@ class MessageRequestsActivity : ScreenLockActionBarActivity(), ConversationClick binding.root.applySafeInsetsPaddings( applyBottom = false, ) - } - override fun onResume() { - super.onResume() - LoaderManager.getInstance(this).restartLoader(0, null, this) - } - - override fun onCreateLoader(id: Int, bundle: Bundle?): Loader { - return MessageRequestsLoader(this@MessageRequestsActivity) - } - - override fun onLoadFinished(loader: Loader, cursor: Cursor?) { - adapter.changeCursor(cursor) - updateEmptyState() - } - - override fun onLoaderReset(cursor: Loader) { - adapter.changeCursor(null) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.threads + .collectLatest { + adapter.conversations = it + updateEmptyState() + } + } + } } override fun onConversationClick(thread: ThreadRecord) { - val intent = Intent(this, ConversationActivityV2::class.java) - intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId) - push(intent) + push(ConversationActivityV2.createIntent(this, thread.recipient.address)) } override fun onBlockConversationClick(thread: ThreadRecord) { fun doBlock() { val recipient = thread.invitingAdminId?.let { - Recipient.from(this, Address.fromSerialized(it), false) - } ?: thread.recipient + Address.fromSerialized(it) + } ?: thread.recipient.address viewModel.blockMessageRequest(thread, recipient) - LoaderManager.getInstance(this).restartLoader(0, null, this) } showSessionDialog { title(R.string.block) text(Phrase.from(context, R.string.blockDescription) - .put(NAME_KEY, thread.recipient.name) + .put(NAME_KEY, thread.recipient.displayName) .format()) dangerButton(R.string.block, R.string.AccessibilityId_blockConfirm) { doBlock() @@ -113,7 +98,6 @@ class MessageRequestsActivity : ScreenLockActionBarActivity(), ConversationClick override fun onDeleteConversationClick(thread: ThreadRecord) { fun doDecline() { viewModel.deleteMessageRequest(thread) - LoaderManager.getInstance(this).restartLoader(0, null, this) } showSessionDialog { @@ -133,7 +117,6 @@ class MessageRequestsActivity : ScreenLockActionBarActivity(), ConversationClick private fun deleteAll() { fun doDeleteAllAndBlock() { viewModel.clearAllMessageRequests(false) - LoaderManager.getInstance(this).restartLoader(0, null, this) } showSessionDialog { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt index 409dfa43e8..fa5c484c30 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt @@ -1,7 +1,5 @@ package org.thoughtcrime.securesms.messagerequests -import android.content.Context -import android.database.Cursor import android.os.Build import android.text.SpannableString import android.text.style.ForegroundColorSpan @@ -9,28 +7,30 @@ import android.view.ContextThemeWrapper import android.view.ViewGroup import android.widget.PopupMenu import androidx.core.graphics.drawable.DrawableCompat +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.RequestManager import network.loki.messenger.R import org.session.libsession.utilities.ThemeUtil -import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter import org.thoughtcrime.securesms.database.model.ThreadRecord -import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import com.bumptech.glide.RequestManager import org.thoughtcrime.securesms.util.DateUtils class MessageRequestsAdapter( - context: Context, - cursor: Cursor?, val dateUtils: DateUtils, val listener: ConversationClickListener -) : CursorRecyclerViewAdapter(context, cursor) { - private val threadDatabase = DatabaseComponent.get(context).threadDatabase() - lateinit var glide: RequestManager +) : RecyclerView.Adapter() { + var conversations: List = emptyList() + set(value) { + if (field != value) { + field = value + notifyDataSetChanged() + } + } class ViewHolder(val view: MessageRequestView) : RecyclerView.ViewHolder(view) - override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = MessageRequestView(context) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = MessageRequestView(parent.context) view.setOnClickListener { view.thread?.let { listener.onConversationClick(it) } } view.setOnLongClickListener { view.thread?.let { thread -> @@ -44,18 +44,21 @@ class MessageRequestsAdapter( return ViewHolder(view) } - override fun onBindItemViewHolder(viewHolder: ViewHolder, cursor: Cursor) { - val thread = getThread(cursor)!! - viewHolder.view.bind(thread, dateUtils) + override fun onBindViewHolder( + holder: ViewHolder, + position: Int + ) { + holder.view.bind(conversations[position], dateUtils) } - override fun onItemViewRecycled(holder: ViewHolder?) { - super.onItemViewRecycled(holder) - holder?.view?.recycle() + override fun getItemCount(): Int = conversations.size + + override fun onViewRecycled(holder: ViewHolder) { + holder.view.recycle() } private fun showPopupMenu(view: MessageRequestView, legacyOrCommunityGroup: Boolean, invitingAdmin: String?) { - val popupMenu = PopupMenu(ContextThemeWrapper(context, R.style.PopupMenu_MessageRequests), view) + val popupMenu = PopupMenu(ContextThemeWrapper(view.context, R.style.PopupMenu_MessageRequests), view) // still show the block option if we have an inviting admin for the group if ((legacyOrCommunityGroup && invitingAdmin == null) || view.thread!!.recipient.isCommunityInboxRecipient) { popupMenu.menuInflater.inflate(R.menu.menu_group_request, popupMenu.menu) @@ -73,7 +76,7 @@ class MessageRequestsAdapter( for (i in 0 until popupMenu.menu.size()) { val item = popupMenu.menu.getItem(i) val s = SpannableString(item.title) - val danger = ThemeUtil.getThemedColor(context, R.attr.danger) + val danger = ThemeUtil.getThemedColor(view.context, R.attr.danger) s.setSpan(ForegroundColorSpan(danger), 0, s.length, 0) item.icon?.let { DrawableCompat.setTint( @@ -88,10 +91,6 @@ class MessageRequestsAdapter( } popupMenu.show() } - - private fun getThread(cursor: Cursor): ThreadRecord? { - return threadDatabase.readerFor(cursor).current - } } interface ConversationClickListener { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsLoader.kt deleted file mode 100644 index 16d83be6db..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsLoader.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.thoughtcrime.securesms.messagerequests - -import android.content.Context -import android.database.Cursor -import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.util.AbstractCursorLoader - -class MessageRequestsLoader(context: Context) : AbstractCursorLoader(context) { - - override fun getCursor(): Cursor { - return DatabaseComponent.get(context).threadDatabase().unapprovedConversationList - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt index 720ac2b31c..5a98b54e45 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt @@ -3,29 +3,59 @@ package org.thoughtcrime.securesms.messagerequests import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.session.libsession.utilities.recipients.Recipient +import kotlinx.coroutines.withContext +import org.session.libsession.utilities.Address +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.repository.ConversationRepository import javax.inject.Inject @HiltViewModel class MessageRequestsViewModel @Inject constructor( - private val repository: ConversationRepository + private val repository: ConversationRepository, + private val threadDatabase: ThreadDatabase ) : ViewModel() { + private val reloadTrigger = MutableSharedFlow(extraBufferCapacity = 1) + + val threads: StateFlow> = reloadTrigger + .onStart { emit(Unit) } + .map { + withContext(Dispatchers.Default) { + threadDatabase.unapprovedConversationList + .apply { sortWith(COMPARATOR) } + } + } + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + // We assume thread.recipient is a contact or thread.invitingAdmin is not null - fun blockMessageRequest(thread: ThreadRecord, blockRecipient: Recipient) = viewModelScope.launch { + fun blockMessageRequest(thread: ThreadRecord, blockRecipient: Address) = viewModelScope.launch { repository.setBlocked(blockRecipient, true) deleteMessageRequest(thread) + reloadTrigger.emit(Unit) } fun deleteMessageRequest(thread: ThreadRecord) = viewModelScope.launch { repository.deleteMessageRequest(thread) + reloadTrigger.emit(Unit) } fun clearAllMessageRequests(block: Boolean) = viewModelScope.launch { repository.clearAllMessageRequests(block) + reloadTrigger.emit(Unit) } + companion object { + private val COMPARATOR get() = compareByDescending { it.lastMessage?.timestamp ?: 0 } + .thenByDescending { it.date } + .thenBy { it.recipient.displayName } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java index 0a24c26fad..28bfa97c86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java @@ -22,15 +22,15 @@ import com.bumptech.glide.load.resource.gif.StreamGifDecoder; import com.bumptech.glide.module.AppGlideModule; -import org.session.libsession.avatars.ContactPhoto; import org.session.libsession.avatars.PlaceholderAvatarPhoto; +import org.session.libsession.utilities.recipients.RemoteFile; import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl; import org.thoughtcrime.securesms.glide.ChunkedImageUrlLoader; -import org.thoughtcrime.securesms.glide.ContactPhotoLoader; import org.thoughtcrime.securesms.glide.OkHttpUrlLoader; import org.thoughtcrime.securesms.glide.PlaceholderAvatarLoader; +import org.thoughtcrime.securesms.glide.RemoteFileLoader; import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapCacheDecoder; import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder; import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder; @@ -69,7 +69,7 @@ public void registerComponents(@NonNull Context context, @NonNull Glide glide, @ registry.prepend(Bitmap.class, new EncryptedBitmapResourceEncoder(secret)); registry.prepend(GifDrawable.class, new EncryptedGifDrawableResourceEncoder(secret)); - registry.append(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context)); + registry.append(RemoteFile.class, InputStream.class, new RemoteFileLoader.Factory(context)); registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context)); registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory()); registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java index b96159aab3..9fb99daf1d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java @@ -15,7 +15,6 @@ import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.recipients.Recipient.VibrateState; import network.loki.messenger.R; @@ -42,25 +41,19 @@ public AbstractNotificationBuilder(Context context, NotificationPrivacyPreferenc protected CharSequence getStyledMessage(@NonNull Recipient recipient, @Nullable CharSequence message) { SpannableStringBuilder builder = new SpannableStringBuilder(); - builder.append(Util.getBoldedString(recipient.getName())); + builder.append(Util.getBoldedString(recipient.getDisplayName())); builder.append(": "); builder.append(message == null ? "" : message); return builder; } - public void setAlarms(@Nullable Uri ringtone, VibrateState vibrate) { + public void setAlarms(@Nullable Uri ringtone) { Uri defaultRingtone = NotificationChannels.getMessageRingtone(context); boolean defaultVibrate = NotificationChannels.getMessageVibrate(context); if (ringtone == null && !TextUtils.isEmpty(defaultRingtone.toString())) setSound(defaultRingtone); else if (ringtone != null && !ringtone.toString().isEmpty()) setSound(ringtone); - - if (vibrate == VibrateState.ENABLED || - (vibrate == VibrateState.DEFAULT && defaultVibrate)) - { - setDefaults(Notification.DEFAULT_VIBRATE); - } } private void setLed() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java index b7051357e1..ff05598929 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java @@ -33,7 +33,6 @@ import org.session.libsession.messaging.sending_receiving.MessageSender; import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.database.MarkedMessageInfo; @@ -69,7 +68,6 @@ public void onReceive(final Context context, Intent intent) final Address address = intent.getParcelableExtra(ADDRESS_EXTRA); final long threadId = intent.getLongExtra(THREAD_ID_EXTRA, -1); final CharSequence responseText = getMessageText(intent); - final Recipient recipient = Recipient.from(context, address, false); if (responseText != null) { new AsyncTask() { @@ -79,7 +77,7 @@ protected Void doInBackground(Void... params) { long replyThreadId; if (threadId == -1) { - replyThreadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient); + replyThreadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(address); } else { replyThreadId = threadId; } @@ -87,15 +85,14 @@ protected Void doInBackground(Void... params) { VisibleMessage message = new VisibleMessage(); message.setText(responseText.toString()); message.setSentTimestamp(SnodeAPI.getNowWithOffset()); - MessageSender.send(message, recipient.getAddress()); - ExpirationConfiguration config = DatabaseComponent.get(context).storage().getExpirationConfiguration(threadId); - ExpiryMode expiryMode = config == null ? null : config.getExpiryMode(); - long expiresInMillis = expiryMode == null ? 0 : expiryMode.getExpiryMillis(); + MessageSender.send(message, address); + ExpiryMode expiryMode = DatabaseComponent.get(context).storage().getExpirationConfiguration(threadId); + long expiresInMillis = expiryMode.getExpiryMillis(); long expireStartedAt = expiryMode instanceof ExpiryMode.AfterSend ? message.getSentTimestamp() : 0L; - if (recipient.isGroupOrCommunityRecipient()) { + if (address.isGroupOrCommunity()) { Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message"); - OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null, expiresInMillis, 0); + OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, address, Collections.emptyList(), null, null, expiresInMillis, 0); try { DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, replyThreadId, false, true); } catch (MmsException e) { @@ -103,7 +100,7 @@ protected Void doInBackground(Void... params) { } } else { Log.w("AndroidAutoReplyReceiver", "Sending regular message "); - OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient, expiresInMillis, expireStartedAt); + OutgoingTextMessage reply = OutgoingTextMessage.from(message, address, expiresInMillis, expireStartedAt); DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(replyThreadId, reply, false, SnodeAPI.getNowWithOffset(), true); } 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 9708714657..d64215bc3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt @@ -32,11 +32,6 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.annimon.stream.Stream import com.squareup.phrase.Phrase -import java.util.concurrent.Executor -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.concurrent.Volatile import me.leolin.shortcutbadger.ShortcutBadger import network.loki.messenger.R import network.loki.messenger.libsession_util.util.BlindKeyAPI @@ -65,19 +60,28 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns.NOTIFIED import org.thoughtcrime.securesms.database.MmsSmsColumns.READ import org.thoughtcrime.securesms.database.MmsSmsDatabase.MMS_TRANSPORT import org.thoughtcrime.securesms.database.MmsSmsDatabase.TRANSPORT +import org.thoughtcrime.securesms.database.LokiThreadDatabase +import org.thoughtcrime.securesms.database.MmsSmsDatabase import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.RecipientRepository +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.SmsDatabase 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.ReactionRecord -import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.util.AvatarUtils -import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.Companion.WEBRTC_NOTIFICATION import org.thoughtcrime.securesms.util.SessionMetaProtocol.canUserReplyToNotification import org.thoughtcrime.securesms.util.SpanUtil +import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.Companion.WEBRTC_NOTIFICATION +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject +import kotlin.concurrent.Volatile /** * Handles posting system notifications for new messages. @@ -86,8 +90,13 @@ import org.thoughtcrime.securesms.util.SpanUtil * @author Moxie Marlinspike */ private const val CONTENT_SIGNATURE = "content_signature" -class DefaultMessageNotifier( - val avatarUtils: AvatarUtils + +class DefaultMessageNotifier @Inject constructor( + val avatarUtils: AvatarUtils, + private val threadDatabase: ThreadDatabase, + private val recipientRepository: RecipientRepository, + private val mmsSmsDatabase: MmsSmsDatabase, + private val lokiThreadDatabase: LokiThreadDatabase, ) : MessageNotifier { override fun setVisibleThread(threadId: Long) { visibleThread = threadId @@ -101,10 +110,6 @@ class DefaultMessageNotifier( lastDesktopActivityTimestamp = timestamp } - override fun notifyMessageDeliveryFailed(context: Context?, recipient: Recipient?, threadId: Long) { - // We do not provide notifications for message delivery failure. - } - override fun cancelDelayedNotifications() { executor.cancel() } @@ -179,17 +184,16 @@ class DefaultMessageNotifier( override fun updateNotification(context: Context, threadId: Long, signal: Boolean) { val isVisible = visibleThread == threadId - val threads = get(context).threadDatabase() - val recipient = threads.getRecipientForThreadId(threadId) + val recipient = threadDatabase.getRecipientForThreadId(threadId)?.let(recipientRepository::getRecipientSync) - if (recipient != null && !recipient.isGroupOrCommunityRecipient && threads.getMessageCount(threadId) == 1 && - !(recipient.isApproved || threads.getLastSeenAndHasSent(threadId).second()) + if (recipient != null && !recipient.isGroupOrCommunityRecipient && threadDatabase.getMessageCount(threadId) == 1 && + !(recipient.approved || threadDatabase.getLastSeenAndHasSent(threadId).second()) ) { removeHasHiddenMessageRequests(context) } if (!isNotificationsEnabled(context) || - (recipient != null && recipient.isMuted) + (recipient != null && recipient.isMuted()) ) { return } @@ -214,7 +218,7 @@ class DefaultMessageNotifier( var telcoCursor: Cursor? = null try { - telcoCursor = get(context).mmsSmsDatabase().unreadOrUnseenReactions // TODO: add a notification specific lighter query here + telcoCursor = mmsSmsDatabase.unreadOrUnseenReactions // TODO: add a notification specific lighter query here if ((telcoCursor == null || telcoCursor.isAfterLast) || getLocalNumber(context) == null) { updateBadge(context, 0) @@ -359,7 +363,7 @@ class DefaultMessageNotifier( } if (signal) { - builder.setAlarms(notificationState.getRingtone(context), notificationState.vibrate) + builder.setAlarms(notificationState.getRingtone(context)) builder.setTicker( notifications[0].individualRecipient, notifications[0].text @@ -418,7 +422,7 @@ class DefaultMessageNotifier( builder.putStringExtra(CONTENT_SIGNATURE, contentSignature) builder.setMessageCount(notificationState.notificationCount, notificationState.threadCount) - builder.setMostRecentSender(notifications[0].individualRecipient, notifications[0].recipient) + builder.setMostRecentSender(notifications[0].individualRecipient) builder.setGroup(NOTIFICATION_GROUP) builder.setDeleteIntent(notificationState.getDeleteIntent(context)) builder.setOnlyAlertOnce(!signal) @@ -443,8 +447,7 @@ class DefaultMessageNotifier( while (iterator.hasPrevious()) { val item = iterator.previous() builder.addMessageBody( - item.individualRecipient, item.recipient, - highlightMentions( + item.individualRecipient, highlightMentions( (if (item.text != null) item.text else "")!!, false, false, @@ -456,7 +459,7 @@ class DefaultMessageNotifier( } if (signal) { - builder.setAlarms(notificationState.getRingtone(context), notificationState.vibrate) + builder.setAlarms(notificationState.getRingtone(context)) val text = notifications[0].text builder.setTicker( notifications[0].individualRecipient, @@ -492,13 +495,12 @@ class DefaultMessageNotifier( private fun constructNotificationState(context: Context, cursor: Cursor): NotificationState { val notificationState = NotificationState() - val reader = get(context).mmsSmsDatabase().readerFor(cursor) + val reader = mmsSmsDatabase.readerFor(cursor) if (reader == null) { Log.e(TAG, "No reader for cursor - aborting constructNotificationState") return NotificationState() } - val threadDatabase = get(context).threadDatabase() val cache: MutableMap = HashMap() var record: MessageRecord? = null @@ -508,19 +510,19 @@ class DefaultMessageNotifier( val threadId = record.threadId val threadRecipients = if (threadId != -1L) { - threadDatabase.getRecipientForThreadId(threadId) + threadDatabase.getRecipientForThreadId(threadId)?.let(recipientRepository::getRecipientSync) } else null // Start by checking various scenario that we should skip // Skip if muted or calls - if (threadRecipients?.isMuted == true) continue + if (threadRecipients?.isMuted() == true) continue if (record.isIncomingCall || record.isOutgoingCall) continue // Handle message requests early val isMessageRequest = threadRecipients != null && !threadRecipients.isGroupOrCommunityRecipient && - !threadRecipients.isApproved && + !threadRecipients.approved && !threadDatabase.getLastSeenAndHasSent(threadId).second() if (isMessageRequest && (threadDatabase.getMessageCount(threadId) > 1 || !hasHiddenMessageRequests(context))) { @@ -638,7 +640,7 @@ class DefaultMessageNotifier( val latestReaction = reactionsFromOthers.maxByOrNull { it.dateSent } if (latestReaction != null) { - val reactor = Recipient.from(context, fromSerialized(latestReaction.author), false) + val reactor = recipientRepository.getRecipientSyncOrEmpty(fromSerialized(latestReaction.author)) val emoji = Phrase.from(context, R.string.emojiReactsNotification) .put(EMOJI_KEY, latestReaction.emoji).format().toString() @@ -679,7 +681,6 @@ class DefaultMessageNotifier( } private fun generateBlindedId(threadId: Long, context: Context): String? { - val lokiThreadDatabase = get(context).lokiThreadDatabase() val openGroup = lokiThreadDatabase.getOpenGroupChat(threadId) val edKeyPair = getUserED25519KeyPair(context) if (openGroup != null && edKeyPair != null) { 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 16df8fa2db..71ad8bef6f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt @@ -10,6 +10,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.session.libsession.database.userAuth +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration.Companion.shared import org.session.libsession.messaging.messages.control.ReadReceipt import org.session.libsession.messaging.sending_receiving.MessageSender.send @@ -19,14 +20,14 @@ import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled import org.session.libsession.utilities.associateByNotNull -import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.recipients.BasicRecipient +import org.session.libsession.utilities.recipients.isGroupOrCommunityRecipient import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType import org.thoughtcrime.securesms.database.ExpirationInfo import org.thoughtcrime.securesms.database.MarkedMessageInfo import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.util.SessionMetaProtocol.shouldSendReadReceipt @AndroidEntryPoint class MarkReadReceiver : BroadcastReceiver() { @@ -73,7 +74,7 @@ class MarkReadReceiver : BroadcastReceiver() { .asSequence() .filter { it.expiryType == ExpiryType.AFTER_READ } .filter { mmsSmsDatabase.getMessageById(it.expirationInfo.id)?.run { - isExpirationTimerUpdate && threadDb.getRecipientForThreadId(threadId)?.isGroupOrCommunityRecipient == true } == false + isExpirationTimerUpdate && threadDb.getRecipientForThreadId(threadId)?.isGroupOrCommunity == true } == false } .forEach { messageExpirationManager.startExpiringNow(it.expirationInfo.id) } @@ -118,14 +119,23 @@ class MarkReadReceiver : BroadcastReceiver() { } } + private val BasicRecipient.shouldSendReadReceipt: Boolean + get() = when (this) { + is BasicRecipient.Contact -> approved && !blocked + is BasicRecipient.Generic -> !isGroupOrCommunityRecipient && !blocked + else -> false + } + private fun sendReadReceipts( context: Context, markedReadMessages: List ) { if (!isReadReceiptsEnabled(context)) return + val recipientRepository = MessagingModuleConfiguration.shared.recipientRepository + markedReadMessages.map { it.syncMessageId } - .filter { shouldSendReadReceipt(Recipient.from(context, it.address, false)) } + .filter { recipientRepository.getBasicRecipientFast(it.address)?.shouldSendReadReceipt == true } .groupBy { it.address } .forEach { (address, messages) -> messages.map { it.timetamp } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.kt index e9da267dcd..726f19f726 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.kt @@ -8,15 +8,12 @@ import android.text.SpannableStringBuilder import androidx.core.app.NotificationCompat import com.squareup.phrase.Phrase import network.loki.messenger.R -import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.NotificationPrivacyPreference import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_COUNT_KEY import org.session.libsession.utilities.StringSubstitutionConstants.MESSAGE_COUNT_KEY import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY import org.session.libsession.utilities.Util.getBoldedString import org.session.libsession.utilities.recipients.Recipient -import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get import org.thoughtcrime.securesms.home.HomeActivity import org.thoughtcrime.securesms.ui.getSubbedString import java.util.LinkedList @@ -39,21 +36,15 @@ class MultipleRecipientNotificationBuilder(context: Context, privacy: Notificati setNumber(messageCount) } - fun setMostRecentSender(recipient: Recipient, threadRecipient: Recipient) { - var displayName = recipient.name - if (threadRecipient.isGroupOrCommunityRecipient) { - displayName = getGroupDisplayName(recipient, threadRecipient.isCommunityRecipient) - } + fun setMostRecentSender(recipient: Recipient) { if (privacy.isDisplayContact) { val txt = Phrase.from(context, R.string.notificationsMostRecent) - .put(NAME_KEY, displayName) + .put(NAME_KEY, recipient.displayName) .format().toString() setContentText(txt) } - if (recipient.notificationChannel != null) { - setChannelId(recipient.notificationChannel!!) - } + recipient.notificationChannel?.let(this::setChannelId) } fun addActions(markAsReadIntent: PendingIntent?) { @@ -68,24 +59,15 @@ class MultipleRecipientNotificationBuilder(context: Context, privacy: Notificati fun putStringExtra(key: String?, value: String?) { extras.putString(key, value) } - fun addMessageBody(sender: Recipient, threadRecipient: Recipient, body: CharSequence?) { - var displayName = sender.name - if (threadRecipient.isGroupOrCommunityRecipient) { - displayName = getGroupDisplayName(sender, threadRecipient.isCommunityRecipient) - } + fun addMessageBody(sender: Recipient, body: CharSequence?) { if (privacy.isDisplayMessage) { val builder = SpannableStringBuilder() - builder.append(getBoldedString(displayName)) + builder.append(getBoldedString(sender.displayName)) builder.append(": ") builder.append(body ?: "") messageBodies.add(builder) } else if (privacy.isDisplayContact) { - messageBodies.add(getBoldedString(displayName)) - } - - // TODO: What on earth is this? Why is it commented out? It's also commented out in dev... remove? -ACL 2024-08-29 - if (privacy.isDisplayContact && sender.contactUri != null) { -// addPerson(sender.getContactUri().toString()); + messageBodies.add(getBoldedString(sender.displayName)) } } @@ -97,15 +79,4 @@ class MultipleRecipientNotificationBuilder(context: Context, privacy: Notificati } return super.build() } - - /** - * @param recipient the * individual * recipient for which to get the display name. - * @param openGroupRecipient whether in an open group context - */ - private fun getGroupDisplayName(recipient: Recipient, openGroupRecipient: Boolean): String { - return MessagingModuleConfiguration.shared.usernameUtils.getContactNameWithAccountID( - accountID = recipient.address.toString(), - contactContext = if (openGroupRecipient) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR - ) - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java index 8880bdc617..0c8b6cbd57 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java @@ -6,30 +6,18 @@ import android.content.Context; import android.media.AudioAttributes; import android.net.Uri; -import android.os.AsyncTask; import android.provider.Settings; -import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; - -import com.annimon.stream.Collectors; -import com.annimon.stream.Stream; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.ServiceUtil; import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.dependencies.DatabaseComponent; -import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; import network.loki.messenger.BuildConfig; import network.loki.messenger.R; @@ -68,10 +56,6 @@ public static synchronized void create(@NonNull Context context) { } onCreate(context, notificationManager); - - AsyncTask.SERIAL_EXECUTOR.execute(() -> { - ensureCustomChannelConsistency(context); - }); } /** @@ -81,23 +65,6 @@ public static synchronized void create(@NonNull Context context) { return getMessagesChannelId(TextSecurePreferences.getNotificationMessagesChannelVersion(context)); } - /** - * @return A name suitable to be displayed as the notification channel title. - */ - public static @NonNull String getChannelDisplayNameFor(@NonNull Context context, @Nullable String systemName, @Nullable String profileName, @NonNull Address address) { - if (!TextUtils.isEmpty(systemName)) { - return systemName; - } else if (!TextUtils.isEmpty(profileName)) { - return profileName; - } else if (!TextUtils.isEmpty(address.toString())) { - return address.toString(); - } else { - return context.getString(R.string.unknown); - } - } - - - /** * @return The message ringtone set for the default message channel. @@ -112,7 +79,7 @@ public static synchronized void create(@NonNull Context context) { NotificationChannel channel = notificationManager.getNotificationChannel(recipient.getNotificationChannel()); if (!channelExists(channel)) { - Log.w(TAG, "Recipient had no channel. Returning null."); + Log.w(TAG, "RecipientV2 had no channel. Returning null."); return null; } @@ -147,36 +114,6 @@ public static synchronized void updateMessageVibrate(@NonNull Context context, b updateMessageChannel(context, channel -> channel.enableVibration(enabled)); } - @WorkerThread - public static synchronized void ensureCustomChannelConsistency(@NonNull Context context) { - NotificationManager notificationManager = ServiceUtil.getNotificationManager(context); - RecipientDatabase db = DatabaseComponent.get(context).recipientDatabase(); - List customRecipients = new ArrayList<>(); - Set customChannelIds = new HashSet<>(); - Set existingChannelIds = Stream.of(notificationManager.getNotificationChannels()).map(NotificationChannel::getId).collect(Collectors.toSet()); - - try (RecipientDatabase.RecipientReader reader = db.getRecipientsWithNotificationChannels()) { - Recipient recipient; - while ((recipient = reader.getNext()) != null) { - customRecipients.add(recipient); - customChannelIds.add(recipient.getNotificationChannel()); - } - } - - for (NotificationChannel existingChannel : notificationManager.getNotificationChannels()) { - if (existingChannel.getId().startsWith(CONTACT_PREFIX) && !customChannelIds.contains(existingChannel.getId())) { - notificationManager.deleteNotificationChannel(existingChannel.getId()); - } else if (existingChannel.getId().startsWith(MESSAGES_PREFIX) && !existingChannel.getId().equals(getMessagesChannel(context))) { - notificationManager.deleteNotificationChannel(existingChannel.getId()); - } - } - - for (Recipient customRecipient : customRecipients) { - if (!existingChannelIds.contains(customRecipient.getNotificationChannel())) { - db.setNotificationChannel(customRecipient, null); - } - } - } private static void onCreate(@NonNull Context context, @NonNull NotificationManager notificationManager) { NotificationChannelGroup messagesGroup = new NotificationChannelGroup(CATEGORY_MESSAGES, context.getResources().getString(R.string.messages)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java index 0d57751171..f2712499a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java @@ -18,18 +18,18 @@ public class NotificationItem { private final long id; private final boolean mms; - private final @NonNull Recipient conversationRecipient; - private final @NonNull Recipient individualRecipient; - private final @Nullable Recipient threadRecipient; + private final @NonNull Recipient conversationRecipient; + private final @NonNull Recipient individualRecipient; + private final @Nullable Recipient threadRecipient; private final long threadId; private final @Nullable CharSequence text; private final long timestamp; private final @Nullable SlideDeck slideDeck; public NotificationItem(long id, boolean mms, - @NonNull Recipient individualRecipient, - @NonNull Recipient conversationRecipient, - @Nullable Recipient threadRecipient, + @NonNull Recipient individualRecipient, + @NonNull Recipient conversationRecipient, + @Nullable Recipient threadRecipient, long threadId, @Nullable CharSequence text, long timestamp, @Nullable SlideDeck slideDeck) { @@ -44,11 +44,11 @@ public NotificationItem(long id, boolean mms, this.slideDeck = slideDeck; } - public @NonNull Recipient getRecipient() { + public @NonNull Recipient getRecipient() { return threadRecipient == null ? conversationRecipient : threadRecipient; } - public @NonNull Recipient getIndividualRecipient() { + public @NonNull Recipient getIndividualRecipient() { return individualRecipient; } @@ -69,11 +69,9 @@ public long getThreadId() { } public PendingIntent getPendingIntent(Context context) { - Intent intent = new Intent(context, ConversationActivityV2.class); - Recipient notifyRecipients = threadRecipient != null ? threadRecipient : conversationRecipient; - if (notifyRecipients != null) intent.putExtra(ConversationActivityV2.ADDRESS, notifyRecipients.getAddress()); - intent.putExtra(ConversationActivityV2.THREAD_ID, threadId); + Recipient notifyRecipients = threadRecipient != null ? threadRecipient : conversationRecipient; + final Intent intent = ConversationActivityV2.Companion.createIntent(context, notifyRecipients.getAddress()); intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java index aa704daa97..7a52d11d04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java @@ -11,7 +11,7 @@ import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; -import org.session.libsession.utilities.recipients.Recipient.VibrateState; + import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; @@ -58,14 +58,6 @@ public void addNotification(@NonNull NotificationItem item) { return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); } - public VibrateState getVibrate() { - if (!notifications.isEmpty()) { - Recipient recipient = notifications.getFirst().getRecipient(); - return recipient.resolve().getMessageVibrate(); - } - return VibrateState.DEFAULT; - } - public boolean hasMultipleThreads() { return threads.size() > 1; } public LinkedHashSet getThreads() { return threads; } public int getThreadCount() { return threads.size(); } @@ -169,9 +161,7 @@ public PendingIntent getAndroidAutoHeardIntent(Context context, int notification public PendingIntent getQuickReplyIntent(Context context, Recipient recipient) { if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications! " + threads.size()); - Intent intent = new Intent(context, ConversationActivityV2.class); - intent.putExtra(ConversationActivityV2.ADDRESS, recipient.getAddress()); - intent.putExtra(ConversationActivityV2.THREAD_ID, (long)threads.toArray()[0]); + final Intent intent = ConversationActivityV2.Companion.createIntent(context, recipient.getAddress()); intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java index cdd87d155c..532d78451d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java @@ -10,7 +10,6 @@ import org.session.libsession.messaging.sending_receiving.pollers.Poller; import org.session.libsession.messaging.sending_receiving.pollers.PollerManager; import org.session.libsession.utilities.Debouncer; -import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.ThreadUtils; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.util.AvatarUtils; @@ -32,8 +31,11 @@ public class OptimizedMessageNotifier implements MessageNotifier { private final PollerManager pollerManager; @Inject - public OptimizedMessageNotifier(AvatarUtils avatarUtils, OpenGroupPollerManager openGroupPollerManager, PollerManager pollerManager) { - this.wrapped = new DefaultMessageNotifier(avatarUtils); + public OptimizedMessageNotifier(AvatarUtils avatarUtils, + OpenGroupPollerManager openGroupPollerManager, + PollerManager pollerManager, + DefaultMessageNotifier defaultMessageNotifier) { + this.wrapped = defaultMessageNotifier; this.openGroupPollerManager = openGroupPollerManager; this.debouncer = new Debouncer(TimeUnit.SECONDS.toMillis(2)); this.pollerManager = pollerManager; @@ -50,10 +52,6 @@ public void setHomeScreenVisible(boolean isVisible) { @Override public void setLastDesktopActivityTimestamp(long timestamp) { wrapped.setLastDesktopActivityTimestamp(timestamp);} - @Override - public void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId) { - wrapped.notifyMessageDeliveryFailed(context, recipient, threadId); - } @Override public void cancelDelayedNotifications() { wrapped.cancelDelayedNotifications(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java index 3144bd33f6..422f86cac1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -26,7 +26,6 @@ import androidx.core.app.RemoteInput; -import org.session.libsession.messaging.messages.ExpirationConfiguration; import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.session.libsession.messaging.messages.visible.VisibleMessage; @@ -34,7 +33,6 @@ import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; import org.session.libsession.snode.SnodeClock; import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.database.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MmsDatabase; @@ -96,19 +94,17 @@ public void onReceive(final Context context, Intent intent) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { - Recipient recipient = Recipient.from(context, address, false); - long threadId = threadDatabase.getOrCreateThreadIdFor(recipient); + long threadId = threadDatabase.getOrCreateThreadIdFor(address); VisibleMessage message = new VisibleMessage(); message.setSentTimestamp(clock.currentTimeMills()); message.setText(responseText.toString()); - ExpirationConfiguration config = storage.getExpirationConfiguration(threadId); - ExpiryMode expiryMode = config == null ? null : config.getExpiryMode(); + ExpiryMode expiryMode = storage.getExpirationConfiguration(threadId); - long expiresInMillis = expiryMode == null ? 0 : expiryMode.getExpiryMillis(); + long expiresInMillis = expiryMode.getExpiryMillis(); long expireStartedAt = expiryMode instanceof ExpiryMode.AfterSend ? message.getSentTimestamp() : 0L; switch (replyMethod) { case GroupMessage: { - OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null, expiresInMillis, 0); + OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, address, Collections.emptyList(), null, null, expiresInMillis, 0); try { message.setId(new MessageId(mmsDatabase.insertMessageOutbox(reply, threadId, false, true), true)); MessageSender.send(message, address); @@ -118,7 +114,7 @@ protected Void doInBackground(Void... params) { break; } case SecureMessage: { - OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient, expiresInMillis, expireStartedAt); + OutgoingTextMessage reply = OutgoingTextMessage.from(message, address, expiresInMillis, expireStartedAt); message.setId(new MessageId(smsDatabase.insertMessageOutbox(threadId, reply, false, System.currentTimeMillis(), true), false)); MessageSender.send(message, address); break; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/ReplyMethod.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/ReplyMethod.java index 43b8b1d55f..76f48f65d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/ReplyMethod.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/ReplyMethod.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.notifications; import android.content.Context; + import androidx.annotation.NonNull; import org.session.libsession.utilities.recipients.Recipient; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index 0c079d73a5..5eae553d7d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -26,9 +26,6 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; -import org.session.libsession.avatars.ContactPhoto; -import org.session.libsession.messaging.MessagingModuleConfiguration; -import org.session.libsession.messaging.contacts.Contact; import org.session.libsession.utilities.NotificationPrivacyPreference; import org.session.libsession.utilities.Util; import org.session.libsession.utilities.recipients.Recipient; @@ -76,20 +73,16 @@ public void setThread(@NonNull Recipient recipient) { setChannelId(channelId != null ? channelId : NotificationChannels.getMessagesChannel(context)); if (privacy.isDisplayContact()) { - setContentTitle(recipient.getName()); + setContentTitle(recipient.getDisplayName()); - if (recipient.getContactUri() != null) { - addPerson(recipient.getContactUri().toString()); - } - - ContactPhoto contactPhoto = recipient.getContactPhoto(); - if (contactPhoto != null) { + Object avatar = recipient.getAvatar(); + if (avatar != null) { try { // AC: For some reason, if not use ".asBitmap()" method, the returned BitmapDrawable // wraps a recycled bitmap and leads to a crash. Bitmap iconBitmap = Glide.with(context.getApplicationContext()) .asBitmap() - .load(contactPhoto) + .load(avatar) .diskCacheStrategy(DiskCacheStrategy.NONE) .circleCrop() .submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width), @@ -115,16 +108,15 @@ public void setMessageCount(int messageCount) { setNumber(messageCount); } - public void setPrimaryMessageBody(@NonNull Recipient threadRecipient, - @NonNull Recipient individualRecipient, + public void setPrimaryMessageBody(@NonNull Recipient threadRecipient, + @NonNull Recipient individualRecipient, @NonNull CharSequence message, @Nullable SlideDeck slideDeck) { SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); if (privacy.isDisplayContact() && threadRecipient.isGroupOrCommunityRecipient()) { - String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isCommunityRecipient()); - stringBuilder.append(Util.getBoldedString(displayName + ": ")); + stringBuilder.append(Util.getBoldedString(individualRecipient.getDisplayName() + ": ")); } if (privacy.isDisplayMessage()) { @@ -211,8 +203,7 @@ public void addMessageBody(@NonNull Recipient threadRecipient, SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); if (privacy.isDisplayContact() && threadRecipient.isGroupOrCommunityRecipient()) { - String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isCommunityRecipient()); - stringBuilder.append(Util.getBoldedString(displayName + ": ")); + stringBuilder.append(Util.getBoldedString(individualRecipient.getDisplayName() + ": ")); } if (privacy.isDisplayMessage()) { @@ -325,19 +316,7 @@ private CharSequence getBigText(List messageBodies) { private static Drawable getPlaceholderDrawable(AvatarUtils avatarUtils, Recipient recipient) { String publicKey = recipient.getAddress().toString(); - String displayName = recipient.getName(); + String displayName = recipient.getDisplayName(); return avatarUtils.generateTextBitmap(ICON_SIZE, publicKey, displayName); } - - /** - * @param recipient the * individual * recipient for which to get the display name. - * @param openGroupRecipient whether in an open group context - */ - private String getGroupDisplayName(Recipient recipient, boolean openGroupRecipient) { - return MessagingModuleConfiguration.getShared().getUsernameUtils().getContactNameWithAccountID( - recipient.getAddress().toString(), - null, - openGroupRecipient ? Contact.ContactContext.OPEN_GROUP : Contact.ContactContext.REGULAR - ); - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt index 9e39c0bfb8..96020e531f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt @@ -2,8 +2,8 @@ package org.thoughtcrime.securesms.onboarding.manager import android.app.Application import org.session.libsession.snode.SnodeModule +import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.UsernameUtils import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.hexEncodedPublicKey @@ -16,15 +16,13 @@ import javax.inject.Singleton class CreateAccountManager @Inject constructor( private val application: Application, private val prefs: TextSecurePreferences, - private val usernameUtils: UsernameUtils, - private val versionDataFetcher: VersionDataFetcher + private val versionDataFetcher: VersionDataFetcher, + private val configFactory: ConfigFactoryProtocol ) { private val database: LokiAPIDatabaseProtocol get() = SnodeModule.shared.storage fun createAccount(displayName: String) { - prefs.setProfileName(displayName) - // This is here to resolve a case where the app restarts before a user completes onboarding // which can result in an invalid database state database.clearAllLastMessageHashes() @@ -42,7 +40,7 @@ class CreateAccountManager @Inject constructor( prefs.setLocalNumber(userHexEncodedPublicKey) prefs.setRestorationTime(0) - usernameUtils.saveCurrentUserName(displayName) + configFactory.withMutableUserConfigs { it.userProfile.setName(displayName) } versionDataFetcher.startTimedVersionCheck() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt index 8ade5b4e8e..b8018ab9e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt @@ -8,10 +8,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.lifecycle.withCreationCallback import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.session.libsession.utilities.TextSecurePreferences -import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.home.startHomeActivity import org.thoughtcrime.securesms.onboarding.messagenotifications.startMessageNotificationsActivity @@ -24,16 +24,14 @@ private const val EXTRA_LOAD_FAILED = "extra_load_failed" @AndroidEntryPoint class PickDisplayNameActivity : BaseActionBarActivity() { - @Inject - internal lateinit var viewModelFactory: PickDisplayNameViewModel.AssistedFactory @Inject internal lateinit var prefs: TextSecurePreferences - private val loadFailed get() = intent.getBooleanExtra(EXTRA_LOAD_FAILED, false) - - private val viewModel: PickDisplayNameViewModel by viewModels { - viewModelFactory.create(loadFailed) - } + private val viewModel: PickDisplayNameViewModel by viewModels(extrasProducer = { + defaultViewModelCreationExtras.withCreationCallback { + it.create(intent.getBooleanExtra(EXTRA_LOAD_FAILED, false)) + } + }) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt index dc67a0e735..2d8274f511 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt @@ -2,10 +2,11 @@ package org.thoughtcrime.securesms.onboarding.pickname import androidx.annotation.StringRes import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -14,14 +15,15 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import network.loki.messenger.R -import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH +import org.session.libsession.messaging.messages.ProfileUpdateHandler +import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.UsernameUtils -internal class PickDisplayNameViewModel( - private val loadFailed: Boolean, +@HiltViewModel(assistedFactory = PickDisplayNameViewModel.Factory::class) +class PickDisplayNameViewModel @AssistedInject constructor( + @Assisted private val loadFailed: Boolean, private val prefs: TextSecurePreferences, - private val usernameUtils: UsernameUtils, + private val configFactory: ConfigFactoryProtocol, ): ViewModel() { private val isCreateAccount = !loadFailed @@ -38,7 +40,7 @@ internal class PickDisplayNameViewModel( when { displayName.isEmpty() -> { _states.update { it.copy(isTextErrorColor = true, error = R.string.displayNameErrorDescription) } } - displayName.toByteArray().size > NAME_PADDED_LENGTH -> { _states.update { it.copy(isTextErrorColor = true, error = R.string.displayNameErrorDescriptionShorter) } } + displayName.toByteArray().size > ProfileUpdateHandler.MAX_PROFILE_NAME_LENGTH -> { _states.update { it.copy(isTextErrorColor = true, error = R.string.displayNameErrorDescriptionShorter) } } else -> { // success - clear the error as we can still see it during the transition to the // next screen. @@ -46,8 +48,10 @@ internal class PickDisplayNameViewModel( viewModelScope.launch(Dispatchers.IO) { if (loadFailed) { - prefs.setProfileName(displayName) - usernameUtils.saveCurrentUserName(displayName) + configFactory.withMutableUserConfigs { + it.userProfile.setName(displayName) + } + _events.emit(Event.LoadAccountComplete) } else _events.emit(Event.CreateAccount(displayName)) } @@ -75,21 +79,9 @@ internal class PickDisplayNameViewModel( _states.update { it.copy(showDialog = false) } } - @dagger.assisted.AssistedFactory - interface AssistedFactory { - fun create(loadFailed: Boolean): Factory - } - - @Suppress("UNCHECKED_CAST") - class Factory @AssistedInject constructor( - @Assisted private val loadFailed: Boolean, - private val prefs: TextSecurePreferences, - private val usernameUtils: UsernameUtils, - ) : ViewModelProvider.Factory { - - override fun create(modelClass: Class): T { - return PickDisplayNameViewModel(loadFailed, prefs, usernameUtils) as T - } + @AssistedFactory + interface Factory { + fun create(loadFailed: Boolean): PickDisplayNameViewModel } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt index e59d86c912..a69d1d5975 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt @@ -8,8 +8,8 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import network.loki.messenger.R import network.loki.messenger.databinding.BlockedContactLayoutBinding -import org.session.libsession.utilities.recipients.Recipient import com.bumptech.glide.Glide +import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.util.adapter.SelectableItem typealias SelectableRecipient = SelectableItem @@ -47,7 +47,7 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap val binding = BlockedContactLayoutBinding.bind(itemView) fun bind(selectable: SelectableRecipient, toggle: (SelectableRecipient) -> Unit) { - binding.recipientName.text = selectable.item.name + binding.recipientName.text = selectable.item.displayName with (binding.profilePictureView) { update(selectable.item) } 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 612cb69af7..c7cbe674d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt @@ -24,7 +24,6 @@ import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY import org.session.libsession.database.StorageProtocol import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.database.DatabaseContentProviders -import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.util.adapter.SelectableItem import javax.inject.Inject @@ -54,7 +53,7 @@ class BlockedContactsViewModel @Inject constructor(private val storage: StorageP executor.launch(IO) { for (update in listUpdateChannel) { val blockedContactState = state.copy( - blockedContacts = storage.blockedContacts().sortedBy { it.name } + blockedContacts = storage.blockedContacts().sortedBy { it.displayName } ) withContext(Main) { _state.value = blockedContactState @@ -65,7 +64,7 @@ class BlockedContactsViewModel @Inject constructor(private val storage: StorageP } fun unblock() { - storage.setBlocked(state.selectedItems, false) + storage.setBlocked(state.selectedItems.map { it.address }, false) _state.value = state.copy(selectedItems = emptySet()) } @@ -83,15 +82,15 @@ class BlockedContactsViewModel @Inject constructor(private val storage: StorageP return when (contactsToUnblock.size) { // Note: We do not have to handle 0 because if no contacts are chosen then the unblock button is deactivated 1 -> Phrase.from(context, R.string.blockUnblockName) - .put(NAME_KEY, contactsToUnblock.elementAt(0).name) + .put(NAME_KEY, contactsToUnblock.elementAt(0).displayName) .format() 2 -> Phrase.from(context, R.string.blockUnblockNameTwo) - .put(NAME_KEY, contactsToUnblock.elementAt(0).name) + .put(NAME_KEY, contactsToUnblock.elementAt(0).displayName) .format() else -> { val othersCount = contactsToUnblock.size - 1 Phrase.from(context, R.string.blockUnblockNameMultiple) - .put(NAME_KEY, contactsToUnblock.elementAt(0).name) + .put(NAME_KEY, contactsToUnblock.elementAt(0).displayName) .put(COUNT_KEY, othersCount) .format() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt index 98d4ef0e0b..bbeb40a32e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.preferences +import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP import android.os.Bundle import android.view.View import androidx.compose.foundation.background @@ -13,9 +14,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -23,11 +21,9 @@ import kotlinx.coroutines.flow.asSharedFlow import network.loki.messenger.R import org.session.libsession.utilities.Address import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.PublicKeyValidation import org.thoughtcrime.securesms.ScreenLockActionBarActivity import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 -import org.thoughtcrime.securesms.database.threadDatabase import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.ui.components.QRScannerScreen import org.thoughtcrime.securesms.ui.components.QrImage @@ -38,7 +34,6 @@ import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.util.applySafeInsetsPaddings -import org.thoughtcrime.securesms.util.start private val TITLES = listOf(R.string.view, R.string.scan) @@ -73,13 +68,13 @@ class QRCodeActivity : ScreenLockActionBarActivity() { if (!PublicKeyValidation.isValid(string)) { errors.tryEmit(getString(R.string.qrNotAccountId)) } else if (!isFinishing) { - val recipient = Recipient.from(this, Address.fromSerialized(string), false) - start { - putExtra(ConversationActivityV2.ADDRESS, recipient.address) - setDataAndType(intent.data, intent.type) - val existingThread = threadDatabase().getThreadIdIfExistsFor(recipient) - putExtra(ConversationActivityV2.THREAD_ID, existingThread) - } + val address = Address.fromSerialized(string) + startActivity( + ConversationActivityV2.createIntent(this, address = address) + .setDataAndType(intent.data, intent.type) + .addFlags(FLAG_ACTIVITY_SINGLE_TOP) + ) + finish() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index 610bca0c98..84fea48efc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -72,9 +72,9 @@ import kotlinx.coroutines.launch import network.loki.messenger.BuildConfig import network.loki.messenger.R import network.loki.messenger.databinding.ActivitySettingsBinding +import org.session.libsession.messaging.messages.ProfileUpdateHandler import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.utilities.NonTranslatableStringConstants.NETWORK_NAME -import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.StringSubstitutionConstants.VERSION_KEY import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.getColorFromAttr @@ -98,10 +98,10 @@ import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.LargeItemButton import org.thoughtcrime.securesms.ui.LargeItemButtonWithDrawable import org.thoughtcrime.securesms.ui.OpenURLAlertDialog +import org.thoughtcrime.securesms.ui.components.AcccentOutlineCopyButton +import org.thoughtcrime.securesms.ui.components.AccentOutlineButton import org.thoughtcrime.securesms.ui.components.Avatar import org.thoughtcrime.securesms.ui.components.BaseBottomSheet -import org.thoughtcrime.securesms.ui.components.AccentOutlineButton -import org.thoughtcrime.securesms.ui.components.AcccentOutlineCopyButton import org.thoughtcrime.securesms.ui.getCellBottomShape import org.thoughtcrime.securesms.ui.getCellTopShape import org.thoughtcrime.securesms.ui.qaTag @@ -112,8 +112,8 @@ import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors -import org.thoughtcrime.securesms.ui.theme.dangerButtonColors import org.thoughtcrime.securesms.ui.theme.accentTextButtonColors +import org.thoughtcrime.securesms.ui.theme.dangerButtonColors import org.thoughtcrime.securesms.util.FileProviderUtil import org.thoughtcrime.securesms.util.applyCommonWindowInsetsOnViews import org.thoughtcrime.securesms.util.push @@ -354,7 +354,6 @@ class SettingsActivity : ScreenLockActionBarActivity() { Log.w(TAG, "Cannot update display name - no network connection.") } else { // if we have a network connection then attempt to update the display name - TextSecurePreferences.setProfileName(this, displayName) viewModel.updateName(displayName) binding.btnGroupNameDisplay.text = displayName updateWasSuccessful = true @@ -383,7 +382,7 @@ class SettingsActivity : ScreenLockActionBarActivity() { return false } - if (displayName.toByteArray().size > ProfileManagerProtocol.NAME_PADDED_LENGTH) { + if (displayName.toByteArray().size > ProfileUpdateHandler.MAX_PROFILE_NAME_LENGTH) { Toast.makeText(this, R.string.displayNameErrorDescriptionShorter, Toast.LENGTH_SHORT).show() return false } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt index ac904bde4b..316b76d97c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt @@ -9,28 +9,23 @@ import com.canhub.cropper.CropImageView import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R import network.loki.messenger.libsession_util.util.UserPic import org.session.libsession.avatars.AvatarHelper -import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.utilities.Address import org.session.libsession.utilities.ProfileKeyUtil import org.session.libsession.utilities.ProfilePictureUtilities import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.UsernameUtils -import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.currentUserName import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.NoExternalStorageException -import org.session.libsignal.utilities.Util.SECURE_RANDOM +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.preferences.SettingsViewModel.AvatarDialogState.TempAvatar import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints @@ -49,8 +44,8 @@ class SettingsViewModel @Inject constructor( private val prefs: TextSecurePreferences, private val configFactory: ConfigFactory, private val connectivity: NetworkConnectivity, - private val usernameUtils: UsernameUtils, - private val avatarUtils: AvatarUtils + private val avatarUtils: AvatarUtils, + private val recipientRepository: RecipientRepository, ) : ViewModel() { private val TAG = "SettingsViewModel" @@ -59,7 +54,7 @@ class SettingsViewModel @Inject constructor( val hexEncodedPublicKey: String = prefs.getLocalNumber() ?: "" private val userRecipient by lazy { - Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false) + recipientRepository.getRecipientSync(Address.fromSerialized(hexEncodedPublicKey)) } private val _avatarDialogState: MutableStateFlow = MutableStateFlow( @@ -97,9 +92,9 @@ class SettingsViewModel @Inject constructor( } } - fun getDisplayName(): String = usernameUtils.getCurrentUsernameWithAccountIdFallback() + fun getDisplayName(): String = configFactory.currentUserName - fun hasAvatar() = prefs.getProfileAvatarId() != 0 + fun hasAvatar() = configFactory.withUserConfigs { it.userProfile.getPic().url.isNotBlank() } fun createTempFile(): File? { try { @@ -202,7 +197,7 @@ class SettingsViewModel @Inject constructor( try { // Grab the profile key and kick of the promise to update the profile picture - val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(context) + val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey() val url = ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, context) // If the online portion of the update succeeded then update the local state @@ -214,21 +209,17 @@ class SettingsViewModel @Inject constructor( // When removing the profile picture the supplied ByteArray is empty so we'll clear the local data if (profilePicture.isEmpty()) { - MessagingModuleConfiguration.shared.storage.clearUserPic() + configFactory.withMutableUserConfigs { + it.userProfile.setPic(UserPic.DEFAULT) + } // update dialog state _avatarDialogState.value = AvatarDialogState.NoAvatar } else { - prefs.setProfileAvatarId(SECURE_RANDOM.nextInt()) - ProfileKeyUtil.setEncodedProfileKey(context, encodedProfileKey) - - // Attempt to grab the details we require to update the profile picture - val profileKey = ProfileKeyUtil.getProfileKey(context) - // If we have a URL and a profile key then set the user's profile picture - if (url.isNotEmpty() && profileKey.isNotEmpty()) { + if (url.isNotBlank() && encodedProfileKey.isNotBlank()) { configFactory.withMutableUserConfigs { - it.userProfile.setPic(UserPic(url, profileKey)) + it.userProfile.setPic(UserPic(url, ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey))) } } @@ -251,7 +242,7 @@ class SettingsViewModel @Inject constructor( } fun updateName(displayName: String) { - usernameUtils.saveCurrentUserName(displayName) + configFactory.withMutableUserConfigs { it.userProfile.setName(displayName) } } fun permanentlyHidePassword() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.kt b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.kt index 86c8b83c35..fbca19fca0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.kt @@ -7,11 +7,11 @@ import org.thoughtcrime.securesms.database.model.MessageId * A UI model for a reaction in the [ReactionsDialogFragment] */ data class ReactionDetails( - val sender: Recipient, - val baseEmoji: String, - val displayEmoji: String, - val timestamp: Long, - val serverId: String, - val localId: MessageId, - val count: Int + val sender: Recipient, + val baseEmoji: String, + val displayEmoji: String, + val timestamp: Long, + val serverId: String, + val localId: MessageId, + val count: Int ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java index 4607246897..1cc7a84a20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java @@ -161,7 +161,7 @@ void bind(@NonNull ReactionDetails reaction) { this.recipient.setText(R.string.you); this.remove.setVisibility(canRemove ? View.VISIBLE : View.GONE); } else { - String name = reaction.getSender().getName(); + String name = reaction.getSender().getDisplayName(); this.recipient.setText(name); this.remove.setVisibility(View.GONE); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDialogFragment.java index e9da05212b..92951b6715 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDialogFragment.java @@ -27,6 +27,7 @@ import java.util.Objects; +import dagger.hilt.android.lifecycle.HiltViewModelExtensions; import network.loki.messenger.R; public final class ReactionsDialogFragment extends BottomSheetDialogFragment implements ReactionViewPagerAdapter.Listener { @@ -143,9 +144,14 @@ private void setUpRecipientsRecyclerView() { } private void setUpViewModel(@NonNull MessageId messageId) { - ReactionsViewModel.Factory factory = new ReactionsViewModel.Factory(messageId); - - ReactionsViewModel viewModel = new ViewModelProvider(this, factory).get(ReactionsViewModel.class); + final ReactionsViewModel viewModel = new ViewModelProvider( + getViewModelStore(), + getDefaultViewModelProviderFactory(), + HiltViewModelExtensions.withCreationCallback( + getDefaultViewModelCreationExtras(), + (ReactionsViewModel.Factory factory) -> factory.create(messageId) + ) + ).get(ReactionsViewModel.class); disposables.add(viewModel.getEmojiCounts().subscribe(emojiCounts -> { if (emojiCounts.size() < 1) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsRepository.kt index b48ab5ad59..19c4741494 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsRepository.kt @@ -7,11 +7,17 @@ import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.utilities.Address import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.components.emoji.EmojiUtil +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.ReactionRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import javax.inject.Inject +import javax.inject.Singleton -class ReactionsRepository { +@Singleton +class ReactionsRepository @Inject constructor( + private val recipientRepository: RecipientRepository, +) { fun getReactions(messageId: MessageId): Observable> { return Observable.create { emitter: ObservableEmitter> -> @@ -24,8 +30,9 @@ class ReactionsRepository { val reactions: List = DatabaseComponent.get(context).reactionDatabase().getReactions(messageId) return reactions.map { reaction -> + val authorAddress = Address.fromSerialized(reaction.author) ReactionDetails( - sender = Recipient.from(context, Address.fromSerialized(reaction.author), false), + sender = recipientRepository.getRecipientSync(authorAddress) ?: Recipient.empty(authorAddress), baseEmoji = EmojiUtil.getCanonicalRepresentation(reaction.emoji), displayEmoji = reaction.emoji, timestamp = reaction.dateReceived, diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsViewModel.java index 8d345f8d3c..0c5c601f77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsViewModel.java @@ -6,23 +6,30 @@ import com.annimon.stream.Stream; +import org.thoughtcrime.securesms.database.RecipientRepository; import org.thoughtcrime.securesms.database.model.MessageId; import java.util.Comparator; import java.util.List; import java.util.Map; +import dagger.assisted.Assisted; +import dagger.assisted.AssistedFactory; +import dagger.assisted.AssistedInject; +import dagger.hilt.android.lifecycle.HiltViewModel; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; +@HiltViewModel(assistedFactory = ReactionsViewModel.Factory.class) public class ReactionsViewModel extends ViewModel { private final MessageId messageId; private final ReactionsRepository repository; - public ReactionsViewModel(@NonNull MessageId messageId) { + @AssistedInject + public ReactionsViewModel(@Assisted @NonNull MessageId messageId, final ReactionsRepository repository) { this.messageId = messageId; - this.repository = new ReactionsRepository(); + this.repository = repository; } public @NonNull @@ -65,17 +72,9 @@ private long getLatestTimestamp(List reactions) { return reactions.get(reactions.size() - 1).getDisplayEmoji(); } - static final class Factory implements ViewModelProvider.Factory { + @AssistedFactory + public interface Factory { - private final MessageId messageId; - - Factory(@NonNull MessageId messageId) { - this.messageId = messageId; - } - - @Override - public @NonNull T create(@NonNull Class modelClass) { - return modelClass.cast(new ReactionsViewModel(messageId)); - } + ReactionsViewModel create(@NonNull MessageId messageId); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiDialogFragment.java index 2fd547eab3..c8f3a2cfda 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiDialogFragment.java @@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView; import org.thoughtcrime.securesms.util.LifecycleDisposable; +import dagger.hilt.android.lifecycle.HiltViewModelExtensions; import network.loki.messenger.R; public final class ReactWithAnyEmojiDialogFragment extends BottomSheetDialogFragment implements EmojiEventListener, @@ -149,10 +150,15 @@ public void onDismiss(@NonNull DialogInterface dialog) { private void initializeViewModel() { Bundle args = requireArguments(); - ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext()); - ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS)); - - viewModel = new ViewModelProvider(this, factory).get(ReactWithAnyEmojiViewModel.class); + final MessageId messageId = new MessageId(args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS)); + viewModel = new ViewModelProvider( + getViewModelStore(), + getDefaultViewModelProviderFactory(), + HiltViewModelExtensions.withCreationCallback( + getDefaultViewModelCreationExtras(), + (ReactWithAnyEmojiViewModel.Factory factory) -> factory.create(messageId) + ) + ).get(ReactWithAnyEmojiViewModel.class); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java index bab8591cb6..6c32e9ea10 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java @@ -17,7 +17,7 @@ import network.loki.messenger.R; -final class ReactWithAnyEmojiRepository { +public final class ReactWithAnyEmojiRepository { private static final String TAG = Log.tag(ReactWithAnyEmojiRepository.class); diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java index 0ac6cf18f4..ad86278da5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java @@ -1,13 +1,13 @@ package org.thoughtcrime.securesms.reactions.any; +import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModel; -import androidx.lifecycle.ViewModelProvider; import com.annimon.stream.Stream; -import org.session.libsession.messaging.MessagingModuleConfiguration; import org.thoughtcrime.securesms.components.emoji.EmojiPageModel; import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter; import org.thoughtcrime.securesms.database.model.MessageId; @@ -17,10 +17,16 @@ import java.util.List; +import dagger.assisted.Assisted; +import dagger.assisted.AssistedFactory; +import dagger.assisted.AssistedInject; +import dagger.hilt.android.lifecycle.HiltViewModel; +import dagger.hilt.android.qualifiers.ApplicationContext; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.subjects.BehaviorSubject; +@HiltViewModel(assistedFactory = ReactWithAnyEmojiViewModel.Factory.class) public final class ReactWithAnyEmojiViewModel extends ViewModel { private static final int SEARCH_LIMIT = 40; @@ -30,16 +36,18 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel { private final Observable emojiList; private final BehaviorSubject searchResults; - private ReactWithAnyEmojiViewModel(@NonNull ReactWithAnyEmojiRepository repository, - long messageId, - boolean isMms, - @NonNull EmojiSearchRepository emojiSearchRepository) + @AssistedInject + public ReactWithAnyEmojiViewModel( + @Assisted @NonNull MessageId messageId, + @ApplicationContext Context context, + @NonNull EmojiSearchRepository emojiSearchRepository, + @NonNull ReactionsRepository reactionsRepository) { - this.repository = repository; + this.repository = new ReactWithAnyEmojiRepository(context); this.emojiSearchRepository = emojiSearchRepository; this.searchResults = BehaviorSubject.createDefault(new EmojiSearchResult()); - Observable> emojiPages = new ReactionsRepository().getReactions(new MessageId(messageId, isMms)) + Observable> emojiPages = reactionsRepository.getReactions(messageId) .map(thisMessagesReactions -> repository.getEmojiPageModels()); Observable emojiList = emojiPages.map(pages -> { @@ -100,23 +108,9 @@ public EmojiSearchResult() { } } - static class Factory implements ViewModelProvider.Factory { - - private final ReactWithAnyEmojiRepository repository; - private final long messageId; - private final boolean isMms; - - Factory(@NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) { - this.repository = repository; - this.messageId = messageId; - this.isMms = isMms; - } - - @Override - public @NonNull T create(@NonNull Class modelClass) { - //noinspection ConstantConditions - return modelClass.cast(new ReactWithAnyEmojiViewModel(repository, messageId, isMms, new EmojiSearchRepository(MessagingModuleConfiguration.getShared().getContext()))); - } + @AssistedFactory + public interface Factory { + ReactWithAnyEmojiViewModel create(@NonNull MessageId messageId); } } 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 28c79a87aa..557f06561e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -1,10 +1,8 @@ package org.thoughtcrime.securesms.repository import android.content.ContentResolver -import android.content.Context import app.cash.copper.Query import app.cash.copper.flow.observeQuery -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -29,13 +27,14 @@ import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.upsertContact import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DraftDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase -import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.MmsSmsDatabase import org.thoughtcrime.securesms.database.SessionJobDatabase import org.thoughtcrime.securesms.database.SmsDatabase @@ -49,49 +48,48 @@ import org.thoughtcrime.securesms.util.observeChanges import javax.inject.Inject interface ConversationRepository { - fun maybeGetRecipientForThreadId(threadId: Long): Recipient? - fun maybeGetBlindedRecipient(recipient: Recipient): Recipient? + fun maybeGetRecipientForThreadId(threadId: Long): Address? + fun maybeGetBlindedRecipient(address: Address): Address? fun changes(threadId: Long): Flow - fun recipientUpdateFlow(threadId: Long): Flow fun saveDraft(threadId: Long, text: String) fun getDraft(threadId: Long): String? fun clearDrafts(threadId: Long) - fun inviteContactsToCommunity(threadId: Long, contacts: List) - fun setBlocked(recipient: Recipient, blocked: Boolean) + fun inviteContactsToCommunity(threadId: Long, contacts: List
) + fun setBlocked(recipient: Address, blocked: Boolean) fun markAsDeletedLocally(messages: Set, displayedMessage: String) fun deleteMessages(messages: Set, threadId: Long) fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord) - fun setApproved(recipient: Recipient, isApproved: Boolean) + fun setApproved(recipient: Address, isApproved: Boolean) fun isGroupReadOnly(recipient: Recipient): Boolean fun getLastSentMessageID(threadId: Long): Flow suspend fun deleteCommunityMessagesRemotely(threadId: Long, messages: Set) suspend fun delete1on1MessagesRemotely( threadId: Long, - recipient: Recipient, + recipient: Address, messages: Set ) suspend fun deleteNoteToSelfMessagesRemotely( threadId: Long, - recipient: Recipient, + recipient: Address, messages: Set ) suspend fun deleteLegacyGroupMessagesRemotely( - recipient: Recipient, + recipient: Address, messages: Set ) - suspend fun deleteGroupV2MessagesRemotely(recipient: Recipient, messages: Set) + suspend fun deleteGroupV2MessagesRemotely(recipient: Address, messages: Set) - suspend fun banUser(threadId: Long, recipient: Recipient): Result - suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): Result + suspend fun banUser(threadId: Long, recipient: Address): Result + suspend fun banAndDeleteAll(threadId: Long, recipient: Address): Result suspend fun deleteThread(threadId: Long): Result suspend fun deleteMessageRequest(thread: ThreadRecord): Result suspend fun clearAllMessageRequests(block: Boolean): Result - suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): Result - suspend fun declineMessageRequest(threadId: Long, recipient: Recipient): Result + suspend fun acceptMessageRequest(threadId: Long, recipient: Address): Result + suspend fun declineMessageRequest(threadId: Long, recipient: Address): Result fun hasReceived(threadId: Long): Boolean - fun getInvitingAdmin(threadId: Long): Recipient? + fun getInvitingAdmin(threadId: Long): Address? /** * This will delete all messages from the database. @@ -105,14 +103,12 @@ interface ConversationRepository { } class DefaultConversationRepository @Inject constructor( - @ApplicationContext private val context: Context, private val textSecurePreferences: TextSecurePreferences, private val messageDataProvider: MessageDataProvider, private val threadDb: ThreadDatabase, private val draftDb: DraftDatabase, private val lokiThreadDb: LokiThreadDatabase, private val smsDb: SmsDatabase, - private val mmsDb: MmsDatabase, private val mmsSmsDb: MmsSmsDatabase, private val storage: Storage, private val lokiMessageDb: LokiMessageDatabase, @@ -123,28 +119,18 @@ class DefaultConversationRepository @Inject constructor( private val clock: SnodeClock, ) : ConversationRepository { - override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? { + override fun maybeGetRecipientForThreadId(threadId: Long): Address? { return threadDb.getRecipientForThreadId(threadId) } - override fun maybeGetBlindedRecipient(recipient: Recipient): Recipient? { - if (!recipient.isCommunityInboxRecipient) return null - return Recipient.from( - context, - Address.fromSerialized(GroupUtil.getDecodedOpenGroupInboxAccountId(recipient.address.toString())), - false - ) + override fun maybeGetBlindedRecipient(address: Address): Address? { + if (!address.isCommunityInbox) return null + return Address.fromSerialized(GroupUtil.getDecodedOpenGroupInboxAccountId(address.toString())) } override fun changes(threadId: Long): Flow = contentResolver.observeQuery(DatabaseContentProviders.Conversation.getUriForThread(threadId)) - override fun recipientUpdateFlow(threadId: Long): Flow { - return contentResolver.observeQuery(DatabaseContentProviders.Conversation.getUriForThread(threadId)).map { - maybeGetRecipientForThreadId(threadId) - } - } - override fun saveDraft(threadId: Long, text: String) { if (text.isEmpty()) return val drafts = DraftDatabase.Drafts() @@ -161,7 +147,7 @@ class DefaultConversationRepository @Inject constructor( draftDb.clearDrafts(threadId) } - override fun inviteContactsToCommunity(threadId: Long, contacts: List) { + override fun inviteContactsToCommunity(threadId: Long, contacts: List
) { val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return for (contact in contacts) { val message = VisibleMessage() @@ -173,13 +159,12 @@ class DefaultConversationRepository @Inject constructor( message.openGroupInvitation = openGroupInvitation val contactThreadId = threadDb.getOrCreateThreadIdFor(contact) val expirationConfig = contactThreadId.let(storage::getExpirationConfiguration) - val expiresInMillis = expirationConfig?.expiryMode?.expiryMillis ?: 0 - val expireStartedAt = if (expirationConfig?.expiryMode is ExpiryMode.AfterSend) message.sentTimestamp!! else 0 + val expireStartedAt = if (expirationConfig is ExpiryMode.AfterSend) message.sentTimestamp!! else 0 val outgoingTextMessage = OutgoingTextMessage.fromOpenGroupInvitation( openGroupInvitation, contact, message.sentTimestamp, - expiresInMillis, + expirationConfig.expiryMillis, expireStartedAt ) @@ -188,7 +173,7 @@ class DefaultConversationRepository @Inject constructor( false ) - MessageSender.send(message, contact.address) + MessageSender.send(message, contact) } } @@ -215,8 +200,8 @@ class DefaultConversationRepository @Inject constructor( } // This assumes that recipient.isContactRecipient is true - override fun setBlocked(recipient: Recipient, blocked: Boolean) { - if (recipient.isContactRecipient) { + override fun setBlocked(recipient: Address, blocked: Boolean) { + if (recipient.isContact) { storage.setBlocked(listOf(recipient), blocked) } } @@ -283,8 +268,16 @@ class DefaultConversationRepository @Inject constructor( } } - override fun setApproved(recipient: Recipient, isApproved: Boolean) { - storage.setRecipientApproved(recipient, isApproved) + override fun setApproved(recipient: Address, isApproved: Boolean) { + if (IdPrefix.fromValue(recipient.address) == IdPrefix.STANDARD) { + configFactory.withMutableUserConfigs { configs -> + configs.contacts.upsertContact(recipient.address) { + approved = isApproved + } + } + } else { + // Can not approve anything that is not a standard contact + } } override suspend fun deleteCommunityMessagesRemotely( @@ -302,11 +295,10 @@ class DefaultConversationRepository @Inject constructor( override suspend fun delete1on1MessagesRemotely( threadId: Long, - recipient: Recipient, + recipient: Address, messages: Set ) { // delete the messages remotely - val publicKey = recipient.address.toString() val userAddress: Address? = textSecurePreferences.getLocalNumber()?.let { Address.fromSerialized(it) } val userAuth = requireNotNull(storage.userAuth) { "User auth is required to delete messages remotely" @@ -316,7 +308,7 @@ class DefaultConversationRepository @Inject constructor( // delete from swarm messageDataProvider.getServerHashForMessage(message.messageId) ?.let { serverHash -> - SnodeAPI.deleteMessage(publicKey, userAuth, listOf(serverHash)) + SnodeAPI.deleteMessage(recipient.address, userAuth, listOf(serverHash)) } // send an UnsendRequest to user's swarm @@ -326,34 +318,32 @@ class DefaultConversationRepository @Inject constructor( // send an UnsendRequest to recipient's swarm buildUnsendRequest(message).let { unsendRequest -> - MessageSender.send(unsendRequest, recipient.address) + MessageSender.send(unsendRequest, recipient) } } } override suspend fun deleteLegacyGroupMessagesRemotely( - recipient: Recipient, + recipient: Address, messages: Set ) { - if (recipient.isLegacyGroupRecipient) { - val publicKey = recipient.address - + if (recipient.isLegacyGroup) { messages.forEach { message -> // send an UnsendRequest to group's swarm buildUnsendRequest(message).let { unsendRequest -> - MessageSender.send(unsendRequest, publicKey) + MessageSender.send(unsendRequest, recipient) } } } } override suspend fun deleteGroupV2MessagesRemotely( - recipient: Recipient, + recipient: Address, messages: Set ) { - require(recipient.isGroupV2Recipient) { "Recipient is not a group v2 recipient" } + require(recipient.isGroupV2) { "Recipient is not a group v2 recipient" } - val groupId = AccountId(recipient.address.toString()) + val groupId = AccountId(recipient.address) val hashes = messages.mapNotNullTo(mutableSetOf()) { msg -> messageDataProvider.getServerHashForMessage(msg.messageId) } @@ -363,11 +353,10 @@ class DefaultConversationRepository @Inject constructor( override suspend fun deleteNoteToSelfMessagesRemotely( threadId: Long, - recipient: Recipient, + recipient: Address, messages: Set ) { // delete the messages remotely - val publicKey = recipient.address.toString() val userAddress: Address? = textSecurePreferences.getLocalNumber()?.let { Address.fromSerialized(it) } val userAuth = requireNotNull(storage.userAuth) { "User auth is required to delete messages remotely" @@ -377,7 +366,7 @@ class DefaultConversationRepository @Inject constructor( // delete from swarm messageDataProvider.getServerHashForMessage(message.messageId) ?.let { serverHash -> - SnodeAPI.deleteMessage(publicKey, userAuth, listOf(serverHash)) + SnodeAPI.deleteMessage(recipient.address, userAuth, listOf(serverHash)) } // send an UnsendRequest to user's swarm @@ -394,18 +383,16 @@ class DefaultConversationRepository @Inject constructor( ) } - override suspend fun banUser(threadId: Long, recipient: Recipient): Result = runCatching { - val accountID = recipient.address.toString() + override suspend fun banUser(threadId: Long, recipient: Address): Result = runCatching { val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!! - OpenGroupApi.ban(accountID, openGroup.room, openGroup.server).await() + OpenGroupApi.ban(recipient.toString(), openGroup.room, openGroup.server).await() } - override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient) = runCatching { + override suspend fun banAndDeleteAll(threadId: Long, recipient: Address) = runCatching { // Note: This accountId could be the blinded Id - val accountID = recipient.address.toString() val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!! - OpenGroupApi.banAndDeleteAll(accountID, openGroup.room, openGroup.server).await() + OpenGroupApi.banAndDeleteAll(recipient.toString(), openGroup.room, openGroup.server).await() } override suspend fun deleteThread(threadId: Long) = runCatching { @@ -416,17 +403,15 @@ class DefaultConversationRepository @Inject constructor( } override suspend fun deleteMessageRequest(thread: ThreadRecord) - = declineMessageRequest(thread.threadId, thread.recipient) + = declineMessageRequest(thread.threadId, thread.recipient.address) override suspend fun clearAllMessageRequests(block: Boolean) = runCatching { withContext(Dispatchers.Default) { - threadDb.readerFor(threadDb.unapprovedConversationList).use { reader -> - while (reader.next != null) { - deleteMessageRequest(reader.current) - val recipient = reader.current.recipient - if (block && !recipient.isGroupV2Recipient) { - setBlocked(recipient, true) - } + threadDb.unapprovedConversationList.forEach { record -> + deleteMessageRequest(record) + val recipient = record.recipient + if (block && !recipient.isGroupV2Recipient) { + setBlocked(recipient.address, true) } } } @@ -447,18 +432,18 @@ class DefaultConversationRepository @Inject constructor( } } - override suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient) = runCatching { + override suspend fun acceptMessageRequest(threadId: Long, recipient: Address) = runCatching { withContext(Dispatchers.Default) { - storage.setRecipientApproved(recipient, true) - if (recipient.isGroupV2Recipient) { + setApproved(recipient, true) + if (recipient.isGroupV2) { groupManager.respondToInvitation( - AccountId(recipient.address.toString()), + AccountId(recipient.toString()), approved = true ) } else { val message = MessageRequestResponse(true) - MessageSender.send(message = message, address = recipient.address) + MessageSender.send(message = message, address = recipient) // add a control message for our user storage.insertMessageRequestResponseFromYou(threadId) @@ -468,16 +453,16 @@ class DefaultConversationRepository @Inject constructor( } } - override suspend fun declineMessageRequest(threadId: Long, recipient: Recipient): Result = runCatching { + override suspend fun declineMessageRequest(threadId: Long, recipient: Address): Result = runCatching { withContext(Dispatchers.Default) { sessionJobDb.cancelPendingMessageSendJobs(threadId) - if (recipient.isGroupV2Recipient) { + if (recipient.isGroupV2) { groupManager.respondToInvitation( - AccountId(recipient.address.toString()), + AccountId(recipient.toString()), approved = false ) } else { - storage.deleteContactAndSyncConfig(recipient.address.toString()) + storage.deleteContactAndSyncConfig(recipient.toString()) } } } @@ -493,9 +478,7 @@ class DefaultConversationRepository @Inject constructor( } // Only call this with a closed group thread ID - override fun getInvitingAdmin(threadId: Long): Recipient? { - return lokiMessageDb.groupInviteReferrer(threadId)?.let { id -> - Recipient.from(context, Address.fromSerialized(id), false) - } + override fun getInvitingAdmin(threadId: Long): Address? { + return lokiMessageDb.groupInviteReferrer(threadId)?.let(Address::fromSerialized) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchModule.kt b/app/src/main/java/org/thoughtcrime/securesms/search/SearchModule.kt deleted file mode 100644 index 65d475336b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/search/SearchModule.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.thoughtcrime.securesms.search - -import android.content.Context -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.android.scopes.ActivityScoped -import dagger.hilt.android.scopes.ViewModelScoped -import org.session.libsession.utilities.concurrent.SignalExecutors -import org.thoughtcrime.securesms.contacts.ContactAccessor -import org.thoughtcrime.securesms.database.GroupDatabase -import org.thoughtcrime.securesms.database.SearchDatabase -import org.thoughtcrime.securesms.database.SessionContactDatabase -import org.thoughtcrime.securesms.database.ThreadDatabase -import org.thoughtcrime.securesms.dependencies.ConfigFactory - -@Module -@InstallIn(ViewModelComponent::class) -object SearchModule { - - @Provides - @ViewModelScoped - fun provideSearchRepository(@ApplicationContext context: Context, - searchDatabase: SearchDatabase, - threadDatabase: ThreadDatabase, - groupDatabase: GroupDatabase, - contactDatabase: SessionContactDatabase, - configFactory: ConfigFactory) = - SearchRepository( - context, searchDatabase, threadDatabase, groupDatabase, contactDatabase, - ContactAccessor.getInstance(), configFactory, SignalExecutors.SERIAL - ) - - -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.kt index d62fb34932..9a8c5aa599 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.kt @@ -2,41 +2,36 @@ package org.thoughtcrime.securesms.search import android.content.Context import android.database.Cursor -import android.database.MergeCursor -import com.annimon.stream.Stream -import org.session.libsession.messaging.contacts.Contact -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.Address.Companion.fromExternal +import dagger.hilt.android.qualifiers.ApplicationContext import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.GroupRecord -import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber +import org.session.libsession.utilities.concurrent.SignalExecutors +import org.session.libsession.utilities.recipients.BasicRecipient import org.session.libsession.utilities.recipients.Recipient -import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.contacts.ContactAccessor import org.thoughtcrime.securesms.database.CursorList import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.MmsSmsColumns +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.SearchDatabase -import org.thoughtcrime.securesms.database.SessionContactDatabase import org.thoughtcrime.securesms.database.ThreadDatabase -import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.search.model.MessageResult import org.thoughtcrime.securesms.search.model.SearchResult import org.thoughtcrime.securesms.util.Stopwatch -import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton // Class to manage data retrieval for search -class SearchRepository( - context: Context, +@Singleton +class SearchRepository @Inject constructor( + @ApplicationContext private val context: Context, private val searchDatabase: SearchDatabase, private val threadDatabase: ThreadDatabase, private val groupDatabase: GroupDatabase, - private val contactDatabase: SessionContactDatabase, private val contactAccessor: ContactAccessor, - private val configFactory: ConfigFactory, - private val executor: Executor + private val recipientRepository: RecipientRepository, ) { - private val context: Context = context.applicationContext + private val executor = SignalExecutors.SERIAL fun query(query: String, callback: (SearchResult) -> Unit) { // If the sanitized search is empty then abort without search @@ -84,69 +79,48 @@ class SearchRepository( } } - // Get set of blocked contact AccountIDs from the ConfigFactory - private fun getBlockedContacts(): MutableSet { - val blockedContacts = mutableSetOf() - configFactory.withUserConfigs { userConfigs -> - userConfigs.contacts.all().forEach { contact -> - if (contact.blocked) { - blockedContacts.add(contact.id) - } - } - } - return blockedContacts + private fun getBlockedContacts(): Set { + return recipientRepository.getConfigBasedConversations( + nts = { false }, + contactFilter = { it.blocked }, + groupFilter = { false }, + communityFilter = { false }, + legacyFilter = { false }, + ).mapTo(hashSetOf()) { it.address } } - fun queryContacts(query: String): CursorList { - val excludingAddresses = getBlockedContacts() - val contacts = contactDatabase.queryContactsByName(query, excludeUserAddresses = excludingAddresses) - val contactList: MutableList
= ArrayList() - - while (contacts.moveToNext()) { - try { - val contact = contactDatabase.contactFromCursor(contacts) - val contactAccountId = contact.accountID - val address = fromSerialized(contactAccountId) - contactList.add(address) - - // Add the address in this query to the excluded addresses so the next query - // won't get the same contact again - excludingAddresses.add(contactAccountId) - } catch (e: Exception) { - Log.e("Loki", "Error building Contact from cursor in query", e) - } - } - - contacts.close() - - val addressThreads = threadDatabase.searchConversationAddresses(query, excludingAddresses)// filtering threads by looking up the accountID itself - val individualRecipients = threadDatabase.getFilteredConversationList(contactList) - if (individualRecipients == null && addressThreads == null) { - return CursorList.emptyList() - } - val merged = MergeCursor(arrayOf(addressThreads, individualRecipients)) - - return CursorList(merged, ContactModelBuilder(contactDatabase, threadDatabase)) + fun queryContacts(searchName: String? = null): List { + return recipientRepository.getConfigBasedConversations( + nts = { false }, + contactFilter = { + !it.blocked && + it.approved && + (searchName == null || + it.name.contains(searchName, ignoreCase = true) || + it.nickname.contains(searchName, ignoreCase = true)) + }, + groupFilter = { false }, + communityFilter = { false }, + legacyFilter = { false }, + ).mapNotNull(recipientRepository::getBasicRecipientFast) + .filterIsInstance() } private fun queryConversations( query: String, - ): CursorList { + ): List { val numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query) - val addresses = numbers.map { fromExternal(context, it) } + val addresses = numbers.map { fromSerialized(it) } - val conversations = threadDatabase.getFilteredConversationList(addresses) - return if (conversations != null) - CursorList(conversations, GroupModelBuilder(threadDatabase, groupDatabase)) - else - CursorList.emptyList() + return threadDatabase.getFilteredConversationList(addresses) + .map { groupDatabase.getGroup(it.recipient.address.toGroupString()).get() } } private fun queryMessages(query: String): CursorList { val blockedContacts = getBlockedContacts() val messages = searchDatabase.queryMessages(query, blockedContacts) return if (messages != null) - CursorList(messages, MessageModelBuilder(context)) + CursorList(messages, MessageModelBuilder()) else CursorList.emptyList() } @@ -155,7 +129,7 @@ class SearchRepository( val blockedContacts = getBlockedContacts() val messages = searchDatabase.queryMessages(query, threadId, blockedContacts) return if (messages != null) - CursorList(messages, MessageModelBuilder(context)) + CursorList(messages, MessageModelBuilder()) else CursorList.emptyList() } @@ -182,40 +156,14 @@ class SearchRepository( return out.toString() } - private class ContactModelBuilder( - private val contactDb: SessionContactDatabase, - private val threadDb: ThreadDatabase - ) : CursorList.ModelBuilder { - override fun build(cursor: Cursor): Contact { - val threadRecord = threadDb.readerFor(cursor).current - var contact = - contactDb.getContactWithAccountID(threadRecord.recipient.address.toString()) - if (contact == null) { - contact = Contact(threadRecord.recipient.address.toString()) - contact.threadID = threadRecord.threadId - } - return contact - } - } - - private class GroupModelBuilder( - private val threadDatabase: ThreadDatabase, - private val groupDatabase: GroupDatabase - ) : CursorList.ModelBuilder { - override fun build(cursor: Cursor): GroupRecord { - val threadRecord = threadDatabase.readerFor(cursor).current - return groupDatabase.getGroup(threadRecord.recipient.address.toGroupString()).get() - } - } - - private class MessageModelBuilder(private val context: Context) : CursorList.ModelBuilder { + private inner class MessageModelBuilder() : CursorList.ModelBuilder { override fun build(cursor: Cursor): MessageResult { val conversationAddress = fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.CONVERSATION_ADDRESS))) val messageAddress = fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.MESSAGE_ADDRESS))) - val conversationRecipient = Recipient.from(context, conversationAddress, false) - val messageRecipient = Recipient.from(context, messageAddress, false) + val conversationRecipient = recipientRepository.getRecipientSync(conversationAddress) ?: Recipient.empty(conversationAddress) + val messageRecipient = recipientRepository.getRecipientSync(messageAddress) ?: Recipient.empty(messageAddress) val body = cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.SNIPPET)) val sentMs = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_SENT)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/model/MessageResult.java b/app/src/main/java/org/thoughtcrime/securesms/search/model/MessageResult.java index 804e47893e..b3ea0a4ecc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/search/model/MessageResult.java +++ b/app/src/main/java/org/thoughtcrime/securesms/search/model/MessageResult.java @@ -33,7 +33,7 @@ public MessageResult(@NonNull Recipient conversationRecipient, @Override public boolean equals(Object o) { if (!(o instanceof MessageResult that)) return false; - return threadId == that.threadId && sentTimestampMs == that.sentTimestampMs && Objects.equals(conversationRecipient, that.conversationRecipient) && Objects.equals(messageRecipient, that.messageRecipient) && Objects.equals(bodySnippet, that.bodySnippet); + return threadId == that.threadId && sentTimestampMs == that.sentTimestampMs && Objects.equals(conversationRecipient, that.conversationRecipient) && Objects.equals(messageRecipient, that.messageRecipient) && Objects.equals(bodySnippet, that.bodySnippet); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/model/SearchResult.java b/app/src/main/java/org/thoughtcrime/securesms/search/model/SearchResult.java index 33c687c939..30284b710b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/search/model/SearchResult.java +++ b/app/src/main/java/org/thoughtcrime/securesms/search/model/SearchResult.java @@ -1,13 +1,11 @@ package org.thoughtcrime.securesms.search.model; -import android.database.ContentObserver; - import androidx.annotation.NonNull; -import org.session.libsession.messaging.contacts.Contact; import org.session.libsession.utilities.GroupRecord; +import org.session.libsession.utilities.recipients.BasicRecipient; +import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.database.CursorList; -import org.thoughtcrime.securesms.database.model.ThreadRecord; import java.util.List; @@ -20,13 +18,13 @@ public class SearchResult { public static final SearchResult EMPTY = new SearchResult("", CursorList.emptyList(), CursorList.emptyList(), CursorList.emptyList()); private final String query; - private final CursorList contacts; - private final CursorList conversations; + private final List contacts; + private final List conversations; private final CursorList messages; public SearchResult(@NonNull String query, - @NonNull CursorList contacts, - @NonNull CursorList conversations, + @NonNull List contacts, + @NonNull List conversations, @NonNull CursorList messages) { this.query = query; @@ -35,7 +33,7 @@ public SearchResult(@NonNull String query, this.messages = messages; } - public List getContacts() { + public List getContacts() { return contacts; } @@ -59,21 +57,7 @@ public boolean isEmpty() { return size() == 0; } - public void registerContentObserver(@NonNull ContentObserver observer) { - contacts.registerContentObserver(observer); - conversations.registerContentObserver(observer); - messages.registerContentObserver(observer); - } - - public void unregisterContentObserver(@NonNull ContentObserver observer) { - contacts.unregisterContentObserver(observer); - conversations.unregisterContentObserver(observer); - messages.unregisterContentObserver(observer); - } - public void close() { - contacts.close(); - conversations.close(); messages.close(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/CallForegroundService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/CallForegroundService.kt index 9ba7051616..7a8b0e3e45 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/CallForegroundService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/CallForegroundService.kt @@ -8,23 +8,30 @@ import android.os.Build import android.os.IBinder import androidx.core.app.ServiceCompat import androidx.core.content.IntentCompat +import dagger.hilt.android.AndroidEntryPoint import org.session.libsession.utilities.Address import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.Companion.TYPE_INCOMING_CONNECTING import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.Companion.WEBRTC_NOTIFICATION +import javax.inject.Inject +@AndroidEntryPoint class CallForegroundService : Service() { + @Inject + lateinit var recipientRepository: RecipientRepository + companion object { const val EXTRA_RECIPIENT_ADDRESS = "RECIPIENT_ID" const val EXTRA_TYPE = "CALL_STEP_TYPE" - fun startIntent(context: Context, type: Int, recipient: Recipient?): Intent { + fun startIntent(context: Context, type: Int, recipient: Address?): Intent { return Intent(context, CallForegroundService::class.java) .putExtra(EXTRA_TYPE, type) - .putExtra(EXTRA_RECIPIENT_ADDRESS, recipient?.address) + .putExtra(EXTRA_RECIPIENT_ADDRESS, recipient) } } @@ -33,7 +40,7 @@ class CallForegroundService : Service() { EXTRA_RECIPIENT_ADDRESS, Address::class.java) ?: return null - return Recipient.from(this, remoteAddress, true) + return recipientRepository.getRecipientSync(remoteAddress) } private fun startForeground(type: Int, recipient: Recipient?) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/DirectShareService.java b/app/src/main/java/org/thoughtcrime/securesms/service/DirectShareService.java index 8a6ed84f3d..67afa37337 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/DirectShareService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/DirectShareService.java @@ -3,30 +3,48 @@ import android.content.ComponentName; import android.content.IntentFilter; -import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Icon; import android.os.Bundle; -import android.os.Parcel; import android.service.chooser.ChooserTarget; import android.service.chooser.ChooserTargetService; import androidx.annotation.NonNull; +import com.bumptech.glide.Glide; + +import org.jetbrains.annotations.NotNull; +import org.session.libsession.database.StorageProtocol; +import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager; import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.ShareActivity; +import org.thoughtcrime.securesms.contacts.ShareContactListLoader; +import org.thoughtcrime.securesms.database.RecipientRepository; import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; -import com.bumptech.glide.Glide; import org.thoughtcrime.securesms.util.BitmapUtil; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint public class DirectShareService extends ChooserTargetService { + @Inject + RecipientRepository recipientRepository; + + @Inject + LegacyGroupDeprecationManager legacyGroupDeprecationManager; + + @Inject + StorageProtocol storage; private static final String TAG = DirectShareService.class.getSimpleName(); @@ -34,57 +52,45 @@ public class DirectShareService extends ChooserTargetService { public List onGetChooserTargets(ComponentName targetActivityName, IntentFilter matchedFilter) { - List results = new LinkedList<>(); + List results = new ArrayList<>(); ComponentName componentName = new ComponentName(this, ShareActivity.class); ThreadDatabase threadDatabase = DatabaseComponent.get(this).threadDatabase(); - try (Cursor cursor = threadDatabase.getDirectShareList()) { - ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor); - ThreadRecord record; - - while ((record = reader.getNext()) != null && results.size() < 10) { - Recipient recipient = Recipient.from(this, record.getRecipient().getAddress(), false); - String name = recipient.getName(); - - Bitmap avatar; - - if (recipient.getContactPhoto() != null) { - try { - avatar = Glide.with(this) - .asBitmap() - .load(recipient.getContactPhoto()) - .circleCrop() - .submit(getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width), - getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width)) - .get(); - } catch (InterruptedException | ExecutionException e) { - Log.w(TAG, e); - avatar = getFallbackDrawable(recipient); - } - } else { - avatar = getFallbackDrawable(recipient); - } - - Parcel parcel = Parcel.obtain(); - parcel.writeParcelable(recipient.getAddress(), 0); - - Bundle bundle = new Bundle(); - bundle.putLong(ShareActivity.EXTRA_THREAD_ID, record.getThreadId()); - bundle.putByteArray(ShareActivity.EXTRA_ADDRESS_MARSHALLED, parcel.marshall()); - bundle.putInt(ShareActivity.EXTRA_DISTRIBUTION_TYPE, record.getDistributionType()); - bundle.setClassLoader(getClassLoader()); - - results.add(new ChooserTarget(name, Icon.createWithBitmap(avatar), 1.0f, componentName, bundle)); - parcel.recycle(); - - } - - return results; - } + List<@NotNull Recipient> items = new ShareContactListLoader(this, null, legacyGroupDeprecationManager, threadDatabase, storage).loadInBackground(); + + for (final Recipient recipient : items) { + Bitmap avatar; + + if (recipient.getAvatar() != null) { + try { + avatar = Glide.with(this) + .asBitmap() + .load(recipient.getAvatar()) + .circleCrop() + .submit(getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width), + getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width)) + .get(); + } catch (InterruptedException | ExecutionException e) { + Log.w(TAG, e); + avatar = getFallbackDrawable(recipient); + } + } else { + avatar = getFallbackDrawable(recipient); + } + + Bundle bundle = new Bundle(1); + bundle.putParcelable(ShareActivity.EXTRA_ADDRESS, recipient.getAddress()); + bundle.setClassLoader(getClassLoader()); + + results.add(new ChooserTarget(recipient.getDisplayName(), Icon.createWithBitmap(avatar), 1.0f, componentName, bundle)); + } + + return results; } private Bitmap getFallbackDrawable(@NonNull Recipient recipient) { - return BitmapUtil.createFromDrawable(recipient.getFallbackContactPhotoDrawable(this, false), + //TODO: Use proper color + return BitmapUtil.createFromDrawable(new ColorDrawable(Color.RED), getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width), getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height)); } 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 4355707a0d..3b5e6356a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt @@ -25,7 +25,6 @@ import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix @@ -33,6 +32,7 @@ import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.guava.Optional 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 import org.thoughtcrime.securesms.database.model.MessageId @@ -54,6 +54,7 @@ class ExpiringMessageManager @Inject constructor( private val clock: SnodeClock, private val storage: Lazy, private val preferences: TextSecurePreferences, + private val recipientRepository: RecipientRepository, ) : MessageExpirationManagerProtocol { private val scheduleDeletionChannel: SendChannel @@ -102,10 +103,10 @@ class ExpiringMessageManager @Inject constructor( val expiresInMillis = message.expiryMode.expiryMillis var groupInfo = Optional.absent() val address = fromSerialized(senderPublicKey!!) - var recipient = Recipient.from(context, address, false) + var recipient = recipientRepository.getRecipientSync(address) // if the sender is blocked, we don't display the update, except if it's in a closed group - if (recipient.isBlocked && groupId == null) return null + if (recipient?.blocked == true && groupId == null) return null return try { if (groupId != null) { val groupAddress: Address @@ -120,9 +121,9 @@ class ExpiringMessageManager @Inject constructor( Optional.of(SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(doubleEncoded), SignalServiceGroup.GroupType.SIGNAL)) } } - recipient = Recipient.from(context, groupAddress, false) + recipient = recipientRepository.getRecipientSync(groupAddress) } - val threadId = storage.get().getThreadId(recipient) ?: return null + val threadId = recipient?.address?.let(storage.get()::getThreadId) ?: return null val mediaMessage = IncomingMediaMessage( address, sentTimestamp!!, -1, expiresInMillis, expireStartedAt, true, @@ -163,11 +164,10 @@ class ExpiringMessageManager @Inject constructor( else -> doubleEncodeGroupID(groupId) } val address = fromSerialized(serializedAddress) - val recipient = Recipient.from(context, address, false) message.threadID = storage.get().getOrCreateThreadIdFor(address) val timerUpdateMessage = OutgoingExpirationUpdateMessage( - recipient, + address, sentTimestamp!!, duration, expireStartedAt, diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java b/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java index 22f0addd71..09803bb793 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java @@ -68,7 +68,7 @@ protected void onHandleIntent(Intent intent) { VisibleMessage message = new VisibleMessage(); message.setText(content); message.setSentTimestamp(SnodeAPI.getNowWithOffset()); - MessageSender.send(message, Address.fromExternal(this, number)); + MessageSender.send(message, Address.fromSerialized(number)); } } catch (URISyntaxException e) { Toast.makeText(this, R.string.errorUnknown, Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt deleted file mode 100644 index da9258d710..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt +++ /dev/null @@ -1,113 +0,0 @@ -package org.thoughtcrime.securesms.sskenvironment - -import android.content.Context -import dagger.Lazy -import network.loki.messenger.libsession_util.util.UserPic -import org.session.libsession.database.StorageProtocol -import org.session.libsession.messaging.contacts.Contact -import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob -import org.session.libsession.utilities.ConfigFactoryProtocol -import org.session.libsession.utilities.SSKEnvironment -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.recipients.Recipient -import org.session.libsession.utilities.upsertContact -import org.session.libsignal.utilities.AccountId -import org.session.libsignal.utilities.IdPrefix -import org.thoughtcrime.securesms.database.RecipientDatabase -import org.thoughtcrime.securesms.database.SessionContactDatabase -import org.thoughtcrime.securesms.database.SessionJobDatabase -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ProfileManager @Inject constructor( - private val configFactory: ConfigFactoryProtocol, - private val storage: Lazy, - private val contactDatabase: SessionContactDatabase, - private val recipientDatabase: RecipientDatabase, - private val jobDatabase: SessionJobDatabase, - private val preferences: TextSecurePreferences, -) : SSKEnvironment.ProfileManagerProtocol { - - override fun setNickname(context: Context, recipient: Recipient, nickname: String?) { - if (recipient.isLocalNumber) return - val accountID = recipient.address.toString() - var contact = contactDatabase.getContactWithAccountID(accountID) - if (contact == null) contact = Contact(accountID) - contact.threadID = storage.get().getThreadId(recipient.address) - if (contact.nickname != nickname) { - contact.nickname = nickname - contactDatabase.setContact(contact) - } - contactUpdatedInternal(contact) - } - - override fun setName(context: Context, recipient: Recipient, name: String?) { - // New API - if (recipient.isLocalNumber) return - val accountID = recipient.address.toString() - var contact = contactDatabase.getContactWithAccountID(accountID) - if (contact == null) contact = Contact(accountID) - contact.threadID = storage.get().getThreadId(recipient.address) - if (contact.name != name) { - contact.name = name - contactDatabase.setContact(contact) - } - // Old API - recipientDatabase.setProfileName(recipient, name) - recipient.notifyListeners() - contactUpdatedInternal(contact) - } - - override fun setProfilePicture( - context: Context, - recipient: Recipient, - profilePictureURL: String?, - profileKey: ByteArray? - ) { - val hasPendingDownload = jobDatabase - .getAllJobs(RetrieveProfileAvatarJob.KEY).any { - (it.value as? RetrieveProfileAvatarJob)?.recipientAddress == recipient.address - } - - recipient.resolve() - - val accountID = recipient.address.toString() - var contact = contactDatabase.getContactWithAccountID(accountID) - if (contact == null) contact = Contact(accountID) - contact.threadID = storage.get().getThreadId(recipient.address) - if (!contact.profilePictureEncryptionKey.contentEquals(profileKey) || contact.profilePictureURL != profilePictureURL) { - contact.profilePictureEncryptionKey = profileKey - contact.profilePictureURL = profilePictureURL - contactDatabase.setContact(contact) - } - contactUpdatedInternal(contact) - if (!hasPendingDownload) { - val job = RetrieveProfileAvatarJob(profilePictureURL, recipient.address, profileKey) - JobQueue.shared.add(job) - } - } - - - override fun contactUpdatedInternal(contact: Contact): String? { - if (contact.accountID == preferences.getLocalNumber()) return null - if (IdPrefix.fromValue(contact.accountID) != IdPrefix.STANDARD) return null // only internally store standard account IDs - return configFactory.withMutableUserConfigs { - val contactConfig = it.contacts - contactConfig.upsertContact(contact.accountID) { - 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) - } else if (url.isNullOrEmpty() && key == null) { - this.profilePicture = UserPic.DEFAULT - } - } - contactConfig.get(contact.accountID)?.hashCode()?.toString() - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/TypingStatusRepository.java b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/TypingStatusRepository.java index 0ab62d8c21..9e36d78bbd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/TypingStatusRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/TypingStatusRepository.java @@ -15,8 +15,8 @@ import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.Log; +import org.thoughtcrime.securesms.database.RecipientRepository; import java.util.ArrayList; import java.util.Collections; @@ -43,9 +43,11 @@ public class TypingStatusRepository implements SSKEnvironment.TypingIndicatorsPr private final Map> notifiers; private final MutableLiveData> threadsNotifier; private final TextSecurePreferences preferences; + private final RecipientRepository recipientRepository; @Inject - public TypingStatusRepository(TextSecurePreferences preferences) { + public TypingStatusRepository(TextSecurePreferences preferences, RecipientRepository recipientRepository) { + this.recipientRepository = recipientRepository; this.typistMap = new HashMap<>(); this.timers = new HashMap<>(); this.notifiers = new HashMap<>(); @@ -59,12 +61,12 @@ public synchronized void didReceiveTypingStartedMessage(@NotNull Context context return; } - if (Recipient.from(context, author, false).isBlocked()) { + if (recipientRepository.getRecipientSyncOrEmpty(author).getBlocked()) { return; } Set typists = Util.getOrDefault(typistMap, threadId, new LinkedHashSet<>()); - Typist typist = new Typist(Recipient.from(context, author, false), device, threadId); + Typist typist = new Typist(author, device, threadId); if (!typists.contains(typist)) { typists.add(typist); @@ -88,12 +90,12 @@ public synchronized void didReceiveTypingStoppedMessage(@NotNull Context context return; } - if (Recipient.from(context, author, false).isBlocked()) { + if (recipientRepository.getRecipientSyncOrEmpty(author).getBlocked()) { return; } Set typists = Util.getOrDefault(typistMap, threadId, new LinkedHashSet<>()); - Typist typist = new Typist(Recipient.from(context, author, false), device, threadId); + Typist typist = new Typist(author, device, threadId); if (typists.contains(typist)) { typists.remove(typist); @@ -145,7 +147,7 @@ private void notifyThread(long threadId, @NonNull Set typists, boolean i MutableLiveData notifier = Util.getOrDefault(notifiers, threadId, new MutableLiveData<>()); notifiers.put(threadId, notifier); - Set uniqueTypists = new LinkedHashSet<>(); + Set
uniqueTypists = new LinkedHashSet<>(); for (Typist typist : typists) { uniqueTypists.add(typist.getAuthor()); } @@ -157,15 +159,15 @@ private void notifyThread(long threadId, @NonNull Set typists, boolean i } public static class TypingState { - private final List typists; + private final List
typists; private final boolean replacedByIncomingMessage; - public TypingState(List typists, boolean replacedByIncomingMessage) { + public TypingState(List
typists, boolean replacedByIncomingMessage) { this.typists = typists; this.replacedByIncomingMessage = replacedByIncomingMessage; } - public List getTypists() { + public List
getTypists() { return typists; } @@ -175,17 +177,17 @@ public boolean isReplacedByIncomingMessage() { } private static class Typist { - private final Recipient author; + private final Address author; private final int device; private final long threadId; - private Typist(@NonNull Recipient author, int device, long threadId) { + private Typist(@NonNull Address author, int device, long threadId) { this.author = author; this.device = device; this.threadId = threadId; } - public Recipient getAuthor() { + public Address getAuthor() { return author; } @@ -206,7 +208,7 @@ public boolean equals(Object o) { if (device != typist.device) return false; if (threadId != typist.threadId) return false; - return author.getAddress().equals(typist.author.getAddress()); + return author.equals(typist.author); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenPageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenPageViewModel.kt index e6eb68c84c..b35f726b3d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenPageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenPageViewModel.kt @@ -23,12 +23,9 @@ import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.NonTranslatableStringConstants.SESSION_NETWORK_DATA_PRICE import org.session.libsession.utilities.NonTranslatableStringConstants.TOKEN_NAME_SHORT import org.session.libsession.utilities.NonTranslatableStringConstants.USD_NAME_SHORT -import org.session.libsession.utilities.StringSubstitutionConstants.DATE_KEY import org.session.libsession.utilities.StringSubstitutionConstants.DATE_TIME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.RELATIVE_TIME_KEY -import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Snode @@ -258,23 +255,20 @@ class TokenPageViewModel @Inject constructor( // Grab the database and reader details we need to count the conversations / groups val threadDatabase = DatabaseComponent.get(context).threadDatabase() - val cursor = threadDatabase.approvedConversationList + val convoList = threadDatabase.approvedConversationList val result = mutableSetOf() // Look through the database to build up our conversation & group counts (still on Dispatchers.IO not the main thread) - threadDatabase.readerFor(cursor).use { reader -> - while (reader.next != null) { - val thread = reader.current - val recipient = thread.recipient - result.add(recipient) - - if (recipient.is1on1) { - num1to1Convos += 1 - } else if (recipient.isGroupV2Recipient) { - numGroupV2Convos += 1 - } else if (recipient.isLegacyGroupRecipient) { - numLegacyGroupConvos += 1 - } + convoList.forEach { thread -> + val recipient = thread.recipient + result.add(recipient) + + if (recipient.is1on1) { + num1to1Convos += 1 + } else if (recipient.isGroupV2Recipient) { + numGroupV2Convos += 1 + } else if (recipient.isLegacyGroupRecipient) { + numLegacyGroupConvos += 1 } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Avatar.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Avatar.kt index 57907d4332..d0dc7bf97d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Avatar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Avatar.kt @@ -29,18 +29,13 @@ import androidx.compose.ui.unit.sp import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.bumptech.glide.integration.compose.placeholder -import com.bumptech.glide.load.engine.DiskCacheStrategy import network.loki.messenger.R -import org.session.libsession.avatars.ProfileContactPhoto -import org.session.libsession.utilities.Address -import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors import org.thoughtcrime.securesms.ui.theme.classicDark3 -import org.thoughtcrime.securesms.ui.theme.classicLight1 import org.thoughtcrime.securesms.ui.theme.primaryBlue import org.thoughtcrime.securesms.ui.theme.primaryGreen import org.thoughtcrime.securesms.util.AvatarBadge @@ -319,10 +314,7 @@ fun PreviewAvatarSinglePhoto(){ listOf(AvatarUIElement( name = "AT", color = primaryGreen, - contactPhoto = ProfileContactPhoto( - Address.fromSerialized("05c0d6db0f2d400c392a745105dc93b666642b9dd43993e97c2c4d7440c453b620"), - "305422957" - ) + contactPhoto = null ))) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtils.kt index 0f5b1200cb..b5ea2e71a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtils.kt @@ -21,14 +21,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import network.loki.messenger.R -import org.session.libsession.avatars.ContactPhoto -import org.session.libsession.avatars.ProfileContactPhoto import org.session.libsession.database.StorageProtocol import org.session.libsession.utilities.Address -import org.session.libsession.utilities.UsernameUtils import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.IdPrefix import org.thoughtcrime.securesms.database.GroupDatabase +import org.thoughtcrime.securesms.database.RecipientRepository import java.math.BigInteger import java.security.MessageDigest import java.util.Locale @@ -38,9 +36,9 @@ import javax.inject.Singleton @Singleton class AvatarUtils @Inject constructor( @ApplicationContext private val context: Context, - private val usernameUtils: UsernameUtils, private val groupDatabase: GroupDatabase, // for legacy groups private val storage: Lazy, + private val recipientRepository: RecipientRepository, ) { // Hardcoded possible bg colors for avatar backgrounds private val avatarBgColors = arrayOf( @@ -55,7 +53,7 @@ class AvatarUtils @Inject constructor( suspend fun getUIDataFromAccountId(accountId: String): AvatarUIData = withContext(Dispatchers.Default) { - getUIDataFromRecipient(Recipient.from(context, Address.fromSerialized(accountId), false)) + getUIDataFromRecipient(recipientRepository.getRecipient(Address.fromSerialized(accountId))) } suspend fun getUIDataFromRecipient(recipient: Recipient?): AvatarUIData { @@ -72,7 +70,7 @@ class AvatarUtils @Inject constructor( // if the group has a custom image, use that // other wise make up a double avatar from the first two members // if there is only one member then use that member + an unknown icon coloured based on the group id - if (recipient.profileAvatar != null) { + if (recipient.avatar != null) { elements.add(getUIElementForRecipient(recipient)) } else { val members = if (recipient.isLegacyGroupRecipient) { @@ -93,11 +91,7 @@ class AvatarUtils @Inject constructor( // and the second should be the unknown icon with a colour based on the group id elements.add( getUIElementForRecipient( - Recipient.from( - context, Address.fromSerialized( - members[0].toString() - ), false - ) + recipientRepository.getRecipientOrEmpty(Address.fromSerialized(members[0].toString())) ) ) @@ -111,9 +105,7 @@ class AvatarUtils @Inject constructor( else -> { members.forEach { elements.add( - getUIElementForRecipient( - Recipient.from(context, it, false) - ) + getUIElementForRecipient(recipientRepository.getRecipientOrEmpty(it)) ) } } @@ -131,15 +123,14 @@ class AvatarUtils @Inject constructor( private fun getUIElementForRecipient(recipient: Recipient): AvatarUIElement { // name - val name = if(recipient.isLocalNumber) usernameUtils.getCurrentUsernameWithAccountIdFallback() - else recipient.name + val name = recipient.displayName val defaultColor = Color(getColorFromKey(recipient.address.toString())) // custom image val (contactPhoto, customIcon, color) = when { // use custom image if there is one - hasAvatar(recipient.contactPhoto) -> Triple(recipient.contactPhoto, null, defaultColor) + recipient.avatar != null -> Triple(recipient.avatar!!, null, defaultColor) // communities without a custom image should use a default image recipient.isCommunityRecipient -> Triple(null, R.drawable.session_logo, null) @@ -154,11 +145,6 @@ class AvatarUtils @Inject constructor( ) } - private fun hasAvatar(contactPhoto: ContactPhoto?): Boolean { - val avatar = (contactPhoto as? ProfileContactPhoto)?.avatarObject - return contactPhoto != null && avatar != "0" && avatar != "" - } - fun getColorFromKey(hashString: String): Int { val hash: Long if (hashString.length >= 12 && hashString.matches(Regex("^[0-9A-Fa-f]+\$"))) { @@ -259,7 +245,7 @@ data class AvatarUIElement( val name: String? = null, val color: Color? = null, @DrawableRes val icon: Int? = null, - val contactPhoto: ContactPhoto? = null, + val contactPhoto: Any? = null, ) sealed class AvatarBadge(@DrawableRes val icon: Int){ diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ClearDataUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ClearDataUtils.kt index 024d73d825..d4f1abab96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ClearDataUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ClearDataUtils.kt @@ -3,25 +3,27 @@ package org.thoughtcrime.securesms.util import android.annotation.SuppressLint import android.app.Application import android.content.Intent +import androidx.core.content.edit +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper -import org.thoughtcrime.securesms.dependencies.ConfigFactory -import org.thoughtcrime.securesms.home.HomeActivity -import javax.inject.Inject -import androidx.core.content.edit -import kotlinx.coroutines.CoroutineDispatcher import okio.ByteString.Companion.decodeHex import org.session.libsession.messaging.notifications.TokenFetcher +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.hexEncodedPublicKey +import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.database.Storage +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.glide.CommunityFileDownloadWorker +import org.thoughtcrime.securesms.glide.EncryptedFileDownloadWorker +import org.thoughtcrime.securesms.home.HomeActivity import org.thoughtcrime.securesms.migration.DatabaseMigrationManager +import javax.inject.Inject class ClearDataUtils @Inject constructor( private val application: Application, @@ -48,8 +50,13 @@ class ClearDataUtils @Inject constructor( TextSecurePreferences.clearAll(application) application.getSharedPreferences(ApplicationContext.PREFERENCES_NAME, 0).edit(commit = true) { clear() } + application.cacheDir.deleteRecursively() + application.filesDir.deleteRecursively() configFactory.clearAll() + EncryptedFileDownloadWorker.cancelAll(application) + CommunityFileDownloadWorker.cancelAll(application) + // The token deletion is nice but not critical, so don't let it block the rest of the process runCatching { tokenFetcher.resetToken() diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ContactUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ContactUtilities.kt deleted file mode 100644 index c9b91d50a4..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ContactUtilities.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.thoughtcrime.securesms.util - -import android.content.Context -import org.session.libsession.utilities.recipients.Recipient -import org.thoughtcrime.securesms.dependencies.DatabaseComponent - -typealias LastMessageSentTimestamp = Long -object ContactUtilities { - - @JvmStatic - fun getAllContacts(context: Context): Set> { - val threadDatabase = DatabaseComponent.get(context).threadDatabase() - val cursor = threadDatabase.conversationList - val result = mutableSetOf>() - threadDatabase.readerFor(cursor).use { reader -> - while (reader.next != null) { - val thread = reader.current - result.add(Pair(thread.recipient, thread.lastMessage?.timestamp ?: 0)) - } - } - return result - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FlowUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/FlowUtils.kt index e5b35b8931..131b6504c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FlowUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FlowUtils.kt @@ -1,12 +1,17 @@ package org.thoughtcrime.securesms.util +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn /** * Buffers items from the flow and emits them in batches. The batch will have size [maxItems] and @@ -40,5 +45,22 @@ fun Flow.timedBuffer(timeoutMillis: Long, maxItems: Int): Flow> { } } +fun Flow.mapToStateFlow( + scope: CoroutineScope, + initialData: T, + valueGetter: (T) -> R +): StateFlow { + return map { valueGetter(it) } + .stateIn(scope, SharingStarted.Eagerly, valueGetter(initialData)) +} + +fun StateFlow.mapStateFlow( + scope: CoroutineScope, + valueGetter: (T) -> R +): StateFlow { + return map { valueGetter(it) } + .stateIn(scope, SharingStarted.Eagerly, valueGetter(value)) +} + @OptIn(ExperimentalCoroutinesApi::class) fun Flow>.flatten(): Flow = flatMapConcat { it.asFlow() } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt b/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt deleted file mode 100644 index 54fe4361f9..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt +++ /dev/null @@ -1,442 +0,0 @@ -package org.thoughtcrime.securesms.util - -import android.content.Context -import network.loki.messenger.libsession_util.Curve25519 -import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.contacts.Contact -import org.session.libsession.messaging.messages.signal.IncomingTextMessage -import org.session.libsession.messaging.messages.signal.OutgoingTextMessage -import org.session.libsession.messaging.open_groups.OpenGroup -import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.GroupUtil -import org.session.libsession.utilities.recipients.Recipient -import org.session.libsignal.crypto.ecc.Curve -import org.session.libsignal.crypto.ecc.DjbECPrivateKey -import org.session.libsignal.crypto.ecc.DjbECPublicKey -import org.session.libsignal.crypto.ecc.ECKeyPair -import org.session.libsignal.messages.SignalServiceGroup -import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.guava.Optional -import org.session.libsignal.utilities.hexEncodedPublicKey -import org.thoughtcrime.securesms.crypto.KeyPairUtilities -import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.groups.GroupManager -import java.security.SecureRandom -import kotlin.random.asKotlinRandom - -object MockDataGenerator { - private var printProgress = true - private var hasStartedGenerationThisRun = false - - // FIXME: Update this to run in a transaction instead of individual db writes (should drastically speed it up) - fun generateMockData(context: Context) { - // Don't re-generate the mock data if it already exists - val mockDataExistsRecipient = Recipient.from(context, Address.fromSerialized("MockDatabaseThread"), false) - val storage = MessagingModuleConfiguration.shared.storage - val threadDb = DatabaseComponent.get(context).threadDatabase() - val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase() - val contactDb = DatabaseComponent.get(context).sessionContactDatabase() - val recipientDb = DatabaseComponent.get(context).recipientDatabase() - val smsDb = DatabaseComponent.get(context).smsDatabase() - - if (hasStartedGenerationThisRun || threadDb.getThreadIdIfExistsFor(mockDataExistsRecipient) != -1L) { - hasStartedGenerationThisRun = true - return - } - - /// The mock data generation is quite slow, there are 3 parts which take a decent amount of time (deleting the account afterwards will - /// also take a long time): - /// Generating the threads & content - ~3m per 100 - val dmThreadCount: Int = 1000 - val closedGroupThreadCount: Int = 50 - val openGroupThreadCount: Int = 20 - val messageRangePerThread: List = listOf(0..500) - val dmRandomSeed: String = "1111" - val cgRandomSeed: String = "2222" - val ogRandomSeed: String = "3333" - val chunkSize: Int = 1000 // Chunk up the thread writing to prevent memory issues - val stringContent: List = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 ".map { it.toString() } - val wordContent: List = listOf("alias", "consequatur", "aut", "perferendis", "sit", "voluptatem", "accusantium", "doloremque", "aperiam", "eaque", "ipsa", "quae", "ab", "illo", "inventore", "veritatis", "et", "quasi", "architecto", "beatae", "vitae", "dicta", "sunt", "explicabo", "aspernatur", "aut", "odit", "aut", "fugit", "sed", "quia", "consequuntur", "magni", "dolores", "eos", "qui", "ratione", "voluptatem", "sequi", "nesciunt", "neque", "dolorem", "ipsum", "quia", "dolor", "sit", "amet", "consectetur", "adipisci", "velit", "sed", "quia", "non", "numquam", "eius", "modi", "tempora", "incidunt", "ut", "labore", "et", "dolore", "magnam", "aliquam", "quaerat", "voluptatem", "ut", "enim", "ad", "minima", "veniam", "quis", "nostrum", "exercitationem", "ullam", "corporis", "nemo", "enim", "ipsam", "voluptatem", "quia", "voluptas", "sit", "suscipit", "laboriosam", "nisi", "ut", "aliquid", "ex", "ea", "commodi", "consequatur", "quis", "autem", "vel", "eum", "iure", "reprehenderit", "qui", "in", "ea", "voluptate", "velit", "esse", "quam", "nihil", "molestiae", "et", "iusto", "odio", "dignissimos", "ducimus", "qui", "blanditiis", "praesentium", "laudantium", "totam", "rem", "voluptatum", "deleniti", "atque", "corrupti", "quos", "dolores", "et", "quas", "molestias", "excepturi", "sint", "occaecati", "cupiditate", "non", "provident", "sed", "ut", "perspiciatis", "unde", "omnis", "iste", "natus", "error", "similique", "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollitia", "animi", "id", "est", "laborum", "et", "dolorum", "fuga", "et", "harum", "quidem", "rerum", "facilis", "est", "et", "expedita", "distinctio", "nam", "libero", "tempore", "cum", "soluta", "nobis", "est", "eligendi", "optio", "cumque", "nihil", "impedit", "quo", "porro", "quisquam", "est", "qui", "minus", "id", "quod", "maxime", "placeat", "facere", "possimus", "omnis", "voluptas", "assumenda", "est", "omnis", "dolor", "repellendus", "temporibus", "autem", "quibusdam", "et", "aut", "consequatur", "vel", "illum", "qui", "dolorem", "eum", "fugiat", "quo", "voluptas", "nulla", "pariatur", "at", "vero", "eos", "et", "accusamus", "officiis", "debitis", "aut", "rerum", "necessitatibus", "saepe", "eveniet", "ut", "et", "voluptates", "repudiandae", "sint", "et", "molestiae", "non", "recusandae", "itaque", "earum", "rerum", "hic", "tenetur", "a", "sapiente", "delectus", "ut", "aut", "reiciendis", "voluptatibus", "maiores", "doloribus", "asperiores", "repellat") - val timestampNow: Long = System.currentTimeMillis() - val userAccountId: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - val logProgress: ((String, String) -> Unit) = logProgress@{ title, event -> - if (!printProgress) { return@logProgress } - - Log.i("[MockDataGenerator]", "${System.currentTimeMillis()} $title - $event") - } - - hasStartedGenerationThisRun = true - - // FIXME: Make sure this data doesn't go off device somehow? - logProgress("", "Start") - - // First create the thread used to indicate that the mock data has been generated - threadDb.getOrCreateThreadIdFor(mockDataExistsRecipient) - - // -- DM Thread - val dmThreadRandomGenerator: SecureRandom = SecureRandom(dmRandomSeed.toByteArray()) - var dmThreadIndex: Int = 0 - logProgress("DM Threads", "Start Generating $dmThreadCount threads") - - while (dmThreadIndex < dmThreadCount) { - val remainingThreads: Int = (dmThreadCount - dmThreadIndex) - - (0 until Math.min(chunkSize, remainingThreads)).forEach { index -> - val threadIndex: Int = (dmThreadIndex + index) - - logProgress("DM Thread $threadIndex", "Start") - - val dataBytes = (0 until 16).map { dmThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() } - val randomAccountId: String = KeyPairUtilities.generate(dataBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey - val isMessageRequest: Boolean = dmThreadRandomGenerator.nextBoolean() - val contactNameLength: Int = (5 + dmThreadRandomGenerator.nextInt(15)) - - val numMessages: Int = ( - messageRangePerThread[threadIndex % messageRangePerThread.count()].first + - dmThreadRandomGenerator.nextInt(messageRangePerThread[threadIndex % messageRangePerThread.count()].last()) - ) - - // Generate the thread - val recipient = Recipient.from(context, Address.fromSerialized(randomAccountId), false) - val contact = Contact(randomAccountId) - val threadId = threadDb.getOrCreateThreadIdFor(recipient) - - // Generate the contact - val contactIsApproved: Boolean = (!isMessageRequest || dmThreadRandomGenerator.nextBoolean()) - contactDb.setContact(contact) - contactDb.setContactIsTrusted(contact, true, threadId) - recipientDb.setApproved(recipient, contactIsApproved) - recipientDb.setApprovedMe(recipient, (!isMessageRequest && (dmThreadRandomGenerator.nextInt(10) < 8))) // 80% approved the current user - - contact.name = (0 until dmThreadRandomGenerator.nextInt(contactNameLength)) - .map { stringContent.random(dmThreadRandomGenerator.asKotlinRandom()) } - .joinToString() - recipientDb.setProfileName(recipient, contact.name) - contactDb.setContact(contact) - - // Generate the message history (Note: Unapproved message requests will only include incoming messages) - logProgress("DM Thread $threadIndex", "Generate $numMessages Messages") - (0 until numMessages).forEach { index -> - val isIncoming: Boolean = ( - dmThreadRandomGenerator.nextBoolean() && - (!isMessageRequest || contactIsApproved) - ) - val messageWords: Int = (1 + dmThreadRandomGenerator.nextInt(19)) - - if (isIncoming) { - smsDb.insertMessageInbox( - IncomingTextMessage( - recipient.address, - 1, - (timestampNow - (index * 5000)), - (0 until messageWords) - .map { wordContent.random(dmThreadRandomGenerator.asKotlinRandom()) } - .joinToString(), - Optional.absent(), - 0, - 0, - false, - -1, - false - ), - (timestampNow - (index * 5000)), - false - ) - } - else { - smsDb.insertMessageOutbox( - threadId, - OutgoingTextMessage( - recipient, - (0 until messageWords) - .map { wordContent.random(dmThreadRandomGenerator.asKotlinRandom()) } - .joinToString(), - 0, - 0, - -1, - (timestampNow - (index * 5000)) - ), - (timestampNow - (index * 5000)), - false - ) - } - } - - logProgress("DM Thread $threadIndex", "Done") - } - logProgress("DM Threads", "Done") - - dmThreadIndex += chunkSize - } - logProgress("DM Threads", "Done") - - // -- Closed Group - - val cgThreadRandomGenerator: SecureRandom = SecureRandom(cgRandomSeed.toByteArray()) - var cgThreadIndex: Int = 0 - logProgress("Closed Group Threads", "Start Generating $closedGroupThreadCount threads") - - while (cgThreadIndex < closedGroupThreadCount) { - val remainingThreads: Int = (closedGroupThreadCount - cgThreadIndex) - - (0 until Math.min(chunkSize, remainingThreads)).forEach { index -> - val threadIndex: Int = (cgThreadIndex + index) - - logProgress("Closed Group Thread $threadIndex", "Start") - - val dataBytes = (0 until 16).map { cgThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() } - val randomGroupPublicKey: String = KeyPairUtilities.generate(dataBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey - val groupNameLength: Int = (5 + cgThreadRandomGenerator.nextInt(15)) - val groupName: String = (0 until groupNameLength) - .map { stringContent.random(cgThreadRandomGenerator.asKotlinRandom()) } - .joinToString() - val numGroupMembers: Int = cgThreadRandomGenerator.nextInt (10) - val numMessages: Int = ( - messageRangePerThread[threadIndex % messageRangePerThread.count()].first + - cgThreadRandomGenerator.nextInt(messageRangePerThread[threadIndex % messageRangePerThread.count()].last()) - ) - - // Generate the Contacts in the group - val members: MutableList = mutableListOf(userAccountId) - logProgress("Closed Group Thread $threadIndex", "Generate $numGroupMembers Contacts") - - (0 until numGroupMembers).forEach { - val contactBytes = (0 until 16).map { cgThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() } - val randomAccountId: String = KeyPairUtilities.generate(contactBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey - val contactNameLength: Int = (5 + cgThreadRandomGenerator.nextInt(15)) - - val recipient = Recipient.from(context, Address.fromSerialized(randomAccountId), false) - val contact = Contact(randomAccountId) - contactDb.setContact(contact) - recipientDb.setApproved(recipient, true) - recipientDb.setApprovedMe(recipient, true) - - contact.name = (0 until cgThreadRandomGenerator.nextInt(contactNameLength)) - .map { stringContent.random(cgThreadRandomGenerator.asKotlinRandom()) } - .joinToString() - recipientDb.setProfileName(recipient, contact.name) - contactDb.setContact(contact) - members.add(randomAccountId) - } - - val groupId = GroupUtil.doubleEncodeGroupID(randomGroupPublicKey) - val threadId = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupId)) - val adminUserId = members.random(cgThreadRandomGenerator.asKotlinRandom()) - storage.createGroup( - groupId, - groupName, - members.map { Address.fromSerialized(it) }, - null, - null, - listOf(Address.fromSerialized(adminUserId)), - timestampNow - ) - storage.setProfileSharing(Address.fromSerialized(groupId), true) - storage.addClosedGroupPublicKey(randomGroupPublicKey) - - // Add the group to the user's set of public keys to poll for and store the key pair - val encryptionKeyPair = Curve25519.generateKeyPair().let { - ECKeyPair( - DjbECPublicKey(it.pubKey.data), - DjbECPrivateKey(it.secretKey.data) - ) - } - storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, randomGroupPublicKey, System.currentTimeMillis()) - storage.createInitialConfigGroup(randomGroupPublicKey, groupName, GroupUtil.createConfigMemberMap(members, setOf(adminUserId)), System.currentTimeMillis(), encryptionKeyPair, 0) - - // Add the group created message - if (userAccountId == adminUserId) { - storage.insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, groupName, members, listOf(adminUserId), threadId, (timestampNow - (numMessages * 5000))) - } else { - storage.insertIncomingInfoMessage(context, adminUserId, groupId, SignalServiceGroup.Type.CREATION, groupName, members, listOf(adminUserId), (timestampNow - (numMessages * 5000))) - } - - // Generate the message history (Note: Unapproved message requests will only include incoming messages) - logProgress("Closed Group Thread $threadIndex", "Generate $numMessages Messages") - - (0 until numGroupMembers).forEach { - val messageWords: Int = (1 + cgThreadRandomGenerator.nextInt(19)) - val senderId: String = members.random(cgThreadRandomGenerator.asKotlinRandom()) - - if (senderId != userAccountId) { - smsDb.insertMessageInbox( - IncomingTextMessage( - Address.fromSerialized(senderId), - 1, - (timestampNow - (index * 5000)), - (0 until messageWords) - .map { wordContent.random(cgThreadRandomGenerator.asKotlinRandom()) } - .joinToString(), - Optional.absent(), - 0, - 0, - false, - -1, - false - ), - (timestampNow - (index * 5000)), - false - ) - } - else { - smsDb.insertMessageOutbox( - threadId, - OutgoingTextMessage( - threadDb.getRecipientForThreadId(threadId), - (0 until messageWords) - .map { wordContent.random(cgThreadRandomGenerator.asKotlinRandom()) } - .joinToString(), - 0, - 0, - -1, - (timestampNow - (index * 5000)) - ), - (timestampNow - (index * 5000)), - false - ) - } - } - - logProgress("Closed Group Thread $threadIndex", "Done") - } - - cgThreadIndex += chunkSize - } - logProgress("Closed Group Threads", "Done") - - // --Open Group - - val ogThreadRandomGenerator: SecureRandom = SecureRandom(cgRandomSeed.toByteArray()) - var ogThreadIndex: Int = 0 - logProgress("Open Group Threads", "Start Generating $openGroupThreadCount threads") - - while (ogThreadIndex < openGroupThreadCount) { - val remainingThreads: Int = (openGroupThreadCount - ogThreadIndex) - - (0 until Math.min(chunkSize, remainingThreads)).forEach { index -> - val threadIndex: Int = (ogThreadIndex + index) - - logProgress("Open Group Thread $threadIndex", "Start") - - val dataBytes = (0 until 32).map { ogThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() } - val randomGroupPublicKey: String = KeyPairUtilities.generate(dataBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey - val serverNameLength: Int = (5 + ogThreadRandomGenerator.nextInt(15)) - val roomNameLength: Int = (5 + ogThreadRandomGenerator.nextInt(15)) - val roomDescriptionLength: Int = (10 + ogThreadRandomGenerator.nextInt(40)) - val serverName: String = (0 until serverNameLength) - .map { stringContent.random(ogThreadRandomGenerator.asKotlinRandom()) } - .joinToString() - val roomName: String = (0 until roomNameLength) - .map { stringContent.random(ogThreadRandomGenerator.asKotlinRandom()) } - .joinToString() - val roomDescription: String = (0 until roomDescriptionLength) - .map { stringContent.random(ogThreadRandomGenerator.asKotlinRandom()) } - .joinToString() - val numGroupMembers: Int = ogThreadRandomGenerator.nextInt(250) - val numMessages: Int = ( - messageRangePerThread[threadIndex % messageRangePerThread.count()].first + - ogThreadRandomGenerator.nextInt(messageRangePerThread[threadIndex % messageRangePerThread.count()].last()) - ) - - // Generate the Contacts in the group - val members: MutableList = mutableListOf(userAccountId) - logProgress("Open Group Thread $threadIndex", "Generate $numGroupMembers Contacts") - - (0 until numGroupMembers).forEach { - val contactBytes = (0 until 16).map { ogThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() } - val randomAccountId: String = KeyPairUtilities.generate(contactBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey - val contactNameLength: Int = (5 + ogThreadRandomGenerator.nextInt(15)) - - val recipient = Recipient.from(context, Address.fromSerialized(randomAccountId), false) - val contact = Contact(randomAccountId) - contactDb.setContact(contact) - recipientDb.setApproved(recipient, true) - recipientDb.setApprovedMe(recipient, true) - - contact.name = (0 until ogThreadRandomGenerator.nextInt(contactNameLength)) - .map { stringContent.random(cgThreadRandomGenerator.asKotlinRandom()) } - .joinToString() - recipientDb.setProfileName(recipient, contact.name) - contactDb.setContact(contact) - members.add(randomAccountId) - } - - // Create the open group model and the thread - val openGroupId = "$serverName.$roomName" - val threadId = GroupManager.createOpenGroup(openGroupId, context, null, roomName).threadId - val hasBlinding: Boolean = ogThreadRandomGenerator.nextBoolean() - - // Generate the capabilities and other data - storage.setOpenGroupPublicKey(serverName, randomGroupPublicKey) - storage.setServerCapabilities( - serverName, - ( - listOf(OpenGroupApi.Capability.SOGS.name.lowercase()) + - if (hasBlinding) { listOf(OpenGroupApi.Capability.BLIND.name.lowercase()) } else { emptyList() } - ) - ) - storage.setUserCount(roomName, serverName, numGroupMembers) - lokiThreadDB.setOpenGroupChat( - OpenGroup( - server = serverName, room = roomName, publicKey = randomGroupPublicKey, - name = roomName, imageId = null, canWrite = true, infoUpdates = 0, - description = null - ), threadId - ) - - // Generate the message history (Note: Unapproved message requests will only include incoming messages) - logProgress("Open Group Thread $threadIndex", "Generate $numMessages Messages") - - (0 until numMessages).forEach { index -> - val messageWords: Int = (1 + ogThreadRandomGenerator.nextInt(19)) - val senderId: String = members.random(ogThreadRandomGenerator.asKotlinRandom()) - - if (senderId != userAccountId) { - smsDb.insertMessageInbox( - IncomingTextMessage( - Address.fromSerialized(senderId), - 1, - (timestampNow - (index * 5000)), - (0 until messageWords) - .map { wordContent.random(ogThreadRandomGenerator.asKotlinRandom()) } - .joinToString(), - Optional.absent(), - 0, - 0, - false, - -1, - false - ), - (timestampNow - (index * 5000)), - false - ) - } else { - smsDb.insertMessageOutbox( - threadId, - OutgoingTextMessage( - threadDb.getRecipientForThreadId(threadId), - (0 until messageWords) - .map { wordContent.random(ogThreadRandomGenerator.asKotlinRandom()) } - .joinToString(), - 0, - 0, - -1, - (timestampNow - (index * 5000)) - ), - (timestampNow - (index * 5000)), - false - ) - } - } - - logProgress("Open Group Thread $threadIndex", "Done") - } - - ogThreadIndex += chunkSize - } - - logProgress("Open Group Threads", "Done") - logProgress("", "Complete") - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt index cf140baefe..07dab8e7f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt @@ -47,13 +47,9 @@ object SessionMetaProtocol { return hasBody || hasAttachment || hasLinkPreview } - @JvmStatic - fun shouldSendReadReceipt(recipient: Recipient): Boolean { - return !recipient.isGroupOrCommunityRecipient && recipient.isApproved && !recipient.isBlocked - } @JvmStatic fun shouldSendTypingIndicator(recipient: Recipient): Boolean { - return !recipient.isGroupOrCommunityRecipient && recipient.isApproved && !recipient.isBlocked + return !recipient.isGroupOrCommunityRecipient && recipient.approved && !recipient.blocked } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtilsImpl.kt b/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtilsImpl.kt deleted file mode 100644 index c5a81da8a8..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtilsImpl.kt +++ /dev/null @@ -1,54 +0,0 @@ -package org.thoughtcrime.securesms.util - -import network.loki.messenger.libsession_util.getOrNull -import org.session.libsession.messaging.contacts.Contact -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.UsernameUtils -import org.session.libsession.utilities.truncateIdForDisplay -import org.session.libsignal.utilities.AccountId -import org.thoughtcrime.securesms.database.SessionContactDatabase -import org.thoughtcrime.securesms.dependencies.ConfigFactory - -class UsernameUtilsImpl( - private val prefs: TextSecurePreferences, - private val configFactory: ConfigFactory, - private val sessionContactDatabase: SessionContactDatabase, -): UsernameUtils { - override fun getCurrentUsernameWithAccountIdFallback(): String = prefs.getProfileName() - ?: truncateIdForDisplay( prefs.getLocalNumber() ?: "") - - override fun getCurrentUsername(): String? = prefs.getProfileName() - - override fun saveCurrentUserName(name: String) { - configFactory.withMutableUserConfigs { - it.userProfile.setName(name) - } - } - - override fun getContactNameWithAccountID( - accountID: String, - groupId: AccountId?, - contactContext: Contact.ContactContext - ): String { - val contact = sessionContactDatabase.getContactWithAccountID(accountID) - return getContactNameWithAccountID(contact, accountID, groupId, contactContext) - } - - override fun getContactNameWithAccountID( - contact: Contact?, - accountID: String, - groupId: AccountId?, - contactContext: Contact.ContactContext) - : String { - // first attempt to get the name from the contact - val userName: String? = contact?.displayName(contactContext) - ?: if(groupId != null){ - configFactory.withGroupConfigs(groupId) { it.groupMembers.getOrNull(accountID)?.name } - } else null - - // if the username is actually set to the user's accountId, truncate it - val validatedUsername = if(userName == accountID) truncateIdForDisplay(accountID) else userName - - return if(validatedUsername.isNullOrEmpty()) truncateIdForDisplay(accountID) else validatedUsername - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt index 094bd615d9..c6ad55b7fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.pm.PackageManager import android.telephony.TelephonyManager import androidx.core.content.ContextCompat +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow @@ -25,7 +26,6 @@ import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.Debouncer import org.session.libsession.utilities.Util -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ICE_CANDIDATES import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.dependencies.DatabaseComponent @@ -61,13 +61,16 @@ import org.webrtc.SurfaceViewRenderer import java.nio.ByteBuffer import java.util.ArrayDeque import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton import kotlin.math.abs import org.thoughtcrime.securesms.webrtc.data.State as CallState -class CallManager( - private val context: Context, +@Singleton +class CallManager @Inject constructor( + @ApplicationContext private val context: Context, audioManager: AudioManagerCompat, - private val storage: StorageProtocol + private val storage: StorageProtocol, ): PeerConnection.Observer, SignalAudioManager.EventListener, CameraEventListener, DataChannel.Observer { @@ -76,7 +79,7 @@ class CallManager( data class VideoEnabled(val isEnabled: Boolean): StateEvent() data class CallStateUpdate(val state: CallState): StateEvent() data class AudioDeviceUpdate(val selectedDevice: AudioDevice, val audioDevices: Set): StateEvent() - data class RecipientUpdate(val recipient: Recipient?): StateEvent() { + data class RecipientUpdate(val recipient: Address?): StateEvent() { companion object { val UNKNOWN = RecipientUpdate(recipient = null) } @@ -92,6 +95,8 @@ class CallManager( private const val DATA_CHANNEL_NAME = "signaling" } + private val Address.isLocalNumber: Boolean get() = address.equals(storage.getUserPublicKey(), ignoreCase = true) + private val signalAudioManager: SignalAudioManager = SignalAudioManager(context, this, audioManager) private val peerConnectionObservers = mutableSetOf() @@ -148,7 +153,7 @@ class CallManager( var pendingOfferTime: Long = -1 var preOfferCallData: PreOffer? = null var callId: UUID? = null - var recipient: Recipient? = null + var recipient: Address? = null set(value) { field = value _recipientEvents.value = RecipientUpdate(value) @@ -310,7 +315,7 @@ class CallManager( queueOutgoingIce(expectedCallId, expectedRecipient) } - private fun queueOutgoingIce(expectedCallId: UUID, expectedRecipient: Recipient) { + private fun queueOutgoingIce(expectedCallId: UUID, expectedRecipient: Address) { postViewModelState(CallViewModel.State.CALL_SENDING_ICE) outgoingIceDebouncer.publish { val currentCallId = this.callId ?: return@publish @@ -330,7 +335,7 @@ class CallManager( currentCallId ) .applyExpiryMode(thread) - .also { MessageSender.sendNonDurably(it, currentRecipient.address, isSyncMessage = currentRecipient.isLocalNumber) } + .also { MessageSender.sendNonDurably(it, currentRecipient, isSyncMessage = currentRecipient.isLocalNumber) } } } @@ -442,7 +447,7 @@ class CallManager( handleMirroring() } - fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) { + fun onPreOffer(callId: UUID, recipient: Address, onSuccess: () -> Unit) { stateProcessor.processEvent(Event.ReceivePreOffer) { if (preOfferCallData != null) { Log.d(TAG, "Received new pre-offer when we are already expecting one") @@ -454,7 +459,7 @@ class CallManager( } } - fun onNewOffer(offer: String, callId: UUID, recipient: Recipient): Promise { + fun onNewOffer(offer: String, callId: UUID, recipient: Address): Promise { if (callId != this.callId) return Promise.ofFail(NullPointerException("No callId")) if (recipient != this.recipient) return Promise.ofFail(NullPointerException("No recipient")) val thread = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient) @@ -474,13 +479,13 @@ class CallManager( pendingIncomingIceUpdates.clear() val answerMessage = CallMessage.answer(answer.description, callId).applyExpiryMode(thread) Log.i("Loki", "Posting new answer") - MessageSender.sendNonDurably(answerMessage, recipient.address, isSyncMessage = recipient.isLocalNumber) + MessageSender.sendNonDurably(answerMessage, recipient, isSyncMessage = recipient.isLocalNumber) } else { Promise.ofFail(Exception("Couldn't reconnect from current state")) } } - fun onIncomingRing(offer: String, callId: UUID, recipient: Recipient, callTime: Long, onSuccess: () -> Unit) { + fun onIncomingRing(offer: String, callId: UUID, recipient: Address, callTime: Long, onSuccess: () -> Unit) { postConnectionEvent(Event.ReceiveOffer) { this.callId = callId this.recipient = recipient @@ -526,9 +531,9 @@ class CallManager( val sendAnswerMessage = MessageSender.sendNonDurably(CallMessage.answer( answer.description, callId - ).applyExpiryMode(thread), recipient.address, isSyncMessage = recipient.isLocalNumber) + ).applyExpiryMode(thread), recipient, isSyncMessage = recipient.isLocalNumber) - insertCallMessage(recipient.address.toString(), CallMessageType.CALL_INCOMING, false) + insertCallMessage(recipient.toString(), CallMessageType.CALL_INCOMING, false) while (pendingIncomingIceUpdates.isNotEmpty()) { val candidate = pendingIncomingIceUpdates.pop() ?: break @@ -579,14 +584,14 @@ class CallManager( val thread = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient) return MessageSender.sendNonDurably(CallMessage.preOffer( callId - ).applyExpiryMode(thread), recipient.address, isSyncMessage = recipient.isLocalNumber).bind { + ).applyExpiryMode(thread), recipient, isSyncMessage = recipient.isLocalNumber).bind { Log.d("Loki", "Sent pre-offer") Log.d("Loki", "Sending offer") postViewModelState(CallViewModel.State.CALL_OFFER_OUTGOING) MessageSender.sendNonDurably(CallMessage.offer( offer.description, callId - ).applyExpiryMode(thread), recipient.address, isSyncMessage = recipient.isLocalNumber).success { + ).applyExpiryMode(thread), recipient, isSyncMessage = recipient.isLocalNumber).success { Log.d("Loki", "Sent offer") }.fail { Log.e("Loki", "Failed to send offer", it) @@ -602,8 +607,8 @@ class CallManager( stateProcessor.processEvent(Event.DeclineCall) { val thread = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient) MessageSender.sendNonDurably(CallMessage.endCall(callId).applyExpiryMode(thread), Address.fromSerialized(userAddress), isSyncMessage = true) - MessageSender.sendNonDurably(CallMessage.endCall(callId).applyExpiryMode(thread), recipient.address, isSyncMessage = recipient.isLocalNumber) - insertCallMessage(recipient.address.toString(), CallMessageType.CALL_INCOMING) + MessageSender.sendNonDurably(CallMessage.endCall(callId).applyExpiryMode(thread), recipient, isSyncMessage = recipient.isLocalNumber) + insertCallMessage(recipient.toString(), CallMessageType.CALL_INCOMING) } } @@ -612,7 +617,7 @@ class CallManager( stateProcessor.processEvent(Event.IgnoreCall) } - fun handleLocalHangup(intentRecipient: Recipient?) { + fun handleLocalHangup(intentRecipient: Address?) { val recipient = recipient ?: return val callId = callId ?: return @@ -627,7 +632,7 @@ class CallManager( } val thread = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient) - MessageSender.sendNonDurably(CallMessage.endCall(callId).applyExpiryMode(thread), recipient.address, isSyncMessage = recipient.isLocalNumber) + MessageSender.sendNonDurably(CallMessage.endCall(callId).applyExpiryMode(thread), recipient, isSyncMessage = recipient.isLocalNumber) } } @@ -786,7 +791,7 @@ class CallManager( } } - fun handleResponseMessage(recipient: Recipient, callId: UUID, answer: SessionDescription) { + fun handleResponseMessage(recipient: Address, callId: UUID, answer: SessionDescription) { if (recipient != this.recipient || callId != this.callId) { Log.w(TAG,"Got answer for recipient and call ID we're not currently dialing") return @@ -859,7 +864,7 @@ class CallManager( }) connection.setLocalDescription(offer) val thread = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient) - MessageSender.sendNonDurably(CallMessage.offer(offer.description, callId).applyExpiryMode(thread), recipient.address, isSyncMessage = recipient.isLocalNumber) + MessageSender.sendNonDurably(CallMessage.offer(offer.description, callId).applyExpiryMode(thread), recipient, isSyncMessage = recipient.isLocalNumber) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt index 11acf5a7a5..0b4ec3ca71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt @@ -14,7 +14,6 @@ import org.session.libsession.messaging.utilities.WebRtcUtils import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ANSWER import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.END_CALL import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ICE_CANDIDATES @@ -22,6 +21,7 @@ import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.OFFER import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PRE_OFFER import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PROVISIONAL_ANSWER import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.permissions.Permissions import org.webrtc.IceCandidate import javax.inject.Inject @@ -32,7 +32,8 @@ class CallMessageProcessor @Inject constructor( @ApplicationContext private val context: Context, private val textSecurePreferences: TextSecurePreferences, private val storage: StorageProtocol, - private val webRtcBridge: WebRtcCallBridge + private val webRtcBridge: WebRtcCallBridge, + private val recipientRepository: RecipientRepository, ) { companion object { @@ -46,7 +47,7 @@ class CallMessageProcessor @Inject constructor( val nextMessage = WebRtcUtils.SIGNAL_QUEUE.receive() Log.d("Loki", nextMessage.type?.name ?: "CALL MESSAGE RECEIVED") val sender = nextMessage.sender ?: continue - val approvedContact = Recipient.from(context, Address.fromSerialized(sender), false).isApproved + val approvedContact = recipientRepository.getRecipient(Address.fromSerialized(sender))?.approved == true Log.i("Loki", "Contact is approved?: $approvedContact") if (!approvedContact && storage.getUserPublicKey() != sender) continue diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallNotificationBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallNotificationBuilder.kt index 6738814846..10d7e37076 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallNotificationBuilder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallNotificationBuilder.kt @@ -5,7 +5,6 @@ import android.app.PendingIntent import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.app.NotificationCompat @@ -48,7 +47,7 @@ class CallNotificationBuilder { .setOngoing(true) var recipName = "Unknown" - recipient?.name?.let { name -> + recipient?.displayName?.let { name -> recipName = name } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt index 88659c4dfc..4ef89f27b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt @@ -14,9 +14,11 @@ import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.stateIn import network.loki.messenger.R import org.session.libsession.utilities.Address -import org.session.libsession.utilities.UsernameUtils -import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsession.utilities.currentUserName +import org.session.libsession.utilities.recipients.displayNameOrFallback import org.thoughtcrime.securesms.conversation.v2.ViewUtil +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_ANSWER_INCOMING import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_ANSWER_OUTGOING import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_CONNECTED @@ -38,8 +40,8 @@ class CallViewModel @Inject constructor( @ApplicationContext private val context: Context, private val callManager: CallManager, private val rtcCallBridge: WebRtcCallBridge, - private val usernameUtils: UsernameUtils - + private val recipientRepository: RecipientRepository, + private val configFactory: ConfigFactoryProtocol, ): ViewModel() { //todo PHONE Can we eventually remove this state and instead use the StateMachine.kt State? @@ -196,13 +198,15 @@ class CallViewModel @Inject constructor( fun denyCall() = rtcCallBridge.handleDenyCall() fun createCall(recipientAddress: Address) = - rtcCallBridge.handleOutgoingCall(Recipient.from(context, recipientAddress, true)) + rtcCallBridge.handleOutgoingCall(recipientAddress) fun hangUp() = rtcCallBridge.handleLocalHangup(null) - fun getContactName(accountID: String) = usernameUtils.getContactNameWithAccountID(accountID) + fun getContactName(accountID: String) = + recipientRepository.getRecipientSync(Address.fromSerialized(accountID)) + .displayNameOrFallback(address = accountID) - fun getCurrentUsername() = usernameUtils.getCurrentUsernameWithAccountIdFallback() + fun getCurrentUsername() = configFactory.currentUserName data class CallState( val callLabelTitle: String?, diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PreOffer.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PreOffer.kt index dfc2dc6fb3..f07599b721 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PreOffer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PreOffer.kt @@ -1,6 +1,6 @@ package org.thoughtcrime.securesms.webrtc -import org.session.libsession.utilities.recipients.Recipient -import java.util.* +import org.session.libsession.utilities.Address +import java.util.UUID -data class PreOffer(val callId: UUID, val recipient: Recipient) \ No newline at end of file +data class PreOffer(val callId: UUID, val recipient: Address) \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallBridge.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallBridge.kt index d65b5f0f22..5d7876ee03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallBridge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallBridge.kt @@ -19,11 +19,13 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import network.loki.messenger.BuildConfig +import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.utilities.Address import org.session.libsession.utilities.FutureTaskListener -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.RecipientRepository +import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.notifications.BackgroundPollWorker import org.thoughtcrime.securesms.service.CallForegroundService import org.thoughtcrime.securesms.util.NetworkConnectivity @@ -63,7 +65,9 @@ import org.thoughtcrime.securesms.webrtc.data.State as CallState class WebRtcCallBridge @Inject constructor( @ApplicationContext private val context: Context, private val callManager: CallManager, - private val networkConnectivity: NetworkConnectivity + private val networkConnectivity: NetworkConnectivity, + private val recipientRepository: RecipientRepository, + private val storage: StorageProtocol, ): CallManager.WebRtcListener { companion object { @@ -108,6 +112,9 @@ class WebRtcCallBridge @Inject constructor( } } + private val Address.isLocalNumber: Boolean + get() = address.equals(storage.getUserPublicKey(), ignoreCase = true) + @Synchronized private fun terminate() { @@ -149,14 +156,12 @@ class WebRtcCallBridge @Inject constructor( } private fun handleBusyCall(address: Address) { - val recipient = getRecipientFromAddress(address) - insertMissedCall(recipient, false) + insertMissedCall(address, false) } private fun handleNewOffer(address: Address, sdp: String, callId: UUID) { Log.d(TAG, "Handle new offer") - val recipient = getRecipientFromAddress(address) - callManager.onNewOffer(sdp, callId, recipient).fail { + callManager.onNewOffer(sdp, callId, address).fail { Log.e("Loki", "Error handling new offer", it) callManager.postConnectionError() terminate() @@ -187,17 +192,15 @@ class WebRtcCallBridge @Inject constructor( return@execute } - val recipient = getRecipientFromAddress(address) - if (isIncomingMessageExpired(callTime)) { Log.d(TAG, "Pre offer expired - message timestamp was deemed expired: ${System.currentTimeMillis() - callTime}s") - insertMissedCall(recipient, true) + insertMissedCall(address, true) terminate() return@execute } - callManager.onPreOffer(callId, recipient) { - setCallNotification(TYPE_INCOMING_PRE_OFFER, recipient) + callManager.onPreOffer(callId, address) { + setCallNotification(TYPE_INCOMING_PRE_OFFER, address) callManager.postViewModelState(CallViewModel.State.CALL_PRE_OFFER_INCOMING) callManager.initializeAudioForCall() callManager.startIncomingRinger() @@ -213,16 +216,15 @@ class WebRtcCallBridge @Inject constructor( private fun handleIncomingPreOffer(address: Address, sdp: String, callId: UUID, callTime: Long) { serviceExecutor.execute { - val recipient = getRecipientFromAddress(address) val preOffer = callManager.preOfferCallData - if (callManager.isPreOffer() && (preOffer == null || preOffer.callId != callId || preOffer.recipient.address != recipient.address)) { + if (callManager.isPreOffer() && (preOffer == null || preOffer.callId != callId || preOffer.recipient != address)) { Log.d(TAG, "Incoming ring from non-matching pre-offer") return@execute } - callManager.onIncomingRing(sdp, callId, recipient, callTime) { + callManager.onIncomingRing(sdp, callId, address, callTime) { if (_hasAcceptedCall.value) { - setCallNotification(TYPE_INCOMING_CONNECTING, recipient) + setCallNotification(TYPE_INCOMING_CONNECTING, address) } else { //No need to do anything here as this case is already taken care of from the pre offer that came before } @@ -238,7 +240,7 @@ class WebRtcCallBridge @Inject constructor( } } - fun handleOutgoingCall(recipient: Recipient) { + fun handleOutgoingCall(recipient: Address) { serviceExecutor.execute { if (!callManager.isIdle()) return@execute @@ -390,7 +392,7 @@ class WebRtcCallBridge @Inject constructor( } } - fun handleLocalHangup(recipient: Recipient?) { + fun handleLocalHangup(recipient: Address?) { serviceExecutor.execute { callManager.handleLocalHangup(recipient) terminate() @@ -419,16 +421,15 @@ class WebRtcCallBridge @Inject constructor( fun handleAnswerIncoming(address: Address, sdp: String, callId: UUID) { serviceExecutor.execute { try { - val recipient = getRecipientFromAddress(address) - if (recipient.isLocalNumber && callManager.currentConnectionState in CallState.CAN_DECLINE_STATES) { - handleLocalHangup(recipient) + if (address.isLocalNumber && callManager.currentConnectionState in CallState.CAN_DECLINE_STATES) { + handleLocalHangup(address) return@execute } callManager.postViewModelState(CallViewModel.State.CALL_ANSWER_OUTGOING) callManager.handleResponseMessage( - recipient, + address, callId, SessionDescription(SessionDescription.Type.ANSWER, sdp) ) @@ -523,7 +524,7 @@ class WebRtcCallBridge @Inject constructor( * - Directly sent by the notification manager * - Displayed as part of a foreground Service */ - private fun setCallNotification(type: Int, recipient: Recipient?) { + private fun setCallNotification(type: Int, recipient: Address?) { // send appropriate notification if we have permission if ( ActivityCompat.checkSelfPermission( @@ -553,10 +554,10 @@ class WebRtcCallBridge @Inject constructor( } @SuppressLint("MissingPermission") - private fun sendNotification(type: Int, recipient: Recipient?){ + private fun sendNotification(type: Int, recipient: Address?){ NotificationManagerCompat.from(context).notify( WEBRTC_NOTIFICATION, - CallNotificationBuilder.getCallInProgressNotification(context, type, recipient) + CallNotificationBuilder.getCallInProgressNotification(context, type, recipient?.let(recipientRepository::getRecipientSync)) ) } @@ -564,7 +565,7 @@ class WebRtcCallBridge @Inject constructor( * This will attempt to start a service with an attached notification, * if the service fails to start a manual notification will be sent */ - private fun startServiceOrShowNotification(type: Int, recipient: Recipient?){ + private fun startServiceOrShowNotification(type: Int, recipient: Address?){ try { ContextCompat.startForegroundService(context, CallForegroundService.startIntent(context, type, recipient)) } catch (e: Exception) { @@ -573,9 +574,7 @@ class WebRtcCallBridge @Inject constructor( } } - private fun getRecipientFromAddress(address: Address): Recipient = Recipient.from(context, address, true) - - private fun insertMissedCall(recipient: Recipient, signal: Boolean) { + private fun insertMissedCall(recipient: Address, signal: Boolean) { callManager.insertCallMessage( threadPublicKey = recipient.address.toString(), callMessageType = CallMessageType.CALL_MISSED, 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 b5f687d72d..3340bd7959 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 @@ -20,7 +20,6 @@ import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupRecord import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.guava.Optional import org.thoughtcrime.securesms.BaseViewModelTest import org.thoughtcrime.securesms.MainCoroutineRule 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 583f926dc2..678b10347c 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 @@ -9,7 +9,6 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.first import network.loki.messenger.libsession_util.util.KeyPair import org.hamcrest.CoreMatchers.equalTo -import org.hamcrest.CoreMatchers.notNullValue import org.hamcrest.CoreMatchers.nullValue import org.hamcrest.MatcherAssert.assertThat import org.junit.Before @@ -24,7 +23,6 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager -import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.BaseViewModelTest import org.thoughtcrime.securesms.MainCoroutineRule import org.thoughtcrime.securesms.database.Storage diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/MentionViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/MentionViewModelTest.kt index ffd2bb318e..afd9cce6a5 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/MentionViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/MentionViewModelTest.kt @@ -20,7 +20,6 @@ import org.robolectric.RobolectricTestRunner import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.open_groups.GroupMemberRole import org.session.libsession.messaging.open_groups.OpenGroup -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.AccountId import org.thoughtcrime.securesms.BaseViewModelTest import org.thoughtcrime.securesms.MainCoroutineRule diff --git a/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java b/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java deleted file mode 100644 index c20ce39fb1..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.thoughtcrime.securesms.recipients; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import junit.framework.TestCase; - -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; -import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.recipients.RecipientExporter; - -//FIXME AC: This test group is outdated. -@Ignore("This test group uses outdated instrumentation and needs a migration to modern tools.") -@RunWith(MockitoJUnitRunner.class) -public final class RecipientExporterTest extends TestCase { - - @Test - public void asAddContactIntent_with_neither_email_nor_phone() { - RecipientExporter exporter = RecipientExporter.export(givenRecipient("Bob", mock(Address.class))); - - assertThatThrownBy(exporter::asAddContactIntent).isExactlyInstanceOf(RuntimeException.class) - .hasMessage("Cannot export Recipient with neither phone nor email"); - } - - private Recipient givenRecipient(String profileName, Address address) { - Recipient recipient = mock(Recipient.class); - when(recipient.getProfileName()).thenReturn(profileName); - when(recipient.getAddress()).thenReturn(address); - return recipient; - } - -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1ad16ad265..7dc3f6c1d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,6 @@ constraintlayoutVersion = "2.2.1" copperFlowVersion = "1.0.0" coreTestingVersion = "2.2.0" espressoCoreVersion = "3.6.1" -eventbusVersion = "3.0.0" exifinterfaceVersion = "1.3.4" firebaseMessagingVersion = "24.0.0" flexboxVersion = "3.0.0" @@ -144,7 +143,6 @@ androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtxVersion" } compose = { module = "com.github.bumptech.glide:compose", version.ref = "composeVersion" } conscrypt-android = { module = "org.conscrypt:conscrypt-android", version.ref = "conscryptAndroidVersion" } -eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbusVersion" } firebase-messaging = { module = "com.google.firebase:firebase-messaging", version.ref = "firebaseMessagingVersion" } flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexboxVersion" } glide = { module = "com.github.bumptech.glide:glide", version.ref = "glideVersion" }