Compare commits

...

3 Commits

Author SHA1 Message Date
ganfra 12b681209f Use awaitRoom on Timeline screen 2022-07-06 18:40:15 +02:00
ganfra 2acfce2d20 Use awaitRoomSummary 2022-07-06 18:39:56 +02:00
ganfra ebd491c6f0 Introduce awaitRoom() and awaitRoomSummary() 2022-07-06 18:11:06 +02:00
17 changed files with 915 additions and 863 deletions

View File

@ -65,6 +65,11 @@ interface Room {
*/ */
fun roomSummary(): RoomSummary? fun roomSummary(): RoomSummary?
/**
* Suspending version of [roomSummary] method
*/
suspend fun awaitRoomSummary(): RoomSummary?
/** /**
* Use this room as a Space, if the type is correct. * Use this room as a Space, if the type is correct.
*/ */

View File

@ -92,6 +92,13 @@ interface RoomService {
*/ */
fun getRoom(roomId: String): Room? fun getRoom(roomId: String): Room?
/**
* Suspending version of [getRoom] method.
* @param roomId the roomId to look for.
* @return a room with roomId or null
*/
suspend fun awaitRoom(roomId: String): Room?
/** /**
* Get a roomSummary from a roomId or a room alias. * Get a roomSummary from a roomId or a room alias.
* @param roomIdOrAlias the roomId or the alias of a room to look for. * @param roomIdOrAlias the roomId or the alias of a room to look for.

View File

@ -82,6 +82,10 @@ internal class DefaultRoom(
return roomSummaryDataSource.getRoomSummary(roomId) return roomSummaryDataSource.getRoomSummary(roomId)
} }
override suspend fun awaitRoomSummary(): RoomSummary? {
return roomSummaryDataSource.awaitRoomSummary(roomId)
}
override fun asSpace(): Space? { override fun asSpace(): Space? {
if (roomSummary()?.roomType != RoomType.SPACE) return null if (roomSummary()?.roomType != RoomType.SPACE) return null
return DefaultSpace(this, roomSummaryDataSource, viaParameterFinder) return DefaultSpace(this, roomSummaryDataSource, viaParameterFinder)

View File

@ -82,6 +82,10 @@ internal class DefaultRoomService @Inject constructor(
return roomGetter.getRoom(roomId) return roomGetter.getRoom(roomId)
} }
override suspend fun awaitRoom(roomId: String): Room? {
return roomGetter.awaitRoom(roomId)
}
override fun getExistingDirectRoomWithUser(otherUserId: String): String? { override fun getExistingDirectRoomWithUser(otherUserId: String): String? {
return roomGetter.getDirectRoomWith(otherUserId) return roomGetter.getDirectRoomWith(otherUserId)
} }

View File

@ -17,6 +17,8 @@
package org.matrix.android.sdk.internal.session.room package org.matrix.android.sdk.internal.session.room
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.RealmSessionProvider
@ -30,13 +32,16 @@ import javax.inject.Inject
internal interface RoomGetter { internal interface RoomGetter {
fun getRoom(roomId: String): Room? fun getRoom(roomId: String): Room?
suspend fun awaitRoom(roomId: String): Room?
fun getDirectRoomWith(otherUserId: String): String? fun getDirectRoomWith(otherUserId: String): String?
} }
@SessionScope @SessionScope
internal class DefaultRoomGetter @Inject constructor( internal class DefaultRoomGetter @Inject constructor(
private val realmSessionProvider: RealmSessionProvider, private val realmSessionProvider: RealmSessionProvider,
private val roomFactory: RoomFactory private val roomFactory: RoomFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
) : RoomGetter { ) : RoomGetter {
override fun getRoom(roomId: String): Room? { override fun getRoom(roomId: String): Room? {
@ -45,6 +50,10 @@ internal class DefaultRoomGetter @Inject constructor(
} }
} }
override suspend fun awaitRoom(roomId: String) = withContext(coroutineDispatchers.io) {
getRoom(roomId)
}
override fun getDirectRoomWith(otherUserId: String): String? { override fun getDirectRoomWith(otherUserId: String): String? {
return realmSessionProvider.withRealm { realm -> return realmSessionProvider.withRealm { realm ->
RoomSummaryEntity.where(realm) RoomSummaryEntity.where(realm)

View File

@ -51,7 +51,7 @@ internal class DefaultLeaveRoomTask @Inject constructor(
} }
private suspend fun leaveRoom(roomId: String, reason: String?) { private suspend fun leaveRoom(roomId: String, reason: String?) {
val roomSummary = roomSummaryDataSource.getRoomSummary(roomId) val roomSummary = roomSummaryDataSource.awaitRoomSummary(roomId)
if (roomSummary?.membership?.isActive() == false) { if (roomSummary?.membership?.isActive() == false) {
Timber.v("Room $roomId is not joined so can't be left") Timber.v("Room $roomId is not joined so can't be left")
return return

View File

@ -26,6 +26,8 @@ import com.zhuinden.monarchy.Monarchy
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.kotlin.where import io.realm.kotlin.where
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.query.SpaceFilter import org.matrix.android.sdk.api.query.SpaceFilter
@ -58,6 +60,7 @@ internal class RoomSummaryDataSource @Inject constructor(
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val roomSummaryMapper: RoomSummaryMapper, private val roomSummaryMapper: RoomSummaryMapper,
private val queryStringValueProcessor: QueryStringValueProcessor, private val queryStringValueProcessor: QueryStringValueProcessor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
) { ) {
fun getRoomSummary(roomIdOrAlias: String): RoomSummary? { fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
@ -75,6 +78,10 @@ internal class RoomSummaryDataSource @Inject constructor(
}) })
} }
suspend fun awaitRoomSummary(roomIdOrAlias: String) = withContext(coroutineDispatchers.io) {
getRoomSummary(roomIdOrAlias)
}
fun getRoomSummaryLive(roomId: String): LiveData<Optional<RoomSummary>> { fun getRoomSummaryLive(roomId: String): LiveData<Optional<RoomSummary>> {
val liveData = monarchy.findAllMappedWithChanges( val liveData = monarchy.findAllMappedWithChanges(
{ realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) }, { realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) },

View File

@ -53,7 +53,7 @@ internal class DefaultSpace(
) { ) {
// Find best via // Find best via
val bestVia = viaServers val bestVia = viaServers
?: (spaceSummaryDataSource.getRoomSummary(roomId) ?: (spaceSummaryDataSource.awaitRoomSummary(roomId)
?.takeIf { it.joinRules == RoomJoinRules.RESTRICTED } ?.takeIf { it.joinRules == RoomJoinRules.RESTRICTED }
?.let { ?.let {
// for restricted room, best to take via from users that can invite in the // for restricted room, best to take via from users that can invite in the

View File

@ -246,7 +246,7 @@ internal class DefaultSpaceService @Inject constructor(
// and if client want to bypass, it could use sendStateEvent directly? // and if client want to bypass, it could use sendStateEvent directly?
if (canonical) { if (canonical) {
// check that we can send m.child in the parent room // check that we can send m.child in the parent room
if (roomSummaryDataSource.getRoomSummary(parentSpaceId)?.membership != Membership.JOIN) { if (roomSummaryDataSource.awaitRoomSummary(parentSpaceId)?.membership != Membership.JOIN) {
throw UnsupportedOperationException("Cannot add canonical child if not member of parent") throw UnsupportedOperationException("Cannot add canonical child if not member of parent")
} }
val powerLevelsEvent = stateEventDataSource.getStateEvent( val powerLevelsEvent = stateEventDataSource.getStateEvent(

View File

@ -58,7 +58,7 @@ class CallUserMapper(private val session: Session, private val protocolsChecker:
protocolsChecker.awaitCheckProtocols() protocolsChecker.awaitCheckProtocols()
if (!protocolsChecker.supportVirtualRooms) return if (!protocolsChecker.supportVirtualRooms) return
val invitedRoom = session.getRoom(invitedRoomId) ?: return val invitedRoom = session.getRoom(invitedRoomId) ?: return
val inviterId = invitedRoom.roomSummary()?.inviterId ?: return val inviterId = invitedRoom.awaitRoomSummary()?.inviterId ?: return
val nativeLookup = session.sipNativeLookup(inviterId).firstOrNull() ?: return val nativeLookup = session.sipNativeLookup(inviterId).firstOrNull() ?: return
if (nativeLookup.fields.containsKey("is_virtual")) { if (nativeLookup.fields.containsKey("is_virtual")) {
val nativeUser = nativeLookup.userId val nativeUser = nativeLookup.userId

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

@ -294,7 +294,7 @@ class RoomListViewModel @AssistedInject constructor(
session.getRoom(action.roomId)?.let { room -> session.getRoom(action.roomId)?.let { room ->
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
if (room.roomSummary()?.hasTag(action.tag) == false) { if (room.awaitRoomSummary()?.hasTag(action.tag) == false) {
// Favorite and low priority tags are exclusive, so maybe delete the other tag first // Favorite and low priority tags are exclusive, so maybe delete the other tag first
action.tag.otherTag() action.tag.otherTag()
?.takeIf { room.roomSummary()?.hasTag(it).orFalse() } ?.takeIf { room.roomSummary()?.hasTag(it).orFalse() }

View File

@ -122,7 +122,7 @@ class InvitesAcceptor @Inject constructor(
// if we got 404 on invites, the inviting user have left or the hs is off. // if we got 404 on invites, the inviting user have left or the hs is off.
if (failure is Failure.ServerError && failure.httpCode == 404) { if (failure is Failure.ServerError && failure.httpCode == 404) {
val room = getRoom(roomId) ?: return val room = getRoom(roomId) ?: return
val inviterId = room.roomSummary()?.inviterId val inviterId = room.awaitRoomSummary()?.inviterId
// if the inviting user is on the same HS, there can only be one cause: they left, so we try to reject the invite. // if the inviting user is on the same HS, there can only be one cause: they left, so we try to reject the invite.
if (inviterId?.endsWith(sessionParams.credentials.homeServer.orEmpty()).orFalse()) { if (inviterId?.endsWith(sessionParams.credentials.homeServer.orEmpty()).orFalse()) {
shouldRejectRoomIds.add(roomId) shouldRejectRoomIds.add(roomId)

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))