Browse Source

Timeline : let FocusOnEvent be cancellable and refactor a bit focus states.

pull/3037/head
ganfra 3 months ago
parent
commit
75b1c22197
  1. 1
      changelog.d/2876.misc
  2. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt
  3. 57
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
  4. 24
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt
  5. 27
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt
  6. 28
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
  7. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateProvider.kt
  8. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt
  9. 8
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
  10. 5
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt

1
changelog.d/2876.misc

@ -0,0 +1 @@ @@ -0,0 +1 @@
Allow cancelling jump to event in timeline.

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt

@ -23,6 +23,7 @@ sealed interface TimelineEvents { @@ -23,6 +23,7 @@ sealed interface TimelineEvents {
data class OnScrollFinished(val firstIndex: Int) : TimelineEvents
data class FocusOnEvent(val eventId: EventId) : TimelineEvents
data object ClearFocusRequestState : TimelineEvents
data object OnFocusEventRender : TimelineEvents
data object JumpToLive : TimelineEvents
/**

57
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt

@ -76,9 +76,6 @@ class TimelinePresenter @AssistedInject constructor( @@ -76,9 +76,6 @@ class TimelinePresenter @AssistedInject constructor(
@Composable
override fun present(): TimelineState {
val localScope = rememberCoroutineScope()
val focusedEventId: MutableState<EventId?> = rememberSaveable {
mutableStateOf(null)
}
val focusRequestState: MutableState<FocusRequestState> = remember {
mutableStateOf(FocusRequestState.None)
}
@ -139,23 +136,16 @@ class TimelinePresenter @AssistedInject constructor( @@ -139,23 +136,16 @@ class TimelinePresenter @AssistedInject constructor(
navigator.onEditPollClick(event.pollStartId)
}
is TimelineEvents.FocusOnEvent -> localScope.launch {
focusedEventId.value = event.eventId
if (timelineItemIndexer.isKnown(event.eventId)) {
val index = timelineItemIndexer.indexOf(event.eventId)
focusRequestState.value = FocusRequestState.Cached(index)
focusRequestState.value = FocusRequestState.Success(eventId = event.eventId, index = index)
} else {
focusRequestState.value = FocusRequestState.Fetching
timelineController.focusOnEvent(event.eventId)
.fold(
onSuccess = {
focusRequestState.value = FocusRequestState.Fetched
},
onFailure = {
focusRequestState.value = FocusRequestState.Failure(it)
}
)
focusRequestState.value = FocusRequestState.Loading(eventId = event.eventId)
}
}
is TimelineEvents.OnFocusEventRender -> {
focusRequestState.value = focusRequestState.value.onFocusEventRender()
}
is TimelineEvents.ClearFocusRequestState -> {
focusRequestState.value = FocusRequestState.None
}
@ -165,16 +155,33 @@ class TimelinePresenter @AssistedInject constructor( @@ -165,16 +155,33 @@ class TimelinePresenter @AssistedInject constructor(
}
}
LaunchedEffect(focusRequestState.value) {
val currentFocusRequestState = focusRequestState.value
if (currentFocusRequestState 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)
}
)
}
}
LaunchedEffect(timelineItems.size) {
computeNewItemState(timelineItems, prevMostRecentItemId, newEventState)
}
LaunchedEffect(timelineItems.size, focusRequestState.value, focusedEventId.value) {
val currentFocusedEventId = focusedEventId.value
if (focusRequestState.value is FocusRequestState.Fetched && currentFocusedEventId != null) {
if (timelineItemIndexer.isKnown(currentFocusedEventId)) {
val index = timelineItemIndexer.indexOf(currentFocusedEventId)
focusRequestState.value = FocusRequestState.Cached(index)
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)
}
}
}
@ -208,7 +215,6 @@ class TimelinePresenter @AssistedInject constructor( @@ -208,7 +215,6 @@ class TimelinePresenter @AssistedInject constructor(
renderReadReceipts = renderReadReceipts,
newEventState = newEventState.value,
isLive = isLive,
focusedEventId = focusedEventId.value,
focusRequestState = focusRequestState.value,
eventSink = { handleEvents(it) }
)
@ -278,3 +284,10 @@ class TimelinePresenter @AssistedInject constructor( @@ -278,3 +284,10 @@ class TimelinePresenter @AssistedInject constructor(
return null
}
}
private fun FocusRequestState.onFocusEventRender(): FocusRequestState {
return when (this) {
is FocusRequestState.Success -> copy(rendered = true)
else -> this
}
}

24
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt

@ -29,20 +29,36 @@ data class TimelineState( @@ -29,20 +29,36 @@ data class TimelineState(
val renderReadReceipts: Boolean,
val newEventState: NewEventState,
val isLive: Boolean,
val focusedEventId: EventId?,
val focusRequestState: FocusRequestState,
val eventSink: (TimelineEvents) -> Unit,
) {
val hasAnyEvent = timelineItems.any { it is TimelineItem.Event }
val focusedEventId = focusRequestState.eventId()
}
@Immutable
sealed interface FocusRequestState {
data object None : FocusRequestState
data class Cached(val index: Int) : FocusRequestState
data object Fetching : FocusRequestState
data object Fetched : FocusRequestState
data class Loading(val eventId: EventId) : FocusRequestState
data class Success(
val eventId: EventId,
val index: Int = -1,
// This is used to know if the event has been rendered yet.
val rendered: Boolean = false,
) : FocusRequestState {
val isIndexed
get() = index != -1
}
data class Failure(val throwable: Throwable) : FocusRequestState
fun eventId(): EventId? {
return when (this) {
is Loading -> eventId
is Success -> eventId
else -> null
}
}
}
@Immutable

27
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt

@ -51,16 +51,23 @@ fun aTimelineState( @@ -51,16 +51,23 @@ fun aTimelineState(
focusedEventIndex: Int = -1,
isLive: Boolean = true,
eventSink: (TimelineEvents) -> Unit = {},
) = TimelineState(
timelineItems = timelineItems,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
newEventState = NewEventState.None,
isLive = isLive,
focusedEventId = timelineItems.filterIsInstance<TimelineItem.Event>().getOrNull(focusedEventIndex)?.eventId,
focusRequestState = FocusRequestState.None,
eventSink = eventSink,
)
): TimelineState {
val focusedEventId = timelineItems.filterIsInstance<TimelineItem.Event>().getOrNull(focusedEventIndex)?.eventId
val focusRequestState = if (focusedEventId != null) {
FocusRequestState.Success(focusedEventId, focusedEventIndex)
} else {
FocusRequestState.None
}
return TimelineState(
timelineItems = timelineItems,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
newEventState = NewEventState.None,
isLive = isLive,
focusRequestState = focusRequestState,
eventSink = eventSink,
)
}
internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList<TimelineItem> {
return persistentListOf(

28
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt

@ -100,6 +100,14 @@ fun TimelineView( @@ -100,6 +100,14 @@ fun TimelineView(
state.eventSink(TimelineEvents.OnScrollFinished(firstVisibleIndex))
}
fun onFocusEventRender() {
state.eventSink(TimelineEvents.OnFocusEventRender)
}
fun onJumpToLive() {
state.eventSink(TimelineEvents.JumpToLive)
}
val context = LocalContext.current
val lazyListState = rememberLazyListState()
// Disable reverse layout when TalkBack is enabled to avoid incorrect ordering issues seen in the current Compose UI version
@ -167,8 +175,8 @@ fun TimelineView( @@ -167,8 +175,8 @@ fun TimelineView(
isLive = state.isLive,
focusRequestState = state.focusRequestState,
onScrollFinishAt = ::onScrollFinishAt,
onClearFocusRequestState = ::clearFocusRequestState,
onJumpToLive = { state.eventSink(TimelineEvents.JumpToLive) },
onJumpToLive = ::onJumpToLive,
onFocusEventRender = ::onFocusEventRender,
)
}
}
@ -182,9 +190,9 @@ private fun BoxScope.TimelineScrollHelper( @@ -182,9 +190,9 @@ private fun BoxScope.TimelineScrollHelper(
isLive: Boolean,
forceJumpToBottomVisibility: Boolean,
focusRequestState: FocusRequestState,
onClearFocusRequestState: () -> Unit,
onScrollFinishAt: (Int) -> Unit,
onJumpToLive: () -> Unit,
onFocusEventRender: () -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
val isScrollFinished by remember { derivedStateOf { !lazyListState.isScrollInProgress } }
@ -212,15 +220,15 @@ private fun BoxScope.TimelineScrollHelper( @@ -212,15 +220,15 @@ private fun BoxScope.TimelineScrollHelper(
}
}
val latestOnClearFocusRequestState by rememberUpdatedState(onClearFocusRequestState)
val latestOnFocusEventRender by rememberUpdatedState(onFocusEventRender)
LaunchedEffect(focusRequestState) {
if (focusRequestState is FocusRequestState.Cached) {
if (focusRequestState is FocusRequestState.Success && focusRequestState.isIndexed) {
if (abs(lazyListState.firstVisibleItemIndex - focusRequestState.index) < 10) {
lazyListState.animateScrollToItem(focusRequestState.index)
} else {
lazyListState.scrollToItem(focusRequestState.index)
}
latestOnClearFocusRequestState()
latestOnFocusEventRender()
}
}
@ -243,8 +251,8 @@ private fun BoxScope.TimelineScrollHelper( @@ -243,8 +251,8 @@ private fun BoxScope.TimelineScrollHelper(
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 24.dp, bottom = 12.dp),
.align(Alignment.BottomEnd)
.padding(end = 24.dp, bottom = 12.dp),
onClick = { jumpToBottom() },
)
}
@ -271,8 +279,8 @@ private fun JumpToBottomButton( @@ -271,8 +279,8 @@ private fun JumpToBottomButton(
) {
Icon(
modifier = Modifier
.size(24.dp)
.rotate(90f),
.size(24.dp)
.rotate(90f),
imageVector = CompoundIcons.ArrowRight(),
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
)

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateProvider.kt

@ -24,7 +24,9 @@ import io.element.android.libraries.matrix.api.room.errors.FocusEventException @@ -24,7 +24,9 @@ import io.element.android.libraries.matrix.api.room.errors.FocusEventException
open class FocusRequestStateProvider : PreviewParameterProvider<FocusRequestState> {
override val values: Sequence<FocusRequestState>
get() = sequenceOf(
FocusRequestState.Fetching,
FocusRequestState.Loading(
eventId = EventId("\$anEventId"),
),
FocusRequestState.Failure(
FocusEventException.EventNotFound(
eventId = EventId("\$anEventId"),

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt

@ -49,7 +49,7 @@ fun FocusRequestStateView( @@ -49,7 +49,7 @@ fun FocusRequestStateView(
modifier = modifier,
)
}
FocusRequestState.Fetching -> {
is FocusRequestState.Loading -> {
ProgressDialog(
modifier = modifier,
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true),

8
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt

@ -495,11 +495,11 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2" @@ -495,11 +495,11 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID))
awaitItem().also { state ->
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Fetching)
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID))
}
skipItems(2)
awaitItem().also { state ->
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Fetched)
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Success(AN_EVENT_ID))
assertThat(state.timelineItems).isNotEmpty()
}
initialState.eventSink.invoke(TimelineEvents.JumpToLive)
@ -539,7 +539,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2" @@ -539,7 +539,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID))
awaitItem().also { state ->
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Cached(0))
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Success(AN_EVENT_ID, 0))
}
}
}
@ -562,7 +562,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2" @@ -562,7 +562,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
initialState.eventSink(TimelineEvents.FocusOnEvent(AN_EVENT_ID))
awaitItem().also { state ->
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Fetching)
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID))
}
awaitItem().also { state ->
assertThat(state.focusRequestState).isInstanceOf(FocusRequestState.Failure::class.java)

5
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt

@ -87,6 +87,7 @@ import org.matrix.rustcomponents.sdk.use @@ -87,6 +87,7 @@ import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import uniffi.matrix_sdk.RoomPowerLevelChanges
import java.io.File
import kotlin.coroutines.cancellation.CancellationException
import org.matrix.rustcomponents.sdk.Room as InnerRoom
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
@ -183,6 +184,10 @@ class RustMatrixRoom( @@ -183,6 +184,10 @@ class RustMatrixRoom(
}
}.mapFailure {
it.toFocusEventException()
}.onFailure {
if (it is CancellationException) {
throw it
}
}
}

Loading…
Cancel
Save