Merge pull request #4010 from element-hq/feature/bma/mediaGalleryUi

Media gallery UI
This commit is contained in:
Benoit Marty 2024-12-11 12:20:30 +01:00 committed by GitHub
commit 06d6ba1899
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
179 changed files with 4636 additions and 277 deletions

View file

@ -14,4 +14,13 @@
<string name="screen_knock_requests_list_empty_state_description">"Když někdo požádá o vstup do místnosti, uvidíte jeho žádost zde."</string>
<string name="screen_knock_requests_list_empty_state_title">"Žádná čekající žádost o vstup"</string>
<string name="screen_knock_requests_list_title">"Žádosti o vstup"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s +%2$d další chce vstoupit do této místnosti"</item>
<item quantity="few">"%1$s +%2$d další chtějí vstoupit do této místnosti"</item>
<item quantity="other">"%1$s +%2$d dalších chce vstoupit do této místnosti"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Zobrazit vše"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Přijmout"</string>
<string name="screen_room_single_knock_request_title">"%1$s chce vstoupit do této místnosti"</string>
<string name="screen_room_single_knock_request_view_button_title">"Zobrazit"</string>
</resources>

View file

@ -14,4 +14,12 @@
<string name="screen_knock_requests_list_empty_state_description">"Falls jemand um Aufnahme in den Raum bittet, können Sie dessen Anfrage hier sehen."</string>
<string name="screen_knock_requests_list_empty_state_title">"Keine ausstehende Beitrittsanfrage"</string>
<string name="screen_knock_requests_list_title">"Beitrittsanfragen"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s+ %2$d andere wollen diesem Chatroom beitreten"</item>
<item quantity="other">"%1$s+ %2$d andere wollen diesem Chatroom beitreten"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Alles ansehen"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Akzeptieren"</string>
<string name="screen_room_single_knock_request_title">"%1$s möchte diesem Chatroom beitreten"</string>
<string name="screen_room_single_knock_request_view_button_title">"Ansicht"</string>
</resources>

View file

@ -14,4 +14,12 @@
<string name="screen_knock_requests_list_empty_state_description">"Όταν κάποιος θα ζητήσει να συμμετάσχει στο δωμάτιο, θα μπορείς να δεις το αίτημά του εδώ."</string>
<string name="screen_knock_requests_list_empty_state_title">"Δεν υπάρχει εκκρεμές αίτημα συμμετοχής"</string>
<string name="screen_knock_requests_list_title">"Αιτήματα συμμετοχής"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"Οι χρήστες %1$s +%2$d ακόμη θέλουν να συμμετάσχουν σε αυτό το δωμάτιο"</item>
<item quantity="other">"Οι χρήστες %1$s +%2$d ακόμη θέλουν να συμμετάσχουν σε αυτό το δωμάτιο"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Προβολή όλων"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Αποδοχή"</string>
<string name="screen_room_single_knock_request_title">"Ο χρήστης %1$s θέλει να μπει σε αυτό το δωμάτιο"</string>
<string name="screen_room_single_knock_request_view_button_title">"Προβολή"</string>
</resources>

View file

@ -14,4 +14,12 @@
<string name="screen_knock_requests_list_empty_state_description">"Kui keegi soovib jututoaga liituda, siis need päringud on kuvatud siin."</string>
<string name="screen_knock_requests_list_empty_state_title">"Pole ühtegi liitumispalvet"</string>
<string name="screen_knock_requests_list_title">"Liitumispalved"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s + veel %2$d kasutaja soovivad selle jututoaga liituda"</item>
<item quantity="other">"%1$s + veel %2$d kasutajat soovivad selle jututoaga liituda"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Vaata kõiki"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Nõustu"</string>
<string name="screen_room_single_knock_request_title">"%1$s soovib selle jututoaga liituda"</string>
<string name="screen_room_single_knock_request_view_button_title">"Vaata"</string>
</resources>

View file

@ -14,4 +14,12 @@
<string name="screen_knock_requests_list_empty_state_description">"Lorsque quelquun demandera à rejoindre le salon, vous pourrez voir sa demande ici."</string>
<string name="screen_knock_requests_list_empty_state_title">"Personne ne demande à rejoindre le salon"</string>
<string name="screen_knock_requests_list_title">"Demandes en attente"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s et %2$d autre personne souhaitent rejoindre ce salon"</item>
<item quantity="other">"%1$s et %2$d autres personnes souhaitent rejoindre ce salon"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Tout afficher"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Accepter"</string>
<string name="screen_room_single_knock_request_title">"%1$s souhaite rejoindre ce salon"</string>
<string name="screen_room_single_knock_request_view_button_title">"Voir"</string>
</resources>

View file

@ -14,4 +14,12 @@
<string name="screen_knock_requests_list_empty_state_description">"Ha valaki csatlakozni kíván a szobához, itt láthatja a kérését."</string>
<string name="screen_knock_requests_list_empty_state_title">"Nincs függőben lévő csatlakozási kérelem"</string>
<string name="screen_knock_requests_list_title">"Csatlakozási kérelmek"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s és még %2$d felhasználó szeretne csatlakozni ehhez a szobához"</item>
<item quantity="other">"%1$s és még %2$d felhasználó szeretne csatlakozni ehhez a szobához"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Összes megtekintése"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Elfogadás"</string>
<string name="screen_room_single_knock_request_title">"%1$s szeretne csatlakozni ehhez a szobához"</string>
<string name="screen_room_single_knock_request_view_button_title">"Megtekintés"</string>
</resources>

View file

@ -14,4 +14,12 @@
<string name="screen_knock_requests_list_empty_state_description">"Quando qualcuno ti chiederà di entrare nella stanza, potrai vedere la sua richiesta qui."</string>
<string name="screen_knock_requests_list_empty_state_title">"Nessuna richiesta di accesso in sospeso"</string>
<string name="screen_knock_requests_list_title">"Richieste di accesso"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s +%2$d vogliono entrare in questa stanza"</item>
<item quantity="other">"%1$s +%2$d vogliono entrare in questa stanza"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Visualizza tutte"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Accetta"</string>
<string name="screen_room_single_knock_request_title">"%1$s vuole entrare in questa stanza"</string>
<string name="screen_room_single_knock_request_view_button_title">"Visualizza"</string>
</resources>

View file

@ -14,4 +14,13 @@
<string name="screen_knock_requests_list_empty_state_description">"Вы сможете увидеть запрос, когда кто-то попросит присоединиться к комнате."</string>
<string name="screen_knock_requests_list_empty_state_title">"Нет ожидающих запросов на присоединение"</string>
<string name="screen_knock_requests_list_title">"Запросы на присоединение"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s +%2$d хочет присоединиться к этой комнате"</item>
<item quantity="few">"%1$s +%2$d хотят присоединиться к этой комнате"</item>
<item quantity="many">"%1$s +%2$d хотят присоединиться к этой комнате"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Показать все"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Принять"</string>
<string name="screen_room_single_knock_request_title">"%1$s хочет присоединиться к этой комнате"</string>
<string name="screen_room_single_knock_request_view_button_title">"Просмотр"</string>
</resources>

View file

@ -5,4 +5,13 @@
<string name="screen_knock_requests_list_empty_state_description">"Keď niekto požiada, aby sa pripojil k miestnosti, jeho žiadosť si môžete pozrieť tu."</string>
<string name="screen_knock_requests_list_empty_state_title">"Žiadna čakajúca žiadosť o pripojenie"</string>
<string name="screen_knock_requests_list_title">"Žiadosti o pripojenie"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s +%2$d ďalší chcú vstúpiť do tejto miestnosti"</item>
<item quantity="few">"%1$s +%2$d ďalší chcú vstúpiť do tejto miestnosti"</item>
<item quantity="other">"%1$s +%2$d ďalších chce vstúpiť do tejto miestnosti"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Zobraziť všetko"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Prijať"</string>
<string name="screen_room_single_knock_request_title">"%1$s chce vstúpiť do tejto miestnosti"</string>
<string name="screen_room_single_knock_request_view_button_title">"Zobraziť"</string>
</resources>

View file

@ -117,6 +117,7 @@ class MessagesFlowNode @AssistedInject constructor(
@Parcelize
data class MediaViewer(
val eventId: EventId?,
val mediaInfo: MediaInfo,
val mediaSource: MediaSource,
val thumbnailSource: MediaSource?,
@ -241,9 +242,11 @@ class MessagesFlowNode @AssistedInject constructor(
}
is NavTarget.MediaViewer -> {
val params = MediaViewerEntryPoint.Params(
eventId = navTarget.eventId,
mediaInfo = navTarget.mediaInfo,
mediaSource = navTarget.mediaSource,
thumbnailSource = navTarget.thumbnailSource,
canShowInfo = true,
canDownload = true,
canShare = true,
)
@ -251,6 +254,10 @@ class MessagesFlowNode @AssistedInject constructor(
override fun onDone() {
overlay.hide()
}
override fun onViewInTimeline(eventId: EventId) {
viewInTimeline(eventId)
}
}
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
.params(params)
@ -311,11 +318,7 @@ class MessagesFlowNode @AssistedInject constructor(
}
override fun onViewInTimelineClick(eventId: EventId) {
val permalinkData = PermalinkData.RoomLink(
roomIdOrAlias = room.roomId.toRoomIdOrAlias(),
eventId = eventId,
)
callbacks.forEach { it.onPermalinkClick(permalinkData, pushToBackstack = false) }
viewInTimeline(eventId)
}
override fun onRoomPermalinkClick(data: PermalinkData.RoomLink) {
@ -341,6 +344,14 @@ class MessagesFlowNode @AssistedInject constructor(
}
}
private fun viewInTimeline(eventId: EventId) {
val permalinkData = PermalinkData.RoomLink(
roomIdOrAlias = room.roomId.toRoomIdOrAlias(),
eventId = eventId,
)
callbacks.forEach { it.onPermalinkClick(permalinkData, pushToBackstack = false) }
}
private fun processEventClick(event: TimelineItem.Event): Boolean {
val navTarget = when (event.content) {
is TimelineItemImageContent -> {
@ -415,13 +426,16 @@ class MessagesFlowNode @AssistedInject constructor(
thumbnailSource: MediaSource?,
): NavTarget {
return NavTarget.MediaViewer(
eventId = event.eventId,
mediaInfo = MediaInfo(
filename = content.filename,
caption = content.caption,
mimeType = content.mimeType,
formattedFileSize = content.formattedFileSize,
fileExtension = content.fileExtension,
senderId = event.senderId,
senderName = event.safeSenderName,
senderAvatar = event.senderAvatar.url,
dateSent = event.sentTime,
),
mediaSource = mediaSource,

View file

@ -15,6 +15,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@ -39,10 +40,13 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.overlay.operation.hide
import io.element.android.libraries.architecture.overlay.operation.show
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
@ -59,6 +63,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
private val messagesEntryPoint: MessagesEntryPoint,
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
private val mediaGalleryEntryPoint: MediaGalleryEntryPoint,
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
backstack = BackStack(
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
@ -98,6 +103,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
@Parcelize
data object PollHistory : NavTarget
@Parcelize
data object MediaGallery : NavTarget
@Parcelize
data object AdminSettings : NavTarget
@ -136,6 +144,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
backstack.push(NavTarget.PollHistory)
}
override fun openMediaGallery() {
backstack.push(NavTarget.MediaGallery)
}
override fun openAdminSettings() {
backstack.push(NavTarget.AdminSettings)
}
@ -213,6 +225,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
override fun onDone() {
overlay.hide()
}
override fun onViewInTimeline(eventId: EventId) {
// Cannot happen
}
}
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
.avatar(
@ -222,10 +238,29 @@ class RoomDetailsFlowNode @AssistedInject constructor(
.callback(callback)
.build()
}
is NavTarget.PollHistory -> {
pollHistoryEntryPoint.createNode(this, buildContext)
}
is NavTarget.MediaGallery -> {
val callback = object : MediaGalleryEntryPoint.Callback {
override fun onBackClick() {
backstack.pop()
}
override fun onViewInTimeline(eventId: EventId) {
val permalinkData = PermalinkData.RoomLink(
roomIdOrAlias = room.roomId.toRoomIdOrAlias(),
eventId = eventId,
)
plugins<RoomDetailsEntryPoint.Callback>().forEach {
it.onPermalinkClick(permalinkData, pushToBackstack = false)
}
}
}
mediaGalleryEntryPoint.nodeBuilder(this, buildContext)
.callback(callback)
.build()
}
is NavTarget.AdminSettings -> {
createNode<RolesAndPermissionsFlowNode>(buildContext)

View file

@ -45,6 +45,7 @@ class RoomDetailsNode @AssistedInject constructor(
fun openRoomNotificationSettings()
fun openAvatarPreview(name: String, url: String)
fun openPollHistory()
fun openMediaGallery()
fun openAdminSettings()
fun openPinnedMessagesList()
fun openKnockRequestsList()
@ -77,6 +78,10 @@ class RoomDetailsNode @AssistedInject constructor(
callbacks.forEach { it.openPollHistory() }
}
private fun openMediaGallery() {
callbacks.forEach { it.openMediaGallery() }
}
private fun onJoinCall() {
callbacks.forEach { it.onJoinCall() }
}
@ -143,6 +148,7 @@ class RoomDetailsNode @AssistedInject constructor(
invitePeople = ::invitePeople,
openAvatarPreview = ::openAvatarPreview,
openPollHistory = ::openPollHistory,
openMediaGallery = ::openMediaGallery,
openAdminSettings = this::openAdminSettings,
onJoinCallClick = ::onJoinCall,
onPinnedMessagesClick = ::openPinnedMessages,

View file

@ -17,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
@ -79,6 +80,10 @@ class RoomDetailsPresenter @Inject constructor(
val isPublic by remember { derivedStateOf { roomInfo?.isPublic.orFalse() } }
val canShowPinnedMessages = isPinnedMessagesFeatureEnabled()
var canShowMediaGallery by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
canShowMediaGallery = featureFlagService.isFeatureEnabled(FeatureFlags.MediaGallery)
}
val pinnedMessagesCount by remember { derivedStateOf { roomInfo?.pinnedEventIds?.size } }
LaunchedEffect(Unit) {
@ -162,6 +167,7 @@ class RoomDetailsPresenter @Inject constructor(
isPublic = isPublic,
heroes = roomInfo?.heroes.orEmpty().toPersistentList(),
canShowPinnedMessages = canShowPinnedMessages,
canShowMediaGallery = canShowMediaGallery,
pinnedMessagesCount = pinnedMessagesCount,
canShowKnockRequests = canShowKnockRequests,
knockRequestsCount = knockRequestsCount,

View file

@ -40,6 +40,7 @@ data class RoomDetailsState(
val isPublic: Boolean,
val heroes: ImmutableList<MatrixUser>,
val canShowPinnedMessages: Boolean,
val canShowMediaGallery: Boolean,
val pinnedMessagesCount: Int?,
val canShowKnockRequests: Boolean,
val knockRequestsCount: Int?,

View file

@ -101,6 +101,7 @@ fun aRoomDetailsState(
isPublic: Boolean = true,
heroes: List<MatrixUser> = emptyList(),
canShowPinnedMessages: Boolean = true,
canShowMediaGallery: Boolean = true,
pinnedMessagesCount: Int? = null,
canShowKnockRequests: Boolean = false,
knockRequestsCount: Int? = null,
@ -126,6 +127,7 @@ fun aRoomDetailsState(
isPublic = isPublic,
heroes = heroes.toPersistentList(),
canShowPinnedMessages = canShowPinnedMessages,
canShowMediaGallery = canShowMediaGallery,
pinnedMessagesCount = pinnedMessagesCount,
canShowKnockRequests = canShowKnockRequests,
knockRequestsCount = knockRequestsCount,

View file

@ -101,6 +101,7 @@ fun RoomDetailsView(
invitePeople: () -> Unit,
openAvatarPreview: (name: String, url: String) -> Unit,
openPollHistory: () -> Unit,
openMediaGallery: () -> Unit,
openAdminSettings: () -> Unit,
onJoinCallClick: () -> Unit,
onPinnedMessagesClick: () -> Unit,
@ -219,7 +220,11 @@ fun RoomDetailsView(
PollsSection(
openPollHistory = openPollHistory
)
if (state.canShowMediaGallery) {
MediaGallerySection(
onClick = openMediaGallery
)
}
if (state.isEncrypted) {
SecuritySection()
}
@ -576,6 +581,19 @@ private fun PollsSection(
}
}
@Composable
private fun MediaGallerySection(
onClick: () -> Unit,
) {
PreferenceCategory {
ListItem(
headlineContent = { Text(stringResource(R.string.screen_room_details_media_gallery_title)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Image())),
onClick = onClick,
)
}
}
@Composable
private fun SecuritySection() {
PreferenceCategory(title = stringResource(R.string.screen_room_details_security_title)) {
@ -631,6 +649,7 @@ private fun ContentToPreview(state: RoomDetailsState) {
invitePeople = {},
openAvatarPreview = { _, _ -> },
openPollHistory = {},
openMediaGallery = {},
openAdminSettings = {},
onJoinCallClick = {},
onPinnedMessagesClick = {},

View file

@ -79,6 +79,17 @@ class RoomDetailsViewTest {
}
}
@Config(qualifiers = "h1024dp")
@Test
fun `click on media gallery invokes expected callback`() {
ensureCalledOnce { callback ->
rule.setRoomDetailView(
openMediaGallery = callback,
)
rule.clickOn(R.string.screen_room_details_media_gallery_title)
}
}
@Config(qualifiers = "h1024dp")
@Test
fun `click on notification invokes expected callback`() {
@ -241,7 +252,7 @@ class RoomDetailsViewTest {
eventsRecorder.assertSingle(RoomDetailsEvent.SetFavorite(true))
}
@Config(qualifiers = "h1024dp")
@Config(qualifiers = "h1500dp")
@Test
fun `click on leave emit expected Event`() {
val eventsRecorder = EventsRecorder<RoomDetailsEvent>()
@ -282,6 +293,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
invitePeople: () -> Unit = EnsureNeverCalled(),
openAvatarPreview: (name: String, url: String) -> Unit = EnsureNeverCalledWithTwoParams(),
openPollHistory: () -> Unit = EnsureNeverCalled(),
openMediaGallery: () -> Unit = EnsureNeverCalled(),
openAdminSettings: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
@ -298,6 +310,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
invitePeople = invitePeople,
openAvatarPreview = openAvatarPreview,
openPollHistory = openPollHistory,
openMediaGallery = openMediaGallery,
openAdminSettings = openAdminSettings,
onJoinCallClick = onJoinCallClick,
onPinnedMessagesClick = onPinnedMessagesClick,

View file

@ -30,6 +30,7 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
@ -82,6 +83,10 @@ class UserProfileFlowNode @AssistedInject constructor(
override fun onDone() {
backstack.pop()
}
override fun onViewInTimeline(eventId: EventId) {
// Cannot happen
}
}
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
.avatar(