Browse Source

Merge pull request #3037 from element-hq/feature/fga/timeline_cancelable_focus

Feature/fga/timeline cancelable focus
pull/3046/head
ganfra 3 months ago committed by GitHub
parent
commit
2b5ea96110
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      changelog.d/2876.misc
  2. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
  3. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt
  4. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt
  5. 55
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
  6. 24
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt
  7. 13
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt
  8. 20
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
  9. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateProvider.kt
  10. 9
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt
  11. 8
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
  12. 15
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt
  13. 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.

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt

@ -292,7 +292,7 @@ private fun AttachmentStateView( @@ -292,7 +292,7 @@ private fun AttachmentStateView(
is AttachmentsState.Sending.Processing -> ProgressDialogType.Indeterminate
},
text = stringResource(id = CommonStrings.common_sending),
isCancellable = true,
showCancelButton = true,
onDismissRequest = onCancel,
)
}

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt

@ -98,7 +98,7 @@ private fun AttachmentSendStateView( @@ -98,7 +98,7 @@ private fun AttachmentSendStateView(
SendActionState.Sending.Processing -> ProgressDialogType.Indeterminate
},
text = stringResource(id = CommonStrings.common_sending),
isCancellable = true,
showCancelButton = true,
onDismissRequest = onDismissClick,
)
}

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
/**

55
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,22 +136,15 @@ class TimelinePresenter @AssistedInject constructor( @@ -139,22 +136,15 @@ 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

13
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(
): 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,
focusedEventId = timelineItems.filterIsInstance<TimelineItem.Event>().getOrNull(focusedEventIndex)?.eventId,
focusRequestState = FocusRequestState.None,
focusRequestState = focusRequestState,
eventSink = eventSink,
)
}
internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList<TimelineItem> {
return persistentListOf(

20
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()
}
}

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"),

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

@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable @@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.window.DialogProperties
import io.element.android.features.messages.impl.timeline.FocusRequestState
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
@ -48,8 +49,12 @@ fun FocusRequestStateView( @@ -48,8 +49,12 @@ fun FocusRequestStateView(
modifier = modifier,
)
}
FocusRequestState.Fetching -> {
ProgressDialog(modifier = modifier, onDismissRequest = onClearFocusRequestState)
is FocusRequestState.Loading -> {
ProgressDialog(
modifier = modifier,
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true),
onDismissRequest = onClearFocusRequestState,
)
}
else -> Unit
}

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)

15
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt

@ -51,7 +51,8 @@ fun ProgressDialog( @@ -51,7 +51,8 @@ fun ProgressDialog(
modifier: Modifier = Modifier,
text: String? = null,
type: ProgressDialogType = ProgressDialogType.Indeterminate,
isCancellable: Boolean = false,
properties: DialogProperties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
showCancelButton: Boolean = false,
onDismissRequest: () -> Unit = {},
) {
DisposableEffect(Unit) {
@ -61,12 +62,12 @@ fun ProgressDialog( @@ -61,12 +62,12 @@ fun ProgressDialog(
}
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)
properties = properties,
) {
ProgressDialogContent(
modifier = modifier,
text = text,
isCancellable = isCancellable,
showCancelButton = showCancelButton,
onCancelClick = onDismissRequest,
progressIndicator = {
when (type) {
@ -97,7 +98,7 @@ sealed interface ProgressDialogType { @@ -97,7 +98,7 @@ sealed interface ProgressDialogType {
private fun ProgressDialogContent(
modifier: Modifier = Modifier,
text: String? = null,
isCancellable: Boolean = false,
showCancelButton: Boolean = false,
onCancelClick: () -> Unit = {},
progressIndicator: @Composable () -> Unit = {
CircularProgressIndicator(
@ -125,7 +126,7 @@ private fun ProgressDialogContent( @@ -125,7 +126,7 @@ private fun ProgressDialogContent(
color = MaterialTheme.colorScheme.primary,
)
}
if (isCancellable) {
if (showCancelButton) {
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier.fillMaxWidth(),
@ -145,12 +146,12 @@ private fun ProgressDialogContent( @@ -145,12 +146,12 @@ private fun ProgressDialogContent(
@Composable
internal fun ProgressDialogContentPreview() = ElementThemedPreview {
DialogPreview {
ProgressDialogContent(text = "test dialog content", isCancellable = true)
ProgressDialogContent(text = "test dialog content", showCancelButton = true)
}
}
@PreviewsDayNight
@Composable
internal fun ProgressDialogPreview() = ElementPreview {
ProgressDialog(text = "test dialog content", isCancellable = true)
ProgressDialog(text = "test dialog content", showCancelButton = true)
}

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