mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 10:25:35 +03:00
Merge pull request #6978 from vector-im/feature/bma/null_room
Fix crash when opening an unknown room
This commit is contained in:
commit
318352f1bd
6 changed files with 192 additions and 45 deletions
1
changelog.d/6978.bugfix
Normal file
1
changelog.d/6978.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix crash when opening an unknown room
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
|
@ -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>
|
||||||
|
|
|
@ -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 didn’t 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 didn’t log in on another session, ignore this request.</string>
|
||||||
|
|
Loading…
Reference in a new issue