From 12b681209fc8c98f907619c932513bacd84140ab Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 6 Jul 2022 18:40:15 +0200 Subject: [PATCH] Use awaitRoom on Timeline screen --- .../home/room/detail/TimelineFragment.kt | 114 +- .../home/room/detail/TimelineViewModel.kt | 631 ++++++------ .../composer/MessageComposerViewModel.kt | 974 +++++++++--------- .../timeline/factory/TimelineFactory.kt | 7 +- .../raw/wellknown/ElementWellKnownExt.kt | 2 +- 5 files changed, 872 insertions(+), 856 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 855df14e60..4b990247ac 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -213,6 +213,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.commonmark.parser.Parser @@ -381,18 +382,20 @@ class TimelineFragment @Inject constructor( ) keyboardStateUtils = KeyboardStateUtils(requireActivity()) lazyLoadedViews.bind(views) - setupToolbar(views.roomToolbar) - .allowBack() - setupRecyclerView() - setupComposer() - setupNotificationView() - setupJumpToReadMarkerView() - setupActiveCallView() - setupJumpToBottomView() - setupEmojiButton() - setupRemoveJitsiWidgetView() - setupVoiceMessageView() - setupLiveLocationIndicator() + viewLifecycleOwner.lifecycleScope.launch { + setupToolbar(views.roomToolbar) + .allowBack() + setupRecyclerView() + setupComposer() + setupNotificationView() + setupJumpToReadMarkerView() + setupActiveCallView() + setupJumpToBottomView() + setupEmojiButton() + setupRemoveJitsiWidgetView() + setupVoiceMessageView() + setupLiveLocationIndicator() + } views.includeRoomToolbar.roomToolbarContentView.debouncedClicks { navigator.openRoomProfile(requireActivity(), timelineArgs.roomId) @@ -979,16 +982,17 @@ class TimelineFragment @Inject constructor( private fun setupJumpToBottomView() { views.jumpToBottomView.visibility = View.INVISIBLE views.jumpToBottomView.debouncedClicks { - timelineViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) - views.jumpToBottomView.visibility = View.INVISIBLE - if (!timelineViewModel.timeline.isLive) { - scrollOnNewMessageCallback.forceScrollOnNextUpdate() - timelineViewModel.timeline.restartWithEventId(null) - } else { - layoutManager.scrollToPosition(0) + viewLifecycleOwner.lifecycleScope.launch { + timelineViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) + views.jumpToBottomView.visibility = View.INVISIBLE + if (!timelineViewModel.timeline.await().isLive) { + scrollOnNewMessageCallback.forceScrollOnNextUpdate() + timelineViewModel.timeline.await().restartWithEventId(null) + } else { + layoutManager.scrollToPosition(0) + } } } - jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager( views.jumpToBottomView, debouncer, @@ -1216,12 +1220,14 @@ class TimelineFragment @Inject constructor( } private fun handleSearchAction() { - navigator.openSearch( - context = requireContext(), - roomId = timelineArgs.roomId, - roomDisplayName = timelineViewModel.getRoomSummary()?.displayName, - roomAvatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl - ) + viewLifecycleOwner.lifecycleScope.launch { + navigator.openSearch( + context = requireContext(), + roomId = timelineArgs.roomId, + roomDisplayName = timelineViewModel.getRoomSummary()?.displayName, + roomAvatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl + ) + } } private fun displayDisabledIntegrationDialog() { @@ -1416,9 +1422,9 @@ class TimelineFragment @Inject constructor( // PRIVATE METHODS ***************************************************************************** - private fun setupRecyclerView() { + private suspend fun setupRecyclerView() { timelineEventController.callback = this - timelineEventController.timeline = timelineViewModel.timeline + timelineEventController.timeline = timelineViewModel.timeline.await() views.timelineRecyclerView.trackItemsVisibilityChange() layoutManager = object : LinearLayoutManager(context, RecyclerView.VERTICAL, true) { @@ -2421,7 +2427,9 @@ class TimelineFragment @Inject constructor( views.composerLayout.views.composerEditText.setText(Command.EMOTE.command + " ") views.composerLayout.views.composerEditText.setSelection(Command.EMOTE.command.length + 1) } else { - val roomMember = timelineViewModel.getMember(userId) + val roomMember = runBlocking { + timelineViewModel.getMember(userId) + } // TODO move logic outside of fragment (roomMember?.displayName ?: userId) .let { sanitizeDisplayName(it) } @@ -2491,18 +2499,21 @@ class TimelineFragment @Inject constructor( * using the ThreadsActivity. */ private fun navigateToThreadTimeline(rootThreadEventId: String, startsThread: Boolean = false, showKeyboard: Boolean = false) { - analyticsTracker.capture(Interaction.Name.MobileRoomThreadSummaryItem.toAnalyticsInteraction()) - context?.let { - val roomThreadDetailArgs = ThreadTimelineArgs( - startsThread = startsThread, - roomId = timelineArgs.roomId, - displayName = timelineViewModel.getRoomSummary()?.displayName, - avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl, - roomEncryptionTrustLevel = timelineViewModel.getRoomSummary()?.roomEncryptionTrustLevel, - rootThreadEventId = rootThreadEventId, - showKeyboard = showKeyboard - ) - navigator.openThread(it, roomThreadDetailArgs) + viewLifecycleOwner.lifecycleScope.launch { + analyticsTracker.capture(Interaction.Name.MobileRoomThreadSummaryItem.toAnalyticsInteraction()) + context?.let { + val roomSummary = timelineViewModel.awaitState().asyncRoomSummary() + val roomThreadDetailArgs = ThreadTimelineArgs( + startsThread = startsThread, + roomId = timelineArgs.roomId, + displayName = roomSummary?.displayName, + avatarUrl = roomSummary?.avatarUrl, + roomEncryptionTrustLevel = roomSummary?.roomEncryptionTrustLevel, + rootThreadEventId = rootThreadEventId, + showKeyboard = showKeyboard + ) + navigator.openThread(it, roomThreadDetailArgs) + } } } @@ -2530,15 +2541,18 @@ class TimelineFragment @Inject constructor( * using the ThreadsActivity. */ private fun navigateToThreadList() { - analyticsTracker.capture(Interaction.Name.MobileRoomThreadListButton.toAnalyticsInteraction()) - context?.let { - val roomThreadDetailArgs = ThreadTimelineArgs( - roomId = timelineArgs.roomId, - displayName = timelineViewModel.getRoomSummary()?.displayName, - roomEncryptionTrustLevel = timelineViewModel.getRoomSummary()?.roomEncryptionTrustLevel, - avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl - ) - navigator.openThreadList(it, roomThreadDetailArgs) + viewLifecycleOwner.lifecycleScope.launch { + analyticsTracker.capture(Interaction.Name.MobileRoomThreadListButton.toAnalyticsInteraction()) + context?.let { + val roomSummary = timelineViewModel.awaitState().asyncRoomSummary() + val roomThreadDetailArgs = ThreadTimelineArgs( + roomId = timelineArgs.roomId, + displayName = roomSummary?.displayName, + roomEncryptionTrustLevel = roomSummary?.roomEncryptionTrustLevel, + avatarUrl = roomSummary?.avatarUrl + ) + navigator.openThreadList(it, roomThreadDetailArgs) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 48f8aef421..1f9eb93d1c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -32,6 +32,7 @@ import im.vector.app.AppStateHandler import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider @@ -63,8 +64,10 @@ import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences import im.vector.app.space import im.vector.lib.core.utils.flow.chunk -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -90,7 +93,6 @@ import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.file.FileService -import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult @@ -136,17 +138,22 @@ class TimelineViewModel @AssistedInject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val locationSharingServiceConnection: LocationSharingServiceConnection, private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase, + private val coroutineDispatchers: CoroutineDispatchers, timelineFactory: TimelineFactory, appStateHandler: AppStateHandler, ) : VectorViewModel(initialState), Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback { - private val room = session.getRoom(initialState.roomId)!! + private val room = viewModelScope.async(start = CoroutineStart.LAZY) { + session.roomService().awaitRoom(initialState.roomId)!! + } + val timeline = viewModelScope.async(start = CoroutineStart.LAZY) { + timelineFactory.createTimeline(viewModelScope, room, eventId, initialState.rootThreadEventId) + } private val eventId = initialState.eventId private val invisibleEventsSource = BehaviorDataSource() private val visibleEventsSource = BehaviorDataSource() private var timelineEvents = MutableSharedFlow>(0) - val timeline = timelineFactory.createTimeline(viewModelScope, room, eventId, initialState.rootThreadEventId) // Same lifecycle than the ViewModel (survive to screen rotation) val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope) @@ -175,78 +182,79 @@ class TimelineViewModel @AssistedInject constructor( } init { - timeline.start(initialState.rootThreadEventId) - timeline.addListener(this) - observeRoomSummary() - observeMembershipChanges() - observeSummaryState() - getUnreadState() - observeSyncState() - observeDataStore() - observeEventDisplayedActions() - observeUnreadState() - observeMyRoomMember() - observeActiveRoomWidgets() - observePowerLevel() - setupPreviewUrlObservers() - room.getRoomSummaryLive() - viewModelScope.launch(Dispatchers.IO) { - tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } - } - // Inform the SDK that the room is displayed - viewModelScope.launch(Dispatchers.IO) { - tryOrNull { session.roomService().onRoomDisplayed(initialState.roomId) } - } - callManager.addProtocolsCheckerListener(this) - callManager.checkForProtocolsSupportIfNeeded() - chatEffectManager.delegate = this + viewModelScope.launch { + timeline.await().also { + it.start(initialState.rootThreadEventId) + it.addListener(this@TimelineViewModel) + } + observeRoomSummary() + observeMembershipChanges() + observeSummaryState() + getUnreadState() + observeSyncState() + observeDataStore() + observeEventDisplayedActions() + observeUnreadState() + observeMyRoomMember() + observeActiveRoomWidgets() + observePowerLevel() + setupPreviewUrlObservers() + withContext(coroutineDispatchers.io) { + tryOrNull { room.await().readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } + // Inform the SDK that the room is displayed + tryOrNull { session.roomService().onRoomDisplayed(initialState.roomId) } + } + callManager.addProtocolsCheckerListener(this@TimelineViewModel) + callManager.checkForProtocolsSupportIfNeeded() + chatEffectManager.delegate = this@TimelineViewModel - // Ensure to share the outbound session keys with all members - if (room.roomCryptoService().isEncrypted()) { - rawService.withElementWellKnown(viewModelScope, session.sessionParams) { - val strategy = it.getOutboundSessionKeySharingStrategyOrDefault() - if (strategy == OutboundSessionKeySharingStrategy.WhenEnteringRoom) { - prepareForEncryption() + // Ensure to share the outbound session keys with all members + if (room.await().roomCryptoService().isEncrypted()) { + rawService.withElementWellKnown(viewModelScope, session.sessionParams) { + val strategy = it.getOutboundSessionKeySharingStrategyOrDefault() + if (strategy == OutboundSessionKeySharingStrategy.WhenEnteringRoom) { + prepareForEncryption() + } } } - } - // If the user had already accepted the invitation in the room list - if (initialState.isInviteAlreadyAccepted) { - handleAcceptInvite() - } + // If the user had already accepted the invitation in the room list + if (initialState.isInviteAlreadyAccepted) { + handleAcceptInvite() + } - if (initialState.switchToParentSpace) { - // We are coming from a notification, try to switch to the most relevant space - // so that when hitting back the room will appear in the list - appStateHandler.getCurrentRoomGroupingMethod()?.space().let { currentSpace -> - val currentRoomSummary = room.roomSummary() ?: return@let - // nothing we are good - if ((currentSpace == null && !vectorPreferences.prefSpacesShowAllRoomInHome()) || - (currentSpace != null && !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId))) { - // take first one or switch to home - appStateHandler.setCurrentSpace( - currentRoomSummary - .flattenParentIds.firstOrNull { it.isNotBlank() }, - // force persist, because if not on resume the AppStateHandler will resume - // the current space from what was persisted on enter background - persistNow = true - ) + if (initialState.switchToParentSpace) { + // We are coming from a notification, try to switch to the most relevant space + // so that when hitting back the room will appear in the list + appStateHandler.getCurrentRoomGroupingMethod()?.space().let { currentSpace -> + val currentRoomSummary = room.await().awaitRoomSummary() ?: return@let + // nothing we are good + if ((currentSpace == null && !vectorPreferences.prefSpacesShowAllRoomInHome()) || + (currentSpace != null && !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId))) { + // take first one or switch to home + appStateHandler.setCurrentSpace( + currentRoomSummary + .flattenParentIds.firstOrNull { it.isNotBlank() }, + // force persist, because if not on resume the AppStateHandler will resume + // the current space from what was persisted on enter background + persistNow = true + ) + } } } + + // Threads + initThreads() + + // Observe location service lifecycle to be able to warn the user + locationSharingServiceConnection.bind(this@TimelineViewModel) } - - // Threads - initThreads() - - // Observe location service lifecycle to be able to warn the user - locationSharingServiceConnection.bind(this) } /** * Threads specific initialization. */ - private fun initThreads() { + private suspend fun initThreads() { markThreadTimelineAsReadLocal() observeLocalThreadNotifications() } @@ -259,27 +267,25 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun prepareForEncryption() { + private suspend fun prepareForEncryption() { // check if there is not already a call made, or if there has been an error if (prepareToEncrypt.shouldLoad) { prepareToEncrypt = Loading() - viewModelScope.launch { - runCatching { - room.roomCryptoService().prepareToEncrypt() - }.fold({ - prepareToEncrypt = Success(Unit) - }, { - prepareToEncrypt = Fail(it) - }) - } + runCatching { + room.await().roomCryptoService().prepareToEncrypt() + }.fold({ + prepareToEncrypt = Success(Unit) + }, { + prepareToEncrypt = Fail(it) + }) } } - private fun observePowerLevel() { - PowerLevelsFlowFactory(room).createFlow() + private suspend fun observePowerLevel() { + PowerLevelsFlowFactory(room.await()).createFlow() .onEach { val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId) - val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId) + val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(initialState.roomId) val isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE) val isAllowedToSetupEncryption = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) setState { @@ -290,7 +296,9 @@ class TimelineViewModel @AssistedInject constructor( isAllowedToSetupEncryption = isAllowedToSetupEncryption ) } - }.launchIn(viewModelScope) + } + .flowOn(coroutineDispatchers.computation) + .collect() } private fun observeActiveRoomWidgets() { @@ -302,6 +310,7 @@ class TimelineViewModel @AssistedInject constructor( .map { widgets -> widgets.filter { it.isActive } } + .flowOn(coroutineDispatchers.computation) .execute { widgets -> copy(activeRoomWidgets = widgets) } @@ -323,11 +332,11 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun observeMyRoomMember() { + private suspend fun observeMyRoomMember() { val queryParams = roomMemberQueryParams { this.userId = QueryStringValue.Equals(session.myUserId, QueryStringValue.Case.SENSITIVE) } - room.flow() + room.await().flow() .liveRoomMembers(queryParams) .map { it.firstOrNull().toOptional() @@ -338,13 +347,13 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun setupPreviewUrlObservers() { + private suspend fun setupPreviewUrlObservers() { if (!vectorPreferences.showUrlPreviews()) { return } combine( timelineEvents, - room.flow().liveRoomSummary() + room.await().flow().liveRoomSummary() .unwrap() .map { it.isEncrypted } .distinctUntilChanged() @@ -352,7 +361,7 @@ class TimelineViewModel @AssistedInject constructor( if (isRoomEncrypted) { return@combine } - withContext(Dispatchers.Default) { + withContext(coroutineDispatchers.computation) { Timber.v("On new timeline events for urlpreview on ${Thread.currentThread()}") snapshot.forEach { previewUrlRetriever.getPreviewUrl(it) @@ -369,7 +378,7 @@ class TimelineViewModel @AssistedInject constructor( private fun markThreadTimelineAsReadLocal() { initialState.rootThreadEventId?.let { session.coroutineScope.launch { - room.threadsLocalService().markThreadAsRead(it) + room.await().threadsLocalService().markThreadAsRead(it) } } } @@ -377,8 +386,8 @@ class TimelineViewModel @AssistedInject constructor( /** * Observe local unread threads. */ - private fun observeLocalThreadNotifications() { - room.flow() + private suspend fun observeLocalThreadNotifications() { + room.await().flow() .liveLocalUnreadThreadList() .execute { val threadList = it.invoke() @@ -395,76 +404,76 @@ class TimelineViewModel @AssistedInject constructor( } } - fun getOtherUserIds() = room.roomSummary()?.otherMemberIds - - fun getRoomSummary() = room.roomSummary() + suspend fun getRoomSummary() = room.await().awaitRoomSummary() override fun handle(action: RoomDetailAction) { - when (action) { - is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) - is RoomDetailAction.SendMedia -> handleSendMedia(action) - is RoomDetailAction.SendSticker -> handleSendSticker(action) - is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) - is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) - is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) - is RoomDetailAction.SendReaction -> handleSendReaction(action) - is RoomDetailAction.AcceptInvite -> handleAcceptInvite() - is RoomDetailAction.RejectInvite -> handleRejectInvite() - is RoomDetailAction.RedactAction -> handleRedactEvent(action) - is RoomDetailAction.UndoReaction -> handleUndoReact(action) - is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) - is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) - is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) - is RoomDetailAction.JoinAndOpenReplacementRoom -> handleJoinAndOpenReplacementRoom() - is RoomDetailAction.OnClickMisconfiguredEncryption -> handleClickMisconfiguredE2E() - is RoomDetailAction.ResendMessage -> handleResendEvent(action) - is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) - is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() - is RoomDetailAction.ReportContent -> handleReportContent(action) - is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) - is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() - is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() - is RoomDetailAction.VoteToPoll -> handleVoteToPoll(action) - is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) - is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) - is RoomDetailAction.RequestVerification -> handleRequestVerification(action) - is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) - is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) - is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) - is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() - is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() - is RoomDetailAction.StartCall -> handleStartCall(action) - is RoomDetailAction.AcceptCall -> handleAcceptCall(action) - is RoomDetailAction.EndCall -> handleEndCall() - is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() - is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) - is RoomDetailAction.UpdateJoinJitsiCallStatus -> handleJitsiCallJoinStatus(action) - is RoomDetailAction.JoinJitsiCall -> handleJoinJitsiCall() - is RoomDetailAction.LeaveJitsiCall -> handleLeaveJitsiCall() - is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) - is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) - is RoomDetailAction.CancelSend -> handleCancel(action) - is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) - RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople() - RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar() - is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action) - RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings) - is RoomDetailAction.ShowRoomAvatarFullScreen -> { - _viewEvents.post( - RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView) - ) - } - is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) - RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() - RoomDetailAction.ResendAll -> handleResendAll() - is RoomDetailAction.RoomUpgradeSuccess -> { - setState { - copy(joinUpgradedRoomAsync = Success(action.replacementRoomId)) + viewModelScope.launch { + when (action) { + is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) + is RoomDetailAction.SendMedia -> handleSendMedia(action) + is RoomDetailAction.SendSticker -> handleSendSticker(action) + is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) + is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) + is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) + is RoomDetailAction.SendReaction -> handleSendReaction(action) + is RoomDetailAction.AcceptInvite -> handleAcceptInvite() + is RoomDetailAction.RejectInvite -> handleRejectInvite() + is RoomDetailAction.RedactAction -> handleRedactEvent(action) + is RoomDetailAction.UndoReaction -> handleUndoReact(action) + is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) + is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) + is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) + is RoomDetailAction.JoinAndOpenReplacementRoom -> handleJoinAndOpenReplacementRoom() + is RoomDetailAction.OnClickMisconfiguredEncryption -> handleClickMisconfiguredE2E() + is RoomDetailAction.ResendMessage -> handleResendEvent(action) + is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) + is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() + is RoomDetailAction.ReportContent -> handleReportContent(action) + is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) + is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() + is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() + is RoomDetailAction.VoteToPoll -> handleVoteToPoll(action) + is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) + is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) + is RoomDetailAction.RequestVerification -> handleRequestVerification(action) + is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) + is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) + is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) + is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() + is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() + is RoomDetailAction.StartCall -> handleStartCall(action) + is RoomDetailAction.AcceptCall -> handleAcceptCall(action) + is RoomDetailAction.EndCall -> handleEndCall() + is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() + is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) + is RoomDetailAction.UpdateJoinJitsiCallStatus -> handleJitsiCallJoinStatus(action) + is RoomDetailAction.JoinJitsiCall -> handleJoinJitsiCall() + is RoomDetailAction.LeaveJitsiCall -> handleLeaveJitsiCall() + is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) + is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) + is RoomDetailAction.CancelSend -> handleCancel(action) + is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) + RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople() + RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar() + is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action) + RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings) + is RoomDetailAction.ShowRoomAvatarFullScreen -> { + _viewEvents.post( + RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView) + ) } - _viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true)) + is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) + RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() + RoomDetailAction.ResendAll -> handleResendAll() + is RoomDetailAction.RoomUpgradeSuccess -> { + setState { + copy(joinUpgradedRoomAsync = Success(action.replacementRoomId)) + } + _viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true)) + } + is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId) + RoomDetailAction.StopLiveLocationSharing -> handleStopLiveLocationSharing() } - is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId) - RoomDetailAction.StopLiveLocationSharing -> handleStopLiveLocationSharing() } } @@ -505,10 +514,10 @@ class TimelineViewModel @AssistedInject constructor( previewUrlRetriever.doNotShowPreviewUrlFor(action.eventId, action.url) } - private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) { - viewModelScope.launch(Dispatchers.IO) { + private suspend fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) { + withContext(coroutineDispatchers.io) { try { - room.stateService().updateAvatar(action.newAvatarUri, action.newAvatarFileName) + room.await().stateService().updateAvatar(action.newAvatarUri, action.newAvatarFileName) _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) @@ -524,12 +533,12 @@ class TimelineViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.OpenSetRoomAvatarDialog) } - private fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) { - room.readService().getUserReadReceipt(action.userId) + private suspend fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) { + room.await().readService().getUserReadReceipt(action.userId) ?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) } } - private fun handleSendSticker(action: RoomDetailAction.SendSticker) { + private suspend fun handleSendSticker(action: RoomDetailAction.SendSticker) { val content = initialState.rootThreadEventId?.let { action.stickerContent.copy( relatesTo = RelationDefaultContent( @@ -540,14 +549,13 @@ class TimelineViewModel @AssistedInject constructor( ) } ?: action.stickerContent - room.sendService().sendEvent(EventType.STICKER, content.toContent()) + room.await().sendService().sendEvent(EventType.STICKER, content.toContent()) } - private fun handleStartCall(action: RoomDetailAction.StartCall) { - viewModelScope.launch { - room.roomSummary()?.otherMemberIds?.firstOrNull()?.let { - callManager.startOutgoingCall(room.roomId, it, action.isVideo) - } + private suspend fun handleStartCall(action: RoomDetailAction.StartCall) { + val room = room.await() + room.roomSummary()?.otherMemberIds?.firstOrNull()?.let { + callManager.startOutgoingCall(room.roomId, it, action.isVideo) } } @@ -555,27 +563,24 @@ class TimelineViewModel @AssistedInject constructor( callManager.endCallForRoom(initialState.roomId) } - private fun handleSelectStickerAttachment() { - viewModelScope.launch { - val viewEvent = stickerPickerActionHandler.handle() - _viewEvents.post(viewEvent) - } + private suspend fun handleSelectStickerAttachment() { + val viewEvent = stickerPickerActionHandler.handle() + _viewEvents.post(viewEvent) } - private fun handleOpenIntegrationManager() { - viewModelScope.launch { - val viewEvent = withContext(Dispatchers.Default) { - if (isIntegrationEnabled()) { - RoomDetailViewEvents.OpenIntegrationManager - } else { - RoomDetailViewEvents.DisplayEnableIntegrationsWarning - } + private suspend fun handleOpenIntegrationManager() { + val viewEvent = withContext(coroutineDispatchers.computation) { + if (isIntegrationEnabled()) { + RoomDetailViewEvents.OpenIntegrationManager + } else { + RoomDetailViewEvents.DisplayEnableIntegrationsWarning } - _viewEvents.post(viewEvent) } + _viewEvents.post(viewEvent) } - private fun handleManageIntegrations() = withState { state -> + private suspend fun handleManageIntegrations() { + val state = awaitState() if (state.activeRoomWidgets().isNullOrEmpty()) { // Directly open integration manager screen handleOpenIntegrationManager() @@ -585,11 +590,11 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) { + private suspend fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) { _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) - viewModelScope.launch(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { try { - val widget = jitsiService.createJitsiWidget(room.roomId, action.withVideo) + val widget = jitsiService.createJitsiWidget(initialState.roomId, action.withVideo) _viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.withVideo)) } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_add_widget))) @@ -599,16 +604,17 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun handleDeleteWidget(widgetId: String) = withState { state -> + private suspend fun handleDeleteWidget(widgetId: String) { + val state = awaitState() val isJitsiWidget = state.jitsiState.widgetId == widgetId - viewModelScope.launch(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { try { if (isJitsiWidget) { setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = true)) } } else { _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) } - session.widgetService().destroyRoomWidget(room.roomId, widgetId) + session.widgetService().destroyRoomWidget(initialState.roomId, widgetId) // local echo setState { copy( @@ -660,7 +666,7 @@ class TimelineViewModel @AssistedInject constructor( if (trackUnreadMessages.getAndSet(false)) { mostRecentDisplayedEvent?.root?.eventId?.also { session.coroutineScope.launch { - tryOrNull { room.readService().setReadMarker(it) } + tryOrNull { room.await().readService().setReadMarker(it) } } } mostRecentDisplayedEvent = null @@ -672,13 +678,13 @@ class TimelineViewModel @AssistedInject constructor( invisibleEventsSource.post(action) } - fun getMember(userId: String): RoomMemberSummary? { - return room.membershipService().getRoomMember(userId) + suspend fun getMember(userId: String): RoomMemberSummary? { + return room.await().membershipService().getRoomMember(userId) } - private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) { + private suspend fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) { // Ensure outbound session keys - if (room.roomCryptoService().isEncrypted()) { + if (room.await().roomCryptoService().isEncrypted()) { rawService.withElementWellKnown(viewModelScope, session.sessionParams) { val strategy = it.getOutboundSessionKeySharingStrategyOrDefault() if (strategy == OutboundSessionKeySharingStrategy.WhenTyping && action.focused) { @@ -689,11 +695,12 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun handleJoinAndOpenReplacementRoom() = withState { state -> - val tombstoneContent = state.tombstoneEvent?.getClearContent()?.toModel() ?: return@withState + private suspend fun handleJoinAndOpenReplacementRoom() { + val state = awaitState() + val tombstoneContent = state.tombstoneEvent?.getClearContent()?.toModel() ?: return val roomId = tombstoneContent.replacementRoomId ?: "" - val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN + val isRoomJoined = session.roomService().awaitRoom(roomId)?.awaitRoomSummary()?.membership == Membership.JOIN if (isRoomJoined) { setState { copy(joinUpgradedRoomAsync = Success(roomId)) } _viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId, closeCurrentRoom = true)) @@ -706,17 +713,15 @@ class TimelineViewModel @AssistedInject constructor( setState { copy(joinUpgradedRoomAsync = Loading()) } - viewModelScope.launch { - val result = runCatchingToAsync { - session.roomService().joinRoom(roomId, viaServers = viaServers) - roomId - } - setState { - copy(joinUpgradedRoomAsync = result) - } - if (result is Success) { - _viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId, closeCurrentRoom = true)) - } + val result = runCatchingToAsync { + session.roomService().joinRoom(roomId, viaServers = viaServers) + roomId + } + setState { + copy(joinUpgradedRoomAsync = result) + } + if (result is Success) { + _viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId, closeCurrentRoom = true)) } } } @@ -761,37 +766,33 @@ class TimelineViewModel @AssistedInject constructor( // PRIVATE METHODS ***************************************************************************** - private fun handleSendReaction(action: RoomDetailAction.SendReaction) { - room.relationService().sendReaction(action.targetEventId, action.reaction) + private suspend fun handleSendReaction(action: RoomDetailAction.SendReaction) { + room.await().relationService().sendReaction(action.targetEventId, action.reaction) } - private fun handleRedactEvent(action: RoomDetailAction.RedactAction) { - val event = room.getTimelineEvent(action.targetEventId) ?: return - room.sendService().redactEvent(event.root, action.reason) + private suspend fun handleRedactEvent(action: RoomDetailAction.RedactAction) { + val event = room.await().getTimelineEvent(action.targetEventId) ?: return + room.await().sendService().redactEvent(event.root, action.reason) } - private fun handleUndoReact(action: RoomDetailAction.UndoReaction) { - viewModelScope.launch { - tryOrNull { - room.relationService().undoReaction(action.targetEventId, action.reaction) - } + private suspend fun handleUndoReact(action: RoomDetailAction.UndoReaction) { + tryOrNull { + room.await().relationService().undoReaction(action.targetEventId, action.reaction) } } - private fun handleUpdateQuickReaction(action: RoomDetailAction.UpdateQuickReactAction) { + private suspend fun handleUpdateQuickReaction(action: RoomDetailAction.UpdateQuickReactAction) { if (action.add) { - room.relationService().sendReaction(action.targetEventId, action.selectedReaction) + room.await().relationService().sendReaction(action.targetEventId, action.selectedReaction) } else { - viewModelScope.launch { - tryOrNull { - room.relationService().undoReaction(action.targetEventId, action.selectedReaction) - } + tryOrNull { + room.await().relationService().undoReaction(action.targetEventId, action.selectedReaction) } } } - private fun handleSendMedia(action: RoomDetailAction.SendMedia) { - room.sendService().sendMedias( + private suspend fun handleSendMedia(action: RoomDetailAction.SendMedia) { + room.await().sendService().sendMedias( action.attachments, action.compressBeforeSending, emptySet(), @@ -799,14 +800,14 @@ class TimelineViewModel @AssistedInject constructor( ) } - private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) { - viewModelScope.launch(Dispatchers.Default) { + private suspend fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) { + withContext(coroutineDispatchers.computation) { if (action.event.root.sendState.isSent()) { // ignore pending/local events visibleEventsSource.post(action) } // We need to update this with the related m.replace also (to move read receipt) action.event.annotations?.editSummary?.sourceEvents?.forEach { - room.getTimelineEvent(it)?.let { event -> + room.await().getTimelineEvent(it)?.let { event -> visibleEventsSource.post(RoomDetailAction.TimelineEventTurnsVisible(event)) } } @@ -826,43 +827,40 @@ class TimelineViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.StopChatEffects) } - private fun handleLoadMore(action: RoomDetailAction.LoadMoreTimelineEvents) { - timeline.paginate(action.direction, PAGINATION_COUNT) + private suspend fun handleLoadMore(action: RoomDetailAction.LoadMoreTimelineEvents) { + timeline.await().paginate(action.direction, PAGINATION_COUNT) } - private fun handleRejectInvite() { + private suspend fun handleRejectInvite() { notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) } - viewModelScope.launch { - try { - session.roomService().leaveRoom(room.roomId) - } catch (throwable: Throwable) { - _viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true)) - } + try { + session.roomService().leaveRoom(initialState.roomId) + } catch (throwable: Throwable) { + _viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true)) } } - private fun handleAcceptInvite() { + private suspend fun handleAcceptInvite() { notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) } - viewModelScope.launch { - try { - session.roomService().joinRoom(room.roomId) - trackRoomJoined() - } catch (throwable: Throwable) { - _viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true)) - } + try { + session.roomService().joinRoom(initialState.roomId) + trackRoomJoined() + } catch (throwable: Throwable) { + _viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true)) } } - private fun trackRoomJoined() { + private suspend fun trackRoomJoined() { val trigger = if (initialState.isInviteAlreadyAccepted) { JoinedRoom.Trigger.Invite } else { JoinedRoom.Trigger.Timeline } - analyticsTracker.capture(room.roomSummary().toAnalyticsJoinedRoom(trigger)) + val roomSummary = room.await().awaitRoomSummary() + analyticsTracker.capture(roomSummary.toAnalyticsJoinedRoom(trigger)) } - private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) { + private suspend fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) { val mxcUrl = action.messageFileContent.getFileUrl() ?: return val isLocalSendingFile = action.senderId == session.myUserId && mxcUrl.startsWith("content://") @@ -876,7 +874,7 @@ class TimelineViewModel @AssistedInject constructor( ) } } else { - viewModelScope.launch { + withContext(coroutineDispatchers.io) { val fileState = session.fileService().fileState(action.messageFileContent) var canOpen = fileState is FileService.FileState.InCache && fileState.decryptedFileInCache if (!canOpen) { @@ -910,12 +908,12 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) { + private suspend fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) { val targetEventId: String = action.eventId - val indexOfEvent = timeline.getIndexOfEvent(targetEventId) + val indexOfEvent = timeline.await().getIndexOfEvent(targetEventId) if (indexOfEvent == null) { // Event is not already in RAM - timeline.restartWithEventId(targetEventId) + timeline.await().restartWithEventId(targetEventId) } if (action.highlight) { setState { copy(highlightedEventId = targetEventId) } @@ -923,8 +921,9 @@ class TimelineViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.NavigateToEvent(targetEventId)) } - private fun handleResendEvent(action: RoomDetailAction.ResendMessage) { + private suspend fun handleResendEvent(action: RoomDetailAction.ResendMessage) { val targetEventId = action.eventId + val room = room.await() room.getTimelineEvent(targetEventId)?.let { // State must be UNDELIVERED or Failed if (!it.root.sendState.hasFailed()) { @@ -941,8 +940,9 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun handleRemove(action: RoomDetailAction.RemoveFailedEcho) { + private suspend fun handleRemove(action: RoomDetailAction.RemoveFailedEcho) { val targetEventId = action.eventId + val room = room.await() room.getTimelineEvent(targetEventId)?.let { // State must be UNDELIVERED or Failed if (!it.root.sendState.hasFailed()) { @@ -953,7 +953,8 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun handleCancel(action: RoomDetailAction.CancelSend) { + private suspend fun handleCancel(action: RoomDetailAction.CancelSend) { + val room = room.await() if (action.force) { room.sendService().cancelSend(action.eventId) return @@ -969,12 +970,12 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun handleResendAll() { - room.sendService().resendAllFailedMessages() + private suspend fun handleResendAll() { + room.await().sendService().resendAllFailedMessages() } - private fun handleRemoveAllFailedMessages() { - room.sendService().cancelAllFailedMessages() + private suspend fun handleRemoveAllFailedMessages() { + room.await().sendService().cancelAllFailedMessages() } private fun observeEventDisplayedActions() { @@ -997,11 +998,11 @@ class TimelineViewModel @AssistedInject constructor( } bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId -> session.coroutineScope.launch { - tryOrNull { room.readService().setReadReceipt(eventId) } + tryOrNull { room.await().readService().setReadReceipt(eventId) } } } } - .flowOn(Dispatchers.Default) + .flowOn(coroutineDispatchers.computation) .launchIn(viewModelScope) } @@ -1009,49 +1010,42 @@ class TimelineViewModel @AssistedInject constructor( * Returns the index of event in the timeline. * Returns Int.MAX_VALUE if not found */ - private fun TimelineEvent.indexOfEvent(): Int = timeline.getIndexOfEvent(eventId) ?: Int.MAX_VALUE + private suspend fun TimelineEvent.indexOfEvent(): Int = timeline.await().getIndexOfEvent(eventId) ?: Int.MAX_VALUE - private fun handleMarkAllAsRead() { + private suspend fun handleMarkAllAsRead() { setState { copy(unreadState = UnreadState.HasNoUnread) } - viewModelScope.launch { - tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.BOTH) } - } + tryOrNull { room.await().readService().markAsRead(ReadService.MarkAsReadParams.BOTH) } } - private fun handleReportContent(action: RoomDetailAction.ReportContent) { - viewModelScope.launch { - val event = try { - room.reportingService().reportContent(action.eventId, -100, action.reason) - RoomDetailViewEvents.ActionSuccess(action) - } catch (failure: Exception) { - RoomDetailViewEvents.ActionFailure(action, failure) - } - _viewEvents.post(event) + private suspend fun handleReportContent(action: RoomDetailAction.ReportContent) { + val event = try { + room.await().reportingService().reportContent(action.eventId, -100, action.reason) + RoomDetailViewEvents.ActionSuccess(action) + } catch (failure: Exception) { + RoomDetailViewEvents.ActionFailure(action, failure) } + _viewEvents.post(event) } - private fun handleIgnoreUser(action: RoomDetailAction.IgnoreUser) { + private suspend fun handleIgnoreUser(action: RoomDetailAction.IgnoreUser) { if (action.userId.isNullOrEmpty()) { return } - - viewModelScope.launch { - val event = try { - session.userService().ignoreUserIds(listOf(action.userId)) - RoomDetailViewEvents.ActionSuccess(action) - } catch (failure: Throwable) { - RoomDetailViewEvents.ActionFailure(action, failure) - } - _viewEvents.post(event) + val event = try { + session.userService().ignoreUserIds(listOf(action.userId)) + RoomDetailViewEvents.ActionSuccess(action) + } catch (failure: Throwable) { + RoomDetailViewEvents.ActionFailure(action, failure) } + _viewEvents.post(event) } private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) { - Timber.v("## SAS handleAcceptVerification ${action.otherUserId}, roomId:${room.roomId}, txId:${action.transactionId}") + Timber.v("## SAS handleAcceptVerification ${action.otherUserId}, roomId:${initialState.roomId}, txId:${action.transactionId}") if (session.cryptoService().verificationService().readyPendingVerificationInDMs( supportedVerificationMethodsProvider.provide(), action.otherUserId, - room.roomId, + initialState.roomId, action.transactionId )) { _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) @@ -1064,7 +1058,7 @@ class TimelineViewModel @AssistedInject constructor( session.cryptoService().verificationService().declineVerificationRequestInDMs( action.otherUserId, action.transactionId, - room.roomId + initialState.roomId ) } @@ -1075,7 +1069,7 @@ class TimelineViewModel @AssistedInject constructor( private fun handleResumeRequestVerification(action: RoomDetailAction.ResumeVerification) { // Check if this request is still active and handled by me - session.cryptoService().verificationService().getExistingVerificationRequestInRoom(room.roomId, action.transactionId)?.let { + session.cryptoService().verificationService().getExistingVerificationRequestInRoom(initialState.roomId, action.transactionId)?.let { if (it.handledByOtherSession) return if (!it.isFinished) { _viewEvents.post( @@ -1089,16 +1083,16 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun handleReRequestKeys(action: RoomDetailAction.ReRequestKeys) { + private suspend fun handleReRequestKeys(action: RoomDetailAction.ReRequestKeys) { // Check if this request is still active and handled by me - room.getTimelineEvent(action.eventId)?.let { + room.await().getTimelineEvent(action.eventId)?.let { session.cryptoService().reRequestRoomKeyForEvent(it.root) _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.e2e_re_request_encryption_key_dialog_content))) } } - private fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) { - room.getTimelineEvent(action.eventId)?.let { + private suspend fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) { + room.await().getTimelineEvent(action.eventId)?.let { val code = when (it.root.mCryptoError) { MXCryptoError.ErrorType.KEYS_WITHHELD -> { WithHeldCode.fromCode(it.root.mCryptoErrorReason) @@ -1110,20 +1104,20 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun handleVoteToPoll(action: RoomDetailAction.VoteToPoll) { + private suspend fun handleVoteToPoll(action: RoomDetailAction.VoteToPoll) { // Do not allow to vote unsent local echo of the poll event if (LocalEcho.isLocalEchoId(action.eventId)) return // Do not allow to vote the same option twice - room.getTimelineEvent(action.eventId)?.let { pollTimelineEvent -> + room.await().getTimelineEvent(action.eventId)?.let { pollTimelineEvent -> val currentVote = pollTimelineEvent.annotations?.pollResponseSummary?.aggregatedContent?.myVote if (currentVote != action.optionKey) { - room.sendService().voteToPoll(action.eventId, action.optionKey) + room.await().sendService().voteToPoll(action.eventId, action.optionKey) } } } - private fun handleEndPoll(eventId: String) { - room.sendService().endPoll(eventId) + private suspend fun handleEndPoll(eventId: String) { + room.await().sendService().endPoll(eventId) } private fun observeSyncState() { @@ -1141,17 +1135,15 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun handleStopLiveLocationSharing() { - viewModelScope.launch { - val result = stopLiveLocationShareUseCase.execute(room.roomId) - if (result is UpdateLiveLocationShareResult.Failure) { - _viewEvents.post(RoomDetailViewEvents.Failure(throwable = result.error, showInDialog = true)) - } + private suspend fun handleStopLiveLocationSharing() { + val result = stopLiveLocationShareUseCase.execute(initialState.roomId) + if (result is UpdateLiveLocationShareResult.Failure) { + _viewEvents.post(RoomDetailViewEvents.Failure(throwable = result.error, showInDialog = true)) } } - private fun observeRoomSummary() { - room.flow().liveRoomSummary() + private suspend fun observeRoomSummary() { + room.await().flow().liveRoomSummary() .unwrap() .execute { async -> copy( @@ -1160,12 +1152,14 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun getUnreadState() { + private suspend fun getUnreadState() { combine( timelineEvents, - room.flow().liveRoomSummary().unwrap() + room.await().flow().liveRoomSummary().unwrap() ) { timelineEvents, roomSummary -> - computeUnreadState(timelineEvents, roomSummary) + withContext(coroutineDispatchers.computation) { + computeUnreadState(timelineEvents, roomSummary) + } } // We don't want live update of unread so we skip when we already had a HasUnread or HasNoUnread // However, we want to update an existing HasUnread, if the readMarkerId hasn't changed, @@ -1184,9 +1178,10 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun computeUnreadState(events: List, roomSummary: RoomSummary): UnreadState { + private suspend fun computeUnreadState(events: List, roomSummary: RoomSummary): UnreadState { if (events.isEmpty()) return UnreadState.Unknown val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown + val timeline = timeline.await() val firstDisplayableEventIndex = timeline.getIndexOfEvent(readMarkerIdSnapshot) ?: return if (timeline.isLive) { UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) @@ -1247,7 +1242,7 @@ class TimelineViewModel @AssistedInject constructor( setState { copy(asyncInviter = Success(it)) } } } - room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)?.also { + room.await().getStateEvent(EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)?.also { setState { copy(tombstoneEvent = it) } } } @@ -1258,7 +1253,7 @@ class TimelineViewModel @AssistedInject constructor( * in the snapshot. The main reason for this function is to support the /relations api */ private var threadPermalinkHandled = false - private fun navigateToThreadEventIfNeeded(snapshot: List) { + private suspend fun navigateToThreadEventIfNeeded(snapshot: List) { if (eventId != null && initialState.rootThreadEventId != null) { // When we have a permalink and we are in a thread timeline if (snapshot.firstOrNull { it.eventId == eventId } != null && !threadPermalinkHandled) { @@ -1267,7 +1262,7 @@ class TimelineViewModel @AssistedInject constructor( threadPermalinkHandled = true } else { // Permalink event not found yet continue paginating - timeline.paginate(Timeline.Direction.BACKWARDS, PAGINATION_COUNT_THREADS_PERMALINK) + timeline.await().paginate(Timeline.Direction.BACKWARDS, PAGINATION_COUNT_THREADS_PERMALINK) } } } @@ -1282,8 +1277,10 @@ class TimelineViewModel @AssistedInject constructor( override fun onTimelineFailure(throwable: Throwable) { // If we have a critical timeline issue, we get back to live. - timeline.restartWithEventId(null) - _viewEvents.post(RoomDetailViewEvents.Failure(throwable)) + viewModelScope.launch { + timeline.await().restartWithEventId(null) + _viewEvents.post(RoomDetailViewEvents.Failure(throwable)) + } } override fun onNewTimelineEvents(eventIds: List) { @@ -1306,11 +1303,13 @@ class TimelineViewModel @AssistedInject constructor( } override fun onCleared() { - timeline.dispose() - timeline.removeAllListeners() - decryptionFailureTracker.onTimeLineDisposed(room.roomId) - if (vectorPreferences.sendTypingNotifs()) { - room.typingService().userStopsTyping() + decryptionFailureTracker.onTimeLineDisposed(initialState.roomId) + session.coroutineScope.launch { + timeline.await().dispose() + timeline.await().removeAllListeners() + if (vectorPreferences.sendTypingNotifs()) { + room.await().typingService().userStopsTyping() + } } chatEffectManager.delegate = null chatEffectManager.dispose() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index ce4235a825..013d772dac 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -23,6 +23,7 @@ import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.analytics.AnalyticsTracker @@ -39,9 +40,11 @@ import im.vector.app.features.home.room.detail.toMessageType import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentAttachmentData @@ -71,7 +74,7 @@ import org.matrix.android.sdk.flow.unwrap import timber.log.Timber class MessageComposerViewModel @AssistedInject constructor( - @Assisted initialState: MessageComposerViewState, + @Assisted private val initialState: MessageComposerViewState, private val session: Session, private val stringProvider: StringProvider, private val vectorPreferences: VectorPreferences, @@ -79,41 +82,48 @@ class MessageComposerViewModel @AssistedInject constructor( private val rainbowGenerator: RainbowGenerator, private val audioMessageHelper: AudioMessageHelper, private val analyticsTracker: AnalyticsTracker, + private val coroutineDispatchers: CoroutineDispatchers, ) : VectorViewModel(initialState) { - private val room = session.getRoom(initialState.roomId)!! + private val room = viewModelScope.async(start = CoroutineStart.LAZY) { + session.roomService().awaitRoom(initialState.roomId)!! + } // Keep it out of state to avoid invalidate being called private var currentComposerText: CharSequence = "" init { - loadDraftIfAny() - observePowerLevelAndEncryption() + viewModelScope.launch { + loadDraftIfAny() + observePowerLevelAndEncryption() + } subscribeToStateInternal() } override fun handle(action: MessageComposerAction) { - when (action) { - is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action) - is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) - is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action) - is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action) - is MessageComposerAction.SendMessage -> handleSendMessage(action) - is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action) - is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action) - is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action) - is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage() - is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled, action.rootThreadEventId) - is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action) - MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage() - MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback() - is MessageComposerAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord) - is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData) - is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText) - is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action) - is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action) - is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action) - is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action) + viewModelScope.launch { + when (action) { + is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action) + is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) + is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action) + is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action) + is MessageComposerAction.SendMessage -> handleSendMessage(action) + is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action) + is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action) + is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action) + is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage() + is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled, action.rootThreadEventId) + is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action) + MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage() + MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback() + is MessageComposerAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord) + is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData) + is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText) + is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action) + is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action) + is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action) + is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action) + } } } @@ -148,13 +158,14 @@ class MessageComposerViewModel @AssistedInject constructor( copy(sendMode = SendMode.Regular(action.text, action.fromSharing)) } - private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) { - room.getTimelineEvent(action.eventId)?.let { timelineEvent -> + private suspend fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) { + room.await().getTimelineEvent(action.eventId)?.let { timelineEvent -> setState { copy(sendMode = SendMode.Edit(timelineEvent, timelineEvent.getTextEditableContent())) } } } - private fun observePowerLevelAndEncryption() { + private suspend fun observePowerLevelAndEncryption() { + val room = room.await() combine( PowerLevelsFlowFactory(room).createFlow(), room.flow().liveRoomSummary().unwrap() @@ -180,439 +191,435 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) { - room.getTimelineEvent(action.eventId)?.let { timelineEvent -> + private suspend fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) { + room.await().getTimelineEvent(action.eventId)?.let { timelineEvent -> setState { copy(sendMode = SendMode.Quote(timelineEvent, action.text)) } } } - private fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) { - room.getTimelineEvent(action.eventId)?.let { timelineEvent -> + private suspend fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) { + room.await().getTimelineEvent(action.eventId)?.let { timelineEvent -> setState { copy(sendMode = SendMode.Reply(timelineEvent, action.text)) } } } - private fun handleSendMessage(action: MessageComposerAction.SendMessage) { - withState { state -> - analyticsTracker.capture(state.toAnalyticsComposer()).also { - setState { copy(startsThread = false) } - } - when (state.sendMode) { - is SendMode.Regular -> { - when (val parsedCommand = commandParser.parseSlashCommand( - textMessage = action.text, - isInThreadTimeline = state.isInThreadTimeline() - )) { - is ParsedCommand.ErrorNotACommand -> { - // Send the text message to the room - if (state.rootThreadEventId != null) { - room.relationService().replyInThread( - rootThreadEventId = state.rootThreadEventId, - replyInThreadText = action.text, - autoMarkdown = action.autoMarkdown - ) - } else { - room.sendService().sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) - } + private suspend fun handleSendMessage(action: MessageComposerAction.SendMessage) { + val state = awaitState() + analyticsTracker.capture(state.toAnalyticsComposer()) + setState { copy(startsThread = false) } + when (state.sendMode) { + is SendMode.Regular -> { + when (val parsedCommand = commandParser.parseSlashCommand( + textMessage = action.text, + isInThreadTimeline = state.isInThreadTimeline() + )) { + is ParsedCommand.ErrorNotACommand -> { + // Send the text message to the room + if (state.rootThreadEventId != null) { + room.await().relationService().replyInThread( + rootThreadEventId = state.rootThreadEventId, + replyInThreadText = action.text, + autoMarkdown = action.autoMarkdown + ) + } else { + room.await().sendService().sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) + } - _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() + _viewEvents.post(MessageComposerViewEvents.MessageSent) + popDraft() + } + is ParsedCommand.ErrorSyntax -> { + _viewEvents.post(MessageComposerViewEvents.SlashCommandError(parsedCommand.command)) + } + is ParsedCommand.ErrorEmptySlashCommand -> { + _viewEvents.post(MessageComposerViewEvents.SlashCommandUnknown("/")) + } + is ParsedCommand.ErrorUnknownSlashCommand -> { + _viewEvents.post(MessageComposerViewEvents.SlashCommandUnknown(parsedCommand.slashCommand)) + } + is ParsedCommand.ErrorCommandNotSupportedInThreads -> { + _viewEvents.post(MessageComposerViewEvents.SlashCommandNotSupportedInThreads(parsedCommand.command)) + } + is ParsedCommand.SendPlainText -> { + // Send the text message to the room, without markdown + if (state.rootThreadEventId != null) { + room.await().relationService().replyInThread( + rootThreadEventId = state.rootThreadEventId, + replyInThreadText = parsedCommand.message, + autoMarkdown = false + ) + } else { + room.await().sendService().sendTextMessage(parsedCommand.message, autoMarkdown = false) } - is ParsedCommand.ErrorSyntax -> { - _viewEvents.post(MessageComposerViewEvents.SlashCommandError(parsedCommand.command)) + _viewEvents.post(MessageComposerViewEvents.MessageSent) + popDraft() + } + is ParsedCommand.ChangeRoomName -> { + handleChangeRoomNameSlashCommand(parsedCommand) + } + is ParsedCommand.Invite -> { + handleInviteSlashCommand(parsedCommand) + } + is ParsedCommand.Invite3Pid -> { + handleInvite3pidSlashCommand(parsedCommand) + } + is ParsedCommand.SetUserPowerLevel -> { + handleSetUserPowerLevel(parsedCommand) + } + is ParsedCommand.ClearScalarToken -> { + // TODO + _viewEvents.post(MessageComposerViewEvents.SlashCommandNotImplemented) + } + is ParsedCommand.SetMarkdown -> { + vectorPreferences.setMarkdownEnabled(parsedCommand.enable) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + popDraft() + } + is ParsedCommand.BanUser -> { + handleBanSlashCommand(parsedCommand) + } + is ParsedCommand.UnbanUser -> { + handleUnbanSlashCommand(parsedCommand) + } + is ParsedCommand.IgnoreUser -> { + handleIgnoreSlashCommand(parsedCommand) + } + is ParsedCommand.UnignoreUser -> { + handleUnignoreSlashCommand(parsedCommand) + } + is ParsedCommand.RemoveUser -> { + handleRemoveSlashCommand(parsedCommand) + } + is ParsedCommand.JoinRoom -> { + handleJoinToAnotherRoomSlashCommand(parsedCommand) + popDraft() + } + is ParsedCommand.PartRoom -> { + handlePartSlashCommand(parsedCommand) + } + is ParsedCommand.SendEmote -> { + if (state.rootThreadEventId != null) { + room.await().relationService().replyInThread( + rootThreadEventId = state.rootThreadEventId, + replyInThreadText = parsedCommand.message, + msgType = MessageType.MSGTYPE_EMOTE, + autoMarkdown = action.autoMarkdown + ) + } else { + room.await().sendService().sendTextMessage( + text = parsedCommand.message, + msgType = MessageType.MSGTYPE_EMOTE, + autoMarkdown = action.autoMarkdown + ) } - is ParsedCommand.ErrorEmptySlashCommand -> { - _viewEvents.post(MessageComposerViewEvents.SlashCommandUnknown("/")) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + popDraft() + } + is ParsedCommand.SendRainbow -> { + val message = parsedCommand.message.toString() + if (state.rootThreadEventId != null) { + room.await().relationService().replyInThread( + rootThreadEventId = state.rootThreadEventId, + replyInThreadText = parsedCommand.message, + formattedText = rainbowGenerator.generate(message) + ) + } else { + room.await().sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message)) } - is ParsedCommand.ErrorUnknownSlashCommand -> { - _viewEvents.post(MessageComposerViewEvents.SlashCommandUnknown(parsedCommand.slashCommand)) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + popDraft() + } + is ParsedCommand.SendRainbowEmote -> { + val message = parsedCommand.message.toString() + if (state.rootThreadEventId != null) { + room.await().relationService().replyInThread( + rootThreadEventId = state.rootThreadEventId, + replyInThreadText = parsedCommand.message, + msgType = MessageType.MSGTYPE_EMOTE, + formattedText = rainbowGenerator.generate(message) + ) + } else { + room.await().sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message), MessageType.MSGTYPE_EMOTE) } - is ParsedCommand.ErrorCommandNotSupportedInThreads -> { - _viewEvents.post(MessageComposerViewEvents.SlashCommandNotSupportedInThreads(parsedCommand.command)) - } - is ParsedCommand.SendPlainText -> { - // Send the text message to the room, without markdown - if (state.rootThreadEventId != null) { - room.relationService().replyInThread( - rootThreadEventId = state.rootThreadEventId, - replyInThreadText = parsedCommand.message, - autoMarkdown = false - ) - } else { - room.sendService().sendTextMessage(parsedCommand.message, autoMarkdown = false) - } - _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() - } - is ParsedCommand.ChangeRoomName -> { - handleChangeRoomNameSlashCommand(parsedCommand) - } - is ParsedCommand.Invite -> { - handleInviteSlashCommand(parsedCommand) - } - is ParsedCommand.Invite3Pid -> { - handleInvite3pidSlashCommand(parsedCommand) - } - is ParsedCommand.SetUserPowerLevel -> { - handleSetUserPowerLevel(parsedCommand) - } - is ParsedCommand.ClearScalarToken -> { - // TODO - _viewEvents.post(MessageComposerViewEvents.SlashCommandNotImplemented) - } - is ParsedCommand.SetMarkdown -> { - vectorPreferences.setMarkdownEnabled(parsedCommand.enable) - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() - } - is ParsedCommand.BanUser -> { - handleBanSlashCommand(parsedCommand) - } - is ParsedCommand.UnbanUser -> { - handleUnbanSlashCommand(parsedCommand) - } - is ParsedCommand.IgnoreUser -> { - handleIgnoreSlashCommand(parsedCommand) - } - is ParsedCommand.UnignoreUser -> { - handleUnignoreSlashCommand(parsedCommand) - } - is ParsedCommand.RemoveUser -> { - handleRemoveSlashCommand(parsedCommand) - } - is ParsedCommand.JoinRoom -> { - handleJoinToAnotherRoomSlashCommand(parsedCommand) - popDraft() - } - is ParsedCommand.PartRoom -> { - handlePartSlashCommand(parsedCommand) - } - is ParsedCommand.SendEmote -> { - if (state.rootThreadEventId != null) { - room.relationService().replyInThread( - rootThreadEventId = state.rootThreadEventId, - replyInThreadText = parsedCommand.message, - msgType = MessageType.MSGTYPE_EMOTE, - autoMarkdown = action.autoMarkdown - ) - } else { - room.sendService().sendTextMessage( - text = parsedCommand.message, - msgType = MessageType.MSGTYPE_EMOTE, - autoMarkdown = action.autoMarkdown - ) - } - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() - } - is ParsedCommand.SendRainbow -> { - val message = parsedCommand.message.toString() - if (state.rootThreadEventId != null) { - room.relationService().replyInThread( - rootThreadEventId = state.rootThreadEventId, - replyInThreadText = parsedCommand.message, - formattedText = rainbowGenerator.generate(message) - ) - } else { - room.sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message)) - } - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() - } - is ParsedCommand.SendRainbowEmote -> { - val message = parsedCommand.message.toString() - if (state.rootThreadEventId != null) { - room.relationService().replyInThread( - rootThreadEventId = state.rootThreadEventId, - replyInThreadText = parsedCommand.message, - msgType = MessageType.MSGTYPE_EMOTE, - formattedText = rainbowGenerator.generate(message) - ) - } else { - room.sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message), MessageType.MSGTYPE_EMOTE) - } + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + popDraft() + } + is ParsedCommand.SendSpoiler -> { + val text = "[${stringProvider.getString(R.string.spoiler)}](${parsedCommand.message})" + val formattedText = "${parsedCommand.message}" + if (state.rootThreadEventId != null) { + room.await().relationService().replyInThread( + rootThreadEventId = state.rootThreadEventId, + replyInThreadText = text, + formattedText = formattedText + ) + } else { + room.await().sendService().sendFormattedTextMessage( + text, + formattedText + ) + } + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + popDraft() + } + is ParsedCommand.SendShrug -> { + sendPrefixedMessage("¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + popDraft() + } + is ParsedCommand.SendLenny -> { + sendPrefixedMessage("( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + popDraft() + } + is ParsedCommand.SendChatEffect -> { + sendChatEffect(parsedCommand) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + popDraft() + } + is ParsedCommand.ChangeTopic -> { + handleChangeTopicSlashCommand(parsedCommand) + } + is ParsedCommand.ChangeDisplayName -> { + handleChangeDisplayNameSlashCommand(parsedCommand) + } + is ParsedCommand.ChangeDisplayNameForRoom -> { + handleChangeDisplayNameForRoomSlashCommand(parsedCommand) + } + is ParsedCommand.ChangeRoomAvatar -> { + handleChangeRoomAvatarSlashCommand(parsedCommand) + } + is ParsedCommand.ChangeAvatarForRoom -> { + handleChangeAvatarForRoomSlashCommand(parsedCommand) + } + is ParsedCommand.ShowUser -> { + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + handleWhoisSlashCommand(parsedCommand) + popDraft() + } + is ParsedCommand.DiscardSession -> { + if (room.await().roomCryptoService().isEncrypted()) { + session.cryptoService().discardOutboundSession(room.await().roomId) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() - } - is ParsedCommand.SendSpoiler -> { - val text = "[${stringProvider.getString(R.string.spoiler)}](${parsedCommand.message})" - val formattedText = "${parsedCommand.message}" - if (state.rootThreadEventId != null) { - room.relationService().replyInThread( - rootThreadEventId = state.rootThreadEventId, - replyInThreadText = text, - formattedText = formattedText - ) - } else { - room.sendService().sendFormattedTextMessage( - text, - formattedText - ) - } + } else { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() - } - is ParsedCommand.SendShrug -> { - sendPrefixedMessage("¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId) - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() - } - is ParsedCommand.SendLenny -> { - sendPrefixedMessage("( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId) - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() - } - is ParsedCommand.SendChatEffect -> { - sendChatEffect(parsedCommand) - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() - } - is ParsedCommand.ChangeTopic -> { - handleChangeTopicSlashCommand(parsedCommand) - } - is ParsedCommand.ChangeDisplayName -> { - handleChangeDisplayNameSlashCommand(parsedCommand) - } - is ParsedCommand.ChangeDisplayNameForRoom -> { - handleChangeDisplayNameForRoomSlashCommand(parsedCommand) - } - is ParsedCommand.ChangeRoomAvatar -> { - handleChangeRoomAvatarSlashCommand(parsedCommand) - } - is ParsedCommand.ChangeAvatarForRoom -> { - handleChangeAvatarForRoomSlashCommand(parsedCommand) - } - is ParsedCommand.ShowUser -> { - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - handleWhoisSlashCommand(parsedCommand) - popDraft() - } - is ParsedCommand.DiscardSession -> { - if (room.roomCryptoService().isEncrypted()) { - session.cryptoService().discardOutboundSession(room.roomId) - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() - } else { - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - _viewEvents.post( - MessageComposerViewEvents - .ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled)) - ) - } - } - is ParsedCommand.CreateSpace -> { - _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) - viewModelScope.launch(Dispatchers.IO) { - try { - val params = CreateSpaceParams().apply { - name = parsedCommand.name - invitedUserIds.addAll(parsedCommand.invitees) - } - val spaceId = session.spaceService().createSpace(params) - session.spaceService().getSpace(spaceId) - ?.addChildren( - state.roomId, - null, - null, - true - ) - popDraft() - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - } catch (failure: Throwable) { - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) - } - } - Unit - } - is ParsedCommand.AddToSpace -> { - _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) - viewModelScope.launch(Dispatchers.IO) { - try { - session.spaceService().getSpace(parsedCommand.spaceId) - ?.addChildren( - room.roomId, - null, - null, - false - ) - popDraft() - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - } catch (failure: Throwable) { - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) - } - } - Unit - } - is ParsedCommand.JoinSpace -> { - _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) - viewModelScope.launch(Dispatchers.IO) { - try { - session.spaceService().joinSpace(parsedCommand.spaceIdOrAlias) - popDraft() - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - } catch (failure: Throwable) { - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) - } - } - Unit - } - is ParsedCommand.LeaveRoom -> { - viewModelScope.launch(Dispatchers.IO) { - try { - session.roomService().leaveRoom(parsedCommand.roomId) - popDraft() - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - } catch (failure: Throwable) { - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) - } - } - Unit - } - is ParsedCommand.UpgradeRoom -> { _viewEvents.post( - MessageComposerViewEvents.ShowRoomUpgradeDialog( - parsedCommand.newVersion, - room.roomSummary()?.isPublic ?: false - ) + MessageComposerViewEvents + .ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled)) ) - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() } } - } - is SendMode.Edit -> { - // is original event a reply? - val relationContent = state.sendMode.timelineEvent.getRelationContent() - val inReplyTo = if (state.rootThreadEventId != null) { - // Thread event - if (relationContent?.shouldRenderInThread() == true) { - // Reply within a thread event - relationContent.inReplyTo?.eventId - } else { - // Normal thread event - null - } - } else { - // Normal event - relationContent?.inReplyTo?.eventId - } - - if (inReplyTo != null) { - // TODO check if same content? - room.getTimelineEvent(inReplyTo)?.let { - room.relationService().editReply(state.sendMode.timelineEvent, it, action.text.toString()) - } - } else { - val messageContent = state.sendMode.timelineEvent.getLastMessageContent() - val existingBody = messageContent?.body ?: "" - if (existingBody != action.text) { - room.relationService().editTextMessage( - state.sendMode.timelineEvent, - messageContent?.msgType ?: MessageType.MSGTYPE_TEXT, - action.text, - action.autoMarkdown - ) - } else { - Timber.w("Same message content, do not send edition") + is ParsedCommand.CreateSpace -> { + _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) + withContext(coroutineDispatchers.io) { + try { + val params = CreateSpaceParams().apply { + name = parsedCommand.name + invitedUserIds.addAll(parsedCommand.invitees) + } + val spaceId = session.spaceService().createSpace(params) + session.spaceService().getSpace(spaceId) + ?.addChildren( + state.roomId, + null, + null, + true + ) + popDraft() + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + } catch (failure: Throwable) { + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) + } } } - _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() - } - is SendMode.Quote -> { - room.sendService().sendQuotedTextMessage( - quotedEvent = state.sendMode.timelineEvent, - text = action.text.toString(), - autoMarkdown = action.autoMarkdown, - rootThreadEventId = state.rootThreadEventId - ) - _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() - } - is SendMode.Reply -> { - val timelineEvent = state.sendMode.timelineEvent - val showInThread = state.sendMode.timelineEvent.root.isThread() && state.rootThreadEventId == null - // If threads are disabled this will make the fallback replies visible to clients with threads enabled - val rootThreadEventId = if (showInThread) timelineEvent.root.getRootThreadEventId() else null - state.rootThreadEventId?.let { - room.relationService().replyInThread( - rootThreadEventId = it, - replyInThreadText = action.text.toString(), - autoMarkdown = action.autoMarkdown, - eventReplied = timelineEvent + is ParsedCommand.AddToSpace -> { + _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) + withContext(coroutineDispatchers.io) { + try { + session.spaceService().getSpace(parsedCommand.spaceId) + ?.addChildren( + room.await().roomId, + null, + null, + false + ) + popDraft() + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + } catch (failure: Throwable) { + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) + } + } + } + is ParsedCommand.JoinSpace -> { + _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) + withContext(coroutineDispatchers.io) { + try { + session.spaceService().joinSpace(parsedCommand.spaceIdOrAlias) + popDraft() + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + } catch (failure: Throwable) { + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) + } + } + } + is ParsedCommand.LeaveRoom -> { + withContext(coroutineDispatchers.io) { + try { + session.roomService().leaveRoom(parsedCommand.roomId) + popDraft() + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + } catch (failure: Throwable) { + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) + } + } + } + is ParsedCommand.UpgradeRoom -> { + _viewEvents.post( + MessageComposerViewEvents.ShowRoomUpgradeDialog( + parsedCommand.newVersion, + room.await().awaitRoomSummary()?.isPublic ?: false + ) ) - } ?: room.relationService().replyToMessage( - eventReplied = timelineEvent, - replyText = action.text.toString(), - autoMarkdown = action.autoMarkdown, - showInThread = showInThread, - rootThreadEventId = rootThreadEventId - ) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + popDraft() + } + } + } + is SendMode.Edit -> { + // is original event a reply? + val relationContent = state.sendMode.timelineEvent.getRelationContent() + val inReplyTo = if (state.rootThreadEventId != null) { + // Thread event + if (relationContent?.shouldRenderInThread() == true) { + // Reply within a thread event + relationContent.inReplyTo?.eventId + } else { + // Normal thread event + null + } + } else { + // Normal event + relationContent?.inReplyTo?.eventId + } - _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() - } - is SendMode.Voice -> { - // do nothing + if (inReplyTo != null) { + // TODO check if same content? + room.await().getTimelineEvent(inReplyTo)?.let { + room.await().relationService().editReply(state.sendMode.timelineEvent, it, action.text.toString()) + } + } else { + val messageContent = state.sendMode.timelineEvent.getLastMessageContent() + val existingBody = messageContent?.body ?: "" + if (existingBody != action.text) { + room.await().relationService().editTextMessage( + state.sendMode.timelineEvent, + messageContent?.msgType ?: MessageType.MSGTYPE_TEXT, + action.text, + action.autoMarkdown + ) + } else { + Timber.w("Same message content, do not send edition") + } } + _viewEvents.post(MessageComposerViewEvents.MessageSent) + popDraft() + } + is SendMode.Quote -> { + room.await().sendService().sendQuotedTextMessage( + quotedEvent = state.sendMode.timelineEvent, + text = action.text.toString(), + autoMarkdown = action.autoMarkdown, + rootThreadEventId = state.rootThreadEventId + ) + _viewEvents.post(MessageComposerViewEvents.MessageSent) + popDraft() + } + is SendMode.Reply -> { + val timelineEvent = state.sendMode.timelineEvent + val showInThread = state.sendMode.timelineEvent.root.isThread() && state.rootThreadEventId == null + // If threads are disabled this will make the fallback replies visible to clients with threads enabled + val rootThreadEventId = if (showInThread) timelineEvent.root.getRootThreadEventId() else null + state.rootThreadEventId?.let { + room.await().relationService().replyInThread( + rootThreadEventId = it, + replyInThreadText = action.text.toString(), + autoMarkdown = action.autoMarkdown, + eventReplied = timelineEvent + ) + } ?: room.await().relationService().replyToMessage( + eventReplied = timelineEvent, + replyText = action.text.toString(), + autoMarkdown = action.autoMarkdown, + showInThread = showInThread, + rootThreadEventId = rootThreadEventId + ) + + _viewEvents.post(MessageComposerViewEvents.MessageSent) + popDraft() + } + is SendMode.Voice -> { + // do nothing } } } - private fun popDraft() = withState { - if (it.sendMode is SendMode.Regular && it.sendMode.fromSharing) { + private suspend fun popDraft() = withContext(coroutineDispatchers.computation) { + val state = awaitState() + if (state.sendMode is SendMode.Regular && state.sendMode.fromSharing) { // If we were sharing, we want to get back our last value from draft loadDraftIfAny() } else { // Otherwise we clear the composer and remove the draft from db setState { copy(sendMode = SendMode.Regular("", false)) } - viewModelScope.launch { - room.draftService().deleteDraft() - } + room.await().draftService().deleteDraft() } } - private fun loadDraftIfAny() { - val currentDraft = room.draftService().getDraft() + private suspend fun loadDraftIfAny() { + val currentDraft = withContext(coroutineDispatchers.computation) { + room.await().draftService().getDraft() + } + val sendMode = when (currentDraft) { + is UserDraft.Regular -> SendMode.Regular(currentDraft.content, false) + is UserDraft.Quote -> { + room.await().getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> + SendMode.Quote(timelineEvent, currentDraft.content) + } + } + is UserDraft.Reply -> { + room.await().getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> + SendMode.Reply(timelineEvent, currentDraft.content) + } + } + is UserDraft.Edit -> { + room.await().getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> + SendMode.Edit(timelineEvent, currentDraft.content) + } + } + is UserDraft.Voice -> SendMode.Voice(currentDraft.content) + else -> null + } ?: SendMode.Regular("", fromSharing = false) setState { copy( // Create a sendMode from a draft and retrieve the TimelineEvent - sendMode = when (currentDraft) { - is UserDraft.Regular -> SendMode.Regular(currentDraft.content, false) - is UserDraft.Quote -> { - room.getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> - SendMode.Quote(timelineEvent, currentDraft.content) - } - } - is UserDraft.Reply -> { - room.getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> - SendMode.Reply(timelineEvent, currentDraft.content) - } - } - is UserDraft.Edit -> { - room.getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> - SendMode.Edit(timelineEvent, currentDraft.content) - } - } - is UserDraft.Voice -> SendMode.Voice(currentDraft.content) - else -> null - } ?: SendMode.Regular("", fromSharing = false) + sendMode = sendMode ) } } - private fun handleUserIsTyping(action: MessageComposerAction.UserIsTyping) { + private suspend fun handleUserIsTyping(action: MessageComposerAction.UserIsTyping) { if (vectorPreferences.sendTypingNotifs()) { if (action.isTyping) { - room.typingService().userIsTyping() + room.await().typingService().userIsTyping() } else { - room.typingService().userStopsTyping() + room.await().typingService().userStopsTyping() } } } - private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) { + private suspend fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) { // If message is blank, convert to an emote, with default message if (sendChatEffect.message.isBlank()) { val defaultMessage = stringProvider.getString( @@ -621,27 +628,25 @@ class MessageComposerViewModel @AssistedInject constructor( ChatEffect.SNOWFALL -> R.string.default_message_emote_snow } ) - room.sendService().sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE) + room.await().sendService().sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE) } else { - room.sendService().sendTextMessage(sendChatEffect.message, sendChatEffect.chatEffect.toMessageType()) + room.await().sendService().sendTextMessage(sendChatEffect.message, sendChatEffect.chatEffect.toMessageType()) } } - private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) { - viewModelScope.launch { - try { - session.roomService().joinRoom(command.roomAlias, command.reason, emptyList()) - } catch (failure: Throwable) { - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) - return@launch - } - session.getRoomSummary(command.roomAlias) - ?.also { analyticsTracker.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.SlashCommand)) } - ?.roomId - ?.let { - _viewEvents.post(MessageComposerViewEvents.JoinRoomCommandSuccess(it)) - } + private suspend fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) { + try { + session.roomService().joinRoom(command.roomAlias, command.reason, emptyList()) + } catch (failure: Throwable) { + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) + return } + session.getRoomSummary(command.roomAlias) + ?.also { analyticsTracker.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.SlashCommand)) } + ?.roomId + ?.let { + _viewEvents.post(MessageComposerViewEvents.JoinRoomCommandSuccess(it)) + } } private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { @@ -664,26 +669,26 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { + private suspend fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { launchSlashCommandFlowSuspendable(changeTopic) { - room.stateService().updateTopic(changeTopic.topic) + room.await().stateService().updateTopic(changeTopic.topic) } } - private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { + private suspend fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { launchSlashCommandFlowSuspendable(invite) { - room.membershipService().invite(invite.userId, invite.reason) + room.await().membershipService().invite(invite.userId, invite.reason) } } - private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) { + private suspend fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) { launchSlashCommandFlowSuspendable(invite) { - room.membershipService().invite3pid(invite.threePid) + room.await().membershipService().invite3pid(invite.threePid) } } - private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { - val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + private suspend fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { + val newPowerLevelsContent = room.await().getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) ?.content ?.toModel() ?.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel) @@ -691,21 +696,21 @@ class MessageComposerViewModel @AssistedInject constructor( ?: return launchSlashCommandFlowSuspendable(setUserPowerLevel) { - room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent) + room.await().stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent) } } - private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) { + private suspend fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) { launchSlashCommandFlowSuspendable(changeDisplayName) { session.profileService().setDisplayName(session.myUserId, changeDisplayName.displayName) } } - private fun handlePartSlashCommand(command: ParsedCommand.PartRoom) { + private suspend fun handlePartSlashCommand(command: ParsedCommand.PartRoom) { launchSlashCommandFlowSuspendable(command) { if (command.roomAlias == null) { // Leave the current room - room + room.await() } else { session.getRoomSummary(roomIdOrAlias = command.roomAlias) ?.roomId @@ -717,65 +722,65 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleRemoveSlashCommand(removeUser: ParsedCommand.RemoveUser) { + private suspend fun handleRemoveSlashCommand(removeUser: ParsedCommand.RemoveUser) { launchSlashCommandFlowSuspendable(removeUser) { - room.membershipService().remove(removeUser.userId, removeUser.reason) + room.await().membershipService().remove(removeUser.userId, removeUser.reason) } } - private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) { + private suspend fun handleBanSlashCommand(ban: ParsedCommand.BanUser) { launchSlashCommandFlowSuspendable(ban) { - room.membershipService().ban(ban.userId, ban.reason) + room.await().membershipService().ban(ban.userId, ban.reason) } } - private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) { + private suspend fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) { launchSlashCommandFlowSuspendable(unban) { - room.membershipService().unban(unban.userId, unban.reason) + room.await().membershipService().unban(unban.userId, unban.reason) } } - private fun handleChangeRoomNameSlashCommand(changeRoomName: ParsedCommand.ChangeRoomName) { + private suspend fun handleChangeRoomNameSlashCommand(changeRoomName: ParsedCommand.ChangeRoomName) { launchSlashCommandFlowSuspendable(changeRoomName) { - room.stateService().updateName(changeRoomName.name) + room.await().stateService().updateName(changeRoomName.name) } } - private fun getMyRoomMemberContent(): RoomMemberContent? { - return room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId)) + private suspend fun getMyRoomMemberContent(): RoomMemberContent? { + return room.await().getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId)) ?.content ?.toModel() } - private fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) { + private suspend fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) { launchSlashCommandFlowSuspendable(changeDisplayName) { getMyRoomMemberContent() ?.copy(displayName = changeDisplayName.displayName) ?.toContent() ?.let { - room.stateService().sendStateEvent(EventType.STATE_ROOM_MEMBER, session.myUserId, it) + room.await().stateService().sendStateEvent(EventType.STATE_ROOM_MEMBER, session.myUserId, it) } } } - private fun handleChangeRoomAvatarSlashCommand(changeAvatar: ParsedCommand.ChangeRoomAvatar) { + private suspend fun handleChangeRoomAvatarSlashCommand(changeAvatar: ParsedCommand.ChangeRoomAvatar) { launchSlashCommandFlowSuspendable(changeAvatar) { - room.stateService().sendStateEvent(EventType.STATE_ROOM_AVATAR, stateKey = "", RoomAvatarContent(changeAvatar.url).toContent()) + room.await().stateService().sendStateEvent(EventType.STATE_ROOM_AVATAR, stateKey = "", RoomAvatarContent(changeAvatar.url).toContent()) } } - private fun handleChangeAvatarForRoomSlashCommand(changeAvatar: ParsedCommand.ChangeAvatarForRoom) { + private suspend fun handleChangeAvatarForRoomSlashCommand(changeAvatar: ParsedCommand.ChangeAvatarForRoom) { launchSlashCommandFlowSuspendable(changeAvatar) { getMyRoomMemberContent() ?.copy(avatarUrl = changeAvatar.url) ?.toContent() ?.let { - room.stateService().sendStateEvent(EventType.STATE_ROOM_MEMBER, session.myUserId, it) + room.await().stateService().sendStateEvent(EventType.STATE_ROOM_MEMBER, session.myUserId, it) } } } - private fun handleIgnoreSlashCommand(ignore: ParsedCommand.IgnoreUser) { + private suspend fun handleIgnoreSlashCommand(ignore: ParsedCommand.IgnoreUser) { launchSlashCommandFlowSuspendable(ignore) { session.userService().ignoreUserIds(listOf(ignore.userId)) } @@ -785,14 +790,14 @@ class MessageComposerViewModel @AssistedInject constructor( _viewEvents.post(MessageComposerViewEvents.SlashCommandConfirmationRequest(unignore)) } - private fun handleSlashCommandConfirmed(action: MessageComposerAction.SlashCommandConfirmed) { + private suspend fun handleSlashCommandConfirmed(action: MessageComposerAction.SlashCommandConfirmed) { when (action.parsedCommand) { is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(action.parsedCommand) else -> TODO("Not handled yet") } } - private fun handleUnignoreSlashCommandConfirmed(unignore: ParsedCommand.UnignoreUser) { + private suspend fun handleUnignoreSlashCommandConfirmed(unignore: ParsedCommand.UnignoreUser) { launchSlashCommandFlowSuspendable(unignore) { session.userService().unIgnoreUserIds(listOf(unignore.userId)) } @@ -802,7 +807,7 @@ class MessageComposerViewModel @AssistedInject constructor( _viewEvents.post(MessageComposerViewEvents.OpenRoomMemberProfile(whois.userId)) } - private fun sendPrefixedMessage(prefix: String, message: CharSequence, rootThreadEventId: String?) { + private suspend fun sendPrefixedMessage(prefix: String, message: CharSequence, rootThreadEventId: String?) { val sequence = buildString { append(prefix) if (message.isNotEmpty()) { @@ -811,52 +816,52 @@ class MessageComposerViewModel @AssistedInject constructor( } } rootThreadEventId?.let { - room.relationService().replyInThread(it, sequence) - } ?: room.sendService().sendTextMessage(sequence) + room.await().relationService().replyInThread(it, sequence) + } ?: room.await().sendService().sendTextMessage(sequence) } /** * Convert a send mode to a draft and save the draft. */ - private fun handleSaveTextDraft(draft: String) = withState { + private suspend fun handleSaveTextDraft(draft: String) = withState { session.coroutineScope.launch { when { it.sendMode is SendMode.Regular && !it.sendMode.fromSharing -> { setState { copy(sendMode = it.sendMode.copy(text = draft)) } - room.draftService().saveDraft(UserDraft.Regular(draft)) + room.await().draftService().saveDraft(UserDraft.Regular(draft)) } it.sendMode is SendMode.Reply -> { setState { copy(sendMode = it.sendMode.copy(text = draft)) } - room.draftService().saveDraft(UserDraft.Reply(it.sendMode.timelineEvent.root.eventId!!, draft)) + room.await().draftService().saveDraft(UserDraft.Reply(it.sendMode.timelineEvent.root.eventId!!, draft)) } it.sendMode is SendMode.Quote -> { setState { copy(sendMode = it.sendMode.copy(text = draft)) } - room.draftService().saveDraft(UserDraft.Quote(it.sendMode.timelineEvent.root.eventId!!, draft)) + room.await().draftService().saveDraft(UserDraft.Quote(it.sendMode.timelineEvent.root.eventId!!, draft)) } it.sendMode is SendMode.Edit -> { setState { copy(sendMode = it.sendMode.copy(text = draft)) } - room.draftService().saveDraft(UserDraft.Edit(it.sendMode.timelineEvent.root.eventId!!, draft)) + room.await().draftService().saveDraft(UserDraft.Edit(it.sendMode.timelineEvent.root.eventId!!, draft)) } } } } - private fun handleStartRecordingVoiceMessage() { + private suspend fun handleStartRecordingVoiceMessage() { try { - audioMessageHelper.startRecording(room.roomId) + audioMessageHelper.startRecording(room.await().roomId) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure)) } } - private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) { + private suspend fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) { audioMessageHelper.stopPlayback() if (isCancelled) { audioMessageHelper.deleteRecording() } else { audioMessageHelper.stopRecording()?.let { audioType -> if (audioType.duration > 1000) { - room.sendService().sendMedia( + room.await().sendService().sendMedia( attachment = audioType.toContentAttachmentData(isVoiceMessage = true), compressBeforeSending = false, roomIds = emptySet(), @@ -870,8 +875,8 @@ class MessageComposerViewModel @AssistedInject constructor( handleEnterRegularMode(MessageComposerAction.EnterRegularMode(text = "", fromSharing = false)) } - private fun handlePlayOrPauseVoicePlayback(action: MessageComposerAction.PlayOrPauseVoicePlayback) { - viewModelScope.launch(Dispatchers.IO) { + private suspend fun handlePlayOrPauseVoicePlayback(action: MessageComposerAction.PlayOrPauseVoicePlayback) { + withContext(coroutineDispatchers.io) { try { // Download can fail val audioFile = session.fileService().downloadFile(action.messageAudioContent) @@ -913,16 +918,15 @@ class MessageComposerViewModel @AssistedInject constructor( audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration) } - private fun handleEntersBackground(composerText: String) { + private suspend fun handleEntersBackground(composerText: String) { // Always stop all voice actions. It may be playing in timeline or active recording val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false) - - val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording } + val isVoiceRecording = awaitState().isVoiceRecording if (isVoiceRecording) { - viewModelScope.launch { + withContext(coroutineDispatchers.io) { playingAudioContent?.toContentAttachmentData()?.let { voiceDraft -> val content = voiceDraft.toJsonString() - room.draftService().saveDraft(UserDraft.Voice(content)) + room.await().draftService().saveDraft(UserDraft.Voice(content)) setState { copy(sendMode = SendMode.Voice(content)) } } } @@ -931,18 +935,16 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun launchSlashCommandFlowSuspendable(parsedCommand: ParsedCommand, block: suspend () -> Unit) { + private suspend fun launchSlashCommandFlowSuspendable(parsedCommand: ParsedCommand, block: suspend () -> Unit) { _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) - viewModelScope.launch { - val event = try { - block() - popDraft() - MessageComposerViewEvents.SlashCommandResultOk(parsedCommand) - } catch (failure: Throwable) { - MessageComposerViewEvents.SlashCommandResultError(failure) - } - _viewEvents.post(event) + val event = try { + block() + popDraft() + MessageComposerViewEvents.SlashCommandResultOk(parsedCommand) + } catch (failure: Throwable) { + MessageComposerViewEvents.SlashCommandResultError(failure) } + _viewEvents.post(event) } @AssistedFactory diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineFactory.kt index 5d2f69eee4..0557f344ed 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineFactory.kt @@ -20,6 +20,7 @@ import im.vector.app.features.call.vectorCallService import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory import im.vector.app.features.home.room.detail.timeline.merged.MergedTimelines import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.getRoom @@ -36,14 +37,14 @@ private val secondaryTimelineAllowedTypes = listOf( class TimelineFactory @Inject constructor(private val session: Session, private val timelineSettingsFactory: TimelineSettingsFactory) { - fun createTimeline( + suspend fun createTimeline( coroutineScope: CoroutineScope, - mainRoom: Room, + room: Deferred, eventId: String?, rootThreadEventId: String? ): Timeline { val settings = timelineSettingsFactory.create(rootThreadEventId) - + val mainRoom = room.await() if (!session.vectorCallService.protocolChecker.supportVirtualRooms) { return mainRoom.timelineService().createTimeline(eventId, settings) } diff --git a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt index 73662613f7..8648f68eed 100644 --- a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt +++ b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt @@ -47,7 +47,7 @@ fun ElementWellKnown?.getOutboundSessionKeySharingStrategyOrDefault(): OutboundS fun RawService.withElementWellKnown( coroutineScope: CoroutineScope, sessionParams: SessionParams, - block: ((ElementWellKnown?) -> Unit) + block: suspend ((ElementWellKnown?) -> Unit) ) = with(coroutineScope) { launch(Dispatchers.IO) { block(getElementWellknown(sessionParams))