Remove runBlocking in ThreadedMessagesNode (#6108)
* Remove `runBlocking` in `ThreadedMessagesNode`. This should help reducing the number of reported ANRs
This commit is contained in:
committed by
GitHub
parent
63f24f0ae1
commit
585ab160ec
@@ -19,6 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.lifecycle.subscribe
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
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.compound.theme.ElementTheme
|
||||||
import io.element.android.features.messages.impl.MessagesNavigator
|
import io.element.android.features.messages.impl.MessagesNavigator
|
||||||
import io.element.android.features.messages.impl.MessagesPresenter
|
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.MessagesView
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
|
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.browser.openUrlInChromeCustomTab
|
||||||
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
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.callback
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||||
import io.element.android.libraries.di.RoomScope
|
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.analytics.toAnalyticsViewRoom
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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 io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class ThreadedMessagesNode(
|
class ThreadedMessagesNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
|
||||||
private val room: JoinedRoom,
|
private val room: JoinedRoom,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
messageComposerPresenterFactory: MessageComposerPresenter.Factory,
|
private val messageComposerPresenterFactory: MessageComposerPresenter.Factory,
|
||||||
timelinePresenterFactory: TimelinePresenter.Factory,
|
private val timelinePresenterFactory: TimelinePresenter.Factory,
|
||||||
presenterFactory: MessagesPresenter.Factory,
|
private val presenterFactory: MessagesPresenter.Factory,
|
||||||
actionListPresenterFactory: ActionListPresenter.Factory,
|
private val actionListPresenterFactory: ActionListPresenter.Factory,
|
||||||
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
||||||
private val mediaPlayer: MediaPlayer,
|
private val mediaPlayer: MediaPlayer,
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
@@ -96,20 +95,29 @@ class ThreadedMessagesNode(
|
|||||||
private val inputs = inputs<Inputs>()
|
private val inputs = inputs<Inputs>()
|
||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
|
|
||||||
// TODO use a loading state node to preload this instead of using `runBlocking`
|
private var timelineController: TimelineController? by mutableStateOf(null)
|
||||||
private val threadedTimeline = runBlocking { room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = inputs.threadRootEventId)).getOrThrow() }
|
private var presenter: Presenter<MessagesState>? by mutableStateOf(null)
|
||||||
private val timelineController = TimelineController(room, threadedTimeline)
|
|
||||||
private val presenter = presenterFactory.create(
|
/**
|
||||||
navigator = this,
|
* This should be fast to load, but not faster than several UI frames, which will cause ANRs.
|
||||||
composerPresenter = messageComposerPresenterFactory.create(timelineController, this),
|
* We'll load the [presenter] in an async way to prevent this.
|
||||||
timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this),
|
*/
|
||||||
// TODO add special processor for threaded timeline
|
private suspend fun createPresenter(): Presenter<MessagesState> {
|
||||||
actionListPresenter = actionListPresenterFactory.create(
|
val threadedTimeline = room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = inputs.threadRootEventId)).getOrThrow()
|
||||||
postProcessor = TimelineItemActionPostProcessor.Default,
|
val timelineController = TimelineController(room, threadedTimeline)
|
||||||
timelineMode = timelineController.mainTimelineMode(),
|
this.timelineController = timelineController
|
||||||
),
|
return presenterFactory.create(
|
||||||
timelineController = timelineController,
|
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 {
|
interface Callback : Plugin {
|
||||||
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
|
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
|
||||||
@@ -130,7 +138,10 @@ class ThreadedMessagesNode(
|
|||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onCreate = {
|
onCreate = {
|
||||||
sessionCoroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) }
|
analyticsService.capture(room.toAnalyticsViewRoom())
|
||||||
|
lifecycleScope.launch {
|
||||||
|
presenter = createPresenter()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onStart = {
|
onStart = {
|
||||||
appNavigationStateService.onNavigateToThread(id, inputs.threadRootEventId)
|
appNavigationStateService.onNavigateToThread(id, inputs.threadRootEventId)
|
||||||
@@ -231,56 +242,61 @@ class ThreadedMessagesNode(
|
|||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
||||||
) {
|
) {
|
||||||
val state = presenter.present()
|
// Only display the actual UI and lifecycle logic if the presenter is loaded
|
||||||
OnLifecycleEvent { _, event ->
|
presenter?.present()?.let { state ->
|
||||||
when (event) {
|
OnLifecycleEvent { _, event ->
|
||||||
Lifecycle.Event.ON_PAUSE -> state.composerState.eventSink(MessageComposerEvent.SaveDraft)
|
when (event) {
|
||||||
else -> Unit
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user