Merge pull request #3597 from element-hq/feature/fga/timeline_better_jump_to_behaviours
Timeline better jump to behaviours
This commit is contained in:
commit
c274665440
26 changed files with 232 additions and 156 deletions
|
|
@ -46,7 +46,6 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationPresenter
|
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
|
|
@ -91,7 +90,6 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
private val composerPresenter: MessageComposerPresenter,
|
private val composerPresenter: MessageComposerPresenter,
|
||||||
private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter,
|
private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter,
|
||||||
timelinePresenterFactory: TimelinePresenter.Factory,
|
timelinePresenterFactory: TimelinePresenter.Factory,
|
||||||
private val typingNotificationPresenter: TypingNotificationPresenter,
|
|
||||||
private val actionListPresenterFactory: ActionListPresenter.Factory,
|
private val actionListPresenterFactory: ActionListPresenter.Factory,
|
||||||
private val customReactionPresenter: CustomReactionPresenter,
|
private val customReactionPresenter: CustomReactionPresenter,
|
||||||
private val reactionSummaryPresenter: ReactionSummaryPresenter,
|
private val reactionSummaryPresenter: ReactionSummaryPresenter,
|
||||||
|
|
@ -125,7 +123,6 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
val composerState = composerPresenter.present()
|
val composerState = composerPresenter.present()
|
||||||
val voiceMessageComposerState = voiceMessageComposerPresenter.present()
|
val voiceMessageComposerState = voiceMessageComposerPresenter.present()
|
||||||
val timelineState = timelinePresenter.present()
|
val timelineState = timelinePresenter.present()
|
||||||
val typingNotificationState = typingNotificationPresenter.present()
|
|
||||||
val actionListState = actionListPresenter.present()
|
val actionListState = actionListPresenter.present()
|
||||||
val customReactionState = customReactionPresenter.present()
|
val customReactionState = customReactionPresenter.present()
|
||||||
val reactionSummaryState = reactionSummaryPresenter.present()
|
val reactionSummaryState = reactionSummaryPresenter.present()
|
||||||
|
|
@ -216,7 +213,6 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
userEventPermissions = userEventPermissions,
|
userEventPermissions = userEventPermissions,
|
||||||
voiceMessageComposerState = voiceMessageComposerState,
|
voiceMessageComposerState = voiceMessageComposerState,
|
||||||
timelineState = timelineState,
|
timelineState = timelineState,
|
||||||
typingNotificationState = typingNotificationState,
|
|
||||||
actionListState = actionListState,
|
actionListState = actionListState,
|
||||||
customReactionState = customReactionState,
|
customReactionState = customReactionState,
|
||||||
reactionSummaryState = reactionSummaryState,
|
reactionSummaryState = reactionSummaryState,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import io.element.android.features.messages.impl.timeline.TimelineState
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
|
@ -33,7 +32,6 @@ data class MessagesState(
|
||||||
val composerState: MessageComposerState,
|
val composerState: MessageComposerState,
|
||||||
val voiceMessageComposerState: VoiceMessageComposerState,
|
val voiceMessageComposerState: VoiceMessageComposerState,
|
||||||
val timelineState: TimelineState,
|
val timelineState: TimelineState,
|
||||||
val typingNotificationState: TypingNotificationState,
|
|
||||||
val actionListState: ActionListState,
|
val actionListState: ActionListState,
|
||||||
val customReactionState: CustomReactionState,
|
val customReactionState: CustomReactionState,
|
||||||
val reactionSummaryState: ReactionSummaryState,
|
val reactionSummaryState: ReactionSummaryState,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
|
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState
|
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState
|
||||||
|
|
@ -122,7 +121,6 @@ fun aMessagesState(
|
||||||
userEventPermissions = userEventPermissions,
|
userEventPermissions = userEventPermissions,
|
||||||
composerState = composerState,
|
composerState = composerState,
|
||||||
voiceMessageComposerState = voiceMessageComposerState,
|
voiceMessageComposerState = voiceMessageComposerState,
|
||||||
typingNotificationState = aTypingNotificationState(),
|
|
||||||
timelineState = timelineState,
|
timelineState = timelineState,
|
||||||
readReceiptBottomSheetState = readReceiptBottomSheetState,
|
readReceiptBottomSheetState = readReceiptBottomSheetState,
|
||||||
actionListState = actionListState,
|
actionListState = actionListState,
|
||||||
|
|
|
||||||
|
|
@ -379,7 +379,6 @@ private fun MessagesViewContent(
|
||||||
val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberExitOnScrollBehavior()
|
val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberExitOnScrollBehavior()
|
||||||
TimelineView(
|
TimelineView(
|
||||||
state = state.timelineState,
|
state = state.timelineState,
|
||||||
typingNotificationState = state.typingNotificationState,
|
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onMessageClick = onMessageClick,
|
onMessageClick = onMessageClick,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import io.element.android.features.messages.impl.crypto.sendfailure.resolve.Reso
|
||||||
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState
|
||||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter
|
||||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||||
|
import io.element.android.features.messages.impl.typing.TypingNotificationPresenter
|
||||||
|
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
|
||||||
|
|
@ -25,4 +27,7 @@ interface MessagesModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
fun bindResolveVerifiedUserSendFailurePresenter(presenter: ResolveVerifiedUserSendFailurePresenter): Presenter<ResolveVerifiedUserSendFailureState>
|
fun bindResolveVerifiedUserSendFailurePresenter(presenter: ResolveVerifiedUserSendFailurePresenter): Presenter<ResolveVerifiedUserSendFailureState>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindTypingNotificationPresenter(presenter: TypingNotificationPresenter): Presenter<TypingNotificationState>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
|
@ -44,6 +45,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
|
@ -87,7 +89,12 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
||||||
userHasPermissionToSendReaction = false,
|
userHasPermissionToSendReaction = false,
|
||||||
isCallOngoing = false,
|
isCallOngoing = false,
|
||||||
// don't compute this value or the pin icon will be shown
|
// don't compute this value or the pin icon will be shown
|
||||||
pinnedEventIds = emptyList()
|
pinnedEventIds = emptyList(),
|
||||||
|
typingNotificationState = TypingNotificationState(
|
||||||
|
renderTypingNotifications = false,
|
||||||
|
typingMembers = persistentListOf(),
|
||||||
|
reserveSpace = false,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,29 +8,39 @@
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.libraries.di.RoomScope
|
|
||||||
import io.element.android.libraries.di.SingleIn
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SingleIn(RoomScope::class)
|
|
||||||
class TimelineItemIndexer @Inject constructor() {
|
class TimelineItemIndexer @Inject constructor() {
|
||||||
|
// This is a latch to wait for the first process call
|
||||||
|
private val firstProcessLatch = CompletableDeferred<Unit>()
|
||||||
private val timelineEventsIndexes = mutableMapOf<EventId, Int>()
|
private val timelineEventsIndexes = mutableMapOf<EventId, Int>()
|
||||||
|
|
||||||
fun isKnown(eventId: EventId): Boolean {
|
private val mutex = Mutex()
|
||||||
return timelineEventsIndexes.containsKey(eventId).also {
|
|
||||||
Timber.d("$eventId isKnown = $it")
|
suspend fun isKnown(eventId: EventId): Boolean {
|
||||||
|
firstProcessLatch.await()
|
||||||
|
return mutex.withLock {
|
||||||
|
timelineEventsIndexes.containsKey(eventId).also {
|
||||||
|
Timber.d("$eventId isKnown = $it")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun indexOf(eventId: EventId): Int {
|
suspend fun indexOf(eventId: EventId): Int {
|
||||||
return (timelineEventsIndexes[eventId] ?: -1).also {
|
firstProcessLatch.await()
|
||||||
Timber.d("indexOf $eventId= $it")
|
return mutex.withLock {
|
||||||
|
(timelineEventsIndexes[eventId] ?: -1).also {
|
||||||
|
Timber.d("indexOf $eventId= $it")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun process(timelineItems: List<TimelineItem>) {
|
suspend fun process(timelineItems: List<TimelineItem>) = mutex.withLock {
|
||||||
Timber.d("process ${timelineItems.size} items")
|
Timber.d("process ${timelineItems.size} items")
|
||||||
timelineEventsIndexes.clear()
|
timelineEventsIndexes.clear()
|
||||||
timelineItems.forEachIndexed { index, timelineItem ->
|
timelineItems.forEachIndexed { index, timelineItem ->
|
||||||
|
|
@ -46,6 +56,7 @@ class TimelineItemIndexer @Inject constructor() {
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
firstProcessLatch.complete(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processEvent(event: TimelineItem.Event, index: Int) {
|
private fun processEvent(event: TimelineItem.Event, index: Int) {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import io.element.android.features.messages.impl.timeline.factories.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
import io.element.android.features.poll.api.actions.EndPollAction
|
import io.element.android.features.poll.api.actions.EndPollAction
|
||||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||||
|
|
@ -54,12 +55,12 @@ import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
const val FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS = 200L
|
const val FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS = 200L
|
||||||
|
|
||||||
class TimelinePresenter @AssistedInject constructor(
|
class TimelinePresenter @AssistedInject constructor(
|
||||||
timelineItemsFactoryCreator: TimelineItemsFactory.Creator,
|
timelineItemsFactoryCreator: TimelineItemsFactory.Creator,
|
||||||
private val timelineItemIndexer: TimelineItemIndexer,
|
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val appScope: CoroutineScope,
|
private val appScope: CoroutineScope,
|
||||||
|
|
@ -69,7 +70,9 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
private val endPollAction: EndPollAction,
|
private val endPollAction: EndPollAction,
|
||||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
private val timelineController: TimelineController,
|
private val timelineController: TimelineController,
|
||||||
|
private val timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
|
||||||
private val resolveVerifiedUserSendFailurePresenter: Presenter<ResolveVerifiedUserSendFailureState>,
|
private val resolveVerifiedUserSendFailurePresenter: Presenter<ResolveVerifiedUserSendFailureState>,
|
||||||
|
private val typingNotificationPresenter: Presenter<TypingNotificationState>,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -87,13 +90,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): TimelineState {
|
override fun present(): TimelineState {
|
||||||
val localScope = rememberCoroutineScope()
|
val localScope = rememberCoroutineScope()
|
||||||
val focusRequestState: MutableState<FocusRequestState> = remember {
|
var focusRequestState: FocusRequestState by remember { mutableStateOf(FocusRequestState.None) }
|
||||||
mutableStateOf(FocusRequestState.None)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
timelineItemsFactory.timelineItems.collect { timelineItems = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
|
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
|
||||||
|
|
||||||
|
|
@ -152,13 +149,13 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
navigator.onEditPollClick(event.pollStartId)
|
navigator.onEditPollClick(event.pollStartId)
|
||||||
}
|
}
|
||||||
is TimelineEvents.FocusOnEvent -> {
|
is TimelineEvents.FocusOnEvent -> {
|
||||||
focusRequestState.value = FocusRequestState.Requested(event.eventId, event.debounce)
|
focusRequestState = FocusRequestState.Requested(event.eventId, event.debounce)
|
||||||
}
|
}
|
||||||
is TimelineEvents.OnFocusEventRender -> {
|
is TimelineEvents.OnFocusEventRender -> {
|
||||||
focusRequestState.value = focusRequestState.value.onFocusEventRender()
|
focusRequestState = focusRequestState.onFocusEventRender()
|
||||||
}
|
}
|
||||||
is TimelineEvents.ClearFocusRequestState -> {
|
is TimelineEvents.ClearFocusRequestState -> {
|
||||||
focusRequestState.value = FocusRequestState.None
|
focusRequestState = FocusRequestState.None
|
||||||
}
|
}
|
||||||
is TimelineEvents.JumpToLive -> {
|
is TimelineEvents.JumpToLive -> {
|
||||||
timelineController.focusOnLive()
|
timelineController.focusOnLive()
|
||||||
|
|
@ -171,49 +168,14 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(focusRequestState.value) {
|
|
||||||
when (val currentFocusRequestState = focusRequestState.value) {
|
|
||||||
is FocusRequestState.Requested -> {
|
|
||||||
delay(currentFocusRequestState.debounce)
|
|
||||||
if (timelineItemIndexer.isKnown(currentFocusRequestState.eventId)) {
|
|
||||||
val index = timelineItemIndexer.indexOf(currentFocusRequestState.eventId)
|
|
||||||
focusRequestState.value = FocusRequestState.Success(eventId = currentFocusRequestState.eventId, index = index)
|
|
||||||
} else {
|
|
||||||
focusRequestState.value = FocusRequestState.Loading(eventId = currentFocusRequestState.eventId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is FocusRequestState.Loading -> {
|
|
||||||
val eventId = currentFocusRequestState.eventId
|
|
||||||
timelineController.focusOnEvent(eventId)
|
|
||||||
.fold(
|
|
||||||
onSuccess = {
|
|
||||||
focusRequestState.value = FocusRequestState.Success(eventId = eventId)
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
focusRequestState.value = FocusRequestState.Failure(throwable = it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(timelineItems.size) {
|
|
||||||
computeNewItemState(timelineItems, prevMostRecentItemId, newEventState)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(timelineItems.size, focusRequestState.value) {
|
|
||||||
val currentFocusRequestState = focusRequestState.value
|
|
||||||
if (currentFocusRequestState is FocusRequestState.Success && !currentFocusRequestState.isIndexed) {
|
|
||||||
val eventId = currentFocusRequestState.eventId
|
|
||||||
if (timelineItemIndexer.isKnown(eventId)) {
|
|
||||||
val index = timelineItemIndexer.indexOf(eventId)
|
|
||||||
focusRequestState.value = FocusRequestState.Success(eventId = eventId, index = index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
timelineItemsFactory.timelineItems
|
||||||
|
.onEach { newTimelineItems ->
|
||||||
|
timelineItemIndexer.process(newTimelineItems)
|
||||||
|
timelineItems = newTimelineItems
|
||||||
|
}
|
||||||
|
.launchIn(this)
|
||||||
|
|
||||||
combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState ->
|
combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState ->
|
||||||
timelineItemsFactory.replaceWith(
|
timelineItemsFactory.replaceWith(
|
||||||
timelineItems = items,
|
timelineItems = items,
|
||||||
|
|
@ -225,7 +187,49 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
val timelineRoomInfo by remember {
|
LaunchedEffect(focusRequestState) {
|
||||||
|
Timber.d("## focusRequestState: $focusRequestState")
|
||||||
|
when (val currentFocusRequestState = focusRequestState) {
|
||||||
|
is FocusRequestState.Requested -> {
|
||||||
|
delay(currentFocusRequestState.debounce)
|
||||||
|
if (timelineItemIndexer.isKnown(currentFocusRequestState.eventId)) {
|
||||||
|
val index = timelineItemIndexer.indexOf(currentFocusRequestState.eventId)
|
||||||
|
focusRequestState = FocusRequestState.Success(eventId = currentFocusRequestState.eventId, index = index)
|
||||||
|
} else {
|
||||||
|
focusRequestState = FocusRequestState.Loading(eventId = currentFocusRequestState.eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FocusRequestState.Loading -> {
|
||||||
|
val eventId = currentFocusRequestState.eventId
|
||||||
|
timelineController.focusOnEvent(eventId)
|
||||||
|
.onSuccess {
|
||||||
|
focusRequestState = FocusRequestState.Success(eventId = eventId)
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
focusRequestState = FocusRequestState.Failure(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(timelineItems.size) {
|
||||||
|
computeNewItemState(timelineItems, prevMostRecentItemId, newEventState)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(timelineItems.size, focusRequestState) {
|
||||||
|
val currentFocusRequestState = focusRequestState
|
||||||
|
if (currentFocusRequestState is FocusRequestState.Success && !currentFocusRequestState.rendered) {
|
||||||
|
val eventId = currentFocusRequestState.eventId
|
||||||
|
if (timelineItemIndexer.isKnown(eventId)) {
|
||||||
|
val index = timelineItemIndexer.indexOf(eventId)
|
||||||
|
focusRequestState = FocusRequestState.Success(eventId = eventId, index = index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val typingNotificationState = typingNotificationPresenter.present()
|
||||||
|
val timelineRoomInfo by remember(typingNotificationState) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
TimelineRoomInfo(
|
TimelineRoomInfo(
|
||||||
name = room.displayName,
|
name = room.displayName,
|
||||||
|
|
@ -234,6 +238,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
||||||
isCallOngoing = roomInfo?.hasRoomCall.orFalse(),
|
isCallOngoing = roomInfo?.hasRoomCall.orFalse(),
|
||||||
pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(),
|
pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(),
|
||||||
|
typingNotificationState = typingNotificationState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -243,7 +248,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
newEventState = newEventState.value,
|
newEventState = newEventState.value,
|
||||||
isLive = isLive,
|
isLive = isLive,
|
||||||
focusRequestState = focusRequestState.value,
|
focusRequestState = focusRequestState,
|
||||||
messageShield = messageShield.value,
|
messageShield = messageShield.value,
|
||||||
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
|
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
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.UniqueId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
@ -29,8 +31,13 @@ data class TimelineState(
|
||||||
val resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState,
|
val resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState,
|
||||||
val eventSink: (TimelineEvents) -> Unit,
|
val eventSink: (TimelineEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val hasAnyEvent = timelineItems.any { it is TimelineItem.Event }
|
private val lastTimelineEvent = timelineItems.firstOrNull { it is TimelineItem.Event } as? TimelineItem.Event
|
||||||
|
val hasAnyEvent = lastTimelineEvent != null
|
||||||
val focusedEventId = focusRequestState.eventId()
|
val focusedEventId = focusRequestState.eventId()
|
||||||
|
|
||||||
|
fun isLastOutgoingMessage(uniqueId: UniqueId): Boolean {
|
||||||
|
return isLive && lastTimelineEvent != null && lastTimelineEvent.isMine && lastTimelineEvent.id == uniqueId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -67,5 +74,6 @@ data class TimelineRoomInfo(
|
||||||
val userHasPermissionToSendMessage: Boolean,
|
val userHasPermissionToSendMessage: Boolean,
|
||||||
val userHasPermissionToSendReaction: Boolean,
|
val userHasPermissionToSendReaction: Boolean,
|
||||||
val isCallOngoing: Boolean,
|
val isCallOngoing: Boolean,
|
||||||
val pinnedEventIds: List<EventId>
|
val pinnedEventIds: List<EventId>,
|
||||||
|
val typingNotificationState: TypingNotificationState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
||||||
|
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
|
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -241,6 +243,7 @@ internal fun aTimelineRoomInfo(
|
||||||
isDm: Boolean = false,
|
isDm: Boolean = false,
|
||||||
userHasPermissionToSendMessage: Boolean = true,
|
userHasPermissionToSendMessage: Boolean = true,
|
||||||
pinnedEventIds: List<EventId> = emptyList(),
|
pinnedEventIds: List<EventId> = emptyList(),
|
||||||
|
typingNotificationState: TypingNotificationState = aTypingNotificationState(),
|
||||||
) = TimelineRoomInfo(
|
) = TimelineRoomInfo(
|
||||||
isDm = isDm,
|
isDm = isDm,
|
||||||
name = name,
|
name = name,
|
||||||
|
|
@ -248,4 +251,5 @@ internal fun aTimelineRoomInfo(
|
||||||
userHasPermissionToSendReaction = true,
|
userHasPermissionToSendReaction = true,
|
||||||
isCallOngoing = false,
|
isCallOngoing = false,
|
||||||
pinnedEventIds = pinnedEventIds,
|
pinnedEventIds = pinnedEventIds,
|
||||||
|
typingNotificationState = typingNotificationState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,11 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
|
|
@ -55,25 +57,21 @@ import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationView
|
|
||||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.AlertDialog
|
import io.element.android.libraries.designsystem.components.dialogs.AlertDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.utils.animateScrollToItemCenter
|
||||||
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.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineView(
|
fun TimelineView(
|
||||||
state: TimelineState,
|
state: TimelineState,
|
||||||
typingNotificationState: TypingNotificationState,
|
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
onMessageClick: (TimelineItem.Event) -> Unit,
|
onMessageClick: (TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -131,11 +129,6 @@ fun TimelineView(
|
||||||
reverseLayout = useReverseLayout,
|
reverseLayout = useReverseLayout,
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
if (state.isLive) {
|
|
||||||
item {
|
|
||||||
TypingNotificationView(state = typingNotificationState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items(
|
items(
|
||||||
items = state.timelineItems,
|
items = state.timelineItems,
|
||||||
contentType = { timelineItem -> timelineItem.contentType() },
|
contentType = { timelineItem -> timelineItem.contentType() },
|
||||||
|
|
@ -145,8 +138,7 @@ fun TimelineView(
|
||||||
timelineItem = timelineItem,
|
timelineItem = timelineItem,
|
||||||
timelineRoomInfo = state.timelineRoomInfo,
|
timelineRoomInfo = state.timelineRoomInfo,
|
||||||
renderReadReceipts = state.renderReadReceipts,
|
renderReadReceipts = state.renderReadReceipts,
|
||||||
isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true &&
|
isLastOutgoingMessage = state.isLastOutgoingMessage(timelineItem.identifier()),
|
||||||
state.timelineItems.first().identifier() == timelineItem.identifier(),
|
|
||||||
focusedEventId = state.focusedEventId,
|
focusedEventId = state.focusedEventId,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
|
|
@ -216,6 +208,7 @@ private fun BoxScope.TimelineScrollHelper(
|
||||||
lazyListState.firstVisibleItemIndex < 3 && isLive
|
lazyListState.firstVisibleItemIndex < 3 && isLive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var jumpToLiveHandled by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
fun scrollToBottom() {
|
fun scrollToBottom() {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
|
@ -231,18 +224,22 @@ private fun BoxScope.TimelineScrollHelper(
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
} else {
|
} else {
|
||||||
|
jumpToLiveHandled = false
|
||||||
onJumpToLive()
|
onJumpToLive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(jumpToLiveHandled, isLive) {
|
||||||
|
if (!jumpToLiveHandled && isLive) {
|
||||||
|
lazyListState.scrollToItem(0)
|
||||||
|
jumpToLiveHandled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val latestOnFocusEventRender by rememberUpdatedState(onFocusEventRender)
|
val latestOnFocusEventRender by rememberUpdatedState(onFocusEventRender)
|
||||||
LaunchedEffect(focusRequestState) {
|
LaunchedEffect(focusRequestState) {
|
||||||
if (focusRequestState is FocusRequestState.Success && focusRequestState.isIndexed) {
|
if (focusRequestState is FocusRequestState.Success && focusRequestState.isIndexed && !focusRequestState.rendered) {
|
||||||
if (abs(lazyListState.firstVisibleItemIndex - focusRequestState.index) < 10) {
|
lazyListState.animateScrollToItemCenter(focusRequestState.index)
|
||||||
lazyListState.animateScrollToItem(focusRequestState.index)
|
|
||||||
} else {
|
|
||||||
lazyListState.scrollToItem(focusRequestState.index)
|
|
||||||
}
|
|
||||||
latestOnFocusEventRender()
|
latestOnFocusEventRender()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -323,7 +320,6 @@ internal fun TimelineViewPreview(
|
||||||
),
|
),
|
||||||
focusedEventIndex = 0,
|
focusedEventIndex = 0,
|
||||||
),
|
),
|
||||||
typingNotificationState = aTypingNotificationState(),
|
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
onMessageClick = {},
|
onMessageClick = {},
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPr
|
||||||
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
@ -36,7 +35,6 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview {
|
||||||
timelineItems = items.toImmutableList(),
|
timelineItems = items.toImmutableList(),
|
||||||
messageShield = messageShield,
|
messageShield = messageShield,
|
||||||
),
|
),
|
||||||
typingNotificationState = aTypingNotificationState(),
|
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
onMessageClick = {},
|
onMessageClick = {},
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemTypingNotificationModel
|
||||||
|
import io.element.android.features.messages.impl.typing.TypingNotificationView
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemVirtualRow(
|
fun TimelineItemVirtualRow(
|
||||||
|
|
@ -46,9 +48,15 @@ fun TimelineItemVirtualRow(
|
||||||
latestEventSink(TimelineEvents.LoadMore(virtual.model.direction))
|
latestEventSink(TimelineEvents.LoadMore(virtual.model.direction))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Empty model trick to avoid timeline jumping during forward pagination.
|
||||||
is TimelineItemLastForwardIndicatorModel -> {
|
is TimelineItemLastForwardIndicatorModel -> {
|
||||||
Spacer(modifier = Modifier)
|
Spacer(modifier = Modifier)
|
||||||
}
|
}
|
||||||
|
is TimelineItemTypingNotificationModel -> {
|
||||||
|
TypingNotificationView(
|
||||||
|
state = timelineRoomInfo.typingNotificationState,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ package io.element.android.features.messages.impl.timeline.factories
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
|
||||||
import io.element.android.features.messages.impl.timeline.diff.TimelineItemsCacheInvalidator
|
import io.element.android.features.messages.impl.timeline.diff.TimelineItemsCacheInvalidator
|
||||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory
|
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
|
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
|
||||||
|
|
@ -36,7 +35,6 @@ class TimelineItemsFactory @AssistedInject constructor(
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val virtualItemFactory: TimelineItemVirtualFactory,
|
private val virtualItemFactory: TimelineItemVirtualFactory,
|
||||||
private val timelineItemGrouper: TimelineItemGrouper,
|
private val timelineItemGrouper: TimelineItemGrouper,
|
||||||
private val timelineItemIndexer: TimelineItemIndexer,
|
|
||||||
) {
|
) {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Creator {
|
interface Creator {
|
||||||
|
|
@ -96,7 +94,6 @@ class TimelineItemsFactory @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()
|
val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()
|
||||||
timelineItemIndexer.process(result)
|
|
||||||
this._timelineItems.emit(result)
|
this._timelineItems.emit(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemTypingNotificationModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
|
|
@ -39,6 +40,7 @@ class TimelineItemVirtualFactory @Inject constructor(
|
||||||
timestamp = inner.timestamp
|
timestamp = inner.timestamp
|
||||||
)
|
)
|
||||||
is VirtualTimelineItem.LastForwardIndicator -> TimelineItemLastForwardIndicatorModel
|
is VirtualTimelineItem.LastForwardIndicator -> TimelineItemLastForwardIndicatorModel
|
||||||
|
VirtualTimelineItem.TypingNotification -> TimelineItemTypingNotificationModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.model.virtual
|
||||||
|
|
||||||
|
data object TimelineItemTypingNotificationModel : TimelineItemVirtualModel {
|
||||||
|
override val type: String = "TimelineItemTypingNotificationModel"
|
||||||
|
}
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.typing
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import io.element.android.features.messages.impl.MessagesView
|
|
||||||
import io.element.android.features.messages.impl.aMessagesState
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun MessagesViewWithTypingPreview(
|
|
||||||
@PreviewParameter(TypingNotificationStateForMessagesProvider::class) typingState: TypingNotificationState
|
|
||||||
) = ElementPreview {
|
|
||||||
MessagesView(
|
|
||||||
state = aMessagesState().copy(typingNotificationState = typingState),
|
|
||||||
onBackClick = {},
|
|
||||||
onRoomDetailsClick = {},
|
|
||||||
onEventClick = { false },
|
|
||||||
onUserDataClick = {},
|
|
||||||
onLinkClick = {},
|
|
||||||
onPreviewAttachments = {},
|
|
||||||
onSendLocationClick = {},
|
|
||||||
onCreatePollClick = {},
|
|
||||||
onJoinCallClick = {},
|
|
||||||
onViewAllPinnedMessagesClick = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -40,7 +40,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationPresenter
|
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
||||||
import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper
|
import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
|
||||||
|
|
@ -1048,6 +1048,7 @@ class MessagesPresenterTest {
|
||||||
timelineItemIndexer = TimelineItemIndexer(),
|
timelineItemIndexer = TimelineItemIndexer(),
|
||||||
timelineController = TimelineController(matrixRoom),
|
timelineController = TimelineController(matrixRoom),
|
||||||
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
|
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
|
||||||
|
typingNotificationPresenter = { aTypingNotificationState() },
|
||||||
)
|
)
|
||||||
val timelinePresenterFactory = object : TimelinePresenter.Factory {
|
val timelinePresenterFactory = object : TimelinePresenter.Factory {
|
||||||
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
||||||
|
|
@ -1055,11 +1056,6 @@ class MessagesPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val featureFlagService = FakeFeatureFlagService()
|
val featureFlagService = FakeFeatureFlagService()
|
||||||
val typingNotificationPresenter = TypingNotificationPresenter(
|
|
||||||
room = matrixRoom,
|
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
|
||||||
)
|
|
||||||
|
|
||||||
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
|
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
|
||||||
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
||||||
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
||||||
|
|
@ -1069,7 +1065,6 @@ class MessagesPresenterTest {
|
||||||
composerPresenter = messageComposerPresenter,
|
composerPresenter = messageComposerPresenter,
|
||||||
voiceMessageComposerPresenter = voiceMessageComposerPresenter,
|
voiceMessageComposerPresenter = voiceMessageComposerPresenter,
|
||||||
timelinePresenterFactory = timelinePresenterFactory,
|
timelinePresenterFactory = timelinePresenterFactory,
|
||||||
typingNotificationPresenter = typingNotificationPresenter,
|
|
||||||
actionListPresenterFactory = FakeActionListPresenter.Factory,
|
actionListPresenterFactory = FakeActionListPresenter.Factory,
|
||||||
customReactionPresenter = customReactionPresenter,
|
customReactionPresenter = customReactionPresenter,
|
||||||
reactionSummaryPresenter = reactionSummaryPresenter,
|
reactionSummaryPresenter = reactionSummaryPresenter,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.fixtures
|
package io.element.android.features.messages.impl.fixtures
|
||||||
|
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
||||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory
|
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory
|
||||||
|
|
@ -40,19 +39,16 @@ import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorW
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
||||||
internal fun TestScope.aTimelineItemsFactoryCreator(
|
internal fun TestScope.aTimelineItemsFactoryCreator(): TimelineItemsFactory.Creator {
|
||||||
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
|
|
||||||
): TimelineItemsFactory.Creator {
|
|
||||||
return object : TimelineItemsFactory.Creator {
|
return object : TimelineItemsFactory.Creator {
|
||||||
override fun create(config: TimelineItemsFactoryConfig): TimelineItemsFactory {
|
override fun create(config: TimelineItemsFactoryConfig): TimelineItemsFactory {
|
||||||
return aTimelineItemsFactory(config, timelineItemIndexer)
|
return aTimelineItemsFactory(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun TestScope.aTimelineItemsFactory(
|
internal fun TestScope.aTimelineItemsFactory(
|
||||||
config: TimelineItemsFactoryConfig,
|
config: TimelineItemsFactoryConfig,
|
||||||
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
|
|
||||||
): TimelineItemsFactory {
|
): TimelineItemsFactory {
|
||||||
val timelineEventFormatter = aTimelineEventFormatter()
|
val timelineEventFormatter = aTimelineEventFormatter()
|
||||||
val matrixClient = FakeMatrixClient()
|
val matrixClient = FakeMatrixClient()
|
||||||
|
|
@ -96,7 +92,6 @@ internal fun TestScope.aTimelineItemsFactory(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
timelineItemGrouper = TimelineItemGrouper(),
|
timelineItemGrouper = TimelineItemGrouper(),
|
||||||
timelineItemIndexer = timelineItemIndexer,
|
|
||||||
config = config
|
config = config
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,12 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline
|
||||||
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.UniqueId
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class TimelineItemIndexerTest {
|
class TimelineItemIndexerTest {
|
||||||
@Test
|
@Test
|
||||||
fun `test TimelineItemIndexer`() {
|
fun `test TimelineItemIndexer`() = runTest {
|
||||||
val eventIds = mutableListOf<EventId>()
|
val eventIds = mutableListOf<EventId>()
|
||||||
val data = listOf(
|
val data = listOf(
|
||||||
aTimelineItemEvent().also { eventIds.add(it.eventId!!) },
|
aTimelineItemEvent().also { eventIds.add(it.eventId!!) },
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryC
|
||||||
import io.element.android.features.messages.impl.timeline.components.aCriticalShield
|
import io.element.android.features.messages.impl.timeline.components.aCriticalShield
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.aRedactedMatrixTimeline
|
import io.element.android.features.messages.impl.voicemessages.timeline.aRedactedMatrixTimeline
|
||||||
|
|
@ -503,7 +504,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
assertThat(state.timelineItems).isNotEmpty()
|
assertThat(state.timelineItems).isNotEmpty()
|
||||||
}
|
}
|
||||||
initialState.eventSink.invoke(TimelineEvents.JumpToLive)
|
initialState.eventSink.invoke(TimelineEvents.JumpToLive)
|
||||||
skipItems(1)
|
skipItems(2)
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
// Event stays focused
|
// Event stays focused
|
||||||
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
||||||
|
|
@ -682,6 +683,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
timelineItemIndexer = timelineItemIndexer,
|
timelineItemIndexer = timelineItemIndexer,
|
||||||
timelineController = TimelineController(room),
|
timelineController = TimelineController(room),
|
||||||
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
|
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
|
||||||
|
typingNotificationPresenter = { aTypingNotificationState() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@ import io.element.android.features.messages.impl.timeline.components.aCriticalSh
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
|
||||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
|
@ -139,7 +137,6 @@ class TimelineViewTest {
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimelineView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimelineView(
|
||||||
state: TimelineState,
|
state: TimelineState,
|
||||||
typingNotificationState: TypingNotificationState = aTypingNotificationState(),
|
|
||||||
onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
|
onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(),
|
onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onMessageClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
onMessageClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
|
@ -155,7 +152,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
|
||||||
setSafeContent {
|
setSafeContent {
|
||||||
TimelineView(
|
TimelineView(
|
||||||
state = state,
|
state = state,
|
||||||
typingNotificationState = typingNotificationState,
|
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onMessageClick = onMessageClick,
|
onMessageClick = onMessageClick,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.utils
|
package io.element.android.libraries.designsystem.utils
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.lazy.LazyListLayoutInfo
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
|
@ -35,3 +37,38 @@ fun LazyListState.isScrollingUp(): Boolean {
|
||||||
}
|
}
|
||||||
}.value
|
}.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun LazyListState.animateScrollToItemCenter(index: Int) {
|
||||||
|
fun LazyListLayoutInfo.containerSize(): Int {
|
||||||
|
return if (orientation == Orientation.Vertical) {
|
||||||
|
viewportSize.height
|
||||||
|
} else {
|
||||||
|
viewportSize.width
|
||||||
|
} - beforeContentPadding - afterContentPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LazyListLayoutInfo.resolveItemOffsetToCenter(index: Int): Int? {
|
||||||
|
val itemInfo = visibleItemsInfo.firstOrNull { it.index == index } ?: return null
|
||||||
|
val containerSize = containerSize()
|
||||||
|
val itemSize = itemInfo.size
|
||||||
|
return if (itemSize > containerSize) {
|
||||||
|
itemSize - containerSize / 2
|
||||||
|
} else {
|
||||||
|
-(containerSize() - itemInfo.size) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// await for the first layout.
|
||||||
|
scroll { }
|
||||||
|
layoutInfo.resolveItemOffsetToCenter(index)?.let { offset ->
|
||||||
|
// Item is already visible, just scroll to center.
|
||||||
|
animateScrollToItem(index, offset)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Item is not visible, jump to it...
|
||||||
|
scrollToItem(index)
|
||||||
|
// and then adjust according to the actual item size.
|
||||||
|
layoutInfo.resolveItemOffsetToCenter(index)?.let { offset ->
|
||||||
|
animateScrollToItem(index, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,6 @@ sealed interface VirtualTimelineItem {
|
||||||
val direction: Timeline.PaginationDirection,
|
val direction: Timeline.PaginationDirection,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
) : VirtualTimelineItem
|
) : VirtualTimelineItem
|
||||||
|
|
||||||
|
data object TypingNotification : VirtualTimelineItem
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTim
|
||||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LastForwardIndicatorsPostProcessor
|
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LastForwardIndicatorsPostProcessor
|
||||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor
|
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor
|
||||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor
|
import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor
|
||||||
|
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TypingNotificationPostProcessor
|
||||||
import io.element.android.libraries.matrix.impl.timeline.reply.InReplyToMapper
|
import io.element.android.libraries.matrix.impl.timeline.reply.InReplyToMapper
|
||||||
import io.element.android.libraries.matrix.impl.util.MessageEventContent
|
import io.element.android.libraries.matrix.impl.util.MessageEventContent
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
|
@ -121,6 +122,7 @@ class RustTimeline(
|
||||||
private val roomBeginningPostProcessor = RoomBeginningPostProcessor(mode)
|
private val roomBeginningPostProcessor = RoomBeginningPostProcessor(mode)
|
||||||
private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock)
|
private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock)
|
||||||
private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode)
|
private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode)
|
||||||
|
private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode)
|
||||||
|
|
||||||
private val backPaginationStatus = MutableStateFlow(
|
private val backPaginationStatus = MutableStateFlow(
|
||||||
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PINNED_EVENTS)
|
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PINNED_EVENTS)
|
||||||
|
|
@ -235,6 +237,9 @@ class RustTimeline(
|
||||||
hasMoreToLoadForward = hasMoreToLoadForward
|
hasMoreToLoadForward = hasMoreToLoadForward
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.let { items ->
|
||||||
|
typingNotificationPostProcessor.process(items = items)
|
||||||
|
}
|
||||||
// Keep lastForwardIndicatorsPostProcessor last
|
// Keep lastForwardIndicatorsPostProcessor last
|
||||||
.let { items ->
|
.let { items ->
|
||||||
lastForwardIndicatorsPostProcessor.process(
|
lastForwardIndicatorsPostProcessor.process(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This post processor is responsible for adding a typing notification item to the timeline items when the timeline is in live mode.
|
||||||
|
*/
|
||||||
|
class TypingNotificationPostProcessor(private val mode: Timeline.Mode) {
|
||||||
|
fun process(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
|
||||||
|
return if (mode == Timeline.Mode.LIVE) {
|
||||||
|
buildList {
|
||||||
|
addAll(items)
|
||||||
|
add(
|
||||||
|
MatrixTimelineItem.Virtual(
|
||||||
|
uniqueId = UniqueId("TypingNotification"),
|
||||||
|
virtual = VirtualTimelineItem.TypingNotification
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue