Use awaitRoom on Timeline screen

This commit is contained in:
ganfra 2022-07-06 18:40:15 +02:00
parent 2acfce2d20
commit 12b681209f
5 changed files with 872 additions and 856 deletions

View File

@ -213,6 +213,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
@ -381,6 +382,7 @@ class TimelineFragment @Inject constructor(
) )
keyboardStateUtils = KeyboardStateUtils(requireActivity()) keyboardStateUtils = KeyboardStateUtils(requireActivity())
lazyLoadedViews.bind(views) lazyLoadedViews.bind(views)
viewLifecycleOwner.lifecycleScope.launch {
setupToolbar(views.roomToolbar) setupToolbar(views.roomToolbar)
.allowBack() .allowBack()
setupRecyclerView() setupRecyclerView()
@ -393,6 +395,7 @@ class TimelineFragment @Inject constructor(
setupRemoveJitsiWidgetView() setupRemoveJitsiWidgetView()
setupVoiceMessageView() setupVoiceMessageView()
setupLiveLocationIndicator() setupLiveLocationIndicator()
}
views.includeRoomToolbar.roomToolbarContentView.debouncedClicks { views.includeRoomToolbar.roomToolbarContentView.debouncedClicks {
navigator.openRoomProfile(requireActivity(), timelineArgs.roomId) navigator.openRoomProfile(requireActivity(), timelineArgs.roomId)
@ -979,16 +982,17 @@ class TimelineFragment @Inject constructor(
private fun setupJumpToBottomView() { private fun setupJumpToBottomView() {
views.jumpToBottomView.visibility = View.INVISIBLE views.jumpToBottomView.visibility = View.INVISIBLE
views.jumpToBottomView.debouncedClicks { views.jumpToBottomView.debouncedClicks {
viewLifecycleOwner.lifecycleScope.launch {
timelineViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) timelineViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
views.jumpToBottomView.visibility = View.INVISIBLE views.jumpToBottomView.visibility = View.INVISIBLE
if (!timelineViewModel.timeline.isLive) { if (!timelineViewModel.timeline.await().isLive) {
scrollOnNewMessageCallback.forceScrollOnNextUpdate() scrollOnNewMessageCallback.forceScrollOnNextUpdate()
timelineViewModel.timeline.restartWithEventId(null) timelineViewModel.timeline.await().restartWithEventId(null)
} else { } else {
layoutManager.scrollToPosition(0) layoutManager.scrollToPosition(0)
} }
} }
}
jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager( jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager(
views.jumpToBottomView, views.jumpToBottomView,
debouncer, debouncer,
@ -1216,6 +1220,7 @@ class TimelineFragment @Inject constructor(
} }
private fun handleSearchAction() { private fun handleSearchAction() {
viewLifecycleOwner.lifecycleScope.launch {
navigator.openSearch( navigator.openSearch(
context = requireContext(), context = requireContext(),
roomId = timelineArgs.roomId, roomId = timelineArgs.roomId,
@ -1223,6 +1228,7 @@ class TimelineFragment @Inject constructor(
roomAvatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl roomAvatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl
) )
} }
}
private fun displayDisabledIntegrationDialog() { private fun displayDisabledIntegrationDialog() {
MaterialAlertDialogBuilder(requireActivity()) MaterialAlertDialogBuilder(requireActivity())
@ -1416,9 +1422,9 @@ class TimelineFragment @Inject constructor(
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun setupRecyclerView() { private suspend fun setupRecyclerView() {
timelineEventController.callback = this timelineEventController.callback = this
timelineEventController.timeline = timelineViewModel.timeline timelineEventController.timeline = timelineViewModel.timeline.await()
views.timelineRecyclerView.trackItemsVisibilityChange() views.timelineRecyclerView.trackItemsVisibilityChange()
layoutManager = object : LinearLayoutManager(context, RecyclerView.VERTICAL, true) { 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.setText(Command.EMOTE.command + " ")
views.composerLayout.views.composerEditText.setSelection(Command.EMOTE.command.length + 1) views.composerLayout.views.composerEditText.setSelection(Command.EMOTE.command.length + 1)
} else { } else {
val roomMember = timelineViewModel.getMember(userId) val roomMember = runBlocking {
timelineViewModel.getMember(userId)
}
// TODO move logic outside of fragment // TODO move logic outside of fragment
(roomMember?.displayName ?: userId) (roomMember?.displayName ?: userId)
.let { sanitizeDisplayName(it) } .let { sanitizeDisplayName(it) }
@ -2491,20 +2499,23 @@ class TimelineFragment @Inject constructor(
* using the ThreadsActivity. * using the ThreadsActivity.
*/ */
private fun navigateToThreadTimeline(rootThreadEventId: String, startsThread: Boolean = false, showKeyboard: Boolean = false) { private fun navigateToThreadTimeline(rootThreadEventId: String, startsThread: Boolean = false, showKeyboard: Boolean = false) {
viewLifecycleOwner.lifecycleScope.launch {
analyticsTracker.capture(Interaction.Name.MobileRoomThreadSummaryItem.toAnalyticsInteraction()) analyticsTracker.capture(Interaction.Name.MobileRoomThreadSummaryItem.toAnalyticsInteraction())
context?.let { context?.let {
val roomSummary = timelineViewModel.awaitState().asyncRoomSummary()
val roomThreadDetailArgs = ThreadTimelineArgs( val roomThreadDetailArgs = ThreadTimelineArgs(
startsThread = startsThread, startsThread = startsThread,
roomId = timelineArgs.roomId, roomId = timelineArgs.roomId,
displayName = timelineViewModel.getRoomSummary()?.displayName, displayName = roomSummary?.displayName,
avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl, avatarUrl = roomSummary?.avatarUrl,
roomEncryptionTrustLevel = timelineViewModel.getRoomSummary()?.roomEncryptionTrustLevel, roomEncryptionTrustLevel = roomSummary?.roomEncryptionTrustLevel,
rootThreadEventId = rootThreadEventId, rootThreadEventId = rootThreadEventId,
showKeyboard = showKeyboard showKeyboard = showKeyboard
) )
navigator.openThread(it, roomThreadDetailArgs) navigator.openThread(it, roomThreadDetailArgs)
} }
} }
}
private fun displayThreadsBetaOptInDialog() { private fun displayThreadsBetaOptInDialog() {
activity?.let { activity?.let {
@ -2530,17 +2541,20 @@ class TimelineFragment @Inject constructor(
* using the ThreadsActivity. * using the ThreadsActivity.
*/ */
private fun navigateToThreadList() { private fun navigateToThreadList() {
viewLifecycleOwner.lifecycleScope.launch {
analyticsTracker.capture(Interaction.Name.MobileRoomThreadListButton.toAnalyticsInteraction()) analyticsTracker.capture(Interaction.Name.MobileRoomThreadListButton.toAnalyticsInteraction())
context?.let { context?.let {
val roomSummary = timelineViewModel.awaitState().asyncRoomSummary()
val roomThreadDetailArgs = ThreadTimelineArgs( val roomThreadDetailArgs = ThreadTimelineArgs(
roomId = timelineArgs.roomId, roomId = timelineArgs.roomId,
displayName = timelineViewModel.getRoomSummary()?.displayName, displayName = roomSummary?.displayName,
roomEncryptionTrustLevel = timelineViewModel.getRoomSummary()?.roomEncryptionTrustLevel, roomEncryptionTrustLevel = roomSummary?.roomEncryptionTrustLevel,
avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl avatarUrl = roomSummary?.avatarUrl
) )
navigator.openThreadList(it, roomThreadDetailArgs) navigator.openThreadList(it, roomThreadDetailArgs)
} }
} }
}
// VectorInviteView.Callback // VectorInviteView.Callback
override fun onAcceptInvite() { override fun onAcceptInvite() {

View File

@ -32,6 +32,7 @@ import im.vector.app.AppStateHandler
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory 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.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider 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.features.settings.VectorPreferences
import im.vector.app.space import im.vector.app.space
import im.vector.lib.core.utils.flow.chunk 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.MutableSharedFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter 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.toContent
import org.matrix.android.sdk.api.session.events.model.toModel 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.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.getStateEvent
import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
@ -136,17 +138,22 @@ class TimelineViewModel @AssistedInject constructor(
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
private val locationSharingServiceConnection: LocationSharingServiceConnection, private val locationSharingServiceConnection: LocationSharingServiceConnection,
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase, private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
private val coroutineDispatchers: CoroutineDispatchers,
timelineFactory: TimelineFactory, timelineFactory: TimelineFactory,
appStateHandler: AppStateHandler, appStateHandler: AppStateHandler,
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback { 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 eventId = initialState.eventId
private val invisibleEventsSource = BehaviorDataSource<RoomDetailAction.TimelineEventTurnsInvisible>() private val invisibleEventsSource = BehaviorDataSource<RoomDetailAction.TimelineEventTurnsInvisible>()
private val visibleEventsSource = BehaviorDataSource<RoomDetailAction.TimelineEventTurnsVisible>() private val visibleEventsSource = BehaviorDataSource<RoomDetailAction.TimelineEventTurnsVisible>()
private var timelineEvents = MutableSharedFlow<List<TimelineEvent>>(0) private var timelineEvents = MutableSharedFlow<List<TimelineEvent>>(0)
val timeline = timelineFactory.createTimeline(viewModelScope, room, eventId, initialState.rootThreadEventId)
// Same lifecycle than the ViewModel (survive to screen rotation) // Same lifecycle than the ViewModel (survive to screen rotation)
val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope) val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope)
@ -175,8 +182,11 @@ class TimelineViewModel @AssistedInject constructor(
} }
init { init {
timeline.start(initialState.rootThreadEventId) viewModelScope.launch {
timeline.addListener(this) timeline.await().also {
it.start(initialState.rootThreadEventId)
it.addListener(this@TimelineViewModel)
}
observeRoomSummary() observeRoomSummary()
observeMembershipChanges() observeMembershipChanges()
observeSummaryState() observeSummaryState()
@ -189,20 +199,17 @@ class TimelineViewModel @AssistedInject constructor(
observeActiveRoomWidgets() observeActiveRoomWidgets()
observePowerLevel() observePowerLevel()
setupPreviewUrlObservers() setupPreviewUrlObservers()
room.getRoomSummaryLive() withContext(coroutineDispatchers.io) {
viewModelScope.launch(Dispatchers.IO) { tryOrNull { room.await().readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) }
tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) }
}
// Inform the SDK that the room is displayed // Inform the SDK that the room is displayed
viewModelScope.launch(Dispatchers.IO) {
tryOrNull { session.roomService().onRoomDisplayed(initialState.roomId) } tryOrNull { session.roomService().onRoomDisplayed(initialState.roomId) }
} }
callManager.addProtocolsCheckerListener(this) callManager.addProtocolsCheckerListener(this@TimelineViewModel)
callManager.checkForProtocolsSupportIfNeeded() callManager.checkForProtocolsSupportIfNeeded()
chatEffectManager.delegate = this chatEffectManager.delegate = this@TimelineViewModel
// Ensure to share the outbound session keys with all members // Ensure to share the outbound session keys with all members
if (room.roomCryptoService().isEncrypted()) { if (room.await().roomCryptoService().isEncrypted()) {
rawService.withElementWellKnown(viewModelScope, session.sessionParams) { rawService.withElementWellKnown(viewModelScope, session.sessionParams) {
val strategy = it.getOutboundSessionKeySharingStrategyOrDefault() val strategy = it.getOutboundSessionKeySharingStrategyOrDefault()
if (strategy == OutboundSessionKeySharingStrategy.WhenEnteringRoom) { if (strategy == OutboundSessionKeySharingStrategy.WhenEnteringRoom) {
@ -220,7 +227,7 @@ class TimelineViewModel @AssistedInject constructor(
// We are coming from a notification, try to switch to the most relevant space // 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 // so that when hitting back the room will appear in the list
appStateHandler.getCurrentRoomGroupingMethod()?.space().let { currentSpace -> appStateHandler.getCurrentRoomGroupingMethod()?.space().let { currentSpace ->
val currentRoomSummary = room.roomSummary() ?: return@let val currentRoomSummary = room.await().awaitRoomSummary() ?: return@let
// nothing we are good // nothing we are good
if ((currentSpace == null && !vectorPreferences.prefSpacesShowAllRoomInHome()) || if ((currentSpace == null && !vectorPreferences.prefSpacesShowAllRoomInHome()) ||
(currentSpace != null && !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId))) { (currentSpace != null && !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId))) {
@ -240,13 +247,14 @@ class TimelineViewModel @AssistedInject constructor(
initThreads() initThreads()
// Observe location service lifecycle to be able to warn the user // Observe location service lifecycle to be able to warn the user
locationSharingServiceConnection.bind(this) locationSharingServiceConnection.bind(this@TimelineViewModel)
}
} }
/** /**
* Threads specific initialization. * Threads specific initialization.
*/ */
private fun initThreads() { private suspend fun initThreads() {
markThreadTimelineAsReadLocal() markThreadTimelineAsReadLocal()
observeLocalThreadNotifications() observeLocalThreadNotifications()
} }
@ -259,13 +267,12 @@ 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 // check if there is not already a call made, or if there has been an error
if (prepareToEncrypt.shouldLoad) { if (prepareToEncrypt.shouldLoad) {
prepareToEncrypt = Loading() prepareToEncrypt = Loading()
viewModelScope.launch {
runCatching { runCatching {
room.roomCryptoService().prepareToEncrypt() room.await().roomCryptoService().prepareToEncrypt()
}.fold({ }.fold({
prepareToEncrypt = Success(Unit) prepareToEncrypt = Success(Unit)
}, { }, {
@ -273,13 +280,12 @@ class TimelineViewModel @AssistedInject constructor(
}) })
} }
} }
}
private fun observePowerLevel() { private suspend fun observePowerLevel() {
PowerLevelsFlowFactory(room).createFlow() PowerLevelsFlowFactory(room.await()).createFlow()
.onEach { .onEach {
val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId) 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 isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE)
val isAllowedToSetupEncryption = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) val isAllowedToSetupEncryption = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
setState { setState {
@ -290,7 +296,9 @@ class TimelineViewModel @AssistedInject constructor(
isAllowedToSetupEncryption = isAllowedToSetupEncryption isAllowedToSetupEncryption = isAllowedToSetupEncryption
) )
} }
}.launchIn(viewModelScope) }
.flowOn(coroutineDispatchers.computation)
.collect()
} }
private fun observeActiveRoomWidgets() { private fun observeActiveRoomWidgets() {
@ -302,6 +310,7 @@ class TimelineViewModel @AssistedInject constructor(
.map { widgets -> .map { widgets ->
widgets.filter { it.isActive } widgets.filter { it.isActive }
} }
.flowOn(coroutineDispatchers.computation)
.execute { widgets -> .execute { widgets ->
copy(activeRoomWidgets = widgets) copy(activeRoomWidgets = widgets)
} }
@ -323,11 +332,11 @@ class TimelineViewModel @AssistedInject constructor(
} }
} }
private fun observeMyRoomMember() { private suspend fun observeMyRoomMember() {
val queryParams = roomMemberQueryParams { val queryParams = roomMemberQueryParams {
this.userId = QueryStringValue.Equals(session.myUserId, QueryStringValue.Case.SENSITIVE) this.userId = QueryStringValue.Equals(session.myUserId, QueryStringValue.Case.SENSITIVE)
} }
room.flow() room.await().flow()
.liveRoomMembers(queryParams) .liveRoomMembers(queryParams)
.map { .map {
it.firstOrNull().toOptional() it.firstOrNull().toOptional()
@ -338,13 +347,13 @@ class TimelineViewModel @AssistedInject constructor(
} }
} }
private fun setupPreviewUrlObservers() { private suspend fun setupPreviewUrlObservers() {
if (!vectorPreferences.showUrlPreviews()) { if (!vectorPreferences.showUrlPreviews()) {
return return
} }
combine( combine(
timelineEvents, timelineEvents,
room.flow().liveRoomSummary() room.await().flow().liveRoomSummary()
.unwrap() .unwrap()
.map { it.isEncrypted } .map { it.isEncrypted }
.distinctUntilChanged() .distinctUntilChanged()
@ -352,7 +361,7 @@ class TimelineViewModel @AssistedInject constructor(
if (isRoomEncrypted) { if (isRoomEncrypted) {
return@combine return@combine
} }
withContext(Dispatchers.Default) { withContext(coroutineDispatchers.computation) {
Timber.v("On new timeline events for urlpreview on ${Thread.currentThread()}") Timber.v("On new timeline events for urlpreview on ${Thread.currentThread()}")
snapshot.forEach { snapshot.forEach {
previewUrlRetriever.getPreviewUrl(it) previewUrlRetriever.getPreviewUrl(it)
@ -369,7 +378,7 @@ class TimelineViewModel @AssistedInject constructor(
private fun markThreadTimelineAsReadLocal() { private fun markThreadTimelineAsReadLocal() {
initialState.rootThreadEventId?.let { initialState.rootThreadEventId?.let {
session.coroutineScope.launch { session.coroutineScope.launch {
room.threadsLocalService().markThreadAsRead(it) room.await().threadsLocalService().markThreadAsRead(it)
} }
} }
} }
@ -377,8 +386,8 @@ class TimelineViewModel @AssistedInject constructor(
/** /**
* Observe local unread threads. * Observe local unread threads.
*/ */
private fun observeLocalThreadNotifications() { private suspend fun observeLocalThreadNotifications() {
room.flow() room.await().flow()
.liveLocalUnreadThreadList() .liveLocalUnreadThreadList()
.execute { .execute {
val threadList = it.invoke() val threadList = it.invoke()
@ -395,11 +404,10 @@ class TimelineViewModel @AssistedInject constructor(
} }
} }
fun getOtherUserIds() = room.roomSummary()?.otherMemberIds suspend fun getRoomSummary() = room.await().awaitRoomSummary()
fun getRoomSummary() = room.roomSummary()
override fun handle(action: RoomDetailAction) { override fun handle(action: RoomDetailAction) {
viewModelScope.launch {
when (action) { when (action) {
is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action)
is RoomDetailAction.SendMedia -> handleSendMedia(action) is RoomDetailAction.SendMedia -> handleSendMedia(action)
@ -467,6 +475,7 @@ class TimelineViewModel @AssistedInject constructor(
RoomDetailAction.StopLiveLocationSharing -> handleStopLiveLocationSharing() RoomDetailAction.StopLiveLocationSharing -> handleStopLiveLocationSharing()
} }
} }
}
private fun handleJitsiCallJoinStatus(action: RoomDetailAction.UpdateJoinJitsiCallStatus) = withState { state -> private fun handleJitsiCallJoinStatus(action: RoomDetailAction.UpdateJoinJitsiCallStatus) = withState { state ->
if (state.jitsiState.confId == null) { if (state.jitsiState.confId == null) {
@ -505,10 +514,10 @@ class TimelineViewModel @AssistedInject constructor(
previewUrlRetriever.doNotShowPreviewUrlFor(action.eventId, action.url) previewUrlRetriever.doNotShowPreviewUrlFor(action.eventId, action.url)
} }
private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) { private suspend fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) {
viewModelScope.launch(Dispatchers.IO) { withContext(coroutineDispatchers.io) {
try { try {
room.stateService().updateAvatar(action.newAvatarUri, action.newAvatarFileName) room.await().stateService().updateAvatar(action.newAvatarUri, action.newAvatarFileName)
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
@ -524,12 +533,12 @@ class TimelineViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.OpenSetRoomAvatarDialog) _viewEvents.post(RoomDetailViewEvents.OpenSetRoomAvatarDialog)
} }
private fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) { private suspend fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) {
room.readService().getUserReadReceipt(action.userId) room.await().readService().getUserReadReceipt(action.userId)
?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) } ?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) }
} }
private fun handleSendSticker(action: RoomDetailAction.SendSticker) { private suspend fun handleSendSticker(action: RoomDetailAction.SendSticker) {
val content = initialState.rootThreadEventId?.let { val content = initialState.rootThreadEventId?.let {
action.stickerContent.copy( action.stickerContent.copy(
relatesTo = RelationDefaultContent( relatesTo = RelationDefaultContent(
@ -540,31 +549,27 @@ class TimelineViewModel @AssistedInject constructor(
) )
} ?: action.stickerContent } ?: action.stickerContent
room.sendService().sendEvent(EventType.STICKER, content.toContent()) room.await().sendService().sendEvent(EventType.STICKER, content.toContent())
} }
private fun handleStartCall(action: RoomDetailAction.StartCall) { private suspend fun handleStartCall(action: RoomDetailAction.StartCall) {
viewModelScope.launch { val room = room.await()
room.roomSummary()?.otherMemberIds?.firstOrNull()?.let { room.roomSummary()?.otherMemberIds?.firstOrNull()?.let {
callManager.startOutgoingCall(room.roomId, it, action.isVideo) callManager.startOutgoingCall(room.roomId, it, action.isVideo)
} }
} }
}
private fun handleEndCall() { private fun handleEndCall() {
callManager.endCallForRoom(initialState.roomId) callManager.endCallForRoom(initialState.roomId)
} }
private fun handleSelectStickerAttachment() { private suspend fun handleSelectStickerAttachment() {
viewModelScope.launch {
val viewEvent = stickerPickerActionHandler.handle() val viewEvent = stickerPickerActionHandler.handle()
_viewEvents.post(viewEvent) _viewEvents.post(viewEvent)
} }
}
private fun handleOpenIntegrationManager() { private suspend fun handleOpenIntegrationManager() {
viewModelScope.launch { val viewEvent = withContext(coroutineDispatchers.computation) {
val viewEvent = withContext(Dispatchers.Default) {
if (isIntegrationEnabled()) { if (isIntegrationEnabled()) {
RoomDetailViewEvents.OpenIntegrationManager RoomDetailViewEvents.OpenIntegrationManager
} else { } else {
@ -573,9 +578,9 @@ class TimelineViewModel @AssistedInject constructor(
} }
_viewEvents.post(viewEvent) _viewEvents.post(viewEvent)
} }
}
private fun handleManageIntegrations() = withState { state -> private suspend fun handleManageIntegrations() {
val state = awaitState()
if (state.activeRoomWidgets().isNullOrEmpty()) { if (state.activeRoomWidgets().isNullOrEmpty()) {
// Directly open integration manager screen // Directly open integration manager screen
handleOpenIntegrationManager() 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) _viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
viewModelScope.launch(Dispatchers.IO) { withContext(coroutineDispatchers.io) {
try { try {
val widget = jitsiService.createJitsiWidget(room.roomId, action.withVideo) val widget = jitsiService.createJitsiWidget(initialState.roomId, action.withVideo)
_viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.withVideo)) _viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.withVideo))
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_add_widget))) _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 val isJitsiWidget = state.jitsiState.widgetId == widgetId
viewModelScope.launch(Dispatchers.IO) { withContext(coroutineDispatchers.io) {
try { try {
if (isJitsiWidget) { if (isJitsiWidget) {
setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = true)) } setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = true)) }
} else { } else {
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView) _viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
} }
session.widgetService().destroyRoomWidget(room.roomId, widgetId) session.widgetService().destroyRoomWidget(initialState.roomId, widgetId)
// local echo // local echo
setState { setState {
copy( copy(
@ -660,7 +666,7 @@ class TimelineViewModel @AssistedInject constructor(
if (trackUnreadMessages.getAndSet(false)) { if (trackUnreadMessages.getAndSet(false)) {
mostRecentDisplayedEvent?.root?.eventId?.also { mostRecentDisplayedEvent?.root?.eventId?.also {
session.coroutineScope.launch { session.coroutineScope.launch {
tryOrNull { room.readService().setReadMarker(it) } tryOrNull { room.await().readService().setReadMarker(it) }
} }
} }
mostRecentDisplayedEvent = null mostRecentDisplayedEvent = null
@ -672,13 +678,13 @@ class TimelineViewModel @AssistedInject constructor(
invisibleEventsSource.post(action) invisibleEventsSource.post(action)
} }
fun getMember(userId: String): RoomMemberSummary? { suspend fun getMember(userId: String): RoomMemberSummary? {
return room.membershipService().getRoomMember(userId) return room.await().membershipService().getRoomMember(userId)
} }
private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) { private suspend fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) {
// Ensure outbound session keys // Ensure outbound session keys
if (room.roomCryptoService().isEncrypted()) { if (room.await().roomCryptoService().isEncrypted()) {
rawService.withElementWellKnown(viewModelScope, session.sessionParams) { rawService.withElementWellKnown(viewModelScope, session.sessionParams) {
val strategy = it.getOutboundSessionKeySharingStrategyOrDefault() val strategy = it.getOutboundSessionKeySharingStrategyOrDefault()
if (strategy == OutboundSessionKeySharingStrategy.WhenTyping && action.focused) { if (strategy == OutboundSessionKeySharingStrategy.WhenTyping && action.focused) {
@ -689,11 +695,12 @@ class TimelineViewModel @AssistedInject constructor(
} }
} }
private fun handleJoinAndOpenReplacementRoom() = withState { state -> private suspend fun handleJoinAndOpenReplacementRoom() {
val tombstoneContent = state.tombstoneEvent?.getClearContent()?.toModel<RoomTombstoneContent>() ?: return@withState val state = awaitState()
val tombstoneContent = state.tombstoneEvent?.getClearContent()?.toModel<RoomTombstoneContent>() ?: return
val roomId = tombstoneContent.replacementRoomId ?: "" 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) { if (isRoomJoined) {
setState { copy(joinUpgradedRoomAsync = Success(roomId)) } setState { copy(joinUpgradedRoomAsync = Success(roomId)) }
_viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId, closeCurrentRoom = true)) _viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId, closeCurrentRoom = true))
@ -706,7 +713,6 @@ class TimelineViewModel @AssistedInject constructor(
setState { setState {
copy(joinUpgradedRoomAsync = Loading()) copy(joinUpgradedRoomAsync = Loading())
} }
viewModelScope.launch {
val result = runCatchingToAsync { val result = runCatchingToAsync {
session.roomService().joinRoom(roomId, viaServers = viaServers) session.roomService().joinRoom(roomId, viaServers = viaServers)
roomId roomId
@ -719,7 +725,6 @@ class TimelineViewModel @AssistedInject constructor(
} }
} }
} }
}
private fun handleClickMisconfiguredE2E() = withState { state -> private fun handleClickMisconfiguredE2E() = withState { state ->
if (state.isAllowedToSetupEncryption) { if (state.isAllowedToSetupEncryption) {
@ -761,37 +766,33 @@ class TimelineViewModel @AssistedInject constructor(
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun handleSendReaction(action: RoomDetailAction.SendReaction) { private suspend fun handleSendReaction(action: RoomDetailAction.SendReaction) {
room.relationService().sendReaction(action.targetEventId, action.reaction) room.await().relationService().sendReaction(action.targetEventId, action.reaction)
} }
private fun handleRedactEvent(action: RoomDetailAction.RedactAction) { private suspend fun handleRedactEvent(action: RoomDetailAction.RedactAction) {
val event = room.getTimelineEvent(action.targetEventId) ?: return val event = room.await().getTimelineEvent(action.targetEventId) ?: return
room.sendService().redactEvent(event.root, action.reason) room.await().sendService().redactEvent(event.root, action.reason)
} }
private fun handleUndoReact(action: RoomDetailAction.UndoReaction) { private suspend fun handleUndoReact(action: RoomDetailAction.UndoReaction) {
viewModelScope.launch {
tryOrNull { tryOrNull {
room.relationService().undoReaction(action.targetEventId, action.reaction) room.await().relationService().undoReaction(action.targetEventId, action.reaction)
}
} }
} }
private fun handleUpdateQuickReaction(action: RoomDetailAction.UpdateQuickReactAction) { private suspend fun handleUpdateQuickReaction(action: RoomDetailAction.UpdateQuickReactAction) {
if (action.add) { if (action.add) {
room.relationService().sendReaction(action.targetEventId, action.selectedReaction) room.await().relationService().sendReaction(action.targetEventId, action.selectedReaction)
} else { } else {
viewModelScope.launch {
tryOrNull { tryOrNull {
room.relationService().undoReaction(action.targetEventId, action.selectedReaction) room.await().relationService().undoReaction(action.targetEventId, action.selectedReaction)
}
} }
} }
} }
private fun handleSendMedia(action: RoomDetailAction.SendMedia) { private suspend fun handleSendMedia(action: RoomDetailAction.SendMedia) {
room.sendService().sendMedias( room.await().sendService().sendMedias(
action.attachments, action.attachments,
action.compressBeforeSending, action.compressBeforeSending,
emptySet(), emptySet(),
@ -799,14 +800,14 @@ class TimelineViewModel @AssistedInject constructor(
) )
} }
private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) { private suspend fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) {
viewModelScope.launch(Dispatchers.Default) { withContext(coroutineDispatchers.computation) {
if (action.event.root.sendState.isSent()) { // ignore pending/local events if (action.event.root.sendState.isSent()) { // ignore pending/local events
visibleEventsSource.post(action) visibleEventsSource.post(action)
} }
// We need to update this with the related m.replace also (to move read receipt) // We need to update this with the related m.replace also (to move read receipt)
action.event.annotations?.editSummary?.sourceEvents?.forEach { action.event.annotations?.editSummary?.sourceEvents?.forEach {
room.getTimelineEvent(it)?.let { event -> room.await().getTimelineEvent(it)?.let { event ->
visibleEventsSource.post(RoomDetailAction.TimelineEventTurnsVisible(event)) visibleEventsSource.post(RoomDetailAction.TimelineEventTurnsVisible(event))
} }
} }
@ -826,43 +827,40 @@ class TimelineViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.StopChatEffects) _viewEvents.post(RoomDetailViewEvents.StopChatEffects)
} }
private fun handleLoadMore(action: RoomDetailAction.LoadMoreTimelineEvents) { private suspend fun handleLoadMore(action: RoomDetailAction.LoadMoreTimelineEvents) {
timeline.paginate(action.direction, PAGINATION_COUNT) timeline.await().paginate(action.direction, PAGINATION_COUNT)
} }
private fun handleRejectInvite() { private suspend fun handleRejectInvite() {
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) } notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) }
viewModelScope.launch {
try { try {
session.roomService().leaveRoom(room.roomId) session.roomService().leaveRoom(initialState.roomId)
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
_viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true)) _viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true))
} }
} }
}
private fun handleAcceptInvite() { private suspend fun handleAcceptInvite() {
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) } notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) }
viewModelScope.launch {
try { try {
session.roomService().joinRoom(room.roomId) session.roomService().joinRoom(initialState.roomId)
trackRoomJoined() trackRoomJoined()
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
_viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true)) _viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true))
} }
} }
}
private fun trackRoomJoined() { private suspend fun trackRoomJoined() {
val trigger = if (initialState.isInviteAlreadyAccepted) { val trigger = if (initialState.isInviteAlreadyAccepted) {
JoinedRoom.Trigger.Invite JoinedRoom.Trigger.Invite
} else { } else {
JoinedRoom.Trigger.Timeline 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 mxcUrl = action.messageFileContent.getFileUrl() ?: return
val isLocalSendingFile = action.senderId == session.myUserId && val isLocalSendingFile = action.senderId == session.myUserId &&
mxcUrl.startsWith("content://") mxcUrl.startsWith("content://")
@ -876,7 +874,7 @@ class TimelineViewModel @AssistedInject constructor(
) )
} }
} else { } else {
viewModelScope.launch { withContext(coroutineDispatchers.io) {
val fileState = session.fileService().fileState(action.messageFileContent) val fileState = session.fileService().fileState(action.messageFileContent)
var canOpen = fileState is FileService.FileState.InCache && fileState.decryptedFileInCache var canOpen = fileState is FileService.FileState.InCache && fileState.decryptedFileInCache
if (!canOpen) { 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 targetEventId: String = action.eventId
val indexOfEvent = timeline.getIndexOfEvent(targetEventId) val indexOfEvent = timeline.await().getIndexOfEvent(targetEventId)
if (indexOfEvent == null) { if (indexOfEvent == null) {
// Event is not already in RAM // Event is not already in RAM
timeline.restartWithEventId(targetEventId) timeline.await().restartWithEventId(targetEventId)
} }
if (action.highlight) { if (action.highlight) {
setState { copy(highlightedEventId = targetEventId) } setState { copy(highlightedEventId = targetEventId) }
@ -923,8 +921,9 @@ class TimelineViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.NavigateToEvent(targetEventId)) _viewEvents.post(RoomDetailViewEvents.NavigateToEvent(targetEventId))
} }
private fun handleResendEvent(action: RoomDetailAction.ResendMessage) { private suspend fun handleResendEvent(action: RoomDetailAction.ResendMessage) {
val targetEventId = action.eventId val targetEventId = action.eventId
val room = room.await()
room.getTimelineEvent(targetEventId)?.let { room.getTimelineEvent(targetEventId)?.let {
// State must be UNDELIVERED or Failed // State must be UNDELIVERED or Failed
if (!it.root.sendState.hasFailed()) { 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 targetEventId = action.eventId
val room = room.await()
room.getTimelineEvent(targetEventId)?.let { room.getTimelineEvent(targetEventId)?.let {
// State must be UNDELIVERED or Failed // State must be UNDELIVERED or Failed
if (!it.root.sendState.hasFailed()) { 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) { if (action.force) {
room.sendService().cancelSend(action.eventId) room.sendService().cancelSend(action.eventId)
return return
@ -969,12 +970,12 @@ class TimelineViewModel @AssistedInject constructor(
} }
} }
private fun handleResendAll() { private suspend fun handleResendAll() {
room.sendService().resendAllFailedMessages() room.await().sendService().resendAllFailedMessages()
} }
private fun handleRemoveAllFailedMessages() { private suspend fun handleRemoveAllFailedMessages() {
room.sendService().cancelAllFailedMessages() room.await().sendService().cancelAllFailedMessages()
} }
private fun observeEventDisplayedActions() { private fun observeEventDisplayedActions() {
@ -997,11 +998,11 @@ class TimelineViewModel @AssistedInject constructor(
} }
bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId -> bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId ->
session.coroutineScope.launch { session.coroutineScope.launch {
tryOrNull { room.readService().setReadReceipt(eventId) } tryOrNull { room.await().readService().setReadReceipt(eventId) }
} }
} }
} }
.flowOn(Dispatchers.Default) .flowOn(coroutineDispatchers.computation)
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
@ -1009,33 +1010,27 @@ class TimelineViewModel @AssistedInject constructor(
* Returns the index of event in the timeline. * Returns the index of event in the timeline.
* Returns Int.MAX_VALUE if not found * 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) } setState { copy(unreadState = UnreadState.HasNoUnread) }
viewModelScope.launch { tryOrNull { room.await().readService().markAsRead(ReadService.MarkAsReadParams.BOTH) }
tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.BOTH) }
}
} }
private fun handleReportContent(action: RoomDetailAction.ReportContent) { private suspend fun handleReportContent(action: RoomDetailAction.ReportContent) {
viewModelScope.launch {
val event = try { val event = try {
room.reportingService().reportContent(action.eventId, -100, action.reason) room.await().reportingService().reportContent(action.eventId, -100, action.reason)
RoomDetailViewEvents.ActionSuccess(action) RoomDetailViewEvents.ActionSuccess(action)
} catch (failure: Exception) { } catch (failure: Exception) {
RoomDetailViewEvents.ActionFailure(action, failure) RoomDetailViewEvents.ActionFailure(action, failure)
} }
_viewEvents.post(event) _viewEvents.post(event)
} }
}
private fun handleIgnoreUser(action: RoomDetailAction.IgnoreUser) { private suspend fun handleIgnoreUser(action: RoomDetailAction.IgnoreUser) {
if (action.userId.isNullOrEmpty()) { if (action.userId.isNullOrEmpty()) {
return return
} }
viewModelScope.launch {
val event = try { val event = try {
session.userService().ignoreUserIds(listOf(action.userId)) session.userService().ignoreUserIds(listOf(action.userId))
RoomDetailViewEvents.ActionSuccess(action) RoomDetailViewEvents.ActionSuccess(action)
@ -1044,14 +1039,13 @@ class TimelineViewModel @AssistedInject constructor(
} }
_viewEvents.post(event) _viewEvents.post(event)
} }
}
private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) { 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( if (session.cryptoService().verificationService().readyPendingVerificationInDMs(
supportedVerificationMethodsProvider.provide(), supportedVerificationMethodsProvider.provide(),
action.otherUserId, action.otherUserId,
room.roomId, initialState.roomId,
action.transactionId action.transactionId
)) { )) {
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
@ -1064,7 +1058,7 @@ class TimelineViewModel @AssistedInject constructor(
session.cryptoService().verificationService().declineVerificationRequestInDMs( session.cryptoService().verificationService().declineVerificationRequestInDMs(
action.otherUserId, action.otherUserId,
action.transactionId, action.transactionId,
room.roomId initialState.roomId
) )
} }
@ -1075,7 +1069,7 @@ class TimelineViewModel @AssistedInject constructor(
private fun handleResumeRequestVerification(action: RoomDetailAction.ResumeVerification) { private fun handleResumeRequestVerification(action: RoomDetailAction.ResumeVerification) {
// Check if this request is still active and handled by me // 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.handledByOtherSession) return
if (!it.isFinished) { if (!it.isFinished) {
_viewEvents.post( _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 // 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) session.cryptoService().reRequestRoomKeyForEvent(it.root)
_viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.e2e_re_request_encryption_key_dialog_content))) _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.e2e_re_request_encryption_key_dialog_content)))
} }
} }
private fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) { private suspend fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) {
room.getTimelineEvent(action.eventId)?.let { room.await().getTimelineEvent(action.eventId)?.let {
val code = when (it.root.mCryptoError) { val code = when (it.root.mCryptoError) {
MXCryptoError.ErrorType.KEYS_WITHHELD -> { MXCryptoError.ErrorType.KEYS_WITHHELD -> {
WithHeldCode.fromCode(it.root.mCryptoErrorReason) 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 // Do not allow to vote unsent local echo of the poll event
if (LocalEcho.isLocalEchoId(action.eventId)) return if (LocalEcho.isLocalEchoId(action.eventId)) return
// Do not allow to vote the same option twice // 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 val currentVote = pollTimelineEvent.annotations?.pollResponseSummary?.aggregatedContent?.myVote
if (currentVote != action.optionKey) { if (currentVote != action.optionKey) {
room.sendService().voteToPoll(action.eventId, action.optionKey) room.await().sendService().voteToPoll(action.eventId, action.optionKey)
} }
} }
} }
private fun handleEndPoll(eventId: String) { private suspend fun handleEndPoll(eventId: String) {
room.sendService().endPoll(eventId) room.await().sendService().endPoll(eventId)
} }
private fun observeSyncState() { private fun observeSyncState() {
@ -1141,17 +1135,15 @@ class TimelineViewModel @AssistedInject constructor(
} }
} }
private fun handleStopLiveLocationSharing() { private suspend fun handleStopLiveLocationSharing() {
viewModelScope.launch { val result = stopLiveLocationShareUseCase.execute(initialState.roomId)
val result = stopLiveLocationShareUseCase.execute(room.roomId)
if (result is UpdateLiveLocationShareResult.Failure) { if (result is UpdateLiveLocationShareResult.Failure) {
_viewEvents.post(RoomDetailViewEvents.Failure(throwable = result.error, showInDialog = true)) _viewEvents.post(RoomDetailViewEvents.Failure(throwable = result.error, showInDialog = true))
} }
} }
}
private fun observeRoomSummary() { private suspend fun observeRoomSummary() {
room.flow().liveRoomSummary() room.await().flow().liveRoomSummary()
.unwrap() .unwrap()
.execute { async -> .execute { async ->
copy( copy(
@ -1160,12 +1152,14 @@ class TimelineViewModel @AssistedInject constructor(
} }
} }
private fun getUnreadState() { private suspend fun getUnreadState() {
combine( combine(
timelineEvents, timelineEvents,
room.flow().liveRoomSummary().unwrap() room.await().flow().liveRoomSummary().unwrap()
) { timelineEvents, roomSummary -> ) { timelineEvents, roomSummary ->
withContext(coroutineDispatchers.computation) {
computeUnreadState(timelineEvents, roomSummary) computeUnreadState(timelineEvents, roomSummary)
}
} }
// We don't want live update of unread so we skip when we already had a HasUnread or HasNoUnread // 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, // 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<TimelineEvent>, roomSummary: RoomSummary): UnreadState { private suspend fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState {
if (events.isEmpty()) return UnreadState.Unknown if (events.isEmpty()) return UnreadState.Unknown
val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown
val timeline = timeline.await()
val firstDisplayableEventIndex = timeline.getIndexOfEvent(readMarkerIdSnapshot) val firstDisplayableEventIndex = timeline.getIndexOfEvent(readMarkerIdSnapshot)
?: return if (timeline.isLive) { ?: return if (timeline.isLive) {
UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
@ -1247,7 +1242,7 @@ class TimelineViewModel @AssistedInject constructor(
setState { copy(asyncInviter = Success(it)) } 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) } 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 * in the snapshot. The main reason for this function is to support the /relations api
*/ */
private var threadPermalinkHandled = false private var threadPermalinkHandled = false
private fun navigateToThreadEventIfNeeded(snapshot: List<TimelineEvent>) { private suspend fun navigateToThreadEventIfNeeded(snapshot: List<TimelineEvent>) {
if (eventId != null && initialState.rootThreadEventId != null) { if (eventId != null && initialState.rootThreadEventId != null) {
// When we have a permalink and we are in a thread timeline // When we have a permalink and we are in a thread timeline
if (snapshot.firstOrNull { it.eventId == eventId } != null && !threadPermalinkHandled) { if (snapshot.firstOrNull { it.eventId == eventId } != null && !threadPermalinkHandled) {
@ -1267,7 +1262,7 @@ class TimelineViewModel @AssistedInject constructor(
threadPermalinkHandled = true threadPermalinkHandled = true
} else { } else {
// Permalink event not found yet continue paginating // 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,9 +1277,11 @@ class TimelineViewModel @AssistedInject constructor(
override fun onTimelineFailure(throwable: Throwable) { override fun onTimelineFailure(throwable: Throwable) {
// If we have a critical timeline issue, we get back to live. // If we have a critical timeline issue, we get back to live.
timeline.restartWithEventId(null) viewModelScope.launch {
timeline.await().restartWithEventId(null)
_viewEvents.post(RoomDetailViewEvents.Failure(throwable)) _viewEvents.post(RoomDetailViewEvents.Failure(throwable))
} }
}
override fun onNewTimelineEvents(eventIds: List<String>) { override fun onNewTimelineEvents(eventIds: List<String>) {
Timber.v("On new timeline events: $eventIds") Timber.v("On new timeline events: $eventIds")
@ -1306,11 +1303,13 @@ class TimelineViewModel @AssistedInject constructor(
} }
override fun onCleared() { override fun onCleared() {
timeline.dispose() decryptionFailureTracker.onTimeLineDisposed(initialState.roomId)
timeline.removeAllListeners() session.coroutineScope.launch {
decryptionFailureTracker.onTimeLineDisposed(room.roomId) timeline.await().dispose()
timeline.await().removeAllListeners()
if (vectorPreferences.sendTypingNotifs()) { if (vectorPreferences.sendTypingNotifs()) {
room.typingService().userStopsTyping() room.await().typingService().userStopsTyping()
}
} }
chatEffectManager.delegate = null chatEffectManager.delegate = null
chatEffectManager.dispose() chatEffectManager.dispose()

View File

@ -23,6 +23,7 @@ import dagger.assisted.AssistedInject
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory 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.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.analytics.AnalyticsTracker 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.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences 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.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
@ -71,7 +74,7 @@ import org.matrix.android.sdk.flow.unwrap
import timber.log.Timber import timber.log.Timber
class MessageComposerViewModel @AssistedInject constructor( class MessageComposerViewModel @AssistedInject constructor(
@Assisted initialState: MessageComposerViewState, @Assisted private val initialState: MessageComposerViewState,
private val session: Session, private val session: Session,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
@ -79,20 +82,26 @@ class MessageComposerViewModel @AssistedInject constructor(
private val rainbowGenerator: RainbowGenerator, private val rainbowGenerator: RainbowGenerator,
private val audioMessageHelper: AudioMessageHelper, private val audioMessageHelper: AudioMessageHelper,
private val analyticsTracker: AnalyticsTracker, private val analyticsTracker: AnalyticsTracker,
private val coroutineDispatchers: CoroutineDispatchers,
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) { ) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(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 // Keep it out of state to avoid invalidate being called
private var currentComposerText: CharSequence = "" private var currentComposerText: CharSequence = ""
init { init {
viewModelScope.launch {
loadDraftIfAny() loadDraftIfAny()
observePowerLevelAndEncryption() observePowerLevelAndEncryption()
}
subscribeToStateInternal() subscribeToStateInternal()
} }
override fun handle(action: MessageComposerAction) { override fun handle(action: MessageComposerAction) {
viewModelScope.launch {
when (action) { when (action) {
is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action) is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action)
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
@ -116,6 +125,7 @@ class MessageComposerViewModel @AssistedInject constructor(
is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action) is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action)
} }
} }
}
private fun handleOnVoiceRecordingUiStateChanged(action: MessageComposerAction.OnVoiceRecordingUiStateChanged) = setState { private fun handleOnVoiceRecordingUiStateChanged(action: MessageComposerAction.OnVoiceRecordingUiStateChanged) = setState {
copy(voiceRecordingUiState = action.uiState) copy(voiceRecordingUiState = action.uiState)
@ -148,13 +158,14 @@ class MessageComposerViewModel @AssistedInject constructor(
copy(sendMode = SendMode.Regular(action.text, action.fromSharing)) copy(sendMode = SendMode.Regular(action.text, action.fromSharing))
} }
private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) { private suspend fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) {
room.getTimelineEvent(action.eventId)?.let { timelineEvent -> room.await().getTimelineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.Edit(timelineEvent, timelineEvent.getTextEditableContent())) } setState { copy(sendMode = SendMode.Edit(timelineEvent, timelineEvent.getTextEditableContent())) }
} }
} }
private fun observePowerLevelAndEncryption() { private suspend fun observePowerLevelAndEncryption() {
val room = room.await()
combine( combine(
PowerLevelsFlowFactory(room).createFlow(), PowerLevelsFlowFactory(room).createFlow(),
room.flow().liveRoomSummary().unwrap() room.flow().liveRoomSummary().unwrap()
@ -180,23 +191,22 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) { private suspend fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) {
room.getTimelineEvent(action.eventId)?.let { timelineEvent -> room.await().getTimelineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.Quote(timelineEvent, action.text)) } setState { copy(sendMode = SendMode.Quote(timelineEvent, action.text)) }
} }
} }
private fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) { private suspend fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) {
room.getTimelineEvent(action.eventId)?.let { timelineEvent -> room.await().getTimelineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.Reply(timelineEvent, action.text)) } setState { copy(sendMode = SendMode.Reply(timelineEvent, action.text)) }
} }
} }
private fun handleSendMessage(action: MessageComposerAction.SendMessage) { private suspend fun handleSendMessage(action: MessageComposerAction.SendMessage) {
withState { state -> val state = awaitState()
analyticsTracker.capture(state.toAnalyticsComposer()).also { analyticsTracker.capture(state.toAnalyticsComposer())
setState { copy(startsThread = false) } setState { copy(startsThread = false) }
}
when (state.sendMode) { when (state.sendMode) {
is SendMode.Regular -> { is SendMode.Regular -> {
when (val parsedCommand = commandParser.parseSlashCommand( when (val parsedCommand = commandParser.parseSlashCommand(
@ -206,13 +216,13 @@ class MessageComposerViewModel @AssistedInject constructor(
is ParsedCommand.ErrorNotACommand -> { is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room // Send the text message to the room
if (state.rootThreadEventId != null) { if (state.rootThreadEventId != null) {
room.relationService().replyInThread( room.await().relationService().replyInThread(
rootThreadEventId = state.rootThreadEventId, rootThreadEventId = state.rootThreadEventId,
replyInThreadText = action.text, replyInThreadText = action.text,
autoMarkdown = action.autoMarkdown autoMarkdown = action.autoMarkdown
) )
} else { } else {
room.sendService().sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) room.await().sendService().sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
} }
_viewEvents.post(MessageComposerViewEvents.MessageSent) _viewEvents.post(MessageComposerViewEvents.MessageSent)
@ -233,13 +243,13 @@ class MessageComposerViewModel @AssistedInject constructor(
is ParsedCommand.SendPlainText -> { is ParsedCommand.SendPlainText -> {
// Send the text message to the room, without markdown // Send the text message to the room, without markdown
if (state.rootThreadEventId != null) { if (state.rootThreadEventId != null) {
room.relationService().replyInThread( room.await().relationService().replyInThread(
rootThreadEventId = state.rootThreadEventId, rootThreadEventId = state.rootThreadEventId,
replyInThreadText = parsedCommand.message, replyInThreadText = parsedCommand.message,
autoMarkdown = false autoMarkdown = false
) )
} else { } else {
room.sendService().sendTextMessage(parsedCommand.message, autoMarkdown = false) room.await().sendService().sendTextMessage(parsedCommand.message, autoMarkdown = false)
} }
_viewEvents.post(MessageComposerViewEvents.MessageSent) _viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft() popDraft()
@ -289,14 +299,14 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
is ParsedCommand.SendEmote -> { is ParsedCommand.SendEmote -> {
if (state.rootThreadEventId != null) { if (state.rootThreadEventId != null) {
room.relationService().replyInThread( room.await().relationService().replyInThread(
rootThreadEventId = state.rootThreadEventId, rootThreadEventId = state.rootThreadEventId,
replyInThreadText = parsedCommand.message, replyInThreadText = parsedCommand.message,
msgType = MessageType.MSGTYPE_EMOTE, msgType = MessageType.MSGTYPE_EMOTE,
autoMarkdown = action.autoMarkdown autoMarkdown = action.autoMarkdown
) )
} else { } else {
room.sendService().sendTextMessage( room.await().sendService().sendTextMessage(
text = parsedCommand.message, text = parsedCommand.message,
msgType = MessageType.MSGTYPE_EMOTE, msgType = MessageType.MSGTYPE_EMOTE,
autoMarkdown = action.autoMarkdown autoMarkdown = action.autoMarkdown
@ -308,13 +318,13 @@ class MessageComposerViewModel @AssistedInject constructor(
is ParsedCommand.SendRainbow -> { is ParsedCommand.SendRainbow -> {
val message = parsedCommand.message.toString() val message = parsedCommand.message.toString()
if (state.rootThreadEventId != null) { if (state.rootThreadEventId != null) {
room.relationService().replyInThread( room.await().relationService().replyInThread(
rootThreadEventId = state.rootThreadEventId, rootThreadEventId = state.rootThreadEventId,
replyInThreadText = parsedCommand.message, replyInThreadText = parsedCommand.message,
formattedText = rainbowGenerator.generate(message) formattedText = rainbowGenerator.generate(message)
) )
} else { } else {
room.sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message)) room.await().sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message))
} }
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft()
@ -322,14 +332,14 @@ class MessageComposerViewModel @AssistedInject constructor(
is ParsedCommand.SendRainbowEmote -> { is ParsedCommand.SendRainbowEmote -> {
val message = parsedCommand.message.toString() val message = parsedCommand.message.toString()
if (state.rootThreadEventId != null) { if (state.rootThreadEventId != null) {
room.relationService().replyInThread( room.await().relationService().replyInThread(
rootThreadEventId = state.rootThreadEventId, rootThreadEventId = state.rootThreadEventId,
replyInThreadText = parsedCommand.message, replyInThreadText = parsedCommand.message,
msgType = MessageType.MSGTYPE_EMOTE, msgType = MessageType.MSGTYPE_EMOTE,
formattedText = rainbowGenerator.generate(message) formattedText = rainbowGenerator.generate(message)
) )
} else { } else {
room.sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message), MessageType.MSGTYPE_EMOTE) room.await().sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message), MessageType.MSGTYPE_EMOTE)
} }
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
@ -339,13 +349,13 @@ class MessageComposerViewModel @AssistedInject constructor(
val text = "[${stringProvider.getString(R.string.spoiler)}](${parsedCommand.message})" val text = "[${stringProvider.getString(R.string.spoiler)}](${parsedCommand.message})"
val formattedText = "<span data-mx-spoiler>${parsedCommand.message}</span>" val formattedText = "<span data-mx-spoiler>${parsedCommand.message}</span>"
if (state.rootThreadEventId != null) { if (state.rootThreadEventId != null) {
room.relationService().replyInThread( room.await().relationService().replyInThread(
rootThreadEventId = state.rootThreadEventId, rootThreadEventId = state.rootThreadEventId,
replyInThreadText = text, replyInThreadText = text,
formattedText = formattedText formattedText = formattedText
) )
} else { } else {
room.sendService().sendFormattedTextMessage( room.await().sendService().sendFormattedTextMessage(
text, text,
formattedText formattedText
) )
@ -389,8 +399,8 @@ class MessageComposerViewModel @AssistedInject constructor(
popDraft() popDraft()
} }
is ParsedCommand.DiscardSession -> { is ParsedCommand.DiscardSession -> {
if (room.roomCryptoService().isEncrypted()) { if (room.await().roomCryptoService().isEncrypted()) {
session.cryptoService().discardOutboundSession(room.roomId) session.cryptoService().discardOutboundSession(room.await().roomId)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft()
} else { } else {
@ -403,7 +413,7 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
is ParsedCommand.CreateSpace -> { is ParsedCommand.CreateSpace -> {
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch(Dispatchers.IO) { withContext(coroutineDispatchers.io) {
try { try {
val params = CreateSpaceParams().apply { val params = CreateSpaceParams().apply {
name = parsedCommand.name name = parsedCommand.name
@ -423,15 +433,14 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
} }
} }
Unit
} }
is ParsedCommand.AddToSpace -> { is ParsedCommand.AddToSpace -> {
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch(Dispatchers.IO) { withContext(coroutineDispatchers.io) {
try { try {
session.spaceService().getSpace(parsedCommand.spaceId) session.spaceService().getSpace(parsedCommand.spaceId)
?.addChildren( ?.addChildren(
room.roomId, room.await().roomId,
null, null,
null, null,
false false
@ -442,11 +451,10 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
} }
} }
Unit
} }
is ParsedCommand.JoinSpace -> { is ParsedCommand.JoinSpace -> {
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch(Dispatchers.IO) { withContext(coroutineDispatchers.io) {
try { try {
session.spaceService().joinSpace(parsedCommand.spaceIdOrAlias) session.spaceService().joinSpace(parsedCommand.spaceIdOrAlias)
popDraft() popDraft()
@ -455,10 +463,9 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
} }
} }
Unit
} }
is ParsedCommand.LeaveRoom -> { is ParsedCommand.LeaveRoom -> {
viewModelScope.launch(Dispatchers.IO) { withContext(coroutineDispatchers.io) {
try { try {
session.roomService().leaveRoom(parsedCommand.roomId) session.roomService().leaveRoom(parsedCommand.roomId)
popDraft() popDraft()
@ -467,13 +474,12 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
} }
} }
Unit
} }
is ParsedCommand.UpgradeRoom -> { is ParsedCommand.UpgradeRoom -> {
_viewEvents.post( _viewEvents.post(
MessageComposerViewEvents.ShowRoomUpgradeDialog( MessageComposerViewEvents.ShowRoomUpgradeDialog(
parsedCommand.newVersion, parsedCommand.newVersion,
room.roomSummary()?.isPublic ?: false room.await().awaitRoomSummary()?.isPublic ?: false
) )
) )
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
@ -500,14 +506,14 @@ class MessageComposerViewModel @AssistedInject constructor(
if (inReplyTo != null) { if (inReplyTo != null) {
// TODO check if same content? // TODO check if same content?
room.getTimelineEvent(inReplyTo)?.let { room.await().getTimelineEvent(inReplyTo)?.let {
room.relationService().editReply(state.sendMode.timelineEvent, it, action.text.toString()) room.await().relationService().editReply(state.sendMode.timelineEvent, it, action.text.toString())
} }
} else { } else {
val messageContent = state.sendMode.timelineEvent.getLastMessageContent() val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
val existingBody = messageContent?.body ?: "" val existingBody = messageContent?.body ?: ""
if (existingBody != action.text) { if (existingBody != action.text) {
room.relationService().editTextMessage( room.await().relationService().editTextMessage(
state.sendMode.timelineEvent, state.sendMode.timelineEvent,
messageContent?.msgType ?: MessageType.MSGTYPE_TEXT, messageContent?.msgType ?: MessageType.MSGTYPE_TEXT,
action.text, action.text,
@ -521,7 +527,7 @@ class MessageComposerViewModel @AssistedInject constructor(
popDraft() popDraft()
} }
is SendMode.Quote -> { is SendMode.Quote -> {
room.sendService().sendQuotedTextMessage( room.await().sendService().sendQuotedTextMessage(
quotedEvent = state.sendMode.timelineEvent, quotedEvent = state.sendMode.timelineEvent,
text = action.text.toString(), text = action.text.toString(),
autoMarkdown = action.autoMarkdown, autoMarkdown = action.autoMarkdown,
@ -536,13 +542,13 @@ class MessageComposerViewModel @AssistedInject constructor(
// If threads are disabled this will make the fallback replies visible to clients with threads enabled // 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 val rootThreadEventId = if (showInThread) timelineEvent.root.getRootThreadEventId() else null
state.rootThreadEventId?.let { state.rootThreadEventId?.let {
room.relationService().replyInThread( room.await().relationService().replyInThread(
rootThreadEventId = it, rootThreadEventId = it,
replyInThreadText = action.text.toString(), replyInThreadText = action.text.toString(),
autoMarkdown = action.autoMarkdown, autoMarkdown = action.autoMarkdown,
eventReplied = timelineEvent eventReplied = timelineEvent
) )
} ?: room.relationService().replyToMessage( } ?: room.await().relationService().replyToMessage(
eventReplied = timelineEvent, eventReplied = timelineEvent,
replyText = action.text.toString(), replyText = action.text.toString(),
autoMarkdown = action.autoMarkdown, autoMarkdown = action.autoMarkdown,
@ -558,61 +564,62 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
} }
}
private fun popDraft() = withState { private suspend fun popDraft() = withContext(coroutineDispatchers.computation) {
if (it.sendMode is SendMode.Regular && it.sendMode.fromSharing) { 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 // If we were sharing, we want to get back our last value from draft
loadDraftIfAny() loadDraftIfAny()
} else { } else {
// Otherwise we clear the composer and remove the draft from db // Otherwise we clear the composer and remove the draft from db
setState { copy(sendMode = SendMode.Regular("", false)) } setState { copy(sendMode = SendMode.Regular("", false)) }
viewModelScope.launch { room.await().draftService().deleteDraft()
room.draftService().deleteDraft()
}
} }
} }
private fun loadDraftIfAny() { private suspend fun loadDraftIfAny() {
val currentDraft = room.draftService().getDraft() val currentDraft = withContext(coroutineDispatchers.computation) {
setState { room.await().draftService().getDraft()
copy( }
// Create a sendMode from a draft and retrieve the TimelineEvent val sendMode = when (currentDraft) {
sendMode = when (currentDraft) {
is UserDraft.Regular -> SendMode.Regular(currentDraft.content, false) is UserDraft.Regular -> SendMode.Regular(currentDraft.content, false)
is UserDraft.Quote -> { is UserDraft.Quote -> {
room.getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> room.await().getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
SendMode.Quote(timelineEvent, currentDraft.content) SendMode.Quote(timelineEvent, currentDraft.content)
} }
} }
is UserDraft.Reply -> { is UserDraft.Reply -> {
room.getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> room.await().getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
SendMode.Reply(timelineEvent, currentDraft.content) SendMode.Reply(timelineEvent, currentDraft.content)
} }
} }
is UserDraft.Edit -> { is UserDraft.Edit -> {
room.getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> room.await().getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
SendMode.Edit(timelineEvent, currentDraft.content) SendMode.Edit(timelineEvent, currentDraft.content)
} }
} }
is UserDraft.Voice -> SendMode.Voice(currentDraft.content) is UserDraft.Voice -> SendMode.Voice(currentDraft.content)
else -> null else -> null
} ?: SendMode.Regular("", fromSharing = false) } ?: SendMode.Regular("", fromSharing = false)
setState {
copy(
// Create a sendMode from a draft and retrieve the TimelineEvent
sendMode = sendMode
) )
} }
} }
private fun handleUserIsTyping(action: MessageComposerAction.UserIsTyping) { private suspend fun handleUserIsTyping(action: MessageComposerAction.UserIsTyping) {
if (vectorPreferences.sendTypingNotifs()) { if (vectorPreferences.sendTypingNotifs()) {
if (action.isTyping) { if (action.isTyping) {
room.typingService().userIsTyping() room.await().typingService().userIsTyping()
} else { } 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 message is blank, convert to an emote, with default message
if (sendChatEffect.message.isBlank()) { if (sendChatEffect.message.isBlank()) {
val defaultMessage = stringProvider.getString( val defaultMessage = stringProvider.getString(
@ -621,19 +628,18 @@ class MessageComposerViewModel @AssistedInject constructor(
ChatEffect.SNOWFALL -> R.string.default_message_emote_snow ChatEffect.SNOWFALL -> R.string.default_message_emote_snow
} }
) )
room.sendService().sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE) room.await().sendService().sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE)
} else { } else {
room.sendService().sendTextMessage(sendChatEffect.message, sendChatEffect.chatEffect.toMessageType()) room.await().sendService().sendTextMessage(sendChatEffect.message, sendChatEffect.chatEffect.toMessageType())
} }
} }
private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) { private suspend fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) {
viewModelScope.launch {
try { try {
session.roomService().joinRoom(command.roomAlias, command.reason, emptyList()) session.roomService().joinRoom(command.roomAlias, command.reason, emptyList())
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
return@launch return
} }
session.getRoomSummary(command.roomAlias) session.getRoomSummary(command.roomAlias)
?.also { analyticsTracker.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.SlashCommand)) } ?.also { analyticsTracker.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.SlashCommand)) }
@ -642,7 +648,6 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.JoinRoomCommandSuccess(it)) _viewEvents.post(MessageComposerViewEvents.JoinRoomCommandSuccess(it))
} }
} }
}
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
@ -664,26 +669,26 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { private suspend fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
launchSlashCommandFlowSuspendable(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) { 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) { launchSlashCommandFlowSuspendable(invite) {
room.membershipService().invite3pid(invite.threePid) room.await().membershipService().invite3pid(invite.threePid)
} }
} }
private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { private suspend fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) val newPowerLevelsContent = room.await().getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content ?.content
?.toModel<PowerLevelsContent>() ?.toModel<PowerLevelsContent>()
?.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel) ?.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel)
@ -691,21 +696,21 @@ class MessageComposerViewModel @AssistedInject constructor(
?: return ?: return
launchSlashCommandFlowSuspendable(setUserPowerLevel) { 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) { launchSlashCommandFlowSuspendable(changeDisplayName) {
session.profileService().setDisplayName(session.myUserId, changeDisplayName.displayName) session.profileService().setDisplayName(session.myUserId, changeDisplayName.displayName)
} }
} }
private fun handlePartSlashCommand(command: ParsedCommand.PartRoom) { private suspend fun handlePartSlashCommand(command: ParsedCommand.PartRoom) {
launchSlashCommandFlowSuspendable(command) { launchSlashCommandFlowSuspendable(command) {
if (command.roomAlias == null) { if (command.roomAlias == null) {
// Leave the current room // Leave the current room
room room.await()
} else { } else {
session.getRoomSummary(roomIdOrAlias = command.roomAlias) session.getRoomSummary(roomIdOrAlias = command.roomAlias)
?.roomId ?.roomId
@ -717,65 +722,65 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleRemoveSlashCommand(removeUser: ParsedCommand.RemoveUser) { private suspend fun handleRemoveSlashCommand(removeUser: ParsedCommand.RemoveUser) {
launchSlashCommandFlowSuspendable(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) { 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) { 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) { launchSlashCommandFlowSuspendable(changeRoomName) {
room.stateService().updateName(changeRoomName.name) room.await().stateService().updateName(changeRoomName.name)
} }
} }
private fun getMyRoomMemberContent(): RoomMemberContent? { private suspend fun getMyRoomMemberContent(): RoomMemberContent? {
return room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId)) return room.await().getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId))
?.content ?.content
?.toModel<RoomMemberContent>() ?.toModel<RoomMemberContent>()
} }
private fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) { private suspend fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) {
launchSlashCommandFlowSuspendable(changeDisplayName) { launchSlashCommandFlowSuspendable(changeDisplayName) {
getMyRoomMemberContent() getMyRoomMemberContent()
?.copy(displayName = changeDisplayName.displayName) ?.copy(displayName = changeDisplayName.displayName)
?.toContent() ?.toContent()
?.let { ?.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) { 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) { launchSlashCommandFlowSuspendable(changeAvatar) {
getMyRoomMemberContent() getMyRoomMemberContent()
?.copy(avatarUrl = changeAvatar.url) ?.copy(avatarUrl = changeAvatar.url)
?.toContent() ?.toContent()
?.let { ?.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) { launchSlashCommandFlowSuspendable(ignore) {
session.userService().ignoreUserIds(listOf(ignore.userId)) session.userService().ignoreUserIds(listOf(ignore.userId))
} }
@ -785,14 +790,14 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.SlashCommandConfirmationRequest(unignore)) _viewEvents.post(MessageComposerViewEvents.SlashCommandConfirmationRequest(unignore))
} }
private fun handleSlashCommandConfirmed(action: MessageComposerAction.SlashCommandConfirmed) { private suspend fun handleSlashCommandConfirmed(action: MessageComposerAction.SlashCommandConfirmed) {
when (action.parsedCommand) { when (action.parsedCommand) {
is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(action.parsedCommand) is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(action.parsedCommand)
else -> TODO("Not handled yet") else -> TODO("Not handled yet")
} }
} }
private fun handleUnignoreSlashCommandConfirmed(unignore: ParsedCommand.UnignoreUser) { private suspend fun handleUnignoreSlashCommandConfirmed(unignore: ParsedCommand.UnignoreUser) {
launchSlashCommandFlowSuspendable(unignore) { launchSlashCommandFlowSuspendable(unignore) {
session.userService().unIgnoreUserIds(listOf(unignore.userId)) session.userService().unIgnoreUserIds(listOf(unignore.userId))
} }
@ -802,7 +807,7 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.OpenRoomMemberProfile(whois.userId)) _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 { val sequence = buildString {
append(prefix) append(prefix)
if (message.isNotEmpty()) { if (message.isNotEmpty()) {
@ -811,52 +816,52 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
rootThreadEventId?.let { rootThreadEventId?.let {
room.relationService().replyInThread(it, sequence) room.await().relationService().replyInThread(it, sequence)
} ?: room.sendService().sendTextMessage(sequence) } ?: room.await().sendService().sendTextMessage(sequence)
} }
/** /**
* Convert a send mode to a draft and save the draft. * 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 { session.coroutineScope.launch {
when { when {
it.sendMode is SendMode.Regular && !it.sendMode.fromSharing -> { it.sendMode is SendMode.Regular && !it.sendMode.fromSharing -> {
setState { copy(sendMode = it.sendMode.copy(text = draft)) } 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 -> { it.sendMode is SendMode.Reply -> {
setState { copy(sendMode = it.sendMode.copy(text = draft)) } 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 -> { it.sendMode is SendMode.Quote -> {
setState { copy(sendMode = it.sendMode.copy(text = draft)) } 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 -> { it.sendMode is SendMode.Edit -> {
setState { copy(sendMode = it.sendMode.copy(text = draft)) } 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 { try {
audioMessageHelper.startRecording(room.roomId) audioMessageHelper.startRecording(room.await().roomId)
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure)) _viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
} }
} }
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) { private suspend fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) {
audioMessageHelper.stopPlayback() audioMessageHelper.stopPlayback()
if (isCancelled) { if (isCancelled) {
audioMessageHelper.deleteRecording() audioMessageHelper.deleteRecording()
} else { } else {
audioMessageHelper.stopRecording()?.let { audioType -> audioMessageHelper.stopRecording()?.let { audioType ->
if (audioType.duration > 1000) { if (audioType.duration > 1000) {
room.sendService().sendMedia( room.await().sendService().sendMedia(
attachment = audioType.toContentAttachmentData(isVoiceMessage = true), attachment = audioType.toContentAttachmentData(isVoiceMessage = true),
compressBeforeSending = false, compressBeforeSending = false,
roomIds = emptySet(), roomIds = emptySet(),
@ -870,8 +875,8 @@ class MessageComposerViewModel @AssistedInject constructor(
handleEnterRegularMode(MessageComposerAction.EnterRegularMode(text = "", fromSharing = false)) handleEnterRegularMode(MessageComposerAction.EnterRegularMode(text = "", fromSharing = false))
} }
private fun handlePlayOrPauseVoicePlayback(action: MessageComposerAction.PlayOrPauseVoicePlayback) { private suspend fun handlePlayOrPauseVoicePlayback(action: MessageComposerAction.PlayOrPauseVoicePlayback) {
viewModelScope.launch(Dispatchers.IO) { withContext(coroutineDispatchers.io) {
try { try {
// Download can fail // Download can fail
val audioFile = session.fileService().downloadFile(action.messageAudioContent) val audioFile = session.fileService().downloadFile(action.messageAudioContent)
@ -913,16 +918,15 @@ class MessageComposerViewModel @AssistedInject constructor(
audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration) 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 // Always stop all voice actions. It may be playing in timeline or active recording
val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false) val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false)
val isVoiceRecording = awaitState().isVoiceRecording
val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording }
if (isVoiceRecording) { if (isVoiceRecording) {
viewModelScope.launch { withContext(coroutineDispatchers.io) {
playingAudioContent?.toContentAttachmentData()?.let { voiceDraft -> playingAudioContent?.toContentAttachmentData()?.let { voiceDraft ->
val content = voiceDraft.toJsonString() val content = voiceDraft.toJsonString()
room.draftService().saveDraft(UserDraft.Voice(content)) room.await().draftService().saveDraft(UserDraft.Voice(content))
setState { copy(sendMode = SendMode.Voice(content)) } setState { copy(sendMode = SendMode.Voice(content)) }
} }
} }
@ -931,9 +935,8 @@ 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) _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch {
val event = try { val event = try {
block() block()
popDraft() popDraft()
@ -943,7 +946,6 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
_viewEvents.post(event) _viewEvents.post(event)
} }
}
@AssistedFactory @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<MessageComposerViewModel, MessageComposerViewState> { interface Factory : MavericksAssistedViewModelFactory<MessageComposerViewModel, MessageComposerViewState> {

View File

@ -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.helper.TimelineSettingsFactory
import im.vector.app.features.home.room.detail.timeline.merged.MergedTimelines import im.vector.app.features.home.room.detail.timeline.merged.MergedTimelines
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import org.matrix.android.sdk.api.session.Session 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.events.model.EventType
import org.matrix.android.sdk.api.session.getRoom 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) { class TimelineFactory @Inject constructor(private val session: Session, private val timelineSettingsFactory: TimelineSettingsFactory) {
fun createTimeline( suspend fun createTimeline(
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
mainRoom: Room, room: Deferred<Room>,
eventId: String?, eventId: String?,
rootThreadEventId: String? rootThreadEventId: String?
): Timeline { ): Timeline {
val settings = timelineSettingsFactory.create(rootThreadEventId) val settings = timelineSettingsFactory.create(rootThreadEventId)
val mainRoom = room.await()
if (!session.vectorCallService.protocolChecker.supportVirtualRooms) { if (!session.vectorCallService.protocolChecker.supportVirtualRooms) {
return mainRoom.timelineService().createTimeline(eventId, settings) return mainRoom.timelineService().createTimeline(eventId, settings)
} }

View File

@ -47,7 +47,7 @@ fun ElementWellKnown?.getOutboundSessionKeySharingStrategyOrDefault(): OutboundS
fun RawService.withElementWellKnown( fun RawService.withElementWellKnown(
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
sessionParams: SessionParams, sessionParams: SessionParams,
block: ((ElementWellKnown?) -> Unit) block: suspend ((ElementWellKnown?) -> Unit)
) = with(coroutineScope) { ) = with(coroutineScope) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
block(getElementWellknown(sessionParams)) block(getElementWellknown(sessionParams))