From f56f4e1160af9e6c448f7a95a6b8789ccacb89d1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 Aug 2022 11:58:05 +0200 Subject: [PATCH] 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()