diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt index 03953e0e29..46145ba6b9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node @@ -29,6 +30,7 @@ import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.MessagesPresenter +import io.element.android.features.messages.impl.MessagesState import io.element.android.features.messages.impl.MessagesView import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor @@ -44,11 +46,11 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.inputs import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -67,22 +69,19 @@ import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking @ContributesNode(RoomScope::class) @AssistedInject class ThreadedMessagesNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val room: JoinedRoom, private val analyticsService: AnalyticsService, - messageComposerPresenterFactory: MessageComposerPresenter.Factory, - timelinePresenterFactory: TimelinePresenter.Factory, - presenterFactory: MessagesPresenter.Factory, - actionListPresenterFactory: ActionListPresenter.Factory, + private val messageComposerPresenterFactory: MessageComposerPresenter.Factory, + private val timelinePresenterFactory: TimelinePresenter.Factory, + private val presenterFactory: MessagesPresenter.Factory, + private val actionListPresenterFactory: ActionListPresenter.Factory, private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val mediaPlayer: MediaPlayer, private val permalinkParser: PermalinkParser, @@ -96,20 +95,29 @@ class ThreadedMessagesNode( private val inputs = inputs() private val callback: Callback = callback() - // TODO use a loading state node to preload this instead of using `runBlocking` - private val threadedTimeline = runBlocking { room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = inputs.threadRootEventId)).getOrThrow() } - private val timelineController = TimelineController(room, threadedTimeline) - private val presenter = presenterFactory.create( - navigator = this, - composerPresenter = messageComposerPresenterFactory.create(timelineController, this), - timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this), - // TODO add special processor for threaded timeline - actionListPresenter = actionListPresenterFactory.create( - postProcessor = TimelineItemActionPostProcessor.Default, - timelineMode = timelineController.mainTimelineMode(), - ), - timelineController = timelineController, - ) + private var timelineController: TimelineController? by mutableStateOf(null) + private var presenter: Presenter? by mutableStateOf(null) + + /** + * This should be fast to load, but not faster than several UI frames, which will cause ANRs. + * We'll load the [presenter] in an async way to prevent this. + */ + private suspend fun createPresenter(): Presenter { + val threadedTimeline = room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = inputs.threadRootEventId)).getOrThrow() + val timelineController = TimelineController(room, threadedTimeline) + this.timelineController = timelineController + return presenterFactory.create( + navigator = this, + composerPresenter = messageComposerPresenterFactory.create(timelineController, this), + timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this), + // TODO add special processor for threaded timeline + actionListPresenter = actionListPresenterFactory.create( + postProcessor = TimelineItemActionPostProcessor.Default, + timelineMode = timelineController.mainTimelineMode(), + ), + timelineController = timelineController, + ) + } interface Callback : Plugin { fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean @@ -130,7 +138,10 @@ class ThreadedMessagesNode( super.onBuilt() lifecycle.subscribe( onCreate = { - sessionCoroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) } + analyticsService.capture(room.toAnalyticsViewRoom()) + lifecycleScope.launch { + presenter = createPresenter() + } }, onStart = { appNavigationStateService.onNavigateToThread(id, inputs.threadRootEventId) @@ -231,56 +242,61 @@ class ThreadedMessagesNode( CompositionLocalProvider( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, ) { - val state = presenter.present() - OnLifecycleEvent { _, event -> - when (event) { - Lifecycle.Event.ON_PAUSE -> state.composerState.eventSink(MessageComposerEvent.SaveDraft) - else -> Unit - } - } - MessagesView( - state = state, - onBackClick = this::navigateUp, - onRoomDetailsClick = {}, - onEventContentClick = { isLive, event -> - if (isLive) { - callback.handleEventClick(timelineController.mainTimelineMode(), event) - } else { - val detachedTimelineMode = timelineController.detachedTimelineMode() - if (detachedTimelineMode != null) { - callback.handleEventClick(detachedTimelineMode, event) - } else { - false - } + // Only display the actual UI and lifecycle logic if the presenter is loaded + presenter?.present()?.let { state -> + OnLifecycleEvent { _, event -> + when (event) { + Lifecycle.Event.ON_PAUSE -> state.composerState.eventSink(MessageComposerEvent.SaveDraft) + else -> Unit } - }, - onUserDataClick = callback::navigateToRoomMemberDetails, - onLinkClick = { url, customTab -> - onLinkClick( - activity = activity, - darkTheme = isDark, - url = url, - eventSink = state.timelineState.eventSink, - customTab = customTab, - ) - }, - onSendLocationClick = callback::navigateToSendLocation, - onCreatePollClick = callback::navigateToCreatePoll, - onJoinCallClick = { callback.navigateToRoomCall(room.roomId) }, - onViewAllPinnedMessagesClick = {}, - modifier = modifier, - knockRequestsBannerView = {}, - ) - - var focusedEventId by rememberSaveable { - mutableStateOf(inputs.focusedEventId) - } - LaunchedEffect(Unit) { - focusedEventId?.also { eventId -> - state.timelineState.eventSink(TimelineEvent.FocusOnEvent(eventId)) } - // Reset the focused event id to null to avoid refocusing when restoring node. - focusedEventId = null + + MessagesView( + state = state, + onBackClick = this::navigateUp, + onRoomDetailsClick = {}, + onEventContentClick = { isLive, event -> + timelineController?.let { controller -> + if (isLive) { + callback.handleEventClick(controller.mainTimelineMode(), event) + } else { + val detachedTimelineMode = controller.detachedTimelineMode() + if (detachedTimelineMode != null) { + callback.handleEventClick(detachedTimelineMode, event) + } else { + false + } + } + } == true + }, + onUserDataClick = callback::navigateToRoomMemberDetails, + onLinkClick = { url, customTab -> + onLinkClick( + activity = activity, + darkTheme = isDark, + url = url, + eventSink = state.timelineState.eventSink, + customTab = customTab, + ) + }, + onSendLocationClick = callback::navigateToSendLocation, + onCreatePollClick = callback::navigateToCreatePoll, + onJoinCallClick = { callback.navigateToRoomCall(room.roomId) }, + onViewAllPinnedMessagesClick = {}, + modifier = modifier, + knockRequestsBannerView = {}, + ) + + var focusedEventId by rememberSaveable { + mutableStateOf(inputs.focusedEventId) + } + LaunchedEffect(Unit) { + focusedEventId?.also { eventId -> + state.timelineState.eventSink(TimelineEvent.FocusOnEvent(eventId)) + } + // Reset the focused event id to null to avoid refocusing when restoring node. + focusedEventId = null + } } } }