Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Media navigation with swipe gesture #4161

Merged
merged 38 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
35cc5df
Create MediaGalleryDataSource and extract logic from MediaGalleryPres…
bmarty Jan 15, 2025
cbf7bf0
Let MediaGalleryDataSource be an interface
bmarty Jan 16, 2025
a06607f
Remove RetryLoading (use LoadMedia)
bmarty Jan 17, 2025
32db42a
Suppress Detekt false positive (?)
bmarty Jan 17, 2025
7df65b0
Add support for files navigation (when coming from the gallery)
bmarty Jan 17, 2025
ca5c06f
Open in SingleMedia mode when coming from the timeline
bmarty Jan 17, 2025
8d6550b
If not displayed, make sure to pause the audio / video
bmarty Jan 17, 2025
98a786b
media viewer : create MediaViewerDataSource
ganfra Jan 20, 2025
d26414f
Provide duration
bmarty Jan 17, 2025
bad4566
Remove unused import
bmarty Jan 20, 2025
3248041
media viewer : use collectAsState in the DataSource
ganfra Jan 21, 2025
c0542c8
Fix and write tests
bmarty Jan 21, 2025
d6ebec8
Improve loading state and add preview.
bmarty Jan 21, 2025
6c904a4
Add exception for Konsist
bmarty Jan 21, 2025
aa68a35
sync strings
bmarty Jan 21, 2025
a4cf1d7
Restore caption rendering
bmarty Jan 22, 2025
f286f6d
Small cleanup
bmarty Jan 22, 2025
428b81c
Add test on SingleMediaGalleryDataSource
bmarty Jan 22, 2025
18c3b5b
Add test on MediaViewerDataSource
bmarty Jan 22, 2025
621558a
MediaViewer: add error case in the UI.
bmarty Jan 22, 2025
e496bc3
Introduce MediaViewerFlickToDismiss and extract to its own file
bmarty Jan 22, 2025
dfda13a
Restore overlay when user cancel the dragging
bmarty Jan 22, 2025
bac69c4
Add timestamp to trigger back pagination.
bmarty Jan 22, 2025
b72f4bb
Fix tests.
bmarty Jan 22, 2025
74be5a5
Update screenshots
ElementBot Jan 22, 2025
1580c73
Ensure gallery is paginating to get new items.
bmarty Jan 23, 2025
77887f9
Fix formatting.
bmarty Jan 23, 2025
dff9005
Remove useless parameter
bmarty Jan 23, 2025
54182b0
Simplify with code `coerceAtLeast(0)``
bmarty Jan 23, 2025
2fde015
Simplify
bmarty Jan 23, 2025
afd8161
Add documentation on buildMediaViewerPageList.
bmarty Jan 23, 2025
a5515b0
Add name to argument for clarity.
bmarty Jan 23, 2025
7dd797b
Use Black for code clarity.
bmarty Jan 23, 2025
beb835c
Fix color for media viewer according to Figma.
bmarty Jan 23, 2025
05660fb
Cleanup
bmarty Jan 23, 2025
4d2edfa
Improve code clarity
bmarty Jan 23, 2025
ba0502c
Update screenshots
ElementBot Jan 23, 2025
da22758
Fix pagination restart issue and cover by unit test.
bmarty Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.model.event.duration
import io.element.android.features.poll.api.create.CreatePollEntryPoint
import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.libraries.architecture.BackstackWithOverlayBox
Expand All @@ -58,6 +59,7 @@ import io.element.android.libraries.architecture.overlay.operation.hide
import io.element.android.libraries.architecture.overlay.operation.show
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.dateformatter.api.toHumanReadableDuration
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
Expand Down Expand Up @@ -246,6 +248,8 @@ class MessagesFlowNode @AssistedInject constructor(
}
is NavTarget.MediaViewer -> {
val params = MediaViewerEntryPoint.Params(
// TODO When we will be able to load a media timeline from a EventId, change mode here (and use a mixed mode?)
mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
eventId = navTarget.eventId,
mediaInfo = navTarget.mediaInfo,
mediaSource = navTarget.mediaSource,
Expand Down Expand Up @@ -447,6 +451,7 @@ class MessagesFlowNode @AssistedInject constructor(
mode = DateFormatterMode.Full,
),
waveform = (content as? TimelineItemVoiceContent)?.waveform,
duration = content.duration()?.toHumanReadableDuration(),
),
mediaSource = mediaSource,
thumbnailSource = thumbnailSource,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.media.MediaSource
import kotlin.time.Duration

@Immutable
sealed interface TimelineItemEventContent {
Expand Down Expand Up @@ -90,3 +91,12 @@
is TimelineItemEventMutableContent -> isEdited
else -> false
}

fun TimelineItemEventContentWithAttachment.duration(): Duration? {
return when (this) {

Check warning on line 96 in features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt

View check run for this annotation

Codecov / codecov/patch

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt#L96

Added line #L96 was not covered by tests
is TimelineItemAudioContent -> duration
is TimelineItemVideoContent -> duration
is TimelineItemVoiceContent -> duration
else -> null

Check warning on line 100 in features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt

View check run for this annotation

Codecov / codecov/patch

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt#L100

Added line #L100 was not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package io.element.android.libraries.dateformatter.api

import java.util.Locale
import kotlin.time.Duration

/**
* Convert milliseconds to human readable duration.
Expand Down Expand Up @@ -38,3 +39,5 @@
String.format(Locale.US, "%d:%02d", minutes, seconds)
}
}

fun Duration.toHumanReadableDuration() = inWholeMilliseconds.toHumanReadableDuration()

Check warning on line 43 in libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DurationFormatter.kt

View check run for this annotation

Codecov / codecov/patch

libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DurationFormatter.kt#L43

Added line #L43 was not covered by tests
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.launchIn
Expand Down Expand Up @@ -213,8 +211,8 @@ class RustTimeline(

override val timelineItems: Flow<List<MatrixTimelineItem>> = combine(
_timelineItems,
backPaginationStatus.filter { !it.isPaginating }.distinctUntilChanged(),
forwardPaginationStatus.filter { !it.isPaginating }.distinctUntilChanged(),
backPaginationStatus,
forwardPaginationStatus,
matrixRoom.roomInfoFlow.map { it.creator },
isTimelineInitialized,
) { timelineItems,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
package io.element.android.libraries.matrix.impl.fixtures.fakes

import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.PaginationStatusListener
import org.matrix.rustcomponents.sdk.TaskHandle
import org.matrix.rustcomponents.sdk.Timeline
import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineListener
import uniffi.matrix_sdk_ui.LiveBackPaginationStatus

class FakeRustTimeline : Timeline(NoPointer) {
private var listener: TimelineListener? = null
Expand All @@ -23,4 +25,16 @@ class FakeRustTimeline : Timeline(NoPointer) {
fun emitDiff(diff: List<TimelineDiff>) {
listener!!.onUpdate(diff)
}

private var paginationStatusListener: PaginationStatusListener? = null
override suspend fun subscribeToBackPaginationStatus(listener: PaginationStatusListener): TaskHandle {
this.paginationStatusListener = listener
return FakeRustTaskHandle()
}

fun emitPaginationStatus(status: LiveBackPaginationStatus) {
paginationStatusListener!!.onUpdate(status)
}

override suspend fun fetchMembers() = Unit
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)

package io.element.android.libraries.matrix.impl.timeline

import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.room.MatrixRoom
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
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimeline
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineDiff
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.TimelineChange
import uniffi.matrix_sdk_ui.LiveBackPaginationStatus
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline

class RustTimelineTest {
@Test
fun `ensure that the timeline emits new loading item when pagination does not bring new events`() = runTest {
val inner = FakeRustTimeline()
val systemClock = FakeSystemClock()
val sut = createRustTimeline(
inner = inner,
systemClock = systemClock,
)
sut.timelineItems.test {
// Give time for the listener to be set
runCurrent()
inner.emitDiff(
listOf(
FakeRustTimelineDiff(
item = null,
change = TimelineChange.RESET,
)
)
)
with(awaitItem()) {
assertThat(size).isEqualTo(1)
// Typing notification
assertThat((get(0) as MatrixTimelineItem.Virtual).virtual).isEqualTo(VirtualTimelineItem.TypingNotification)
}
with(awaitItem()) {
assertThat(size).isEqualTo(2)
// The loading
assertThat((get(0) as MatrixTimelineItem.Virtual).virtual).isEqualTo(
VirtualTimelineItem.LoadingIndicator(
direction = Timeline.PaginationDirection.BACKWARDS,
timestamp = A_FAKE_TIMESTAMP,
)
)
// Typing notification
assertThat((get(1) as MatrixTimelineItem.Virtual).virtual).isEqualTo(VirtualTimelineItem.TypingNotification)
}
systemClock.epochMillisResult = A_FAKE_TIMESTAMP + 1
// Start pagination
sut.paginate(Timeline.PaginationDirection.BACKWARDS)
// Simulate SDK starting pagination
inner.emitPaginationStatus(LiveBackPaginationStatus.Paginating)
// No new events received
// Simulate SDK stopping pagination, more event to load
inner.emitPaginationStatus(LiveBackPaginationStatus.Idle(hitStartOfTimeline = false))
// expect an item to be emitted, with an updated timestamp
with(awaitItem()) {
assertThat(size).isEqualTo(2)
// The loading
assertThat((get(0) as MatrixTimelineItem.Virtual).virtual).isEqualTo(
VirtualTimelineItem.LoadingIndicator(
direction = Timeline.PaginationDirection.BACKWARDS,
timestamp = A_FAKE_TIMESTAMP + 1,
)
)
// Typing notification
assertThat((get(1) as MatrixTimelineItem.Virtual).virtual).isEqualTo(VirtualTimelineItem.TypingNotification)
}
}
}
}

private fun TestScope.createRustTimeline(
inner: InnerTimeline,
mode: Timeline.Mode = Timeline.Mode.LIVE,
systemClock: SystemClock = FakeSystemClock(),
matrixRoom: MatrixRoom = FakeMatrixRoom().apply { givenRoomInfo(aRoomInfo()) },
coroutineScope: CoroutineScope = backgroundScope,
dispatcher: CoroutineDispatcher = testCoroutineDispatchers().io,
roomContentForwarder: RoomContentForwarder = RoomContentForwarder(FakeRustRoomListService()),
featureFlagsService: FeatureFlagService = FakeFeatureFlagService(),
onNewSyncedEvent: () -> Unit = {},
): RustTimeline {
return RustTimeline(
inner = inner,
mode = mode,
systemClock = systemClock,
matrixRoom = matrixRoom,
coroutineScope = coroutineScope,
dispatcher = dispatcher,
roomContentForwarder = roomContentForwarder,
featureFlagsService = featureFlagsService,
onNewSyncedEvent = onNewSyncedEvent,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
val dateSent: String?,
val dateSentFull: String?,
val waveform: List<Float>?,
val duration: String?,
) : Parcelable

fun anImageMediaInfo(
Expand All @@ -45,13 +46,15 @@
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
)

fun aVideoMediaInfo(
caption: String? = null,
senderName: String? = null,
dateSent: String? = null,
dateSentFull: String? = null,
duration: String? = null,
): MediaInfo = MediaInfo(
filename = "a video file.mp4",
caption = caption,
Expand All @@ -64,6 +67,7 @@
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = duration,
)

fun aPdfMediaInfo(
Expand All @@ -84,6 +88,7 @@
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
)

fun anApkMediaInfo(
Expand All @@ -103,6 +108,7 @@
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
)

fun anAudioMediaInfo(
Expand All @@ -112,6 +118,7 @@
dateSent: String? = null,
dateSentFull: String? = null,
waveForm: List<Float>? = null,
duration: String? = null,
): MediaInfo = MediaInfo(
filename = filename,
caption = caption,
Expand All @@ -124,6 +131,7 @@
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = waveForm,
duration = duration,
)

fun aVoiceMediaInfo(
Expand All @@ -133,6 +141,7 @@
dateSent: String? = null,
dateSentFull: String? = null,
waveForm: List<Float>? = null,
duration: String? = null,

Check warning on line 144 in libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt

View check run for this annotation

Codecov / codecov/patch

libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt#L144

Added line #L144 was not covered by tests
): MediaInfo = MediaInfo(
filename = filename,
caption = caption,
Expand All @@ -145,4 +154,5 @@
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = waveForm,
duration = duration,
)
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,17 @@ interface MediaViewerEntryPoint : FeatureEntryPoint {
}

data class Params(
val mode: MediaViewerMode,
val eventId: EventId?,
val mediaInfo: MediaInfo,
val mediaSource: MediaSource,
val thumbnailSource: MediaSource?,
val canShowInfo: Boolean,
) : NodeInputs

enum class MediaViewerMode {
SingleMedia,
TimelineImagesAndVideos,
TimelineFilesAndAudios,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
val mimeType = MimeTypes.Images
return params(
MediaViewerEntryPoint.Params(
mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia,

Check warning on line 45 in libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt

View check run for this annotation

Codecov / codecov/patch

libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt#L45

Added line #L45 was not covered by tests
eventId = null,
mediaInfo = MediaInfo(
filename = filename,
Expand All @@ -55,6 +56,7 @@
dateSent = null,
dateSentFull = null,
waveform = null,
duration = null,

Check warning on line 59 in libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt

View check run for this annotation

Codecov / codecov/patch

libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt#L59

Added line #L59 was not covered by tests
),
mediaSource = MediaSource(url = avatarUrl),
thumbnailSource = null,
Expand Down
Loading
Loading