Fix crashes on conversation screen (#1590)

* Fixed onion request crashes

* Use kotlin.Result instead

---------

Co-authored-by: fanchao <git@fanchao.dev>
This commit is contained in:
Fanchao Liu 2024-08-01 19:36:38 +10:00 committed by GitHub
parent 2a5f86ea06
commit 268b6cc097
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 38 additions and 87 deletions

View File

@ -8,7 +8,6 @@ import app.cash.copper.Query
import app.cash.copper.flow.observeQuery import app.cash.copper.flow.observeQuery
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -59,15 +58,15 @@ interface ConversationRepository {
fun deleteLocally(recipient: Recipient, message: MessageRecord) fun deleteLocally(recipient: Recipient, message: MessageRecord)
fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord) fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord)
fun setApproved(recipient: Recipient, isApproved: Boolean) fun setApproved(recipient: Recipient, isApproved: Boolean)
suspend fun deleteForEveryone(threadId: Long, recipient: Recipient, message: MessageRecord): ResultOf<Unit> suspend fun deleteForEveryone(threadId: Long, recipient: Recipient, message: MessageRecord): Result<Unit>
fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest?
suspend fun deleteMessageWithoutUnsendRequest(threadId: Long, messages: Set<MessageRecord>): ResultOf<Unit> suspend fun deleteMessageWithoutUnsendRequest(threadId: Long, messages: Set<MessageRecord>): Result<Unit>
suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit> suspend fun banUser(threadId: Long, recipient: Recipient): Result<Unit>
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): Result<Unit>
suspend fun deleteThread(threadId: Long): ResultOf<Unit> suspend fun deleteThread(threadId: Long): Result<Unit>
suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf<Unit> suspend fun deleteMessageRequest(thread: ThreadRecord): Result<Unit>
suspend fun clearAllMessageRequests(block: Boolean): ResultOf<Unit> suspend fun clearAllMessageRequests(block: Boolean): Result<Unit>
suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf<Unit> suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): Result<Unit>
fun declineMessageRequest(threadId: Long) fun declineMessageRequest(threadId: Long)
fun hasReceived(threadId: Long): Boolean fun hasReceived(threadId: Long): Boolean
} }
@ -185,7 +184,7 @@ class DefaultConversationRepository @Inject constructor(
threadId: Long, threadId: Long,
recipient: Recipient, recipient: Recipient,
message: MessageRecord message: MessageRecord
): ResultOf<Unit> = suspendCoroutine { continuation -> ): Result<Unit> = suspendCoroutine { continuation ->
buildUnsendRequest(recipient, message)?.let { unsendRequest -> buildUnsendRequest(recipient, message)?.let { unsendRequest ->
MessageSender.send(unsendRequest, recipient.address) MessageSender.send(unsendRequest, recipient.address)
} }
@ -196,10 +195,10 @@ class DefaultConversationRepository @Inject constructor(
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server) OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server)
.success { .success {
messageDataProvider.deleteMessage(message.id, !message.isMms) messageDataProvider.deleteMessage(message.id, !message.isMms)
continuation.resume(ResultOf.Success(Unit)) continuation.resume(Result.success(Unit))
}.fail { error -> }.fail { error ->
Log.w("TAG", "Call to OpenGroupApi.deleteForEveryone failed - attempting to resume..") Log.w("TAG", "Call to OpenGroupApi.deleteForEveryone failed - attempting to resume..")
continuation.resumeWithException(error) continuation.resume(Result.failure(error))
} }
} }
@ -229,10 +228,10 @@ class DefaultConversationRepository @Inject constructor(
} }
SnodeAPI.deleteMessage(publicKey, listOf(serverHash)) SnodeAPI.deleteMessage(publicKey, listOf(serverHash))
.success { .success {
continuation.resume(ResultOf.Success(Unit)) continuation.resume(Result.success(Unit))
}.fail { error -> }.fail { error ->
Log.w("ConversationRepository", "Call to SnodeAPI.deleteMessage failed - attempting to resume..") Log.w("ConversationRepository", "Call to SnodeAPI.deleteMessage failed - attempting to resume..")
continuation.resumeWithException(error) continuation.resume(Result.failure(error))
} }
} }
} }
@ -250,7 +249,7 @@ class DefaultConversationRepository @Inject constructor(
override suspend fun deleteMessageWithoutUnsendRequest( override suspend fun deleteMessageWithoutUnsendRequest(
threadId: Long, threadId: Long,
messages: Set<MessageRecord> messages: Set<MessageRecord>
): ResultOf<Unit> = suspendCoroutine { continuation -> ): Result<Unit> = suspendCoroutine { continuation ->
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
if (openGroup != null) { if (openGroup != null) {
val messageServerIDs = mutableMapOf<Long, MessageRecord>() val messageServerIDs = mutableMapOf<Long, MessageRecord>()
@ -264,7 +263,7 @@ class DefaultConversationRepository @Inject constructor(
.success { .success {
messageDataProvider.deleteMessage(message.id, !message.isMms) messageDataProvider.deleteMessage(message.id, !message.isMms)
}.fail { error -> }.fail { error ->
continuation.resumeWithException(error) continuation.resume(Result.failure(error))
} }
} }
} else { } else {
@ -276,22 +275,22 @@ class DefaultConversationRepository @Inject constructor(
} }
} }
} }
continuation.resume(ResultOf.Success(Unit)) continuation.resume(Result.success(Unit))
} }
override suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit> = override suspend fun banUser(threadId: Long, recipient: Recipient): Result<Unit> =
suspendCoroutine { continuation -> suspendCoroutine { continuation ->
val accountID = recipient.address.toString() val accountID = recipient.address.toString()
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!! val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
OpenGroupApi.ban(accountID, openGroup.room, openGroup.server) OpenGroupApi.ban(accountID, openGroup.room, openGroup.server)
.success { .success {
continuation.resume(ResultOf.Success(Unit)) continuation.resume(Result.success(Unit))
}.fail { error -> }.fail { error ->
continuation.resumeWithException(error) continuation.resume(Result.failure(error))
} }
} }
override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> = override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): Result<Unit> =
suspendCoroutine { continuation -> suspendCoroutine { continuation ->
// Note: This accountId could be the blinded Id // Note: This accountId could be the blinded Id
val accountID = recipient.address.toString() val accountID = recipient.address.toString()
@ -299,25 +298,25 @@ class DefaultConversationRepository @Inject constructor(
OpenGroupApi.banAndDeleteAll(accountID, openGroup.room, openGroup.server) OpenGroupApi.banAndDeleteAll(accountID, openGroup.room, openGroup.server)
.success { .success {
continuation.resume(ResultOf.Success(Unit)) continuation.resume(Result.success(Unit))
}.fail { error -> }.fail { error ->
continuation.resumeWithException(error) continuation.resume(Result.failure(error))
} }
} }
override suspend fun deleteThread(threadId: Long): ResultOf<Unit> { override suspend fun deleteThread(threadId: Long): Result<Unit> {
sessionJobDb.cancelPendingMessageSendJobs(threadId) sessionJobDb.cancelPendingMessageSendJobs(threadId)
storage.deleteConversation(threadId) storage.deleteConversation(threadId)
return ResultOf.Success(Unit) return Result.success(Unit)
} }
override suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf<Unit> { override suspend fun deleteMessageRequest(thread: ThreadRecord): Result<Unit> {
sessionJobDb.cancelPendingMessageSendJobs(thread.threadId) sessionJobDb.cancelPendingMessageSendJobs(thread.threadId)
storage.deleteConversation(thread.threadId) storage.deleteConversation(thread.threadId)
return ResultOf.Success(Unit) return Result.success(Unit)
} }
override suspend fun clearAllMessageRequests(block: Boolean): ResultOf<Unit> { override suspend fun clearAllMessageRequests(block: Boolean): Result<Unit> {
threadDb.readerFor(threadDb.unapprovedConversationList).use { reader -> threadDb.readerFor(threadDb.unapprovedConversationList).use { reader ->
while (reader.next != null) { while (reader.next != null) {
deleteMessageRequest(reader.current) deleteMessageRequest(reader.current)
@ -325,18 +324,18 @@ class DefaultConversationRepository @Inject constructor(
if (block) { setBlocked(recipient, true) } if (block) { setBlocked(recipient, true) }
} }
} }
return ResultOf.Success(Unit) return Result.success(Unit)
} }
override suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf<Unit> = suspendCoroutine { continuation -> override suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): Result<Unit> = suspendCoroutine { continuation ->
storage.setRecipientApproved(recipient, true) storage.setRecipientApproved(recipient, true)
val message = MessageRequestResponse(true) val message = MessageRequestResponse(true)
MessageSender.send(message, Destination.from(recipient.address), isSyncMessage = recipient.isLocalNumber) MessageSender.send(message, Destination.from(recipient.address), isSyncMessage = recipient.isLocalNumber)
.success { .success {
threadDb.setHasSent(threadId, true) threadDb.setHasSent(threadId, true)
continuation.resume(ResultOf.Success(Unit)) continuation.resume(Result.success(Unit))
}.fail { error -> }.fail { error ->
continuation.resumeWithException(error) continuation.resume(Result.failure(error))
} }
} }

View File

@ -1,43 +0,0 @@
package org.thoughtcrime.securesms.repository
import kotlinx.coroutines.CancellationException
sealed class ResultOf<out T> {
data class Success<out R>(val value: R) : ResultOf<R>()
data class Failure(val throwable: Throwable) : ResultOf<Nothing>()
inline fun onFailure(block: (throwable: Throwable) -> Unit) = this.also {
if (this is Failure) {
block(throwable)
}
}
inline fun onSuccess(block: (value: T) -> Unit) = this.also {
if (this is Success) {
block(value)
}
}
inline fun <R> flatMap(mapper: (T) -> R): ResultOf<R> = when (this) {
is Success -> wrap { mapper(value) }
is Failure -> Failure(throwable)
}
fun getOrThrow(): T = when (this) {
is Success -> value
is Failure -> throw throwable
}
companion object {
inline fun <T> wrap(block: () -> T): ResultOf<T> =
try {
Success(block())
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Failure(e)
}
}
}

View File

@ -3,14 +3,12 @@ package org.thoughtcrime.securesms.conversation.v2
import com.goterl.lazysodium.utils.KeyPair import com.goterl.lazysodium.utils.KeyPair
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers.endsWith import org.hamcrest.CoreMatchers.endsWith
import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.notNullValue import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.CoreMatchers.nullValue import org.hamcrest.CoreMatchers.nullValue
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test import org.junit.Test
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.anyLong import org.mockito.Mockito.anyLong
@ -20,14 +18,11 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.BaseViewModelTest import org.thoughtcrime.securesms.BaseViewModelTest
import org.thoughtcrime.securesms.NoOpLogger
import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.repository.ConversationRepository
import org.thoughtcrime.securesms.repository.ResultOf
class ConversationViewModelTest: BaseViewModelTest() { class ConversationViewModelTest: BaseViewModelTest() {
@ -107,7 +102,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
val message = mock<MessageRecord>() val message = mock<MessageRecord>()
val error = Throwable() val error = Throwable()
whenever(repository.deleteForEveryone(anyLong(), any(), any())) whenever(repository.deleteForEveryone(anyLong(), any(), any()))
.thenReturn(ResultOf.Failure(error)) .thenReturn(Result.failure(error))
viewModel.deleteForEveryone(message) viewModel.deleteForEveryone(message)
@ -120,7 +115,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
val message = mock<MessageRecord>() val message = mock<MessageRecord>()
val error = Throwable() val error = Throwable()
whenever(repository.deleteMessageWithoutUnsendRequest(anyLong(), anySet())) whenever(repository.deleteMessageWithoutUnsendRequest(anyLong(), anySet()))
.thenReturn(ResultOf.Failure(error)) .thenReturn(Result.failure(error))
viewModel.deleteMessagesWithoutUnsendRequest(setOf(message)) viewModel.deleteMessagesWithoutUnsendRequest(setOf(message))
@ -130,7 +125,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Test @Test
fun `should emit error message on ban user failure`() = runBlockingTest { fun `should emit error message on ban user failure`() = runBlockingTest {
val error = Throwable() val error = Throwable()
whenever(repository.banUser(anyLong(), any())).thenReturn(ResultOf.Failure(error)) whenever(repository.banUser(anyLong(), any())).thenReturn(Result.failure(error))
viewModel.banUser(recipient) viewModel.banUser(recipient)
@ -139,7 +134,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Test @Test
fun `should emit a message on ban user success`() = runBlockingTest { fun `should emit a message on ban user success`() = runBlockingTest {
whenever(repository.banUser(anyLong(), any())).thenReturn(ResultOf.Success(Unit)) whenever(repository.banUser(anyLong(), any())).thenReturn(Result.success(Unit))
viewModel.banUser(recipient) viewModel.banUser(recipient)
@ -152,7 +147,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Test @Test
fun `should emit error message on ban user and delete all failure`() = runBlockingTest { fun `should emit error message on ban user and delete all failure`() = runBlockingTest {
val error = Throwable() val error = Throwable()
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Failure(error)) whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(Result.failure(error))
viewModel.banAndDeleteAll(messageRecord) viewModel.banAndDeleteAll(messageRecord)
@ -161,7 +156,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Test @Test
fun `should emit a message on ban user and delete all success`() = runBlockingTest { fun `should emit a message on ban user and delete all success`() = runBlockingTest {
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Success(Unit)) whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(Result.success(Unit))
viewModel.banAndDeleteAll(messageRecord) viewModel.banAndDeleteAll(messageRecord)
@ -188,7 +183,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Test @Test
fun `should remove shown message`() = runBlockingTest { fun `should remove shown message`() = runBlockingTest {
// Given that a message is generated // Given that a message is generated
whenever(repository.banUser(anyLong(), any())).thenReturn(ResultOf.Success(Unit)) whenever(repository.banUser(anyLong(), any())).thenReturn(Result.success(Unit))
viewModel.banUser(recipient) viewModel.banUser(recipient)
assertThat(viewModel.uiState.value.uiMessages.size, equalTo(1)) assertThat(viewModel.uiState.value.uiMessages.size, equalTo(1))
// When the message is shown // When the message is shown