From 36ebc5c5211c05fcdeb1e5dac16a54e43dabf1ec Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 16 Oct 2025 09:55:18 +1100 Subject: [PATCH 01/10] Making sure we have access to the threadId when receiving an Unsend Request --- .../libsession/messaging/jobs/BatchMessageReceiveJob.kt | 2 +- .../messaging/sending_receiving/ReceivedMessageHandler.kt | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index fc1bbfac42..484a95ca64 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -253,7 +253,7 @@ class BatchMessageReceiveJob @AssistedInject constructor( } is UnsendRequest -> { - val deletedMessage = receivedMessageHandler.handleUnsendRequest(message) + val deletedMessage = receivedMessageHandler.handleUnsendRequest(message, threadId) // If we removed a message then ensure it isn't in the 'messageIds' if (deletedMessage != null) { 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 a3f42c7a81..4bba3fbc69 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 @@ -128,7 +128,7 @@ class ReceivedMessageHandler @Inject constructor( } } is DataExtractionNotification -> handleDataExtractionNotification(message) - is UnsendRequest -> handleUnsendRequest(message) + is UnsendRequest -> handleUnsendRequest(message, threadId) is MessageRequestResponse -> messageRequestResponseHandler.get().handleExplicitRequestResponseMessage(message) is VisibleMessage -> handleVisibleMessage( message = message, @@ -221,7 +221,7 @@ class ReceivedMessageHandler @Inject constructor( } - fun handleUnsendRequest(message: UnsendRequest): MessageId? { + fun handleUnsendRequest(message: UnsendRequest, threadId: Long): MessageId? { val userPublicKey = storage.getUserPublicKey() val userAuth = storage.userAuth ?: return null val isLegacyGroupAdmin: Boolean = message.groupPublicKey?.let { key -> @@ -244,7 +244,6 @@ class ReceivedMessageHandler @Inject constructor( val timestamp = message.timestamp ?: return null val author = message.author ?: return null - val threadId = message.threadID ?: return null val messageToDelete = storage.getMessageBy(threadId, timestamp, author) ?: return null val messageIdToDelete = messageToDelete.messageId val messageType = messageToDelete.individualRecipient?.getType() From 7bec6b13b3b9084b9dc53031483637ec8f1e60e5 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 16 Oct 2025 17:02:59 +1100 Subject: [PATCH 02/10] Removing Promises from the OpenGroupAPI and its cascading classes --- .../messaging/jobs/AttachmentDownloadJob.kt | 3 +- .../messaging/jobs/AttachmentUploadJob.kt | 8 +- .../messaging/jobs/InviteContactsJob.kt | 2 - .../messaging/jobs/MessageSendJob.kt | 3 +- .../messaging/open_groups/OpenGroupApi.kt | 490 ++++++++---------- .../sending_receiving/MessageSender.kt | 67 ++- .../ReceivedMessageHandler.kt | 8 +- .../pollers/OpenGroupPoller.kt | 4 +- .../attachments/RemoteFileDownloadWorker.kt | 2 +- .../conversation/v2/ConversationActivityV2.kt | 51 +- .../v2/utilities/ResendMessageUtilities.kt | 6 +- .../securesms/groups/GroupManagerV2Impl.kt | 4 +- .../securesms/groups/OpenGroupManager.kt | 3 +- .../community/JoinCommunityViewModel.kt | 6 +- .../preferences/SettingsViewModel.kt | 3 +- .../repository/ConversationRepository.kt | 6 +- .../securesms/webrtc/CallManager.kt | 119 +++-- .../securesms/webrtc/WebRtcCallBridge.kt | 51 +- 18 files changed, 415 insertions(+), 421 deletions(-) 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 f02e548701..a052562d29 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 @@ -14,7 +14,6 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.utilities.Data import org.session.libsession.snode.OnionRequestAPI -import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.Address import org.session.libsession.utilities.DecodedAudio import org.session.libsession.utilities.DownloadUtilities @@ -167,7 +166,7 @@ class AttachmentDownloadJob @AssistedInject constructor( Log.d("AttachmentDownloadJob", "downloading open group attachment") val url = attachment.url.toHttpUrlOrNull()!! val fileID = url.pathSegments.last() - OpenGroupApi.download(fileID, room = threadRecipient.address.room, server = threadRecipient.address.serverUrl).await() + OpenGroupApi.download(fileID, room = threadRecipient.address.room, server = threadRecipient.address.serverUrl) } tempFile = createTempFile().also { file -> diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index 20a497eaba..86db6428fa 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -80,8 +80,8 @@ class AttachmentUploadJob @AssistedInject constructor( } handleSuccess(dispatcherName, attachment, keyAndResult.first, keyAndResult.second) } else { - val keyAndResult = upload(attachment, FileServerApi.FILE_SERVER_URL, true) { - FileServerApi.upload(it).map { it.fileId } + val keyAndResult = upload(attachment, FileServerApi.FILE_SERVER_URL, true) { file -> + FileServerApi.upload(file).map { it.fileId }.await() } handleSuccess(dispatcherName, attachment, keyAndResult.first, keyAndResult.second) } @@ -94,7 +94,7 @@ class AttachmentUploadJob @AssistedInject constructor( } } - private suspend fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise): Pair { + private suspend fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: suspend (ByteArray) -> String): Pair { // Key val key = if (encrypt) Util.getSecretBytes(64) else ByteArray(0) // Length @@ -120,7 +120,7 @@ class AttachmentUploadJob @AssistedInject constructor( drb.writeTo(b) val data = b.readByteArray() // Upload the data - val id = upload(data).await() + val id = upload(data) val digest = drb.transmittedDigest // Return return Pair(key, UploadResult(id, "${server}/file/$id", digest)) 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 e94134bd9a..1a4c5cc9f9 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 @@ -16,7 +16,6 @@ import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.Data import org.session.libsession.messaging.utilities.MessageAuthentication.buildGroupInviteSignature import org.session.libsession.snode.SnodeAPI -import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.getGroup import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteMessage import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage @@ -78,7 +77,6 @@ class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array< } MessageSender.sendNonDurably(update, Destination.Contact(memberSessionId), false) - .await() } } } diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt index 44395deca3..8cdc660584 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt @@ -20,7 +20,6 @@ import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.Data -import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigUpdateNotification import org.session.libsignal.utilities.AccountId @@ -97,7 +96,7 @@ class MessageSendJob @AssistedInject constructor( .waitForGroupEncryptionKeys(AccountId(destination.publicKey)) } - MessageSender.sendNonDurably(this@MessageSendJob.message, destination, isSync).await() + MessageSender.sendNonDurably(this@MessageSendJob.message, destination, isSync) } this.handleSuccess(dispatcherName) diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt index 7692f118ce..6fd65cebd9 100644 --- a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt +++ b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt @@ -5,17 +5,14 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.PropertyNamingStrategy import com.fasterxml.jackson.databind.annotation.JsonNaming -import com.fasterxml.jackson.databind.type.TypeFactory -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.decodeFromStream import network.loki.messenger.libsession_util.ED25519 import network.loki.messenger.libsession_util.Hash import network.loki.messenger.libsession_util.util.BlindKeyAPI -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.functional.map import okhttp3.Headers.Companion.toHeaders import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.MediaType.Companion.toMediaType @@ -24,7 +21,6 @@ import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionResponse import org.session.libsession.snode.SnodeAPI -import org.session.libsession.snode.utilities.asyncPromise import org.session.libsession.snode.utilities.await import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64.encodeBytes @@ -40,9 +36,6 @@ import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log import java.security.SecureRandom import java.util.concurrent.TimeUnit -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set object OpenGroupApi { val defaultRooms = MutableSharedFlow>(replay = 1) @@ -215,12 +208,16 @@ object OpenGroupApi { val index: Long = 0 ) + @Serializable data class AddReactionResponse( + @SerialName("seqno") val seqNo: Long, val added: Boolean ) + @Serializable data class DeleteReactionResponse( + @SerialName("seqno") val seqNo: Long, val removed: Boolean ) @@ -283,24 +280,23 @@ object OpenGroupApi { return RequestBody.create("application/json".toMediaType(), parametersAsJSON) } - private fun getResponseBody( + private suspend fun getResponseBody( request: Request, signRequest: Boolean = true, serverPubKeyHex: String? = null - ): Promise { - return send(request, signRequest = signRequest, serverPubKeyHex = serverPubKeyHex).map { response -> - response.body ?: throw Error.ParsingFailed - } + ): ByteArraySlice { + val response = send(request, signRequest = signRequest, serverPubKeyHex = serverPubKeyHex) + + return response.body ?: throw Error.ParsingFailed } - private fun getResponseBodyJson( + private suspend fun getResponseBodyJson( request: Request, signRequest: Boolean = true, serverPubKeyHex: String? = null - ): Promise, Exception> { - return send(request, signRequest = signRequest, serverPubKeyHex = serverPubKeyHex).map { - JsonUtil.fromJson(it.body, Map::class.java) - } + ): Map<*, *> { + val response = send(request, signRequest = signRequest, serverPubKeyHex = serverPubKeyHex) + return JsonUtil.fromJson(response.body, Map::class.java) } suspend fun getOrFetchServerCapabilities(server: String): List { @@ -313,14 +309,15 @@ object OpenGroupApi { val fetched = getCapabilities(server, serverPubKeyHex = defaultServerPublicKey.takeIf { server == defaultServer } - ).await() + ) storage.setServerCapabilities(server, fetched.capabilities) return fetched.capabilities } - private fun send(request: Request, signRequest: Boolean, serverPubKeyHex: String? = null): Promise { - request.server.toHttpUrlOrNull() ?: return Promise.ofFail(Error.InvalidURL) + private suspend fun send(request: Request, signRequest: Boolean, serverPubKeyHex: String? = null): OnionResponse { + request.server.toHttpUrlOrNull() ?: throw(Error.InvalidURL) + val urlBuilder = StringBuilder("${request.server}/${request.endpoint.value}") if (request.verb == GET && request.queryParameters.isNotEmpty()) { urlBuilder.append("?") @@ -329,113 +326,109 @@ object OpenGroupApi { } } - suspend fun execute(): OnionResponse { - val serverPublicKey = serverPubKeyHex - ?: MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server) - ?: throw Error.NoPublicKey - val urlRequest = urlBuilder.toString() - - val headers = if (signRequest) { - val serverCapabilities = getOrFetchServerCapabilities(request.server) - - val ed25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair() - ?: throw Error.NoEd25519KeyPair - - val headers = request.headers.toMutableMap() - val nonce = ByteArray(16).also { SecureRandom().nextBytes(it) } - val timestamp = TimeUnit.MILLISECONDS.toSeconds(SnodeAPI.nowWithOffset) - val bodyHash = if (request.parameters != null) { - val parameterBytes = JsonUtil.toJson(request.parameters).toByteArray() - Hash.hash64(parameterBytes) - } else if (request.body != null) { - Hash.hash64(request.body) - } else { - byteArrayOf() - } + val serverPublicKey = serverPubKeyHex + ?: MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server) + ?: throw Error.NoPublicKey + val urlRequest = urlBuilder.toString() + + val headers = if (signRequest) { + val serverCapabilities = getOrFetchServerCapabilities(request.server) + + val ed25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair() + ?: throw Error.NoEd25519KeyPair + + val headers = request.headers.toMutableMap() + val nonce = ByteArray(16).also { SecureRandom().nextBytes(it) } + val timestamp = TimeUnit.MILLISECONDS.toSeconds(SnodeAPI.nowWithOffset) + val bodyHash = if (request.parameters != null) { + val parameterBytes = JsonUtil.toJson(request.parameters).toByteArray() + Hash.hash64(parameterBytes) + } else if (request.body != null) { + Hash.hash64(request.body) + } else { + byteArrayOf() + } - val messageBytes = Hex.fromStringCondensed(serverPublicKey) - .plus(nonce) - .plus("$timestamp".toByteArray(Charsets.US_ASCII)) - .plus(request.verb.rawValue.toByteArray()) - .plus("/${request.endpoint.value}".toByteArray()) - .plus(bodyHash) - - val signature: ByteArray - val pubKey: String - - if (serverCapabilities.isEmpty() || serverCapabilities.contains(Capability.BLIND.name.lowercase())) { - pubKey = AccountId( - IdPrefix.BLINDED, - BlindKeyAPI.blind15KeyPair( - ed25519SecretKey = ed25519KeyPair.secretKey.data, - serverPubKey = Hex.fromStringCondensed(serverPublicKey) - ).pubKey.data - ).hexString - - try { - signature = BlindKeyAPI.blind15Sign( - ed25519SecretKey = ed25519KeyPair.secretKey.data, - serverPubKey = serverPublicKey, - message = messageBytes - ) - } catch (e: Exception) { - throw Error.SigningFailed - } - } else { - pubKey = AccountId( - IdPrefix.UN_BLINDED, - ed25519KeyPair.pubKey.data - ).hexString - - signature = ED25519.sign( - ed25519PrivateKey = ed25519KeyPair.secretKey.data, + val messageBytes = Hex.fromStringCondensed(serverPublicKey) + .plus(nonce) + .plus("$timestamp".toByteArray(Charsets.US_ASCII)) + .plus(request.verb.rawValue.toByteArray()) + .plus("/${request.endpoint.value}".toByteArray()) + .plus(bodyHash) + + val signature: ByteArray + val pubKey: String + + if (serverCapabilities.isEmpty() || serverCapabilities.contains(Capability.BLIND.name.lowercase())) { + pubKey = AccountId( + IdPrefix.BLINDED, + BlindKeyAPI.blind15KeyPair( + ed25519SecretKey = ed25519KeyPair.secretKey.data, + serverPubKey = Hex.fromStringCondensed(serverPublicKey) + ).pubKey.data + ).hexString + + try { + signature = BlindKeyAPI.blind15Sign( + ed25519SecretKey = ed25519KeyPair.secretKey.data, + serverPubKey = serverPublicKey, message = messageBytes ) + } catch (e: Exception) { + throw Error.SigningFailed } - headers["X-SOGS-Nonce"] = encodeBytes(nonce) - headers["X-SOGS-Timestamp"] = "$timestamp" - headers["X-SOGS-Pubkey"] = pubKey - headers["X-SOGS-Signature"] = encodeBytes(signature) - headers - } else { - request.headers - } - - val requestBuilder = okhttp3.Request.Builder() - .url(urlRequest) - .headers(headers.toHeaders()) - when (request.verb) { - GET -> requestBuilder.get() - PUT -> requestBuilder.put(createBody(request.body, request.parameters)!!) - POST -> requestBuilder.post(createBody(request.body, request.parameters)!!) - DELETE -> requestBuilder.delete(createBody(request.body, request.parameters)) - } - if (!request.room.isNullOrEmpty()) { - requestBuilder.header("Room", request.room) - } - return if (request.useOnionRouting) { - OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, serverPublicKey).fail { e -> - when (e) { - // No need for the stack trace for HTTP errors - is HTTP.HTTPRequestFailedException -> Log.e("SOGS", "Failed onion request: ${e.message}") - else -> Log.e("SOGS", "Failed onion request", e) - } - }.await() } else { - throw IllegalStateException("It's currently not allowed to send non onion routed requests.") + pubKey = AccountId( + IdPrefix.UN_BLINDED, + ed25519KeyPair.pubKey.data + ).hexString + + signature = ED25519.sign( + ed25519PrivateKey = ed25519KeyPair.secretKey.data, + message = messageBytes + ) } + headers["X-SOGS-Nonce"] = encodeBytes(nonce) + headers["X-SOGS-Timestamp"] = "$timestamp" + headers["X-SOGS-Pubkey"] = pubKey + headers["X-SOGS-Signature"] = encodeBytes(signature) + headers + } else { + request.headers } - return GlobalScope.asyncPromise(block=::execute) + val requestBuilder = okhttp3.Request.Builder() + .url(urlRequest) + .headers(headers.toHeaders()) + when (request.verb) { + GET -> requestBuilder.get() + PUT -> requestBuilder.put(createBody(request.body, request.parameters)!!) + POST -> requestBuilder.post(createBody(request.body, request.parameters)!!) + DELETE -> requestBuilder.delete(createBody(request.body, request.parameters)) + } + if (!request.room.isNullOrEmpty()) { + requestBuilder.header("Room", request.room) + } + return if (request.useOnionRouting) { + OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, serverPublicKey).fail { e -> + when (e) { + // No need for the stack trace for HTTP errors + is HTTP.HTTPRequestFailedException -> Log.e("SOGS", "Failed onion request: ${e.message}") + else -> Log.e("SOGS", "Failed onion request", e) + } + }.await() + } else { + throw IllegalStateException("It's currently not allowed to send non onion routed requests.") + } } - fun downloadOpenGroupProfilePicture( + suspend fun downloadOpenGroupProfilePicture( server: String, roomID: String, imageId: String, signRequest: Boolean = true, serverPubKeyHex: String? = null, - ): Promise { + ): ByteArraySlice { val request = Request( verb = GET, room = roomID, @@ -446,7 +439,7 @@ object OpenGroupApi { } // region Upload/Download - fun upload(file: ByteArray, room: String, server: String): Promise { + suspend fun upload(file: ByteArray, room: String, server: String): String { val request = Request( verb = POST, room = room, @@ -458,12 +451,11 @@ object OpenGroupApi { "Content-Type" to "application/octet-stream" ) ) - return getResponseBodyJson(request, signRequest = true).map { json -> - json["id"]?.toString() ?: throw Error.ParsingFailed - } + val json = getResponseBodyJson(request, signRequest = true) + return json["id"]?.toString() ?: throw Error.ParsingFailed } - fun download(fileId: String, room: String, server: String): Promise { + suspend fun download(fileId: String, room: String, server: String): ByteArraySlice { val request = Request( verb = GET, room = room, @@ -475,15 +467,15 @@ object OpenGroupApi { // endregion // region Sending - fun sendMessage( + suspend fun sendMessage( message: OpenGroupMessage, room: String, server: String, whisperTo: List? = null, whisperMods: Boolean? = null, fileIds: List? = null - ): Promise { - val signedMessage = message.sign(server) ?: return Promise.ofFail(Error.SigningFailed) + ): OpenGroupMessage { + val signedMessage = message.sign(server) ?:throw Error.SigningFailed val parameters = signedMessage.toJSON().toMutableMap() // add file IDs if there are any (from attachments) @@ -498,18 +490,18 @@ object OpenGroupApi { endpoint = Endpoint.RoomMessage(room), parameters = parameters ) - return getResponseBodyJson(request, signRequest = true).map { json -> - @Suppress("UNCHECKED_CAST") val rawMessage = json as? Map - ?: throw Error.ParsingFailed - val result = OpenGroupMessage.fromJSON(rawMessage) ?: throw Error.ParsingFailed - val storage = MessagingModuleConfiguration.shared.storage - storage.addReceivedMessageTimestamp(result.sentTimestamp) - result - } + val json = getResponseBodyJson(request, signRequest = true) + @Suppress("UNCHECKED_CAST") val rawMessage = json as? Map + ?: throw Error.ParsingFailed + val result = OpenGroupMessage.fromJSON(rawMessage) ?: throw Error.ParsingFailed + val storage = MessagingModuleConfiguration.shared.storage + storage.addReceivedMessageTimestamp(result.sentTimestamp) + return result } // endregion - fun addReaction(room: String, server: String, messageId: Long, emoji: String): Promise { + @OptIn(ExperimentalSerializationApi::class) + suspend fun addReaction(room: String, server: String, messageId: Long, emoji: String): AddReactionResponse { val request = Request( verb = PUT, room = room, @@ -517,89 +509,53 @@ object OpenGroupApi { endpoint = Endpoint.Reaction(room, messageId, emoji), parameters = emptyMap() ) - val pendingReaction = PendingReaction(server, room, messageId, emoji, true) - return getResponseBody(request, signRequest = true).map { response -> - JsonUtil.fromJson(response, AddReactionResponse::class.java).also { - val index = pendingReactions.indexOf(pendingReaction) - pendingReactions[index].seqNo = it.seqNo - } - } + + val response = getResponseBody(request, signRequest = true) + val reaction: AddReactionResponse = MessagingModuleConfiguration.shared.json.decodeFromStream(response.inputStream()) + + return reaction } - fun deleteReaction(room: String, server: String, messageId: Long, emoji: String): Promise { + @OptIn(ExperimentalSerializationApi::class) + suspend fun deleteReaction(room: String, server: String, messageId: Long, emoji: String): DeleteReactionResponse { val request = Request( verb = DELETE, room = room, server = server, endpoint = Endpoint.Reaction(room, messageId, emoji) ) - val pendingReaction = PendingReaction(server, room, messageId, emoji, true) - return getResponseBody(request, signRequest = true).map { response -> - JsonUtil.fromJson(response, DeleteReactionResponse::class.java).also { - val index = pendingReactions.indexOf(pendingReaction) - pendingReactions[index].seqNo = it.seqNo - } - } + + val response = getResponseBody(request, signRequest = true) + val reaction: DeleteReactionResponse = MessagingModuleConfiguration.shared.json.decodeFromStream(response.inputStream()) + + return reaction } - fun deleteAllReactions(room: String, server: String, messageId: Long, emoji: String): Promise { + suspend fun deleteAllReactions(room: String, server: String, messageId: Long, emoji: String): DeleteAllReactionsResponse { val request = Request( verb = DELETE, room = room, server = server, endpoint = Endpoint.ReactionDelete(room, messageId, emoji) ) - return getResponseBody(request, signRequest = true).map { response -> - JsonUtil.fromJson(response, DeleteAllReactionsResponse::class.java) - } + val response = getResponseBody(request, signRequest = true) + return JsonUtil.fromJson(response, DeleteAllReactionsResponse::class.java) } // endregion // region Message Deletion @JvmStatic - fun deleteMessage(serverID: Long, room: String, server: String): Promise { + suspend fun deleteMessage(serverID: Long, room: String, server: String) { val request = Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID)) - return send(request, signRequest = true).map { - Log.d("Loki", "Message deletion successful.") - } + send(request, signRequest = true) + Log.d("Loki", "Message deletion successful.") } - fun getDeletedMessages( - room: String, - server: String - ): Promise, Exception> { - val storage = MessagingModuleConfiguration.shared.storage - val queryParameters = mutableMapOf() - storage.getLastDeletionServerID(room, server)?.let { last -> - queryParameters["from_server_id"] = last.toString() - } - val request = Request( - verb = GET, - room = room, - server = server, - endpoint = Endpoint.RoomDeleteMessages(room, storage.getUserPublicKey() ?: ""), - queryParameters = queryParameters - ) - return getResponseBody(request, signRequest = true).map { response -> - val json = JsonUtil.fromJson(response, Map::class.java) - val type = TypeFactory.defaultInstance() - .constructCollectionType(List::class.java, MessageDeletion::class.java) - val idsAsString = JsonUtil.toJson(json["ids"]) - val serverIDs = JsonUtil.fromJson>(idsAsString, type) - ?: throw Error.ParsingFailed - val lastMessageServerId = storage.getLastDeletionServerID(room, server) ?: 0 - val serverID = serverIDs.maxByOrNull { it.id } ?: MessageDeletion.empty - if (serverID.id > lastMessageServerId) { - storage.setLastDeletionServerID(room, server, serverID.id) - } - serverIDs - } - } // endregion // region Moderation @JvmStatic - fun ban(publicKey: String, room: String, server: String): Promise { + suspend fun ban(publicKey: String, room: String, server: String) { val parameters = mapOf("rooms" to listOf(room)) val request = Request( verb = POST, @@ -608,12 +564,12 @@ object OpenGroupApi { endpoint = Endpoint.UserBan(publicKey), parameters = parameters ) - return send(request, signRequest = true).map { - Log.d("Loki", "Banned user: $publicKey from: $server.$room.") - } + + send(request, signRequest = true) + Log.d("Loki", "Banned user: $publicKey from: $server.$room.") } - fun banAndDeleteAll(publicKey: String, room: String, server: String): Promise { + suspend fun banAndDeleteAll(publicKey: String, room: String, server: String) { val requests = mutableListOf>( // Ban request @@ -633,25 +589,23 @@ object OpenGroupApi { responseType = object: TypeReference(){} ) ) - return sequentialBatch(server, requests).map { - Log.d("Loki", "Banned user: $publicKey from: $server.$room.") - } + sequentialBatch(server, requests) + Log.d("Loki", "Banned user: $publicKey from: $server.$room.") } - fun unban(publicKey: String, room: String, server: String): Promise { + suspend fun unban(publicKey: String, room: String, server: String) { val request = Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.UserUnban(publicKey)) - return send(request, signRequest = true).map { - Log.d("Loki", "Unbanned user: $publicKey from: $server.$room") - } + send(request, signRequest = true) + Log.d("Loki", "Unbanned user: $publicKey from: $server.$room") } // endregion // region General - fun parallelBatch( + suspend fun parallelBatch( server: String, requests: MutableList> - ): Promise>, Exception> { + ): List> { val request = Request( verb = POST, room = null, @@ -662,10 +616,10 @@ object OpenGroupApi { return getBatchResponseJson(request, requests) } - private fun sequentialBatch( + private suspend fun sequentialBatch( server: String, requests: MutableList> - ): Promise>, Exception> { + ): List> { val request = Request( verb = POST, room = null, @@ -676,97 +630,93 @@ object OpenGroupApi { return getBatchResponseJson(request, requests) } - private fun getBatchResponseJson( + private suspend fun getBatchResponseJson( request: Request, requests: MutableList>, signRequest: Boolean = true - ): Promise>, Exception> { - return getResponseBody(request, signRequest = signRequest).map { batch -> - val results = JsonUtil.fromJson(batch, List::class.java) ?: throw Error.ParsingFailed - results.mapIndexed { idx, result -> - val response = result as? Map<*, *> ?: throw Error.ParsingFailed - val code = response["code"] as Int - BatchResponse( - endpoint = requests[idx].endpoint, - code = code, - headers = response["headers"] as Map, - body = if (code in 200..299) { - requests[idx].responseType?.let { respType -> - JsonUtil.toJson(response["body"]).takeIf { it != "[]" }?.let { - JsonUtil.fromJson(it, respType) - } ?: response["body"] - } - - } else null - ) - } + ): List> { + val batch = getResponseBody(request, signRequest = signRequest) + val results = JsonUtil.fromJson(batch, List::class.java) ?: throw Error.ParsingFailed + return results.mapIndexed { idx, result -> + val response = result as? Map<*, *> ?: throw Error.ParsingFailed + val code = response["code"] as Int + BatchResponse( + endpoint = requests[idx].endpoint, + code = code, + headers = response["headers"] as Map, + body = if (code in 200..299) { + requests[idx].responseType?.let { respType -> + JsonUtil.toJson(response["body"]).takeIf { it != "[]" }?.let { + JsonUtil.fromJson(it, respType) + } ?: response["body"] + } + + } else null + ) } } - fun getDefaultServerCapabilities(): Promise, Exception> { - return GlobalScope.asyncPromise { getOrFetchServerCapabilities(defaultServer) } + suspend fun getDefaultServerCapabilities(): List { + return getOrFetchServerCapabilities(defaultServer) } - fun getDefaultRoomsIfNeeded(): Promise, Exception> { - return GlobalScope.asyncPromise { - val groups = getAllRooms().await() + suspend fun getDefaultRoomsIfNeeded(): List { + val groups = getAllRooms() - val earlyGroups = groups.map { group -> - DefaultGroup(group.token, group.name, null) - } - // See if we have any cached rooms, and if they already have images don't overwrite them with early non-image results - defaultRooms.replayCache.firstOrNull()?.let { replayed -> - if (replayed.none { it.image?.isNotEmpty() == true }) { - defaultRooms.tryEmit(earlyGroups) - } - } - val images = groups.associate { group -> - group.token to group.imageId?.let { downloadOpenGroupProfilePicture( - server = defaultServer, - roomID = group.token, - imageId = it, - signRequest = false, - serverPubKeyHex = defaultServerPublicKey, - ) } + val earlyGroups = groups.map { group -> + DefaultGroup(group.token, group.name, null) + } + // See if we have any cached rooms, and if they already have images don't overwrite them with early non-image results + defaultRooms.replayCache.firstOrNull()?.let { replayed -> + if (replayed.none { it.image?.isNotEmpty() == true }) { + defaultRooms.tryEmit(earlyGroups) } - groups.map { group -> - val image = try { - images[group.token]!!.await() - } catch (e: Exception) { - // No image or image failed to download - null - } - DefaultGroup(group.token, group.name, image) - }.also(defaultRooms::tryEmit) } + val images = groups.associate { group -> + group.token to group.imageId?.let { downloadOpenGroupProfilePicture( + server = defaultServer, + roomID = group.token, + imageId = it, + signRequest = false, + serverPubKeyHex = defaultServerPublicKey, + ) } + } + + return groups.map { group -> + val image = try { + images[group.token]!! + } catch (e: Exception) { + // No image or image failed to download + null + } + DefaultGroup(group.token, group.name, image) + }.also(defaultRooms::tryEmit) } - private fun getAllRooms(): Promise, Exception> { + private suspend fun getAllRooms(): List { val request = Request( verb = GET, room = null, server = defaultServer, endpoint = Endpoint.Rooms ) - return getResponseBody( + val response = getResponseBody( request = request, signRequest = false, serverPubKeyHex = defaultServerPublicKey - ).map { response -> - MessagingModuleConfiguration.shared.json - .decodeFromStream>(response.inputStream()) - .toList() - } + ) + + return MessagingModuleConfiguration.shared.json + .decodeFromStream>(response.inputStream()).toList() } - fun getCapabilities(server: String, serverPubKeyHex: String? = null): Promise { + suspend fun getCapabilities(server: String, serverPubKeyHex: String? = null): Capabilities { val request = Request(verb = GET, room = null, server = server, endpoint = Endpoint.Capabilities) - return getResponseBody(request, signRequest = false, serverPubKeyHex).map { response -> - JsonUtil.fromJson(response, Capabilities::class.java) - } + val response = getResponseBody(request, signRequest = false, serverPubKeyHex) + return JsonUtil.fromJson(response, Capabilities::class.java) } - fun sendDirectMessage(message: String, blindedAccountId: String, server: String): Promise { + suspend fun sendDirectMessage(message: String, blindedAccountId: String, server: String): DirectMessage { val request = Request( verb = POST, room = null, @@ -774,21 +724,19 @@ object OpenGroupApi { endpoint = Endpoint.InboxFor(blindedAccountId), parameters = mapOf("message" to message) ) - return getResponseBody(request).map { response -> - JsonUtil.fromJson(response, DirectMessage::class.java) - } + val response = getResponseBody(request) + return JsonUtil.fromJson(response, DirectMessage::class.java) } - fun deleteAllInboxMessages(server: String): Promise, java.lang.Exception> { + suspend fun deleteAllInboxMessages(server: String): Map<*, *> { val request = Request( verb = DELETE, room = null, server = server, endpoint = Endpoint.Inbox ) - return getResponseBody(request).map { response -> - JsonUtil.fromJson(response, Map::class.java) - } + val response = getResponseBody(request) + return JsonUtil.fromJson(response, Map::class.java) } // endregion 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 8be647e12c..817efd1c50 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 @@ -11,8 +11,6 @@ import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISI import network.loki.messenger.libsession_util.Namespace import network.loki.messenger.libsession_util.util.BlindKeyAPI import network.loki.messenger.libsession_util.util.ExpiryMode -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.deferred import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.messages.Destination @@ -32,7 +30,6 @@ import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI.nowWithOffset import org.session.libsession.snode.SnodeMessage -import org.session.libsession.snode.utilities.asyncPromise import org.session.libsession.utilities.Address import org.session.libsession.utilities.SSKEnvironment import org.session.libsignal.crypto.PushTransportDetails @@ -72,13 +69,11 @@ object MessageSender { } // Convenience - fun sendNonDurably(message: Message, destination: Destination, isSyncMessage: Boolean): Promise { + suspend fun sendNonDurably(message: Message, destination: Destination, isSyncMessage: Boolean) { return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) { sendToOpenGroupDestination(destination, message) } else { - GlobalScope.asyncPromise { - sendToSnodeDestination(destination, message, isSyncMessage) - } + sendToSnodeDestination(destination, message, isSyncMessage) } } @@ -197,12 +192,7 @@ object MessageSender { // One-on-One Chats & Closed Groups private suspend fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false) = supervisorScope { - val storage = MessagingModuleConfiguration.shared.storage val configFactory = MessagingModuleConfiguration.shared.configFactory - val userPublicKey = storage.getUserPublicKey() - - // recipient will be set later, so initialize it as a function here - val isSelfSend = { message.recipient == userPublicKey } // Set the failure handler (need it here already for precondition failure handling) fun handleFailure(error: Exception) { @@ -293,8 +283,7 @@ object MessageSender { } // Open Groups - private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise { - val deferred = deferred() + private suspend fun sendToOpenGroupDestination(destination: Destination, message: Message) { val storage = MessagingModuleConfiguration.shared.storage val configFactory = MessagingModuleConfiguration.shared.configFactory if (message.sentTimestamp == null) { @@ -343,11 +332,7 @@ object MessageSender { AccountId(IdPrefix.UN_BLINDED, userEdKeyPair.pubKey.data).hexString } message.sender = messageSender - // Set the failure handler (need it here already for precondition failure handling) - fun handleFailure(error: Exception) { - handleFailedMessageSend(message, error) - deferred.reject(error) - } + try { // Attach the user's profile if needed if (message is VisibleMessage) { @@ -372,13 +357,19 @@ object MessageSender { sentTimestamp = message.sentTimestamp!!, base64EncodedData = Base64.encodeBytes(plaintext), ) - OpenGroupApi.sendMessage(openGroupMessage, destination.roomToken, destination.server, destination.whisperTo, destination.whisperMods, destination.fileIds).success { - message.openGroupServerMessageID = it.serverID - handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = it.sentTimestamp) - deferred.resolve(Unit) - }.fail { - handleFailure(it) - } + + val response = OpenGroupApi.sendMessage( + openGroupMessage, + destination.roomToken, + destination.server, + destination.whisperTo, + destination.whisperMods, + destination.fileIds + ) + + message.openGroupServerMessageID = response.serverID + handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = response.sentTimestamp) + return } is Destination.OpenGroupInbox -> { message.recipient = destination.blindedPublicKey @@ -394,20 +385,22 @@ object MessageSender { destination.serverPublicKey ) val base64EncodedData = Base64.encodeBytes(ciphertext) - OpenGroupApi.sendDirectMessage(base64EncodedData, destination.blindedPublicKey, destination.server).success { - message.openGroupServerMessageID = it.id - handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = TimeUnit.SECONDS.toMillis(it.postedAt)) - deferred.resolve(Unit) - }.fail { - handleFailure(it) - } + val response = OpenGroupApi.sendDirectMessage( + base64EncodedData, + destination.blindedPublicKey, + destination.server + ) + + message.openGroupServerMessageID = response.id + handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = TimeUnit.SECONDS.toMillis(response.postedAt)) + return } else -> throw IllegalStateException("Invalid destination.") } } catch (exception: Exception) { - handleFailure(exception) + handleFailedMessageSend(message, exception) + throw exception } - return deferred.promise } // Result Handling @@ -552,10 +545,10 @@ object MessageSender { resultChannel.receive().getOrThrow() } - fun sendNonDurably(message: Message, address: Address, isSyncMessage: Boolean): Promise { + suspend fun sendNonDurably(message: Message, address: Address, isSyncMessage: Boolean) { val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) message.threadID = threadID val destination = Destination.from(address) - return sendNonDurably(message, destination, isSyncMessage) + sendNonDurably(message, destination, isSyncMessage) } } \ No newline at end of file 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 4bba3fbc69..4ed48b75cc 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 @@ -769,17 +769,13 @@ fun constructReactionRecords( out: MutableMap> ) { if (reactions.isNullOrEmpty()) return - val communityAddress = context.threadAddress as? Address.Community ?: return + if (context.threadAddress !is Address.Community) return val messageId = context.messageDataProvider.getMessageID(openGroupMessageServerID, context.threadId) ?: return val outList = out.getOrPut(messageId) { arrayListOf() } for ((emoji, reaction) in reactions) { - val pendingUserReaction = OpenGroupApi.pendingReactions - .filter { it.server == communityAddress.serverUrl && it.room == communityAddress.room && it.messageId == openGroupMessageServerID && it.add } - .sortedByDescending { it.seqNo } - .any { it.emoji == emoji } - val shouldAddUserReaction = pendingUserReaction || reaction.you || reaction.reactors.contains(context.userPublicKey) + val shouldAddUserReaction = reaction.you || reaction.reactors.contains(context.userPublicKey) val reactorIds = reaction.reactors.filter { it != context.userBlindedKey && it != context.userPublicKey } val count = if (reaction.you) reaction.count - 1 else reaction.count // Add the first reaction (with the count) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index 9411049169..043f1f390f 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -37,7 +37,6 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi.parallelBatch import org.session.libsession.messaging.open_groups.OpenGroupMessage import org.session.libsession.messaging.sending_receiving.MessageReceiver import org.session.libsession.messaging.sending_receiving.ReceivedMessageHandler -import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigFactoryProtocol @@ -283,7 +282,7 @@ class OpenGroupPoller @AssistedInject constructor( } ) } - return parallelBatch(server, requests).await() + return parallelBatch(server, requests) } @@ -295,7 +294,6 @@ class OpenGroupPoller @AssistedInject constructor( val sortedMessages = messages.sortedBy { it.seqno } sortedMessages.maxOfOrNull { it.seqno }?.let { seqNo -> storage.setLastMessageServerID(roomToken, server, seqNo) - OpenGroupApi.pendingReactions.removeAll { !(it.seqNo == null || it.seqNo!! > seqNo) } } val (deletions, additions) = sortedMessages.partition { it.deleted } handleNewMessages(server, roomToken, additions.map { diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/RemoteFileDownloadWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/RemoteFileDownloadWorker.kt index c86568de4d..864fe1757f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/RemoteFileDownloadWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/RemoteFileDownloadWorker.kt @@ -244,7 +244,7 @@ class RemoteFileDownloadWorker @AssistedInject constructor( fileId = file.fileId, room = file.roomId, server = file.communityServerBaseUrl - ).await() + ) data to FileMetadata() } 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 c68d14c1de..d41a3f1863 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 @@ -62,6 +62,7 @@ import com.squareup.phrase.Phrase import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.withCreationCallback import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow @@ -170,6 +171,7 @@ 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.ConfigFactory +import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.giph.ui.GiphyActivity import org.thoughtcrime.securesms.groups.GroupMembersActivity import org.thoughtcrime.securesms.groups.OpenGroupManager @@ -262,6 +264,8 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, @Inject lateinit var openGroupManager: OpenGroupManager @Inject lateinit var attachmentDatabase: AttachmentDatabase @Inject lateinit var clock: SnodeClock + @Inject @ManagerScope + lateinit var scope: CoroutineScope override val applyDefaultWindowInsets: Boolean get() = false @@ -1747,11 +1751,14 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val messageServerId = lokiMessageDb.getServerID(originalMessage.messageId) ?: return Log.w(TAG, "Failed to find message server ID when adding emoji reaction") - OpenGroupApi.addReaction( - room = recipient.address.room, - server = recipient.address.serverUrl, - messageId = messageServerId, - emoji = emoji) + scope.launch { + OpenGroupApi.addReaction( + room = recipient.address.room, + server = recipient.address.serverUrl, + messageId = messageServerId, + emoji = emoji + ) + } } else { MessageSender.send(reactionMessage, recipient.address) } @@ -1780,16 +1787,24 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, ) val originalAuthor = if (originalMessage.isOutgoing) { - fromSerialized(viewModel.blindedPublicKey ?: textSecurePreferences.getLocalNumber()!!) + fromSerialized(viewModel.blindedPublicKey ?: author) } else originalMessage.individualRecipient.address - message.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.toString(), emoji, false) + message.reaction = Reaction.from( + timestamp = originalMessage.timestamp, + author = originalAuthor.address, + emoji = emoji, + react = false + ) + if (recipient.address is Address.Community) { val messageServerId = lokiMessageDb.getServerID(originalMessage.messageId) ?: return Log.w(TAG, "Failed to find message server ID when removing emoji reaction") - OpenGroupApi.deleteReaction(recipient.address.room, recipient.address.serverUrl, messageServerId, emoji) + scope.launch { + OpenGroupApi.deleteReaction(recipient.address.room, recipient.address.serverUrl, messageServerId, emoji) + } } else { MessageSender.send(message, recipient.address) } @@ -2502,15 +2517,27 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } override fun resyncMessage(messages: Set) { - messages.iterator().forEach { messageRecord -> - ResendMessageUtilities.resend(this, messageRecord, viewModel.blindedPublicKey, isResync = true) + val accountId = textSecurePreferences.getLocalNumber() + scope.launch { + messages.iterator().forEach { messageRecord -> + ResendMessageUtilities.resend( + accountId, + messageRecord, + viewModel.blindedPublicKey, + isResync = true + ) + } } + endActionMode() } override fun resendMessage(messages: Set) { - messages.iterator().forEach { messageRecord -> - ResendMessageUtilities.resend(this, messageRecord, viewModel.blindedPublicKey) + val accountId = textSecurePreferences.getLocalNumber() + scope.launch { + messages.iterator().forEach { messageRecord -> + ResendMessageUtilities.resend(accountId, messageRecord, viewModel.blindedPublicKey) + } } endActionMode() } 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 cf00cd0f9d..2f47041204 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 @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.conversation.v2.utilities -import android.content.Context import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.visible.LinkPreview @@ -9,7 +8,6 @@ import org.session.libsession.messaging.messages.visible.Quote 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.isGroupOrCommunity import org.session.libsession.utilities.toGroupString import org.thoughtcrime.securesms.database.model.MessageRecord @@ -17,7 +15,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord object ResendMessageUtilities { - fun resend(context: Context, messageRecord: MessageRecord, userBlindedKey: String?, isResync: Boolean = false) { + suspend fun resend(accountId: String?, messageRecord: MessageRecord, userBlindedKey: String?, isResync: Boolean = false) { val recipient = messageRecord.recipient.address val message = VisibleMessage() message.id = messageRecord.messageId @@ -45,7 +43,7 @@ object ResendMessageUtilities { messageRecord.linkPreviews.firstOrNull()?.let { message.linkPreview = LinkPreview.from(it) } messageRecord.quote?.quoteModel?.let { message.quote = Quote.from(it)?.apply { - if (userBlindedKey != null && publicKey == TextSecurePreferences.getLocalNumber(context)) { + if (userBlindedKey != null && publicKey == accountId) { publicKey = userBlindedKey } } 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 30f49b1978..c75071712d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt @@ -46,8 +46,8 @@ import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.Address import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY import org.session.libsession.utilities.getGroup -import org.session.libsession.utilities.recipients.RecipientData import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.recipients.RecipientData import org.session.libsession.utilities.waitUntilGroupConfigsPushed import org.session.libsignal.protos.SignalServiceProtos.DataMessage import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage @@ -527,7 +527,7 @@ class GroupManagerV2Impl @Inject constructor( message = promoteMessage, address = Address.fromSerialized(member.hexString), isSyncMessage = false, - ).await() + ) } } 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 154ef35869..93159ed2ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt @@ -6,7 +6,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerManager -import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.LokiAPIDatabase @@ -30,7 +29,7 @@ class OpenGroupManager @Inject constructor( // for the user to see if the server they are adding is reachable. // The addition of the community to the config later will always succeed and the poller // will be started regardless of the server's status. - val caps = OpenGroupApi.getCapabilities(server, serverPubKeyHex = publicKey).await() + val caps = OpenGroupApi.getCapabilities(server, serverPubKeyHex = publicKey) lokiAPIDatabase.setServerCapabilities(server, caps.capabilities) // We should be good, now go ahead and add the community to the config diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt index e6951effbf..e73913a434 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt @@ -45,8 +45,10 @@ class JoinCommunityViewModel @Inject constructor( private val qrDebounceTime = 3000L init { - OpenGroupApi.getDefaultServerCapabilities().map { - OpenGroupApi.getDefaultRoomsIfNeeded() + viewModelScope.launch(Dispatchers.Default) { + OpenGroupApi.getDefaultServerCapabilities().map { + OpenGroupApi.getDefaultRoomsIfNeeded() + } } viewModelScope.launch(Dispatchers.Default) { 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 ddbbb89eb0..d272784213 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt @@ -46,7 +46,6 @@ import org.thoughtcrime.securesms.attachments.AvatarUploadManager import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.textSizeInBytes import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.dependencies.ConfigFactory -import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.pro.SubscriptionState import org.thoughtcrime.securesms.pro.getDefaultSubscriptionStateData @@ -440,7 +439,7 @@ class SettingsViewModel @Inject constructor( coroutineScope { allCommunityServers.map { server -> launch { - runCatching { OpenGroupApi.deleteAllInboxMessages(server).await() } + runCatching { OpenGroupApi.deleteAllInboxMessages(server) } .onFailure { Log.e(TAG, "Error deleting messages for $server", it) } } }.joinAll() 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 6d94fc83f7..56b8d0bef7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -386,7 +386,7 @@ class DefaultConversationRepository @Inject constructor( ) { messages.forEach { message -> lokiMessageDb.getServerID(message.messageId)?.let { messageServerID -> - OpenGroupApi.deleteMessage(messageServerID, community.room, community.serverUrl).await() + OpenGroupApi.deleteMessage(messageServerID, community.room, community.serverUrl) } } } @@ -484,7 +484,7 @@ class DefaultConversationRepository @Inject constructor( publicKey = userId.hexString, room = community.room, server = community.serverUrl, - ).await() + ) } override suspend fun banAndDeleteAll(community: Address.Community, userId: AccountId) = runCatching { @@ -493,7 +493,7 @@ class DefaultConversationRepository @Inject constructor( publicKey = userId.hexString, room = community.room, server = community.serverUrl - ).await() + ) } override suspend fun deleteMessageRequest(thread: ThreadRecord): Result { 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 0fb4d6755b..e66321c48a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -5,11 +5,13 @@ import android.content.pm.PackageManager import android.telephony.TelephonyManager import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.boolean @@ -29,6 +31,7 @@ import org.session.libsession.utilities.Debouncer import org.session.libsession.utilities.Util import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ICE_CANDIDATES import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioDeviceUpdate import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioEnabled import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat @@ -68,6 +71,7 @@ import org.thoughtcrime.securesms.webrtc.data.State as CallState @Singleton class CallManager @Inject constructor( @param:ApplicationContext private val context: Context, + @param:ManagerScope private val scope: CoroutineScope, audioManager: AudioManagerCompat, private val storage: StorageProtocol, ): PeerConnection.Observer, @@ -333,7 +337,15 @@ class CallManager @Inject constructor( currentCallId ) .applyExpiryMode(expectedRecipient) - .also { MessageSender.sendNonDurably(it, currentRecipient, isSyncMessage = currentRecipient.isLocalNumber) } + .also { + scope.launch { + MessageSender.sendNonDurably( + it, + currentRecipient, + isSyncMessage = currentRecipient.isLocalNumber + ) + } + } } } @@ -457,10 +469,10 @@ class CallManager @Inject constructor( } } - 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 connection = peerConnection ?: return Promise.ofFail(NullPointerException("No peer connection wrapper")) + suspend fun onNewOffer(offer: String, callId: UUID, recipient: Address) { + if (callId != this.callId) throw NullPointerException("No callId") + if (recipient != this.recipient) throw NullPointerException("No recipient") + val connection = peerConnection ?: throw NullPointerException("No peer connection wrapper") val reconnected = stateProcessor.processEvent(Event.ReceiveOffer) && stateProcessor.processEvent(Event.SendAnswer) return if (reconnected) { @@ -477,7 +489,7 @@ class CallManager @Inject constructor( Log.i("Loki", "Posting new answer") MessageSender.sendNonDurably(answerMessage, recipient, isSyncMessage = recipient.isLocalNumber) } else { - Promise.ofFail(Exception("Couldn't reconnect from current state")) + throw Exception("Couldn't reconnect from current state") } } @@ -493,15 +505,15 @@ class CallManager @Inject constructor( } } - fun onIncomingCall(context: Context, isAlwaysTurn: Boolean = false): Promise { + suspend fun onIncomingCall(context: Context, isAlwaysTurn: Boolean = false) { lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING) - val callId = callId ?: return Promise.ofFail(NullPointerException("callId is null")) - val recipient = recipient ?: return Promise.ofFail(NullPointerException("recipient is null")) - val offer = pendingOffer ?: return Promise.ofFail(NullPointerException("pendingOffer is null")) - val factory = peerConnectionFactory ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null")) - val local = floatingRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null")) - val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null")) + val callId = callId ?: throw NullPointerException("callId is null") + val recipient = recipient ?: throw NullPointerException("recipient is null") + val offer = pendingOffer ?: throw NullPointerException("pendingOffer is null") + val factory = peerConnectionFactory ?: throw NullPointerException("peerConnectionFactory is null") + val local = floatingRenderer ?: throw NullPointerException("localRenderer is null") + val base = eglBase ?: throw NullPointerException("eglBase is null") val connection = PeerConnectionWrapper( context, @@ -521,9 +533,9 @@ class CallManager @Inject constructor( val answer = connection.createAnswer(MediaConstraints()) connection.setLocalDescription(answer) val answerMessage = CallMessage.answer(answer.description, callId).applyExpiryMode(recipient) - val userAddress = storage.getUserPublicKey() ?: return Promise.ofFail(NullPointerException("No user public key")) + val userAddress = storage.getUserPublicKey() ?: throw NullPointerException("No user public key") MessageSender.sendNonDurably(answerMessage, Address.fromSerialized(userAddress), isSyncMessage = true) - val sendAnswerMessage = MessageSender.sendNonDurably(CallMessage.answer( + MessageSender.sendNonDurably(CallMessage.answer( answer.description, callId ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) @@ -534,28 +546,27 @@ class CallManager @Inject constructor( val candidate = pendingIncomingIceUpdates.pop() ?: break connection.addIceCandidate(candidate) } - return sendAnswerMessage.success { - pendingOffer = null - pendingOfferTime = -1 - } + + pendingOffer = null + pendingOfferTime = -1 } - fun onOutgoingCall(context: Context, isAlwaysTurn: Boolean = false): Promise { + suspend fun onOutgoingCall(context: Context, isAlwaysTurn: Boolean = false) { lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL) - val callId = callId ?: return Promise.ofFail(NullPointerException("callId is null")) + val callId = callId ?: throw NullPointerException("callId is null") val recipient = recipient - ?: return Promise.ofFail(NullPointerException("recipient is null")) + ?: throw NullPointerException("recipient is null") val factory = peerConnectionFactory - ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null")) + ?: throw NullPointerException("peerConnectionFactory is null") val local = floatingRenderer - ?: return Promise.ofFail(NullPointerException("localRenderer is null")) - val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null")) + ?: throw NullPointerException("localRenderer is null") + val base = eglBase ?: throw NullPointerException("eglBase is null") val sentOffer = stateProcessor.processEvent(Event.SendOffer) if (!sentOffer) { - return Promise.ofFail(Exception("Couldn't transition to sent offer state")) + throw Exception("Couldn't transition to sent offer state") } else { val connection = PeerConnectionWrapper( context, @@ -576,20 +587,24 @@ class CallManager @Inject constructor( connection.setLocalDescription(offer) Log.d("Loki", "Sending pre-offer") - return MessageSender.sendNonDurably(CallMessage.preOffer( + MessageSender.sendNonDurably(CallMessage.preOffer( callId - ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber).bind { - Log.d("Loki", "Sent pre-offer") - Log.d("Loki", "Sending offer") - postViewModelState(CallViewModel.State.CALL_OFFER_OUTGOING) + ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) + + Log.d("Loki", "Sent pre-offer") + Log.d("Loki", "Sending offer") + postViewModelState(CallViewModel.State.CALL_OFFER_OUTGOING) + + try { MessageSender.sendNonDurably(CallMessage.offer( offer.description, callId - ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber).success { - Log.d("Loki", "Sent offer") - }.fail { - Log.e("Loki", "Failed to send offer", it) - } + ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) + + Log.d("Loki", "Sent offer") + } catch (e: Exception) { + Log.e("Loki", "Failed to send offer", e) + throw e } } } @@ -599,9 +614,19 @@ class CallManager @Inject constructor( val recipient = recipient ?: return val userAddress = storage.getUserPublicKey() ?: return stateProcessor.processEvent(Event.DeclineCall) { - MessageSender.sendNonDurably(CallMessage.endCall(callId).applyExpiryMode(recipient), Address.fromSerialized(userAddress), isSyncMessage = true) - MessageSender.sendNonDurably(CallMessage.endCall(callId).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) - insertCallMessage(recipient.toString(), CallMessageType.CALL_INCOMING) + scope.launch { + MessageSender.sendNonDurably( + CallMessage.endCall(callId).applyExpiryMode(recipient), + Address.fromSerialized(userAddress), + isSyncMessage = true + ) + MessageSender.sendNonDurably( + CallMessage.endCall(callId).applyExpiryMode(recipient), + recipient, + isSyncMessage = recipient.isLocalNumber + ) + insertCallMessage(recipient.toString(), CallMessageType.CALL_INCOMING) + } } } @@ -624,7 +649,13 @@ class CallManager @Inject constructor( channel.send(buffer) } - MessageSender.sendNonDurably(CallMessage.endCall(callId).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) + scope.launch { + MessageSender.sendNonDurably( + CallMessage.endCall(callId).applyExpiryMode(recipient), + recipient, + isSyncMessage = recipient.isLocalNumber + ) + } } } @@ -855,7 +886,13 @@ class CallManager @Inject constructor( mandatory.add(MediaConstraints.KeyValuePair("IceRestart", "true")) }) connection.setLocalDescription(offer) - MessageSender.sendNonDurably(CallMessage.offer(offer.description, callId).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) + scope.launch { + MessageSender.sendNonDurably( + CallMessage.offer(offer.description, callId).applyExpiryMode(recipient), + recipient, + isSyncMessage = recipient.isLocalNumber + ) + } } } 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 9ed60cbdd5..7655089752 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallBridge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallBridge.kt @@ -66,7 +66,7 @@ class WebRtcCallBridge @Inject constructor( private val networkConnectivity: NetworkConnectivity, private val recipientRepository: RecipientRepository, private val storage: StorageProtocol, - @ManagerScope scope: CoroutineScope, + @ManagerScope private val scope: CoroutineScope, ): CallManager.WebRtcListener, OnAppStartupComponent { companion object { @@ -159,10 +159,14 @@ class WebRtcCallBridge @Inject constructor( private fun handleNewOffer(address: Address, sdp: String, callId: UUID) { Log.d(TAG, "Handle new offer") - callManager.onNewOffer(sdp, callId, address).fail { - Log.e("Loki", "Error handling new offer", it) - callManager.postConnectionError() - terminate() + scope.launch { + try { + callManager.onNewOffer(sdp, callId, address) + } catch (e: Exception) { + Log.e("Loki", "Error handling new offer", e) + callManager.postConnectionError() + terminate() + } } } @@ -268,9 +272,10 @@ class WebRtcCallBridge @Inject constructor( val expectedState = callManager.currentConnectionState val expectedCallId = callManager.callId - try { - val offerFuture = callManager.onOutgoingCall(context) - offerFuture.fail { e -> + scope.launch { + try { + callManager.onOutgoingCall(context) + } catch (e: Exception) { if (isConsistentState( expectedState, expectedCallId, @@ -278,16 +283,13 @@ class WebRtcCallBridge @Inject constructor( callManager.callId ) ) { - Log.e(TAG, e) callManager.postViewModelState(CallViewModel.State.NETWORK_FAILURE) - callManager.postConnectionError() - terminate() } + + Log.e(TAG, e) + callManager.postConnectionError() + terminate() } - } catch (e: Exception) { - Log.e(TAG, e) - callManager.postConnectionError() - terminate() } } } @@ -348,9 +350,12 @@ class WebRtcCallBridge @Inject constructor( val expectedState = callManager.currentConnectionState val expectedCallId = callManager.callId - try { - val answerFuture = callManager.onIncomingCall(context) - answerFuture.fail { e -> + scope.launch { + try { + callManager.onIncomingCall(context) + } catch (e: Exception) { + Log.e(TAG, "incoming call error: $e") + if (isConsistentState( expectedState, expectedCallId, @@ -358,19 +363,15 @@ class WebRtcCallBridge @Inject constructor( callManager.callId ) ) { - Log.e(TAG, "incoming call error: $e") insertMissedCall( recipient, true ) //todo PHONE do we want a missed call in this case? Or just [xxx] called you ? - callManager.postConnectionError() - terminate() } + + callManager.postConnectionError() + terminate() } - } catch (e: Exception) { - Log.e(TAG, e) - callManager.postConnectionError() - terminate() } } } From 05e447542fcd8b7edcf4a8954dfbdb438b93a502 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 16 Oct 2025 17:12:31 +1100 Subject: [PATCH 03/10] Fixing reaction removal --- .../messaging/sending_receiving/ReceivedMessageHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4ed48b75cc..85cd48902b 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 @@ -375,7 +375,7 @@ class ReceivedMessageHandler @Inject constructor( emoji = reaction.emoji!!, messageTimestamp = reaction.timestamp!!, threadId = context.threadId, - author = reaction.publicKey!!, + author = senderAddress.address, notifyUnread = threadIsGroup ) } From 36dcf0a887651c5242c3e1a28cb92e677b888164 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 17 Oct 2025 09:42:14 +1100 Subject: [PATCH 04/10] Making sure the reactions position themselves p roperly --- .../securesms/conversation/v2/messages/VisibleMessageView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 4162074c0e..8796834bbe 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 @@ -277,10 +277,10 @@ class VisibleMessageView : FrameLayout { if (capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase())) { emojiReactionsBinding.value.root.let { root -> root.setReactions(message.messageId, message.reactions, message.isOutgoing, delegate) - root.isVisible = true - (root.layoutParams as ConstraintLayout.LayoutParams).apply { + root.layoutParams = (root.layoutParams as ConstraintLayout.LayoutParams).apply { horizontalBias = if (message.isOutgoing) 1f else 0f } + root.isVisible = true } } else if (emojiReactionsBinding.isInitialized()) { emojiReactionsBinding.value.root.isVisible = false From 4e315b35d0db5b41104623ce0ffaed8dd78138db Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 17 Oct 2025 12:42:42 +1100 Subject: [PATCH 05/10] Catching fire and forget coroutine calls --- .../messaging/open_groups/OpenGroupApi.kt | 4 +- .../sending_receiving/MessageSender.kt | 3 +- .../conversation/v2/ConversationActivityV2.kt | 47 +++++--- .../conversation/v2/ConversationViewModel.kt | 14 ++- .../securesms/groups/GroupManagerV2Impl.kt | 13 +- .../community/JoinCommunityViewModel.kt | 6 +- .../securesms/webrtc/CallManager.kt | 113 +++++++++++------- .../securesms/webrtc/WebRtcCallBridge.kt | 4 + 8 files changed, 132 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt index 6fd65cebd9..3d650db223 100644 --- a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt +++ b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt @@ -511,7 +511,7 @@ object OpenGroupApi { ) val response = getResponseBody(request, signRequest = true) - val reaction: AddReactionResponse = MessagingModuleConfiguration.shared.json.decodeFromStream(response.inputStream()) + val reaction: AddReactionResponse = response.inputStream().use( MessagingModuleConfiguration.shared.json::decodeFromStream) return reaction } @@ -544,7 +544,6 @@ object OpenGroupApi { // endregion // region Message Deletion - @JvmStatic suspend fun deleteMessage(serverID: Long, room: String, server: String) { val request = Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID)) send(request, signRequest = true) @@ -554,7 +553,6 @@ object OpenGroupApi { // endregion // region Moderation - @JvmStatic suspend fun ban(publicKey: String, room: String, server: String) { val parameters = mapOf("rooms" to listOf(room)) val request = Request( 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 817efd1c50..0b5158ce5f 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 @@ -43,6 +43,7 @@ import org.session.libsignal.utilities.defaultRequiresAuth import org.session.libsignal.utilities.hasNamespaces import org.session.libsignal.utilities.hexEncodedPublicKey import java.util.concurrent.TimeUnit +import kotlin.coroutines.cancellation.CancellationException import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreview import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote @@ -398,7 +399,7 @@ object MessageSender { else -> throw IllegalStateException("Invalid destination.") } } catch (exception: Exception) { - handleFailedMessageSend(message, exception) + if (exception !is CancellationException) handleFailedMessageSend(message, exception) throw exception } } 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 d41a3f1863..79f4982934 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 @@ -1752,12 +1752,14 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, return Log.w(TAG, "Failed to find message server ID when adding emoji reaction") scope.launch { - OpenGroupApi.addReaction( - room = recipient.address.room, - server = recipient.address.serverUrl, - messageId = messageServerId, - emoji = emoji - ) + runCatching { + OpenGroupApi.addReaction( + room = recipient.address.room, + server = recipient.address.serverUrl, + messageId = messageServerId, + emoji = emoji + ) + } } } else { MessageSender.send(reactionMessage, recipient.address) @@ -1803,7 +1805,14 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, return Log.w(TAG, "Failed to find message server ID when removing emoji reaction") scope.launch { - OpenGroupApi.deleteReaction(recipient.address.room, recipient.address.serverUrl, messageServerId, emoji) + runCatching { + OpenGroupApi.deleteReaction( + recipient.address.room, + recipient.address.serverUrl, + messageServerId, + emoji + ) + } } } else { MessageSender.send(message, recipient.address) @@ -2519,13 +2528,15 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, override fun resyncMessage(messages: Set) { val accountId = textSecurePreferences.getLocalNumber() scope.launch { - messages.iterator().forEach { messageRecord -> - ResendMessageUtilities.resend( - accountId, - messageRecord, - viewModel.blindedPublicKey, - isResync = true - ) + runCatching { + messages.iterator().forEach { messageRecord -> + ResendMessageUtilities.resend( + accountId, + messageRecord, + viewModel.blindedPublicKey, + isResync = true + ) + } } } @@ -2536,7 +2547,13 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val accountId = textSecurePreferences.getLocalNumber() scope.launch { messages.iterator().forEach { messageRecord -> - ResendMessageUtilities.resend(accountId, messageRecord, viewModel.blindedPublicKey) + runCatching { + ResendMessageUtilities.resend( + accountId, + messageRecord, + viewModel.blindedPublicKey + ) + } } } endActionMode() 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 2d72b4cf5a..89c980c0c5 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 @@ -1257,12 +1257,14 @@ class ConversationViewModel @AssistedInject constructor( reactionDb.deleteEmojiReactions(emoji, messageId) (address as? Address.Community)?.let { openGroup -> lokiMessageDb.getServerID(messageId)?.let { serverId -> - OpenGroupApi.deleteAllReactions( - openGroup.room, - openGroup.serverUrl, - serverId, - emoji - ) + runCatching { + OpenGroupApi.deleteAllReactions( + openGroup.room, + openGroup.serverUrl, + serverId, + emoji + ) + } } } } 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 c75071712d..c5e3acf8dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt @@ -4,6 +4,7 @@ import android.content.Context import com.google.protobuf.ByteString import com.squareup.phrase.Phrase import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async @@ -657,11 +658,13 @@ class GroupManagerV2Impl @Inject constructor( .setInviteResponse(inviteResponse) val responseMessage = GroupUpdated(responseData.build(), profile = storage.getUserProfile()) // this will fail the first couple of times :) - MessageSender.sendNonDurably( - responseMessage, - Destination.ClosedGroup(group.groupAccountId), - isSyncMessage = false - ) + runCatching { + MessageSender.sendNonDurably( + responseMessage, + Destination.ClosedGroup(group.groupAccountId), + isSyncMessage = false + ) + } } else { // If we are invited as admin, we can just update the group info ourselves configFactory.withMutableGroupConfigs(AccountId(group.groupAccountId)) { configs -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt index e73913a434..5c2ba0e75b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt @@ -46,8 +46,10 @@ class JoinCommunityViewModel @Inject constructor( init { viewModelScope.launch(Dispatchers.Default) { - OpenGroupApi.getDefaultServerCapabilities().map { - OpenGroupApi.getDefaultRoomsIfNeeded() + runCatching { + OpenGroupApi.getDefaultServerCapabilities().map { + OpenGroupApi.getDefaultRoomsIfNeeded() + } } } 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 e66321c48a..5a864b0f80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -339,11 +339,13 @@ class CallManager @Inject constructor( .applyExpiryMode(expectedRecipient) .also { scope.launch { - MessageSender.sendNonDurably( - it, - currentRecipient, - isSyncMessage = currentRecipient.isLocalNumber - ) + runCatching { + MessageSender.sendNonDurably( + it, + currentRecipient, + isSyncMessage = currentRecipient.isLocalNumber + ) + } } } @@ -475,7 +477,7 @@ class CallManager @Inject constructor( val connection = peerConnection ?: throw NullPointerException("No peer connection wrapper") val reconnected = stateProcessor.processEvent(Event.ReceiveOffer) && stateProcessor.processEvent(Event.SendAnswer) - return if (reconnected) { + if (reconnected) { Log.i("Loki", "Handling new offer, restarting ice session") connection.setNewRemoteDescription(SessionDescription(SessionDescription.Type.OFFER, offer)) // re-established an ice @@ -487,7 +489,14 @@ class CallManager @Inject constructor( pendingIncomingIceUpdates.clear() val answerMessage = CallMessage.answer(answer.description, callId).applyExpiryMode(recipient) Log.i("Loki", "Posting new answer") - MessageSender.sendNonDurably(answerMessage, recipient, isSyncMessage = recipient.isLocalNumber) + + runCatching { + MessageSender.sendNonDurably( + answerMessage, + recipient, + isSyncMessage = recipient.isLocalNumber + ) + } } else { throw Exception("Couldn't reconnect from current state") } @@ -534,11 +543,23 @@ class CallManager @Inject constructor( connection.setLocalDescription(answer) val answerMessage = CallMessage.answer(answer.description, callId).applyExpiryMode(recipient) val userAddress = storage.getUserPublicKey() ?: throw NullPointerException("No user public key") - MessageSender.sendNonDurably(answerMessage, Address.fromSerialized(userAddress), isSyncMessage = true) - MessageSender.sendNonDurably(CallMessage.answer( - answer.description, - callId - ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) + + runCatching { + MessageSender.sendNonDurably( + answerMessage, + Address.fromSerialized(userAddress), + isSyncMessage = true + ) + } + + runCatching { + MessageSender.sendNonDurably( + CallMessage.answer( + answer.description, + callId + ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber + ) + } insertCallMessage(recipient.toString(), CallMessageType.CALL_INCOMING, false) @@ -587,15 +608,17 @@ class CallManager @Inject constructor( connection.setLocalDescription(offer) Log.d("Loki", "Sending pre-offer") - MessageSender.sendNonDurably(CallMessage.preOffer( - callId - ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) + try { + MessageSender.sendNonDurably( + CallMessage.preOffer( + callId + ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber + ) - Log.d("Loki", "Sent pre-offer") - Log.d("Loki", "Sending offer") - postViewModelState(CallViewModel.State.CALL_OFFER_OUTGOING) + Log.d("Loki", "Sent pre-offer") + Log.d("Loki", "Sending offer") + postViewModelState(CallViewModel.State.CALL_OFFER_OUTGOING) - try { MessageSender.sendNonDurably(CallMessage.offer( offer.description, callId @@ -615,19 +638,25 @@ class CallManager @Inject constructor( val userAddress = storage.getUserPublicKey() ?: return stateProcessor.processEvent(Event.DeclineCall) { scope.launch { - MessageSender.sendNonDurably( - CallMessage.endCall(callId).applyExpiryMode(recipient), - Address.fromSerialized(userAddress), - isSyncMessage = true - ) - MessageSender.sendNonDurably( - CallMessage.endCall(callId).applyExpiryMode(recipient), - recipient, - isSyncMessage = recipient.isLocalNumber - ) - insertCallMessage(recipient.toString(), CallMessageType.CALL_INCOMING) + runCatching { + MessageSender.sendNonDurably( + CallMessage.endCall(callId).applyExpiryMode(recipient), + Address.fromSerialized(userAddress), + isSyncMessage = true + ) + } + } + scope.launch { + runCatching { + MessageSender.sendNonDurably( + CallMessage.endCall(callId).applyExpiryMode(recipient), + recipient, + isSyncMessage = recipient.isLocalNumber + ) + } } + insertCallMessage(recipient.toString(), CallMessageType.CALL_INCOMING) } } @@ -650,11 +679,13 @@ class CallManager @Inject constructor( } scope.launch { - MessageSender.sendNonDurably( - CallMessage.endCall(callId).applyExpiryMode(recipient), - recipient, - isSyncMessage = recipient.isLocalNumber - ) + runCatching { + MessageSender.sendNonDurably( + CallMessage.endCall(callId).applyExpiryMode(recipient), + recipient, + isSyncMessage = recipient.isLocalNumber + ) + } } } } @@ -887,11 +918,13 @@ class CallManager @Inject constructor( }) connection.setLocalDescription(offer) scope.launch { - MessageSender.sendNonDurably( - CallMessage.offer(offer.description, callId).applyExpiryMode(recipient), - recipient, - isSyncMessage = recipient.isLocalNumber - ) + runCatching { + MessageSender.sendNonDurably( + CallMessage.offer(offer.description, callId).applyExpiryMode(recipient), + recipient, + isSyncMessage = recipient.isLocalNumber + ) + } } } } 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 7655089752..70c0a68b21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallBridge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallBridge.kt @@ -50,6 +50,7 @@ import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton +import kotlin.coroutines.cancellation.CancellationException import org.thoughtcrime.securesms.webrtc.data.State as CallState //todo PHONE We want to eventually remove this bridging class and move the logic here to a better place, probably in the callManager @@ -162,6 +163,9 @@ class WebRtcCallBridge @Inject constructor( scope.launch { try { callManager.onNewOffer(sdp, callId, address) + } catch (e: CancellationException) { + Log.d(TAG, "onNewOffer coroutine cancelled", e) + throw e } catch (e: Exception) { Log.e("Loki", "Error handling new offer", e) callManager.postConnectionError() From 85908cecc4b7ca80716e6703e7f4e7f7683be85a Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 17 Oct 2025 14:38:21 +1100 Subject: [PATCH 06/10] PR feedback --- .../securesms/conversation/v2/ConversationActivityV2.kt | 4 ++-- .../startconversation/community/JoinCommunityViewModel.kt | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) 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 79f4982934..ffd2153f91 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 @@ -2528,8 +2528,8 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, override fun resyncMessage(messages: Set) { val accountId = textSecurePreferences.getLocalNumber() scope.launch { - runCatching { - messages.iterator().forEach { messageRecord -> + messages.iterator().forEach { messageRecord -> + runCatching { ResendMessageUtilities.resend( accountId, messageRecord, diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt index 5c2ba0e75b..ab7961b1b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt @@ -47,9 +47,8 @@ class JoinCommunityViewModel @Inject constructor( init { viewModelScope.launch(Dispatchers.Default) { runCatching { - OpenGroupApi.getDefaultServerCapabilities().map { - OpenGroupApi.getDefaultRoomsIfNeeded() - } + OpenGroupApi.getDefaultServerCapabilities() + OpenGroupApi.getDefaultRoomsIfNeeded() } } From 2243e53ec6326a869161cfbd838c9228d845c8eb Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 17 Oct 2025 17:04:15 +1100 Subject: [PATCH 07/10] Recreating the DeleteNotificationReceiver in Kotlin and using coroutines instead of AsyncTask --- app/src/main/AndroidManifest.xml | 5 +- .../notifications/MessageNotifier.kt | 1 - .../dependencies/DatabaseComponent.kt | 14 -- .../notifications/DefaultMessageNotifier.kt | 124 +++--------------- .../DeleteNotificationReceiver.java | 41 ------ .../DeleteNotificationReceiver.kt | 55 ++++++++ .../OptimizedMessageNotifier.java | 5 - 7 files changed, 74 insertions(+), 171 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a97d1610f8..0ddf065ec3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -432,10 +432,7 @@ - - - + android:exported="false"> = getRepeatAlertsCount(context)) { - return - } - - val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - val alarmIntent = Intent(ReminderReceiver.REMINDER_ACTION) - alarmIntent.putExtra("reminder_count", count) - - val pendingIntent = PendingIntent.getBroadcast( - context, - 0, - alarmIntent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - val timeout = TimeUnit.MINUTES.toMillis(2) - - alarmManager[AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout] = pendingIntent - } - - override fun clearReminder(context: Context) { - val alarmIntent = Intent(ReminderReceiver.REMINDER_ACTION) - val pendingIntent = PendingIntent.getBroadcast( - context, - 0, - alarmIntent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - alarmManager.cancel(pendingIntent) - } - - class ReminderReceiver : BroadcastReceiver() { - @SuppressLint("StaticFieldLeak") - override fun onReceive(context: Context, intent: Intent) { - object : AsyncTask() { - - override fun doInBackground(vararg params: Void?): Void? { - val reminderCount = intent.getIntExtra("reminder_count", 0) - ApplicationContext.getInstance(context).messageNotifier.updateNotification( - context, - true, - reminderCount + 1 - ) - return null - } - - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) - } - - companion object { - const val REMINDER_ACTION: String = - "network.loki.securesms.MessageNotifier.REMINDER_ACTION" - } - } - private class DelayedNotification(private val context: Context, private val threadId: Long) : Runnable { private val canceled = AtomicBoolean(false) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java deleted file mode 100644 index e95bbbb6f8..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.thoughtcrime.securesms.notifications; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; - -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.dependencies.DatabaseComponent; - -public class DeleteNotificationReceiver extends BroadcastReceiver { - - public static String DELETE_NOTIFICATION_ACTION = "network.loki.securesms.DELETE_NOTIFICATION"; - - public static String EXTRA_IDS = "message_ids"; - public static String EXTRA_MMS = "is_mms"; - - @Override - public void onReceive(final Context context, Intent intent) { - if (DELETE_NOTIFICATION_ACTION.equals(intent.getAction())) { - ApplicationContext.getInstance(context).getMessageNotifier().clearReminder(context); - - final long[] ids = intent.getLongArrayExtra(EXTRA_IDS); - final boolean[] mms = intent.getBooleanArrayExtra(EXTRA_MMS); - - if (ids == null || mms == null || ids.length != mms.length) return; - - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - for (int i=0;i { From fbc0067e4a16a1457d7963883d861b675e876595 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Mon, 20 Oct 2025 09:45:05 +1100 Subject: [PATCH 08/10] Updating the notification logic around the swipe off action Swiping off a notification from the tray now sets the last_seen timestamp to help with handling the reaction notifications --- .../libsession/database/StorageProtocol.kt | 2 +- .../notifications/MessageNotifier.kt | 2 - .../conversation/v2/ConversationActivityV2.kt | 16 +++- .../securesms/database/Storage.kt | 7 +- .../securesms/database/ThreadDatabase.java | 6 +- .../notifications/DefaultMessageNotifier.kt | 94 +------------------ .../DeleteNotificationReceiver.kt | 15 +++ .../notifications/NotificationState.java | 7 ++ .../OptimizedMessageNotifier.java | 7 -- 9 files changed, 44 insertions(+), 112 deletions(-) diff --git a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt index b10e71628a..8b3dcd632e 100644 --- a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -181,7 +181,7 @@ interface StorageProtocol { attachments: List, runThreadUpdate: Boolean ): MessageId? - fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean = false) + fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean = false, updateNotification: Boolean = true) fun markConversationAsUnread(threadId: Long) fun getLastSeen(threadId: Long): Long fun ensureMessageHashesAreSender(hashes: Set, sender: String, closedGroupId: String): Boolean 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 9b52eb48bf..3b09af6919 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 @@ -5,8 +5,6 @@ import android.content.Context interface MessageNotifier { fun setHomeScreenVisible(isVisible: Boolean) fun setVisibleThread(threadId: Long) - fun setLastDesktopActivityTimestamp(timestamp: Long) - fun cancelDelayedNotifications() fun updateNotification(context: Context) fun updateNotification(context: Context, threadId: Long) fun updateNotification(context: Context, threadId: Long, signal: Boolean) 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 ffd2153f91..76729db389 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 @@ -1376,10 +1376,18 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val maybeTargetVisiblePosition = layoutManager?.findLastVisibleItemPosition() val targetVisiblePosition = maybeTargetVisiblePosition ?: RecyclerView.NO_POSITION if (!firstLoad.get() && targetVisiblePosition != RecyclerView.NO_POSITION) { - adapter.getTimestampForItemAt(targetVisiblePosition)?.let { visibleItemTimestamp -> - bufferedLastSeenChannel.trySend(visibleItemTimestamp).apply { - if (isFailure) Log.e(TAG, "trySend failed", exceptionOrNull()) - } + val timestampToSend: Long? = if (binding.conversationRecyclerView.isFullyScrolled) { + // We are at the bottom, so mark "now" as the last seen time + clock.currentTimeMills() + } else { + // We are not at the bottom, so just mark the timestamp of the last visible message + adapter.getTimestampForItemAt(targetVisiblePosition) + } + + timestampToSend?.let { + bufferedLastSeenChannel.trySend(it).apply { + if (isFailure) Log.e(TAG, "trySend failed", exceptionOrNull()) + } } } 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 d3b175264d..d356cfb19b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -191,16 +191,17 @@ open class Storage @Inject constructor( return messages.map { it.second } // return the message hashes } - override fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean) { + override fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean, updateNotification: Boolean) { val threadDb = threadDatabase 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, lastSeenTime, force) && !force) return + if (!threadDb.markAllAsRead(threadId, lastSeenTime, force, updateNotification) && !force) return // don't process configs for inbox recipients if (recipient.isCommunityInboxRecipient) return + val currentLastRead = threadDb.getLastSeenAndHasSent(threadId).first() + configFactory.withMutableUserConfigs { configs -> val config = configs.convoInfoVolatile val convo = getConvo(recipient, config) ?: return@withMutableUserConfigs 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 26e01384f9..ec0cdbf912 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -761,13 +761,15 @@ public boolean isRead(long threadId) { /** * @param threadId * @param lastSeenTime + * @param force + * @param updateNotifications - if true, update the notification state. Set to false if you already came from a notification interaction * @return true if we have set the last seen for the thread, false if there were no messages in the thread */ - public boolean markAllAsRead(long threadId, long lastSeenTime, boolean force) { + public boolean markAllAsRead(long threadId, long lastSeenTime, boolean force, boolean updateNotifications) { if (mmsSmsDatabase.get().getConversationCount(threadId) <= 0 && !force) return false; List messages = setRead(threadId, lastSeenTime); MarkReadReceiver.process(context, messages); - messageNotifier.get().updateNotification(context, threadId); + if(updateNotifications) messageNotifier.get().updateNotification(context, threadId); return setLastSeen(threadId, lastSeenTime); } 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 46f9c4113b..9201e153af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt @@ -16,14 +16,11 @@ */ package org.thoughtcrime.securesms.notifications -import android.Manifest import android.annotation.SuppressLint import android.content.Context -import android.content.pm.PackageManager import android.database.Cursor import android.graphics.Bitmap import android.text.TextUtils -import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import coil3.ImageLoader @@ -44,8 +41,6 @@ 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.session.libsignal.utilities.Util -import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions import org.thoughtcrime.securesms.crypto.KeyPairUtilities.getUserED25519KeyPair import org.thoughtcrime.securesms.database.MmsSmsColumns.NOTIFIED @@ -62,10 +57,7 @@ import org.thoughtcrime.securesms.util.AvatarUtils 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 javax.inject.Provider import kotlin.concurrent.Volatile @@ -94,14 +86,6 @@ class DefaultMessageNotifier @Inject constructor( homeScreenVisible = isVisible } - override fun setLastDesktopActivityTimestamp(timestamp: Long) { - lastDesktopActivityTimestamp = timestamp - } - - override fun cancelDelayedNotifications() { - executor.cancel() - } - private fun cancelActiveNotifications(context: Context): Boolean { val notifications = ServiceUtil.getNotificationManager(context) val hasNotifications = notifications.activeNotifications.size > 0 @@ -172,12 +156,7 @@ class DefaultMessageNotifier @Inject constructor( } override fun updateNotification(context: Context, threadId: Long) { - if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DESKTOP_ACTIVITY_PERIOD) { - Log.i(TAG, "Scheduling delayed notification...") - executor.execute(DelayedNotification(context, threadId)) - } else { - updateNotification(context, threadId, true) - } + updateNotification(context, threadId, true) } override fun updateNotification(context: Context, threadId: Long, signal: Boolean) { @@ -822,72 +801,6 @@ class DefaultMessageNotifier @Inject constructor( return null } - private class DelayedNotification(private val context: Context, private val threadId: Long) : - Runnable { - private val canceled = AtomicBoolean(false) - - private val delayUntil: Long - - init { - this.delayUntil = System.currentTimeMillis() + DELAY - } - - override fun run() { - val delayMillis = delayUntil - System.currentTimeMillis() - Log.i(TAG, "Waiting to notify: $delayMillis") - - if (delayMillis > 0) { - Util.sleep(delayMillis) - } - - if (!canceled.get()) { - Log.i(TAG, "Not canceled, notifying...") - ApplicationContext.getInstance(context).messageNotifier.updateNotification( - context, - threadId, - true - ) - ApplicationContext.getInstance(context).messageNotifier.cancelDelayedNotifications() - } else { - Log.w(TAG, "Canceled, not notifying...") - } - } - - fun cancel() { - canceled.set(true) - } - - companion object { - private val DELAY = TimeUnit.SECONDS.toMillis(5) - } - } - - private class CancelableExecutor { - private val executor: Executor = Executors.newSingleThreadExecutor() - private val tasks: MutableSet = HashSet() - - fun execute(runnable: DelayedNotification) { - synchronized(tasks) { tasks.add(runnable) } - - val wrapper = Runnable { - runnable.run() - synchronized(tasks) { - tasks.remove(runnable) - } - } - - executor.execute(wrapper) - } - - fun cancel() { - synchronized(tasks) { - for (task in tasks) { - task.cancel() - } - } - } - } - companion object { private val TAG: String = DefaultMessageNotifier::class.java.simpleName @@ -908,7 +821,6 @@ class DefaultMessageNotifier @Inject constructor( private const val REQUEST_TAG = "message_request" private val MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(5) - private val DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1) @Volatile private var visibleThread: Long = -1 @@ -916,11 +828,7 @@ class DefaultMessageNotifier @Inject constructor( @Volatile private var homeScreenVisible = false - @Volatile - private var lastDesktopActivityTimestamp: Long = -1 - @Volatile private var lastAudibleNotification: Long = -1 - private val executor = CancelableExecutor() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.kt index cb252435f5..064b10c507 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.withContext import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.SmsDatabase +import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.dependencies.ManagerScope import javax.inject.Inject @@ -21,6 +22,7 @@ class DeleteNotificationReceiver : BroadcastReceiver() { const val DELETE_NOTIFICATION_ACTION = "network.loki.securesms.DELETE_NOTIFICATION" const val EXTRA_IDS = "message_ids" const val EXTRA_MMS = "is_mms" + const val EXTRA_THREAD_IDS = "thread_ids" } @Inject @ManagerScope @@ -30,18 +32,31 @@ class DeleteNotificationReceiver : BroadcastReceiver() { @Inject lateinit var smsDb: SmsDatabase @Inject lateinit var mmsDb: MmsDatabase + @Inject lateinit var storage: Storage override fun onReceive(context: Context, intent: Intent) { if (intent.action != DELETE_NOTIFICATION_ACTION) return val ids = intent.getLongArrayExtra(EXTRA_IDS) ?: return val mms = intent.getBooleanArrayExtra(EXTRA_MMS) ?: return + val threadIds = intent.getLongArrayExtra(EXTRA_THREAD_IDS)?.toSet() ?: return + if (ids.size != mms.size) return val pending = goAsync() // extends the receiver's lifecycle scope.launch { try { withContext(Dispatchers.IO) { + val now = System.currentTimeMillis() + for(threadId in threadIds){ + storage.markConversationAsRead( + threadId = threadId, + lastSeenTime = now, + force = false, + updateNotification = false + ) + } + for (i in ids.indices) { if (!mms[i]) smsDb.markAsNotified(ids[i]) else mmsDb.markAsNotified(ids[i]) 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 c86b46bdce..301c04a5a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java @@ -228,9 +228,16 @@ public PendingIntent getDeleteIntent(Context context) { mms[index++] = notificationItem.isMms(); } + long[] threadIds = new long[threads.size()]; + index = 0; + for (long thread : threads) { + threadIds[index++] = thread; + } + Intent intent = new Intent(context, DeleteNotificationReceiver.class); intent.setAction(DeleteNotificationReceiver.DELETE_NOTIFICATION_ACTION); intent.putExtra(DeleteNotificationReceiver.EXTRA_IDS, ids); + intent.putExtra(DeleteNotificationReceiver.EXTRA_THREAD_IDS, threadIds); intent.putExtra(DeleteNotificationReceiver.EXTRA_MMS, mms); intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); 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 cc5204c1aa..480b2819c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java @@ -47,13 +47,6 @@ public void setHomeScreenVisible(boolean isVisible) { wrapped.setHomeScreenVisible(isVisible); } - @Override - public void setLastDesktopActivityTimestamp(long timestamp) { wrapped.setLastDesktopActivityTimestamp(timestamp);} - - - @Override - public void cancelDelayedNotifications() { wrapped.cancelDelayedNotifications(); } - @Override public void updateNotification(@NonNull Context context) { boolean isCaughtUp = true; From 085256056c9bd37b93f9a51cbccf0bdd8dad1983 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Mon, 20 Oct 2025 09:45:28 +1100 Subject: [PATCH 09/10] Optimise our compositionLocal as these two aren't changed often but read a lot --- .../main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt index 6c1395d1be..aa2ef6f7cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt @@ -10,6 +10,7 @@ import androidx.compose.material3.Shapes import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -19,8 +20,8 @@ import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.ApplicationContext // Globally accessible composition local objects -val LocalColors = compositionLocalOf { ClassicDark() } -val LocalType = compositionLocalOf { sessionTypography } +val LocalColors = staticCompositionLocalOf { ClassicDark() } +val LocalType = staticCompositionLocalOf { sessionTypography } var cachedColorsProvider: ThemeColorsProvider? = null From 4b59add2ed948811b5e0f7b33deb0f825ad6fdc3 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Mon, 20 Oct 2025 12:06:39 +1100 Subject: [PATCH 10/10] PR feedback --- .../securesms/notifications/DefaultMessageNotifier.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9201e153af..11af694ca2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt @@ -432,7 +432,7 @@ class DefaultMessageNotifier @Inject constructor( NotificationManagerCompat.from(context).notify(notificationId, notification) } - Log.i(TAG, "Posted notification. $notification") + Log.i(TAG, "Posted notification. $notificationId") } }