Merge pull request #6978 from vector-im/feature/bma/null_room

Fix crash when opening an unknown room
This commit is contained in:
Benoit Marty 2022-08-31 17:51:13 +02:00 committed by GitHub
commit 318352f1bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 192 additions and 45 deletions

1
changelog.d/6978.bugfix Normal file
View file

@ -0,0 +1 @@
Fix crash when opening an unknown room

View file

@ -62,6 +62,7 @@ import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.epoxy.addGlidePreloader import com.airbnb.epoxy.addGlidePreloader
import com.airbnb.epoxy.glidePreloader import com.airbnb.epoxy.glidePreloader
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -161,6 +162,7 @@ import im.vector.app.features.home.room.detail.composer.SendMode
import im.vector.app.features.home.room.detail.composer.boolean import im.vector.app.features.home.room.detail.composer.boolean
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
import im.vector.app.features.home.room.detail.error.RoomNotFound
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction
@ -992,9 +994,9 @@ class TimelineFragment :
views.jumpToBottomView.debouncedClicks { views.jumpToBottomView.debouncedClicks {
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?.isLive == false) {
scrollOnNewMessageCallback.forceScrollOnNextUpdate() scrollOnNewMessageCallback.forceScrollOnNextUpdate()
timelineViewModel.timeline.restartWithEventId(null) timelineViewModel.timeline?.restartWithEventId(null)
} else { } else {
layoutManager.scrollToPosition(0) layoutManager.scrollToPosition(0)
} }
@ -1224,12 +1226,12 @@ class TimelineFragment :
} }
} }
private fun handleSearchAction() { private fun handleSearchAction() = withState(timelineViewModel) { state ->
navigator.openSearch( navigator.openSearch(
context = requireContext(), context = requireContext(),
roomId = timelineArgs.roomId, roomId = timelineArgs.roomId,
roomDisplayName = timelineViewModel.getRoomSummary()?.displayName, roomDisplayName = state.asyncRoomSummary()?.displayName,
roomAvatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl roomAvatarUrl = state.asyncRoomSummary()?.avatarUrl
) )
} }
@ -1640,6 +1642,10 @@ class TimelineFragment :
override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState -> override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState ->
invalidateOptionsMenu() invalidateOptionsMenu()
if (mainState.asyncRoomSummary is Fail) {
handleRoomSummaryFailure(mainState.asyncRoomSummary)
return@withState
}
val summary = mainState.asyncRoomSummary() val summary = mainState.asyncRoomSummary()
renderToolbar(summary) renderToolbar(summary)
renderTypingMessageNotification(summary, mainState) renderTypingMessageNotification(summary, mainState)
@ -1695,6 +1701,23 @@ class TimelineFragment :
updateLiveLocationIndicator(mainState.isSharingLiveLocation) updateLiveLocationIndicator(mainState.isSharingLiveLocation)
} }
private fun handleRoomSummaryFailure(asyncRoomSummary: Fail<RoomSummary>) {
views.roomNotFound.isVisible = true
views.roomNotFoundText.text = when (asyncRoomSummary.error) {
is RoomNotFound -> {
getString(
R.string.timeline_error_room_not_found,
if (vectorPreferences.developerMode()) {
"\nDeveloper info: $timelineArgs"
} else {
""
}
)
}
else -> errorFormatter.toHumanReadable(asyncRoomSummary.error)
}
}
private fun updateLiveLocationIndicator(isSharingLiveLocation: Boolean) { private fun updateLiveLocationIndicator(isSharingLiveLocation: Boolean) {
views.liveLocationStatusIndicator.isVisible = isSharingLiveLocation views.liveLocationStatusIndicator.isVisible = isSharingLiveLocation
} }
@ -2520,15 +2543,19 @@ class TimelineFragment :
* Navigate to Threads timeline for the specified rootThreadEventId * Navigate to Threads timeline for the specified rootThreadEventId
* 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,
) = withState(timelineViewModel) { state ->
analyticsTracker.capture(Interaction.Name.MobileRoomThreadSummaryItem.toAnalyticsInteraction()) analyticsTracker.capture(Interaction.Name.MobileRoomThreadSummaryItem.toAnalyticsInteraction())
context?.let { context?.let {
val roomThreadDetailArgs = ThreadTimelineArgs( val roomThreadDetailArgs = ThreadTimelineArgs(
startsThread = startsThread, startsThread = startsThread,
roomId = timelineArgs.roomId, roomId = timelineArgs.roomId,
displayName = timelineViewModel.getRoomSummary()?.displayName, displayName = state.asyncRoomSummary()?.displayName,
avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl, avatarUrl = state.asyncRoomSummary()?.avatarUrl,
roomEncryptionTrustLevel = timelineViewModel.getRoomSummary()?.roomEncryptionTrustLevel, roomEncryptionTrustLevel = state.asyncRoomSummary()?.roomEncryptionTrustLevel,
rootThreadEventId = rootThreadEventId, rootThreadEventId = rootThreadEventId,
showKeyboard = showKeyboard showKeyboard = showKeyboard
) )
@ -2559,14 +2586,14 @@ class TimelineFragment :
* Navigate to Threads list for the current room * Navigate to Threads list for the current room
* using the ThreadsActivity. * using the ThreadsActivity.
*/ */
private fun navigateToThreadList() { private fun navigateToThreadList() = withState(timelineViewModel) { state ->
analyticsTracker.capture(Interaction.Name.MobileRoomThreadListButton.toAnalyticsInteraction()) analyticsTracker.capture(Interaction.Name.MobileRoomThreadListButton.toAnalyticsInteraction())
context?.let { context?.let {
val roomThreadDetailArgs = ThreadTimelineArgs( val roomThreadDetailArgs = ThreadTimelineArgs(
roomId = timelineArgs.roomId, roomId = timelineArgs.roomId,
displayName = timelineViewModel.getRoomSummary()?.displayName, displayName = state.asyncRoomSummary()?.displayName,
roomEncryptionTrustLevel = timelineViewModel.getRoomSummary()?.roomEncryptionTrustLevel, roomEncryptionTrustLevel = state.asyncRoomSummary()?.roomEncryptionTrustLevel,
avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl avatarUrl = state.asyncRoomSummary()?.avatarUrl
) )
navigator.openThreadList(it, roomThreadDetailArgs) navigator.openThreadList(it, roomThreadDetailArgs)
} }

View file

@ -48,6 +48,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.createdirect.DirectRoomHelper
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
import im.vector.app.features.home.room.detail.error.RoomNotFound
import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
@ -93,6 +94,7 @@ 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.getRoom
import org.matrix.android.sdk.api.session.room.Room
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
@ -143,16 +145,16 @@ class TimelineViewModel @AssistedInject constructor(
private val cryptoConfig: CryptoConfig, private val cryptoConfig: CryptoConfig,
buildMeta: BuildMeta, buildMeta: BuildMeta,
timelineFactory: TimelineFactory, timelineFactory: TimelineFactory,
spaceStateHandler: SpaceStateHandler, private val spaceStateHandler: SpaceStateHandler,
) : 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 = session.getRoom(initialState.roomId)
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) val timeline: Timeline?
// Same lifecycle than the ViewModel (survive to screen rotation) // Same lifecycle than the ViewModel (survive to screen rotation)
val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope, buildMeta) val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope, buildMeta)
@ -181,9 +183,20 @@ class TimelineViewModel @AssistedInject constructor(
} }
init { init {
// This method will take care of a null room to update the state.
observeRoomSummary()
if (room == null) {
timeline = null
} else {
// Nominal case, we have retrieved the room.
timeline = timelineFactory.createTimeline(viewModelScope, room, eventId, initialState.rootThreadEventId)
initSafe(room, timeline)
}
}
private fun initSafe(room: Room, timeline: Timeline) {
timeline.start(initialState.rootThreadEventId) timeline.start(initialState.rootThreadEventId)
timeline.addListener(this) timeline.addListener(this)
observeRoomSummary()
observeMembershipChanges() observeMembershipChanges()
observeSummaryState() observeSummaryState()
getUnreadState() getUnreadState()
@ -195,7 +208,6 @@ class TimelineViewModel @AssistedInject constructor(
observeActiveRoomWidgets() observeActiveRoomWidgets()
observePowerLevel() observePowerLevel()
setupPreviewUrlObservers() setupPreviewUrlObservers()
room.getRoomSummaryLive()
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) }
} }
@ -266,6 +278,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun prepareForEncryption() { private fun prepareForEncryption() {
if (room == null) return
// 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()
@ -282,6 +295,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun observePowerLevel() { private fun observePowerLevel() {
if (room == null) return
PowerLevelsFlowFactory(room).createFlow() PowerLevelsFlowFactory(room).createFlow()
.onEach { .onEach {
val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId) val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId)
@ -330,6 +344,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun observeMyRoomMember() { private fun observeMyRoomMember() {
if (room == null) return
val queryParams = roomMemberQueryParams { val queryParams = roomMemberQueryParams {
this.userId = QueryStringValue.Equals(session.myUserId, QueryStringValue.Case.SENSITIVE) this.userId = QueryStringValue.Equals(session.myUserId, QueryStringValue.Case.SENSITIVE)
} }
@ -345,6 +360,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun setupPreviewUrlObservers() { private fun setupPreviewUrlObservers() {
if (room == null) return
if (!vectorPreferences.showUrlPreviews()) { if (!vectorPreferences.showUrlPreviews()) {
return return
} }
@ -373,6 +389,7 @@ class TimelineViewModel @AssistedInject constructor(
* This is a local implementation has nothing to do with APIs. * This is a local implementation has nothing to do with APIs.
*/ */
private fun markThreadTimelineAsReadLocal() { private fun markThreadTimelineAsReadLocal() {
if (room == null) return
initialState.rootThreadEventId?.let { initialState.rootThreadEventId?.let {
session.coroutineScope.launch { session.coroutineScope.launch {
room.threadsLocalService().markThreadAsRead(it) room.threadsLocalService().markThreadAsRead(it)
@ -384,6 +401,7 @@ class TimelineViewModel @AssistedInject constructor(
* Observe local unread threads. * Observe local unread threads.
*/ */
private fun observeLocalThreadNotifications() { private fun observeLocalThreadNotifications() {
if (room == null) return
room.flow() room.flow()
.liveLocalUnreadThreadList() .liveLocalUnreadThreadList()
.execute { .execute {
@ -401,10 +419,6 @@ class TimelineViewModel @AssistedInject constructor(
} }
} }
fun getOtherUserIds() = room.roomSummary()?.otherMemberIds
fun getRoomSummary() = room.roomSummary()
override fun handle(action: RoomDetailAction) { override fun handle(action: RoomDetailAction) {
when (action) { when (action) {
is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action)
@ -519,6 +533,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) { private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) {
if (room == null) return
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
room.stateService().updateAvatar(action.newAvatarUri, action.newAvatarFileName) room.stateService().updateAvatar(action.newAvatarUri, action.newAvatarFileName)
@ -538,11 +553,13 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) { private fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) {
if (room == null) return
room.readService().getUserReadReceipt(action.userId) room.readService().getUserReadReceipt(action.userId)
?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) } ?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) }
} }
private fun handleSendSticker(action: RoomDetailAction.SendSticker) { private fun handleSendSticker(action: RoomDetailAction.SendSticker) {
if (room == null) return
val content = initialState.rootThreadEventId?.let { val content = initialState.rootThreadEventId?.let {
action.stickerContent.copy( action.stickerContent.copy(
relatesTo = RelationDefaultContent( relatesTo = RelationDefaultContent(
@ -557,6 +574,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleStartCall(action: RoomDetailAction.StartCall) { private fun handleStartCall(action: RoomDetailAction.StartCall) {
if (room == null) return
viewModelScope.launch { viewModelScope.launch {
room.roomSummary()?.otherMemberIds?.firstOrNull()?.let { room.roomSummary()?.otherMemberIds?.firstOrNull()?.let {
callManager.startOutgoingCall(room.roomId, it, action.isVideo) callManager.startOutgoingCall(room.roomId, it, action.isVideo)
@ -602,7 +620,7 @@ class TimelineViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView) _viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.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)))
@ -621,7 +639,7 @@ class TimelineViewModel @AssistedInject constructor(
} 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(
@ -670,6 +688,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun stopTrackingUnreadMessages() { private fun stopTrackingUnreadMessages() {
if (room == null) return
if (trackUnreadMessages.getAndSet(false)) { if (trackUnreadMessages.getAndSet(false)) {
mostRecentDisplayedEvent?.root?.eventId?.also { mostRecentDisplayedEvent?.root?.eventId?.also {
session.coroutineScope.launch { session.coroutineScope.launch {
@ -686,10 +705,11 @@ class TimelineViewModel @AssistedInject constructor(
} }
fun getMember(userId: String): RoomMemberSummary? { fun getMember(userId: String): RoomMemberSummary? {
return room.membershipService().getRoomMember(userId) return room?.membershipService()?.getRoomMember(userId)
} }
private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) { private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) {
if (room == null) return
// Ensure outbound session keys // Ensure outbound session keys
if (room.roomCryptoService().isEncrypted()) { if (room.roomCryptoService().isEncrypted()) {
rawService.withElementWellKnown(viewModelScope, session.sessionParams) { rawService.withElementWellKnown(viewModelScope, session.sessionParams) {
@ -779,11 +799,12 @@ class TimelineViewModel @AssistedInject constructor(
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun handleSendReaction(action: RoomDetailAction.SendReaction) { private fun handleSendReaction(action: RoomDetailAction.SendReaction) {
if (room == null) return
room.relationService().sendReaction(action.targetEventId, action.reaction) room.relationService().sendReaction(action.targetEventId, action.reaction)
} }
private fun handleRedactEvent(action: RoomDetailAction.RedactAction) { private fun handleRedactEvent(action: RoomDetailAction.RedactAction) {
val event = room.getTimelineEvent(action.targetEventId) ?: return val event = room?.getTimelineEvent(action.targetEventId) ?: return
if (event.isLiveLocation()) { if (event.isLiveLocation()) {
viewModelScope.launch { viewModelScope.launch {
redactLiveLocationShareEventUseCase.execute(event.root, room, action.reason) redactLiveLocationShareEventUseCase.execute(event.root, room, action.reason)
@ -794,6 +815,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleUndoReact(action: RoomDetailAction.UndoReaction) { private fun handleUndoReact(action: RoomDetailAction.UndoReaction) {
if (room == null) return
viewModelScope.launch { viewModelScope.launch {
tryOrNull { tryOrNull {
room.relationService().undoReaction(action.targetEventId, action.reaction) room.relationService().undoReaction(action.targetEventId, action.reaction)
@ -802,6 +824,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleUpdateQuickReaction(action: RoomDetailAction.UpdateQuickReactAction) { private fun handleUpdateQuickReaction(action: RoomDetailAction.UpdateQuickReactAction) {
if (room == null) return
if (action.add) { if (action.add) {
room.relationService().sendReaction(action.targetEventId, action.selectedReaction) room.relationService().sendReaction(action.targetEventId, action.selectedReaction)
} else { } else {
@ -814,6 +837,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleSendMedia(action: RoomDetailAction.SendMedia) { private fun handleSendMedia(action: RoomDetailAction.SendMedia) {
if (room == null) return
room.sendService().sendMedias( room.sendService().sendMedias(
action.attachments, action.attachments,
action.compressBeforeSending, action.compressBeforeSending,
@ -823,6 +847,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) { private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) {
if (room == null) return
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
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)
@ -850,6 +875,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleLoadMore(action: RoomDetailAction.LoadMoreTimelineEvents) { private fun handleLoadMore(action: RoomDetailAction.LoadMoreTimelineEvents) {
if (timeline == null) return
timeline.paginate(action.direction, PAGINATION_COUNT) timeline.paginate(action.direction, PAGINATION_COUNT)
} }
@ -857,7 +883,7 @@ class TimelineViewModel @AssistedInject constructor(
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) } notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) }
viewModelScope.launch { 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))
} }
@ -868,7 +894,7 @@ class TimelineViewModel @AssistedInject constructor(
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) } notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) }
viewModelScope.launch { 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))
@ -877,6 +903,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun trackRoomJoined() { private fun trackRoomJoined() {
if (room == null) return
val trigger = if (initialState.isInviteAlreadyAccepted) { val trigger = if (initialState.isInviteAlreadyAccepted) {
JoinedRoom.Trigger.Invite JoinedRoom.Trigger.Invite
} else { } else {
@ -934,6 +961,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) { private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) {
if (timeline == null) return
val targetEventId: String = action.eventId val targetEventId: String = action.eventId
val indexOfEvent = timeline.getIndexOfEvent(targetEventId) val indexOfEvent = timeline.getIndexOfEvent(targetEventId)
if (indexOfEvent == null) { if (indexOfEvent == null) {
@ -947,6 +975,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleResendEvent(action: RoomDetailAction.ResendMessage) { private fun handleResendEvent(action: RoomDetailAction.ResendMessage) {
if (room == null) return
val targetEventId = action.eventId val targetEventId = action.eventId
room.getTimelineEvent(targetEventId)?.let { room.getTimelineEvent(targetEventId)?.let {
// State must be UNDELIVERED or Failed // State must be UNDELIVERED or Failed
@ -965,6 +994,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleRemove(action: RoomDetailAction.RemoveFailedEcho) { private fun handleRemove(action: RoomDetailAction.RemoveFailedEcho) {
if (room == null) return
val targetEventId = action.eventId val targetEventId = action.eventId
room.getTimelineEvent(targetEventId)?.let { room.getTimelineEvent(targetEventId)?.let {
// State must be UNDELIVERED or Failed // State must be UNDELIVERED or Failed
@ -977,6 +1007,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleCancel(action: RoomDetailAction.CancelSend) { private fun handleCancel(action: RoomDetailAction.CancelSend) {
if (room == null) return
if (action.force) { if (action.force) {
room.sendService().cancelSend(action.eventId) room.sendService().cancelSend(action.eventId)
return return
@ -993,14 +1024,17 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleResendAll() { private fun handleResendAll() {
if (room == null) return
room.sendService().resendAllFailedMessages() room.sendService().resendAllFailedMessages()
} }
private fun handleRemoveAllFailedMessages() { private fun handleRemoveAllFailedMessages() {
if (room == null) return
room.sendService().cancelAllFailedMessages() room.sendService().cancelAllFailedMessages()
} }
private fun observeEventDisplayedActions() { private fun observeEventDisplayedActions() {
if (room == null) return
// We are buffering scroll events for one second // We are buffering scroll events for one second
// and keep the most recent one to set the read receipt on. // and keep the most recent one to set the read receipt on.
@ -1032,9 +1066,10 @@ 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 fun TimelineEvent.indexOfEvent(): Int = timeline?.getIndexOfEvent(eventId) ?: Int.MAX_VALUE
private fun handleMarkAllAsRead() { private fun handleMarkAllAsRead() {
if (room == null) return
setState { copy(unreadState = UnreadState.HasNoUnread) } setState { copy(unreadState = UnreadState.HasNoUnread) }
viewModelScope.launch { viewModelScope.launch {
tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.BOTH) } tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.BOTH) }
@ -1042,6 +1077,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleReportContent(action: RoomDetailAction.ReportContent) { private fun handleReportContent(action: RoomDetailAction.ReportContent) {
if (room == null) return
viewModelScope.launch { viewModelScope.launch {
val event = try { val event = try {
room.reportingService().reportContent(action.eventId, -100, action.reason) room.reportingService().reportContent(action.eventId, -100, action.reason)
@ -1070,11 +1106,11 @@ class TimelineViewModel @AssistedInject constructor(
} }
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))
@ -1087,7 +1123,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
) )
} }
@ -1098,7 +1134,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(
@ -1113,6 +1149,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleReRequestKeys(action: RoomDetailAction.ReRequestKeys) { private fun handleReRequestKeys(action: RoomDetailAction.ReRequestKeys) {
if (room == null) return
// 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.getTimelineEvent(action.eventId)?.let {
session.cryptoService().reRequestRoomKeyForEvent(it.root) session.cryptoService().reRequestRoomKeyForEvent(it.root)
@ -1121,6 +1158,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) { private fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) {
if (room == null) return
room.getTimelineEvent(action.eventId)?.let { room.getTimelineEvent(action.eventId)?.let {
val code = when (it.root.mCryptoError) { val code = when (it.root.mCryptoError) {
MXCryptoError.ErrorType.KEYS_WITHHELD -> { MXCryptoError.ErrorType.KEYS_WITHHELD -> {
@ -1134,6 +1172,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleVoteToPoll(action: RoomDetailAction.VoteToPoll) { private fun handleVoteToPoll(action: RoomDetailAction.VoteToPoll) {
if (room == null) return
// 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
@ -1146,6 +1185,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleEndPoll(eventId: String) { private fun handleEndPoll(eventId: String) {
if (room == null) return
room.sendService().endPoll(eventId) room.sendService().endPoll(eventId)
} }
@ -1165,7 +1205,7 @@ class TimelineViewModel @AssistedInject constructor(
private fun handleStopLiveLocationSharing() { private fun handleStopLiveLocationSharing() {
viewModelScope.launch { viewModelScope.launch {
val result = stopLiveLocationShareUseCase.execute(room.roomId) val result = stopLiveLocationShareUseCase.execute(initialState.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))
} }
@ -1173,16 +1213,26 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun observeRoomSummary() { private fun observeRoomSummary() {
room.flow().liveRoomSummary() if (room == null) {
.unwrap() Timber.w("Warning, room with Id ${initialState.roomId} is not found.")
.execute { async -> setState {
copy( copy(
asyncRoomSummary = async asyncRoomSummary = Fail(RoomNotFound())
) )
} }
} else {
room.flow().liveRoomSummary()
.unwrap()
.execute { async ->
copy(
asyncRoomSummary = async
)
}
}
} }
private fun getUnreadState() { private fun getUnreadState() {
if (room == null) return
combine( combine(
timelineEvents, timelineEvents,
room.flow().liveRoomSummary().unwrap() room.flow().liveRoomSummary().unwrap()
@ -1207,6 +1257,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState { private fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState {
if (timeline == null) return UnreadState.Unknown
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 firstDisplayableEventIndex = timeline.getIndexOfEvent(readMarkerIdSnapshot) val firstDisplayableEventIndex = timeline.getIndexOfEvent(readMarkerIdSnapshot)
@ -1253,6 +1304,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun observeSummaryState() { private fun observeSummaryState() {
if (room == null) return
onAsync(RoomDetailViewState::asyncRoomSummary) { summary -> onAsync(RoomDetailViewState::asyncRoomSummary) { summary ->
setState { setState {
val typingMessage = typingHelper.getTypingMessage(summary.typingUsers) val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
@ -1296,6 +1348,7 @@ class TimelineViewModel @AssistedInject constructor(
*/ */
private var threadPermalinkHandled = false private var threadPermalinkHandled = false
private fun navigateToThreadEventIfNeeded(snapshot: List<TimelineEvent>) { private fun navigateToThreadEventIfNeeded(snapshot: List<TimelineEvent>) {
if (timeline == null) return
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) {
@ -1318,6 +1371,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
override fun onTimelineFailure(throwable: Throwable) { override fun onTimelineFailure(throwable: Throwable) {
if (timeline == null) return
// 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) timeline.restartWithEventId(null)
_viewEvents.post(RoomDetailViewEvents.Failure(throwable)) _viewEvents.post(RoomDetailViewEvents.Failure(throwable))
@ -1343,11 +1397,11 @@ class TimelineViewModel @AssistedInject constructor(
} }
override fun onCleared() { override fun onCleared() {
timeline.dispose() timeline?.dispose()
timeline.removeAllListeners() timeline?.removeAllListeners()
decryptionFailureTracker.onTimeLineDisposed(room.roomId) decryptionFailureTracker.onTimeLineDisposed(initialState.roomId)
if (vectorPreferences.sendTypingNotifs()) { if (vectorPreferences.sendTypingNotifs()) {
room.typingService().userStopsTyping() room?.typingService()?.userStopsTyping()
} }
chatEffectManager.delegate = null chatEffectManager.delegate = null
chatEffectManager.dispose() chatEffectManager.dispose()

View file

@ -0,0 +1,19 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.error
class RoomNotFound : Throwable()

View file

@ -194,4 +194,47 @@
android:background="?vctr_chat_effect_snow_background" android:background="?vctr_chat_effect_snow_background"
android:visibility="invisible" /> android:visibility="invisible" />
<!-- Room not found layout -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/roomNotFound"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?android:colorBackground"
android:elevation="10dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone">
<ImageView
android:id="@+id/roomNotFoundIcon"
android:layout_width="60dp"
android:layout_height="60dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_alert_triangle"
app:layout_constraintBottom_toTopOf="@id/roomNotFoundText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/roomNotFoundText"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:gravity="center"
android:paddingHorizontal="16dp"
android:text="@string/timeline_error_room_not_found"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomNotFoundIcon" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1349,6 +1349,9 @@
<string name="settings_labs_native_camera_summary">Start the system camera instead of the custom camera screen.</string> <string name="settings_labs_native_camera_summary">Start the system camera instead of the custom camera screen.</string>
<string name="widget_integration_review_terms">To continue you need to accept the Terms of this service.</string> <string name="widget_integration_review_terms">To continue you need to accept the Terms of this service.</string>
<!-- Room not found. %s will contain some debug info in developer mode. -->
<string name="timeline_error_room_not_found">Sorry, this room has not been found.\nPlease retry later.%s</string>
<!-- share keys --> <!-- share keys -->
<string name="you_added_a_new_device">You added a new session \'%s\', which is requesting encryption keys.</string> <string name="you_added_a_new_device">You added a new session \'%s\', which is requesting encryption keys.</string>
<string name="you_added_a_new_device_with_info">A new session is requesting encryption keys.\nSession name: %1$s\nLast seen: %2$s\nIf you didnt log in on another session, ignore this request.</string> <string name="you_added_a_new_device_with_info">A new session is requesting encryption keys.\nSession name: %1$s\nLast seen: %2$s\nIf you didnt log in on another session, ignore this request.</string>