From b6deff8a0bd3b937fd2ff975e13235e46e219b28 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 Aug 2022 11:11:27 +0200 Subject: [PATCH 1/7] Remove useless code. Returned value is not used. `observeRoomSummary()` is called a few lines above. --- .../im/vector/app/features/home/room/detail/TimelineViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index cea845a490..5fa23c8f40 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -195,7 +195,6 @@ class TimelineViewModel @AssistedInject constructor( observeActiveRoomWidgets() observePowerLevel() setupPreviewUrlObservers() - room.getRoomSummaryLive() viewModelScope.launch(Dispatchers.IO) { tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } } From 3ed66d636b7c6de94d7d7e055fc26dbe97341988 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 Aug 2022 11:15:32 +0200 Subject: [PATCH 2/7] Rework: avoid shortcut to access ViewModel state. --- .../home/room/detail/TimelineFragment.kt | 26 +++++++++++-------- .../home/room/detail/TimelineViewModel.kt | 2 -- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index b5eb0608d4..9de8f8ee4c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1224,12 +1224,12 @@ class TimelineFragment : } } - private fun handleSearchAction() { + private fun handleSearchAction() = withState(timelineViewModel) { state -> navigator.openSearch( context = requireContext(), roomId = timelineArgs.roomId, - roomDisplayName = timelineViewModel.getRoomSummary()?.displayName, - roomAvatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl + roomDisplayName = state.asyncRoomSummary()?.displayName, + roomAvatarUrl = state.asyncRoomSummary()?.avatarUrl ) } @@ -2520,15 +2520,19 @@ class TimelineFragment : * Navigate to Threads timeline for the specified rootThreadEventId * 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()) context?.let { val roomThreadDetailArgs = ThreadTimelineArgs( startsThread = startsThread, roomId = timelineArgs.roomId, - displayName = timelineViewModel.getRoomSummary()?.displayName, - avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl, - roomEncryptionTrustLevel = timelineViewModel.getRoomSummary()?.roomEncryptionTrustLevel, + displayName = state.asyncRoomSummary()?.displayName, + avatarUrl = state.asyncRoomSummary()?.avatarUrl, + roomEncryptionTrustLevel = state.asyncRoomSummary()?.roomEncryptionTrustLevel, rootThreadEventId = rootThreadEventId, showKeyboard = showKeyboard ) @@ -2559,14 +2563,14 @@ class TimelineFragment : * Navigate to Threads list for the current room * using the ThreadsActivity. */ - private fun navigateToThreadList() { + private fun navigateToThreadList() = withState(timelineViewModel) { state -> analyticsTracker.capture(Interaction.Name.MobileRoomThreadListButton.toAnalyticsInteraction()) context?.let { val roomThreadDetailArgs = ThreadTimelineArgs( roomId = timelineArgs.roomId, - displayName = timelineViewModel.getRoomSummary()?.displayName, - roomEncryptionTrustLevel = timelineViewModel.getRoomSummary()?.roomEncryptionTrustLevel, - avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl + displayName = state.asyncRoomSummary()?.displayName, + roomEncryptionTrustLevel = state.asyncRoomSummary()?.roomEncryptionTrustLevel, + avatarUrl = state.asyncRoomSummary()?.avatarUrl ) navigator.openThreadList(it, roomThreadDetailArgs) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 5fa23c8f40..857902b50c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -402,8 +402,6 @@ class TimelineViewModel @AssistedInject constructor( fun getOtherUserIds() = room.roomSummary()?.otherMemberIds - fun getRoomSummary() = room.roomSummary() - override fun handle(action: RoomDetailAction) { when (action) { is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) From 388cb1648117a8901398072d2739597d17f33db4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 Aug 2022 11:15:58 +0200 Subject: [PATCH 3/7] Remove unused method --- .../vector/app/features/home/room/detail/TimelineViewModel.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 857902b50c..476c9021cd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -400,8 +400,6 @@ class TimelineViewModel @AssistedInject constructor( } } - fun getOtherUserIds() = room.roomSummary()?.otherMemberIds - override fun handle(action: RoomDetailAction) { when (action) { is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) From f56f4e1160af9e6c448f7a95a6b8789ccacb89d1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 Aug 2022 11:58:05 +0200 Subject: [PATCH 4/7] Make room and timeline nullable. Sometimes use `initialState.roomId` instead of `room.roomId`. --- .../home/room/detail/TimelineFragment.kt | 4 +- .../home/room/detail/TimelineViewModel.kt | 112 +++++++++++++----- 2 files changed, 87 insertions(+), 29 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 9de8f8ee4c..c4d29e6819 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -992,9 +992,9 @@ class TimelineFragment : views.jumpToBottomView.debouncedClicks { timelineViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) views.jumpToBottomView.visibility = View.INVISIBLE - if (!timelineViewModel.timeline.isLive) { + if (timelineViewModel.timeline?.isLive == false) { scrollOnNewMessageCallback.forceScrollOnNextUpdate() - timelineViewModel.timeline.restartWithEventId(null) + timelineViewModel.timeline?.restartWithEventId(null) } else { layoutManager.scrollToPosition(0) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 476c9021cd..2ab9ce95dd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -93,6 +93,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.file.FileService 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.getTimelineEvent import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult @@ -143,16 +144,16 @@ class TimelineViewModel @AssistedInject constructor( private val cryptoConfig: CryptoConfig, buildMeta: BuildMeta, timelineFactory: TimelineFactory, - spaceStateHandler: SpaceStateHandler, + private val spaceStateHandler: SpaceStateHandler, ) : VectorViewModel(initialState), 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 invisibleEventsSource = BehaviorDataSource() private val visibleEventsSource = BehaviorDataSource() private var timelineEvents = MutableSharedFlow>(0) - val timeline = timelineFactory.createTimeline(viewModelScope, room, eventId, initialState.rootThreadEventId) + val timeline: Timeline? // Same lifecycle than the ViewModel (survive to screen rotation) val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope, buildMeta) @@ -181,9 +182,20 @@ class TimelineViewModel @AssistedInject constructor( } 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.addListener(this) - observeRoomSummary() observeMembershipChanges() observeSummaryState() getUnreadState() @@ -265,6 +277,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun prepareForEncryption() { + if (room == null) return // check if there is not already a call made, or if there has been an error if (prepareToEncrypt.shouldLoad) { prepareToEncrypt = Loading() @@ -281,6 +294,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun observePowerLevel() { + if (room == null) return PowerLevelsFlowFactory(room).createFlow() .onEach { val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId) @@ -329,6 +343,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun observeMyRoomMember() { + if (room == null) return val queryParams = roomMemberQueryParams { this.userId = QueryStringValue.Equals(session.myUserId, QueryStringValue.Case.SENSITIVE) } @@ -344,6 +359,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun setupPreviewUrlObservers() { + if (room == null) return if (!vectorPreferences.showUrlPreviews()) { return } @@ -372,6 +388,7 @@ class TimelineViewModel @AssistedInject constructor( * This is a local implementation has nothing to do with APIs. */ private fun markThreadTimelineAsReadLocal() { + if (room == null) return initialState.rootThreadEventId?.let { session.coroutineScope.launch { room.threadsLocalService().markThreadAsRead(it) @@ -383,6 +400,7 @@ class TimelineViewModel @AssistedInject constructor( * Observe local unread threads. */ private fun observeLocalThreadNotifications() { + if (room == null) return room.flow() .liveLocalUnreadThreadList() .execute { @@ -514,6 +532,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) { + if (room == null) return viewModelScope.launch(Dispatchers.IO) { try { room.stateService().updateAvatar(action.newAvatarUri, action.newAvatarFileName) @@ -533,11 +552,13 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) { + if (room == null) return room.readService().getUserReadReceipt(action.userId) ?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) } } private fun handleSendSticker(action: RoomDetailAction.SendSticker) { + if (room == null) return val content = initialState.rootThreadEventId?.let { action.stickerContent.copy( relatesTo = RelationDefaultContent( @@ -552,6 +573,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleStartCall(action: RoomDetailAction.StartCall) { + if (room == null) return viewModelScope.launch { room.roomSummary()?.otherMemberIds?.firstOrNull()?.let { callManager.startOutgoingCall(room.roomId, it, action.isVideo) @@ -597,7 +619,7 @@ class TimelineViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) viewModelScope.launch(Dispatchers.IO) { try { - val widget = jitsiService.createJitsiWidget(room.roomId, action.withVideo) + val widget = jitsiService.createJitsiWidget(initialState.roomId, action.withVideo) _viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.withVideo)) } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_add_widget))) @@ -616,7 +638,7 @@ class TimelineViewModel @AssistedInject constructor( } else { _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) } - session.widgetService().destroyRoomWidget(room.roomId, widgetId) + session.widgetService().destroyRoomWidget(initialState.roomId, widgetId) // local echo setState { copy( @@ -665,6 +687,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun stopTrackingUnreadMessages() { + if (room == null) return if (trackUnreadMessages.getAndSet(false)) { mostRecentDisplayedEvent?.root?.eventId?.also { session.coroutineScope.launch { @@ -681,10 +704,11 @@ class TimelineViewModel @AssistedInject constructor( } fun getMember(userId: String): RoomMemberSummary? { - return room.membershipService().getRoomMember(userId) + return room?.membershipService()?.getRoomMember(userId) } private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) { + if (room == null) return // Ensure outbound session keys if (room.roomCryptoService().isEncrypted()) { rawService.withElementWellKnown(viewModelScope, session.sessionParams) { @@ -774,11 +798,12 @@ class TimelineViewModel @AssistedInject constructor( // PRIVATE METHODS ***************************************************************************** private fun handleSendReaction(action: RoomDetailAction.SendReaction) { + if (room == null) return room.relationService().sendReaction(action.targetEventId, action.reaction) } private fun handleRedactEvent(action: RoomDetailAction.RedactAction) { - val event = room.getTimelineEvent(action.targetEventId) ?: return + val event = room?.getTimelineEvent(action.targetEventId) ?: return if (event.isLiveLocation()) { viewModelScope.launch { redactLiveLocationShareEventUseCase.execute(event.root, room, action.reason) @@ -789,6 +814,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleUndoReact(action: RoomDetailAction.UndoReaction) { + if (room == null) return viewModelScope.launch { tryOrNull { room.relationService().undoReaction(action.targetEventId, action.reaction) @@ -797,6 +823,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleUpdateQuickReaction(action: RoomDetailAction.UpdateQuickReactAction) { + if (room == null) return if (action.add) { room.relationService().sendReaction(action.targetEventId, action.selectedReaction) } else { @@ -809,6 +836,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleSendMedia(action: RoomDetailAction.SendMedia) { + if (room == null) return room.sendService().sendMedias( action.attachments, action.compressBeforeSending, @@ -818,6 +846,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) { + if (room == null) return viewModelScope.launch(Dispatchers.Default) { if (action.event.root.sendState.isSent()) { // ignore pending/local events visibleEventsSource.post(action) @@ -845,6 +874,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleLoadMore(action: RoomDetailAction.LoadMoreTimelineEvents) { + if (timeline == null) return timeline.paginate(action.direction, PAGINATION_COUNT) } @@ -852,7 +882,7 @@ class TimelineViewModel @AssistedInject constructor( notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) } viewModelScope.launch { try { - session.roomService().leaveRoom(room.roomId) + session.roomService().leaveRoom(initialState.roomId) } catch (throwable: Throwable) { _viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true)) } @@ -863,7 +893,7 @@ class TimelineViewModel @AssistedInject constructor( notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) } viewModelScope.launch { try { - session.roomService().joinRoom(room.roomId) + session.roomService().joinRoom(initialState.roomId) trackRoomJoined() } catch (throwable: Throwable) { _viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true)) @@ -872,6 +902,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun trackRoomJoined() { + if (room == null) return val trigger = if (initialState.isInviteAlreadyAccepted) { JoinedRoom.Trigger.Invite } else { @@ -929,6 +960,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) { + if (timeline == null) return val targetEventId: String = action.eventId val indexOfEvent = timeline.getIndexOfEvent(targetEventId) if (indexOfEvent == null) { @@ -942,6 +974,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleResendEvent(action: RoomDetailAction.ResendMessage) { + if (room == null) return val targetEventId = action.eventId room.getTimelineEvent(targetEventId)?.let { // State must be UNDELIVERED or Failed @@ -960,6 +993,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleRemove(action: RoomDetailAction.RemoveFailedEcho) { + if (room == null) return val targetEventId = action.eventId room.getTimelineEvent(targetEventId)?.let { // State must be UNDELIVERED or Failed @@ -972,6 +1006,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleCancel(action: RoomDetailAction.CancelSend) { + if (room == null) return if (action.force) { room.sendService().cancelSend(action.eventId) return @@ -988,14 +1023,17 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleResendAll() { + if (room == null) return room.sendService().resendAllFailedMessages() } private fun handleRemoveAllFailedMessages() { + if (room == null) return room.sendService().cancelAllFailedMessages() } private fun observeEventDisplayedActions() { + if (room == null) return // We are buffering scroll events for one second // and keep the most recent one to set the read receipt on. @@ -1027,9 +1065,10 @@ class TimelineViewModel @AssistedInject constructor( * Returns the index of event in the timeline. * Returns Int.MAX_VALUE if not found */ - private fun TimelineEvent.indexOfEvent(): Int = timeline.getIndexOfEvent(eventId) ?: Int.MAX_VALUE + private fun TimelineEvent.indexOfEvent(): Int = timeline?.getIndexOfEvent(eventId) ?: Int.MAX_VALUE private fun handleMarkAllAsRead() { + if (room == null) return setState { copy(unreadState = UnreadState.HasNoUnread) } viewModelScope.launch { tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.BOTH) } @@ -1037,6 +1076,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleReportContent(action: RoomDetailAction.ReportContent) { + if (room == null) return viewModelScope.launch { val event = try { room.reportingService().reportContent(action.eventId, -100, action.reason) @@ -1065,11 +1105,11 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) { - Timber.v("## SAS handleAcceptVerification ${action.otherUserId}, roomId:${room.roomId}, txId:${action.transactionId}") + Timber.v("## SAS handleAcceptVerification ${action.otherUserId}, roomId:${initialState.roomId}, txId:${action.transactionId}") if (session.cryptoService().verificationService().readyPendingVerificationInDMs( supportedVerificationMethodsProvider.provide(), action.otherUserId, - room.roomId, + initialState.roomId, action.transactionId )) { _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) @@ -1082,7 +1122,7 @@ class TimelineViewModel @AssistedInject constructor( session.cryptoService().verificationService().declineVerificationRequestInDMs( action.otherUserId, action.transactionId, - room.roomId + initialState.roomId ) } @@ -1093,7 +1133,7 @@ class TimelineViewModel @AssistedInject constructor( private fun handleResumeRequestVerification(action: RoomDetailAction.ResumeVerification) { // Check if this request is still active and handled by me - session.cryptoService().verificationService().getExistingVerificationRequestInRoom(room.roomId, action.transactionId)?.let { + session.cryptoService().verificationService().getExistingVerificationRequestInRoom(initialState.roomId, action.transactionId)?.let { if (it.handledByOtherSession) return if (!it.isFinished) { _viewEvents.post( @@ -1108,6 +1148,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleReRequestKeys(action: RoomDetailAction.ReRequestKeys) { + if (room == null) return // Check if this request is still active and handled by me room.getTimelineEvent(action.eventId)?.let { session.cryptoService().reRequestRoomKeyForEvent(it.root) @@ -1116,6 +1157,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) { + if (room == null) return room.getTimelineEvent(action.eventId)?.let { val code = when (it.root.mCryptoError) { MXCryptoError.ErrorType.KEYS_WITHHELD -> { @@ -1129,6 +1171,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleVoteToPoll(action: RoomDetailAction.VoteToPoll) { + if (room == null) return // Do not allow to vote unsent local echo of the poll event if (LocalEcho.isLocalEchoId(action.eventId)) return // Do not allow to vote the same option twice @@ -1141,6 +1184,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleEndPoll(eventId: String) { + if (room == null) return room.sendService().endPoll(eventId) } @@ -1160,7 +1204,7 @@ class TimelineViewModel @AssistedInject constructor( private fun handleStopLiveLocationSharing() { viewModelScope.launch { - val result = stopLiveLocationShareUseCase.execute(room.roomId) + val result = stopLiveLocationShareUseCase.execute(initialState.roomId) if (result is UpdateLiveLocationShareResult.Failure) { _viewEvents.post(RoomDetailViewEvents.Failure(throwable = result.error, showInDialog = true)) } @@ -1168,16 +1212,26 @@ class TimelineViewModel @AssistedInject constructor( } private fun observeRoomSummary() { - room.flow().liveRoomSummary() - .unwrap() - .execute { async -> - copy( - asyncRoomSummary = async - ) - } + if (room == null) { + Timber.w("Warning, room with Id ${initialState.roomId} is not found.") + setState { + copy( + asyncRoomSummary = Fail(IllegalStateException("Room Not Found")) + ) + } + } else { + room.flow().liveRoomSummary() + .unwrap() + .execute { async -> + copy( + asyncRoomSummary = async + ) + } + } } private fun getUnreadState() { + if (room == null) return combine( timelineEvents, room.flow().liveRoomSummary().unwrap() @@ -1202,6 +1256,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun computeUnreadState(events: List, roomSummary: RoomSummary): UnreadState { + if (timeline == null) return UnreadState.Unknown if (events.isEmpty()) return UnreadState.Unknown val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown val firstDisplayableEventIndex = timeline.getIndexOfEvent(readMarkerIdSnapshot) @@ -1248,6 +1303,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun observeSummaryState() { + if (room == null) return onAsync(RoomDetailViewState::asyncRoomSummary) { summary -> setState { val typingMessage = typingHelper.getTypingMessage(summary.typingUsers) @@ -1291,6 +1347,7 @@ class TimelineViewModel @AssistedInject constructor( */ private var threadPermalinkHandled = false private fun navigateToThreadEventIfNeeded(snapshot: List) { + if (timeline == null) return if (eventId != null && initialState.rootThreadEventId != null) { // When we have a permalink and we are in a thread timeline if (snapshot.firstOrNull { it.eventId == eventId } != null && !threadPermalinkHandled) { @@ -1313,6 +1370,7 @@ class TimelineViewModel @AssistedInject constructor( } override fun onTimelineFailure(throwable: Throwable) { + if (timeline == null) return // If we have a critical timeline issue, we get back to live. timeline.restartWithEventId(null) _viewEvents.post(RoomDetailViewEvents.Failure(throwable)) @@ -1338,11 +1396,11 @@ class TimelineViewModel @AssistedInject constructor( } override fun onCleared() { - timeline.dispose() - timeline.removeAllListeners() - decryptionFailureTracker.onTimeLineDisposed(room.roomId) + timeline?.dispose() + timeline?.removeAllListeners() + decryptionFailureTracker.onTimeLineDisposed(initialState.roomId) if (vectorPreferences.sendTypingNotifs()) { - room.typingService().userStopsTyping() + room?.typingService()?.userStopsTyping() } chatEffectManager.delegate = null chatEffectManager.dispose() From 1ced47fbd2cfa6515cf0b52b40f0a304d9487fb9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 Aug 2022 15:13:45 +0200 Subject: [PATCH 5/7] Display an error message when the room is not known. --- .../home/room/detail/TimelineFragment.kt | 23 ++++++++++ .../home/room/detail/TimelineViewModel.kt | 3 +- .../home/room/detail/error/RoomNotFound.kt | 19 ++++++++ .../src/main/res/layout/fragment_timeline.xml | 43 +++++++++++++++++++ vector/src/main/res/values/strings.xml | 3 ++ 5 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/error/RoomNotFound.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index c4d29e6819..80e41c9a67 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -62,6 +62,7 @@ import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.addGlidePreloader import com.airbnb.epoxy.glidePreloader +import com.airbnb.mvrx.Fail import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel 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.voice.VoiceMessageRecorderView 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.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction @@ -1640,6 +1642,10 @@ class TimelineFragment : override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState -> invalidateOptionsMenu() + if (mainState.asyncRoomSummary is Fail) { + handleRoomSummaryFailure(mainState.asyncRoomSummary) + return@withState + } val summary = mainState.asyncRoomSummary() renderToolbar(summary) renderTypingMessageNotification(summary, mainState) @@ -1695,6 +1701,23 @@ class TimelineFragment : updateLiveLocationIndicator(mainState.isSharingLiveLocation) } + private fun handleRoomSummaryFailure(asyncRoomSummary: Fail) { + 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) { views.liveLocationStatusIndicator.isVisible = isSharingLiveLocation } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 2ab9ce95dd..8a31b6b958 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -48,6 +48,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy 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.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory @@ -1216,7 +1217,7 @@ class TimelineViewModel @AssistedInject constructor( Timber.w("Warning, room with Id ${initialState.roomId} is not found.") setState { copy( - asyncRoomSummary = Fail(IllegalStateException("Room Not Found")) + asyncRoomSummary = Fail(RoomNotFound()) ) } } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/error/RoomNotFound.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/error/RoomNotFound.kt new file mode 100644 index 0000000000..616f538b37 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/error/RoomNotFound.kt @@ -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() diff --git a/vector/src/main/res/layout/fragment_timeline.xml b/vector/src/main/res/layout/fragment_timeline.xml index d505e23103..f8a31d3281 100644 --- a/vector/src/main/res/layout/fragment_timeline.xml +++ b/vector/src/main/res/layout/fragment_timeline.xml @@ -194,4 +194,47 @@ android:background="?vctr_chat_effect_snow_background" android:visibility="invisible" /> + + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 4d39dcae87..8550cee4a8 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1349,6 +1349,9 @@ Start the system camera instead of the custom camera screen. To continue you need to accept the Terms of this service. + + Sorry, this room has not been found.\nPlease retry later.%s + You added a new session \'%s\', which is requesting encryption keys. A new session is requesting encryption keys.\nSession name: %1$s\nLast seen: %2$s\nIf you didn’t log in on another session, ignore this request. From 642da5a8e83f7532697e3223f26906fb29c260b3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 Aug 2022 15:25:12 +0200 Subject: [PATCH 6/7] Changelog --- changelog.d/6978.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6978.bugfix diff --git a/changelog.d/6978.bugfix b/changelog.d/6978.bugfix new file mode 100644 index 0000000000..878730062b --- /dev/null +++ b/changelog.d/6978.bugfix @@ -0,0 +1 @@ +Fix crash when opening an unknown room From fe42cdc5829a97aa2b43a565bbf3ff246d5f8f81 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 Aug 2022 15:39:19 +0200 Subject: [PATCH 7/7] Fix formatting issue. --- .../vector/app/features/home/room/detail/error/RoomNotFound.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/error/RoomNotFound.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/error/RoomNotFound.kt index 616f538b37..3a8554ae4c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/error/RoomNotFound.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/error/RoomNotFound.kt @@ -16,4 +16,4 @@ package im.vector.app.features.home.room.detail.error -class RoomNotFound: Throwable() +class RoomNotFound : Throwable()