diff --git a/CHANGES.md b/CHANGES.md index 42f8d875cb..08b9c01efc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,16 @@ +Changes in RiotX 0.18.1 (2020-03-17) +=================================================== + +Improvements 🙌: + - Implementation of /join command + +Bugfix 🐛: + - Message transitions in encrypted rooms are jarring #518 + - Images that failed to send are waiting to be sent forever #1145 + - Fix / Crashed when trying to send a gif from the Gboard #1136 + - Fix / Cannot click on key backup banner when new keys are available + + Changes in RiotX 0.18.0 (2020-03-11) =================================================== diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 84b76345c8..25dc939196 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -193,6 +193,7 @@ internal class DefaultSession @Inject constructor( stopAnyBackgroundSync() liveEntityObservers.forEach { it.cancelProcess() } cacheService.get().clearCache(callback) + workManagerProvider.cancelAllWorks() } @Subscribe(threadMode = ThreadMode.MAIN) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 1dde25fd78..1c88f87804 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -77,6 +77,16 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter return Result.success(inputData) } + // Just defensive code to ensure that we never have an uncaught exception that could break the queue + return try { + internalDoWork(params) + } catch (failure: Throwable) { + Timber.e(failure) + handleFailure(params, failure) + } + } + + private suspend fun internalDoWork(params: Params): Result { val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt index 72f5ee56b8..e4424f1cb3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt @@ -23,7 +23,10 @@ import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.android.internal.worker.SessionWorkerParams @@ -96,6 +99,22 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) type = safeResult.eventType, content = safeResult.eventContent ) + // Better handling of local echo, to avoid decrypting transition on remote echo + // Should I only do it for text messages? + if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) { + val decryptionLocalEcho = MXEventDecryptionResult( + clearEvent = Event( + type = localEvent.type, + content = localEvent.content, + roomId = localEvent.roomId + ).toContent(), + forwardingCurve25519KeyChain = emptyList(), + senderCurve25519Key = result.eventContent["sender_key"] as? String, + claimedEd25519Key = crypto.getMyDevice().fingerprint() + ) + localEchoUpdater.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho) + } + val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent) return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) } else { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt index 60d1a217a7..09d82c7b07 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt @@ -17,7 +17,11 @@ package im.vector.matrix.android.internal.session.room.send import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult +import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where import timber.log.Timber @@ -38,4 +42,15 @@ internal class LocalEchoUpdater @Inject constructor(private val monarchy: Monarc } } } + + fun updateEncryptedEcho(eventId: String, encryptedContent: Content, mxEventDecryptionResult: MXEventDecryptionResult) { + monarchy.writeAsync { realm -> + val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() + if (sendingEventEntity != null) { + sendingEventEntity.type = EventType.ENCRYPTED + sendingEventEntity.content = ContentMapper.map(encryptedContent) + sendingEventEntity.setDecryptionResult(mxEventDecryptionResult) + } + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt index 03db817dd6..8c31dd1682 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -23,6 +23,7 @@ import androidx.work.OneTimeWorkRequest import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.worker.SessionWorkerParams @@ -49,6 +50,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo @Inject lateinit var workManagerProvider: WorkManagerProvider @Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon + @Inject lateinit var localEchoUpdater: LocalEchoUpdater override suspend fun doWork(): Result { Timber.v("Start dispatch sending multiple event work") @@ -57,14 +59,17 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo Timber.e("Work cancelled due to input error from parent") } - if (params.lastFailureMessage != null) { - // Transmit the error - return Result.success(inputData) - } - val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) + if (params.lastFailureMessage != null) { + params.events.forEach { event -> + event.eventId?.let { localEchoUpdater.updateSendState(it, SendState.UNDELIVERED) } + } + // Transmit the error if needed? + return Result.success(inputData) + } + // Create a work for every event params.events.forEach { event -> if (params.isEncrypted) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 5b7c39a3d9..70c1e39334 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -25,6 +25,8 @@ import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.crypto.DefaultCryptoService +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.database.helper.addOrUpdate import im.vector.matrix.android.internal.database.helper.addTimelineEvent import im.vector.matrix.android.internal.database.mapper.ContentMapper @@ -38,6 +40,7 @@ import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoo import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.getOrNull import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.mapWithProgress import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater @@ -260,6 +263,14 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle if (sendingEventEntity != null) { Timber.v("Remove local echo for tx:$it") roomEntity.sendingTimelineEvents.remove(sendingEventEntity) + if (event.isEncrypted() && event.content?.get("algorithm") as? String == MXCRYPTO_ALGORITHM_MEGOLM) { + // updated with echo decryption, to avoid seeing it decrypt again + val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) + sendingEventEntity.root?.decryptionResultJson?.let { json -> + eventEntity.decryptionResultJson = json + event.mxDecryptionResult = adapter.fromJson(json) + } + } } else { Timber.v("Can't find corresponding local echo for tx:$it") } diff --git a/vector/build.gradle b/vector/build.gradle index 6d24a26838..2a5a8b3b34 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -16,7 +16,7 @@ androidExtensions { ext.versionMajor = 0 ext.versionMinor = 18 -ext.versionPatch = 0 +ext.versionPatch = 1 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt index 7ce394b954..8d314f9e58 100755 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt @@ -123,6 +123,7 @@ class KeysBackupBanner @JvmOverloads constructor( is State.Setup -> { delegate?.setupKeysBackup() } + is State.Update, is State.Recover -> { delegate?.recoverKeysBackup() } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt index ba1197b787..c576ebe1b9 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt @@ -177,19 +177,28 @@ class AttachmentsHelper private constructor(private val context: Context, fun handleShareIntent(intent: Intent): Boolean { val type = intent.resolveType(context) ?: return false if (type.startsWith("image")) { - imagePicker.submit(IntentUtils.getPickerIntentForSharing(intent)) + imagePicker.submit(safeShareIntent(intent)) } else if (type.startsWith("video")) { - videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent)) + videoPicker.submit(safeShareIntent(intent)) } else if (type.startsWith("audio")) { - videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent)) + videoPicker.submit(safeShareIntent(intent)) } else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) { - filePicker.submit(IntentUtils.getPickerIntentForSharing(intent)) + filePicker.submit(safeShareIntent(intent)) } else { return false } return true } + private fun safeShareIntent(intent: Intent): Intent { + // Work around for getPickerIntentForSharing doing NPE in android 10 + return try { + IntentUtils.getPickerIntentForSharing(intent) + } catch (failure: Throwable) { + intent + } + } + private fun getPickerManagerForRequestCode(requestCode: Int): PickerManager? { return when (requestCode) { PICK_IMAGE_DEVICE -> imagePicker diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt index 4f2d806ce3..1fec404f7d 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt @@ -31,10 +31,12 @@ import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.ui.list.GenericItem import im.vector.riotx.core.ui.list.genericItem +import im.vector.riotx.features.settings.VectorPreferences import java.util.UUID import javax.inject.Inject class KeysBackupSettingsRecyclerViewController @Inject constructor(private val stringProvider: StringProvider, + private val vectorPreferences: VectorPreferences, private val session: Session) : TypedEpoxyController() { var listener: Listener? = null @@ -149,7 +151,9 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s description(keyVersionResult?.algorithm ?: "") } - buildKeysBackupTrust(data.keysBackupVersionTrust) + if (vectorPreferences.developerMode()) { + buildKeysBackupTrust(data.keysBackupVersionTrust) + } } // Footer diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index ad4e9694db..f83adaf8a7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -292,16 +292,23 @@ class RoomDetailFragment @Inject constructor( is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) - is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it) - is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG) - is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it) - is RoomDetailViewEvents.FileTooBigError -> displayFileTooBigError(it) - is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it) - is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) + is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it) + is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG) + is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it) + is RoomDetailViewEvents.FileTooBigError -> displayFileTooBigError(it) + is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it) + is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) + is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) }.exhaustive } } + private fun handleJoinedToAnotherRoom(action: RoomDetailViewEvents.JoinRoomCommandSuccess) { + updateComposerText("") + lockSendButton = false + navigator.openRoom(vectorBaseActivity, action.roomId) + } + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) if (savedInstanceState == null) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt index fcbe7f37c0..b24c2ea23e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt @@ -50,6 +50,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents { abstract class SendMessageResult : RoomDetailViewEvents() object MessageSent : SendMessageResult() + data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult() class SlashCommandError(val command: Command) : SendMessageResult() class SlashCommandUnknown(val command: String) : SendMessageResult() data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 4ab9125c4f..f7a6c09022 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -379,8 +379,8 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) } is ParsedCommand.JoinRoom -> { - // TODO - _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) + handleJoinToAnotherRoomSlashCommand(slashCommandResult) + popDraft() } is ParsedCommand.PartRoom -> { // TODO @@ -512,6 +512,22 @@ class RoomDetailViewModel @AssistedInject constructor( room.deleteDraft(NoOpMatrixCallback()) } + private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) { + session.joinRoom(command.roomAlias, command.reason, object : MatrixCallback { + override fun onSuccess(data: Unit) { + session.getRoomSummary(command.roomAlias) + ?.roomId + ?.let { + _viewEvents.post(RoomDetailViewEvents.JoinRoomCommandSuccess(it)) + } + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) + } + }) + } + private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() return buildString { diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 6c56211e57..f0c3bdc23d 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1461,7 +1461,7 @@ Why choose Riot.im? Never lose encrypted messages Use Key Backup - New encrypted messages keys + New secure message keys Manage in Key Backup Backing up keys…