Merge pull request #6650 from element-hq/feature/bma/a11yFixes

[a11y] Fix a set of issues
This commit is contained in:
Benoit Marty 2026-05-04 11:50:15 +02:00 committed by GitHub
commit abc540b379
44 changed files with 271 additions and 169 deletions

View file

@ -19,6 +19,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
@ -90,7 +93,11 @@ fun ChooseSelfVerificationModeView(
Text( Text(
modifier = Modifier modifier = Modifier
.clickable(onClick = onLearnMore) .clickable(onClick = onLearnMore)
.padding(vertical = 4.dp, horizontal = 16.dp), .padding(vertical = 4.dp, horizontal = 16.dp)
.semantics {
// Note: there is no Role.Link, so we use Role.Button for better accessibility support
role = Role.Button
},
text = stringResource(CommonStrings.action_learn_more), text = stringResource(CommonStrings.action_learn_more),
style = ElementTheme.typography.fontBodyLgMedium style = ElementTheme.typography.fontBodyLgMedium
) )

View file

@ -237,6 +237,7 @@ private fun SpaceFilterButton(
else -> Unit else -> Unit
} }
} }
val isSelected = spaceFiltersState is SpaceFiltersState.Selected val isSelected = spaceFiltersState is SpaceFiltersState.Selected
IconButton( IconButton(
onClick = ::onClick, onClick = ::onClick,
@ -320,7 +321,15 @@ private fun AccountIcon(
Avatar( Avatar(
avatarData = avatarData, avatarData = avatarData,
avatarType = AvatarType.User, avatarType = AvatarType.User,
contentDescription = if (isCurrentAccount) stringResource(CommonStrings.common_settings) else null, contentDescription = if (isCurrentAccount) {
if (showAvatarIndicator) {
stringResource(CommonStrings.a11y_settings_with_required_action)
} else {
stringResource(CommonStrings.common_settings)
}
} else {
null
},
) )
if (showAvatarIndicator) { if (showAvatarIndicator) {
RedIndicatorAtom( RedIndicatorAtom(

View file

@ -143,6 +143,7 @@ class MessagesFlowNode(
val mediaInfo: MediaInfo, val mediaInfo: MediaInfo,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val canUseOverlay: Boolean,
) : NavTarget ) : NavTarget
@Parcelize @Parcelize
@ -227,10 +228,11 @@ class MessagesFlowNode(
callback.navigateToRoomDetails() callback.navigateToRoomDetails()
} }
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean { override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean {
return processEventClick( return processEventClick(
timelineMode = timelineMode, timelineMode = timelineMode,
event = event, event = event,
canUseOverlay = canUseOverlay,
) )
} }
@ -320,7 +322,11 @@ class MessagesFlowNode(
) )
val callback = object : MediaViewerEntryPoint.Callback { val callback = object : MediaViewerEntryPoint.Callback {
override fun onDone() { override fun onDone() {
overlay.hide() if (navTarget.canUseOverlay) {
overlay.hide()
} else {
backstack.pop()
}
} }
override fun viewInTimeline(eventId: EventId) { override fun viewInTimeline(eventId: EventId) {
@ -414,10 +420,11 @@ class MessagesFlowNode(
} }
NavTarget.PinnedMessagesList -> { NavTarget.PinnedMessagesList -> {
val callback = object : PinnedMessagesListNode.Callback { val callback = object : PinnedMessagesListNode.Callback {
override fun handleEventClick(event: TimelineItem.Event) { override fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean) {
processEventClick( processEventClick(
timelineMode = Timeline.Mode.PinnedEvents, timelineMode = Timeline.Mode.PinnedEvents,
event = event, event = event,
canUseOverlay = canUseOverlay,
) )
} }
@ -456,10 +463,11 @@ class MessagesFlowNode(
focusedEventId = navTarget.focusedEventId, focusedEventId = navTarget.focusedEventId,
) )
val callback = object : ThreadedMessagesNode.Callback { val callback = object : ThreadedMessagesNode.Callback {
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean { override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean {
return processEventClick( return processEventClick(
timelineMode = timelineMode, timelineMode = timelineMode,
event = event, event = event,
canUseOverlay = canUseOverlay,
) )
} }
@ -547,6 +555,7 @@ class MessagesFlowNode(
private fun processEventClick( private fun processEventClick(
timelineMode: Timeline.Mode, timelineMode: Timeline.Mode,
event: TimelineItem.Event, event: TimelineItem.Event,
canUseOverlay: Boolean,
): Boolean { ): Boolean {
val navTarget = when (event.content) { val navTarget = when (event.content) {
is TimelineItemImageContent -> { is TimelineItemImageContent -> {
@ -556,6 +565,7 @@ class MessagesFlowNode(
content = event.content, content = event.content,
mediaSource = event.content.mediaSource, mediaSource = event.content.mediaSource,
thumbnailSource = event.content.thumbnailSource, thumbnailSource = event.content.thumbnailSource,
canUseOverlay = canUseOverlay,
) )
} }
is TimelineItemVideoContent -> { is TimelineItemVideoContent -> {
@ -565,6 +575,7 @@ class MessagesFlowNode(
content = event.content, content = event.content,
mediaSource = event.content.mediaSource, mediaSource = event.content.mediaSource,
thumbnailSource = event.content.thumbnailSource, thumbnailSource = event.content.thumbnailSource,
canUseOverlay = canUseOverlay,
) )
} }
is TimelineItemFileContent -> { is TimelineItemFileContent -> {
@ -574,6 +585,7 @@ class MessagesFlowNode(
content = event.content, content = event.content,
mediaSource = event.content.mediaSource, mediaSource = event.content.mediaSource,
thumbnailSource = event.content.thumbnailSource, thumbnailSource = event.content.thumbnailSource,
canUseOverlay = canUseOverlay,
) )
} }
is TimelineItemAudioContent -> { is TimelineItemAudioContent -> {
@ -583,6 +595,7 @@ class MessagesFlowNode(
content = event.content, content = event.content,
mediaSource = event.content.mediaSource, mediaSource = event.content.mediaSource,
thumbnailSource = null, thumbnailSource = null,
canUseOverlay = canUseOverlay,
) )
} }
is TimelineItemLocationContent -> { is TimelineItemLocationContent -> {
@ -603,7 +616,11 @@ class MessagesFlowNode(
} }
return when (navTarget) { return when (navTarget) {
is NavTarget.MediaViewer -> { is NavTarget.MediaViewer -> {
overlay.show(navTarget) if (canUseOverlay) {
overlay.show(navTarget)
} else {
backstack.push(navTarget)
}
true true
} }
is NavTarget.LocationViewer -> { is NavTarget.LocationViewer -> {
@ -620,6 +637,7 @@ class MessagesFlowNode(
content: TimelineItemEventContentWithAttachment, content: TimelineItemEventContentWithAttachment,
mediaSource: MediaSource, mediaSource: MediaSource,
thumbnailSource: MediaSource?, thumbnailSource: MediaSource?,
canUseOverlay: Boolean,
): NavTarget { ): NavTarget {
return NavTarget.MediaViewer( return NavTarget.MediaViewer(
mode = mode, mode = mode,
@ -647,6 +665,7 @@ class MessagesFlowNode(
), ),
mediaSource = mediaSource, mediaSource = mediaSource,
thumbnailSource = thumbnailSource, thumbnailSource = thumbnailSource,
canUseOverlay = canUseOverlay,
) )
} }

View file

@ -68,6 +68,8 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.libraries.mediaplayer.api.MediaPlayer
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadMessagesUi import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadMessagesUi
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.finishLongRunningTransaction import io.element.android.services.analytics.api.finishLongRunningTransaction
@ -115,7 +117,7 @@ class MessagesNode(
) )
interface Callback : Plugin { interface Callback : Plugin {
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean
fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
fun navigateToRoomMemberDetails(userId: UserId) fun navigateToRoomMemberDetails(userId: UserId)
fun handlePermalinkClick(data: PermalinkData) fun handlePermalinkClick(data: PermalinkData)
@ -247,6 +249,7 @@ class MessagesNode(
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val activity = requireNotNull(LocalActivity.current) val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not() val isDark = ElementTheme.isLightTheme.not()
val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard()
CompositionLocalProvider( CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
) { ) {
@ -268,11 +271,11 @@ class MessagesNode(
onRoomDetailsClick = callback::navigateToRoomDetails, onRoomDetailsClick = callback::navigateToRoomDetails,
onEventContentClick = { isLive, event -> onEventContentClick = { isLive, event ->
if (isLive) { if (isLive) {
callback.handleEventClick(timelineController.mainTimelineMode(), event) callback.handleEventClick(timelineController.mainTimelineMode(), event, canUseOverlay)
} else { } else {
val detachedTimelineMode = timelineController.detachedTimelineMode() val detachedTimelineMode = timelineController.detachedTimelineMode()
if (detachedTimelineMode != null) { if (detachedTimelineMode != null) {
callback.handleEventClick(detachedTimelineMode, event) callback.handleEventClick(detachedTimelineMode, event, canUseOverlay)
} else { } else {
false false
} }

View file

@ -38,6 +38,8 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
@ContributesNode(RoomScope::class) @ContributesNode(RoomScope::class)
@AssistedInject @AssistedInject
@ -50,7 +52,7 @@ class PinnedMessagesListNode(
private val permalinkParser: PermalinkParser, private val permalinkParser: PermalinkParser,
) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator { ) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator {
interface Callback : Plugin { interface Callback : Plugin {
fun handleEventClick(event: TimelineItem.Event) fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean)
fun navigateToRoomMemberDetails(userId: UserId) fun navigateToRoomMemberDetails(userId: UserId)
fun viewInTimeline(eventId: EventId) fun viewInTimeline(eventId: EventId)
fun handlePermalinkClick(data: PermalinkData.RoomLink) fun handlePermalinkClick(data: PermalinkData.RoomLink)
@ -103,6 +105,7 @@ class PinnedMessagesListNode(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard()
CompositionLocalProvider( CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
) { ) {
@ -113,7 +116,9 @@ class PinnedMessagesListNode(
PinnedMessagesListView( PinnedMessagesListView(
state = state, state = state,
onBackClick = ::navigateUp, onBackClick = ::navigateUp,
onEventClick = callback::handleEventClick, onEventClick = {
callback.handleEventClick(it, canUseOverlay)
},
onUserDataClick = { callback.navigateToRoomMemberDetails(it.userId) }, onUserDataClick = { callback.navigateToRoomMemberDetails(it.userId) },
onLinkClick = { link -> onLinkClick(context, link.url) }, onLinkClick = { link -> onLinkClick(context, link.url) },
onLinkLongClick = { onLinkLongClick = {

View file

@ -68,6 +68,8 @@ import io.element.android.libraries.matrix.api.room.alias.matches
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.libraries.mediaplayer.api.MediaPlayer
import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -124,7 +126,7 @@ class ThreadedMessagesNode(
} }
interface Callback : Plugin { interface Callback : Plugin {
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean
fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
fun navigateToRoomMemberDetails(userId: UserId) fun navigateToRoomMemberDetails(userId: UserId)
fun handlePermalinkClick(data: PermalinkData) fun handlePermalinkClick(data: PermalinkData)
@ -252,6 +254,7 @@ class ThreadedMessagesNode(
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val activity = requireNotNull(LocalActivity.current) val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not() val isDark = ElementTheme.isLightTheme.not()
val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard()
CompositionLocalProvider( CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
) { ) {
@ -271,11 +274,11 @@ class ThreadedMessagesNode(
onEventContentClick = { isLive, event -> onEventContentClick = { isLive, event ->
timelineController?.let { controller -> timelineController?.let { controller ->
if (isLive) { if (isLive) {
callback.handleEventClick(controller.mainTimelineMode(), event) callback.handleEventClick(controller.mainTimelineMode(), event, canUseOverlay)
} else { } else {
val detachedTimelineMode = controller.detachedTimelineMode() val detachedTimelineMode = controller.detachedTimelineMode()
if (detachedTimelineMode != null) { if (detachedTimelineMode != null) {
callback.handleEventClick(detachedTimelineMode, event) callback.handleEventClick(detachedTimelineMode, event, canUseOverlay)
} else { } else {
false false
} }

View file

@ -77,7 +77,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.link.Link import io.element.android.wysiwyg.link.Link
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest

View file

@ -47,8 +47,8 @@ import io.element.android.libraries.designsystem.theme.messageFromMeBackground
import io.element.android.libraries.designsystem.theme.messageFromOtherBackground import io.element.android.libraries.designsystem.theme.messageFromOtherBackground
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.libraries.ui.utils.graphics.drawInLayer import io.element.android.libraries.ui.utils.graphics.drawInLayer
import io.element.android.libraries.ui.utils.time.isTalkbackActive
private val BUBBLE_RADIUS = 12.dp private val BUBBLE_RADIUS = 12.dp
private val avatarRadius = AvatarSize.TimelineSender.dp / 2 private val avatarRadius = AvatarSize.TimelineSender.dp / 2

View file

@ -120,7 +120,7 @@ import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonPlurals import io.element.android.libraries.ui.strings.CommonPlurals
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.link.Link import io.element.android.wysiwyg.link.Link
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.abs import kotlin.math.abs

View file

@ -34,7 +34,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
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.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.link.Link import io.element.android.wysiwyg.link.Link
@Composable @Composable

View file

@ -47,7 +47,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.link.Link import io.element.android.wysiwyg.link.Link
import kotlin.time.DurationUnit import kotlin.time.DurationUnit

View file

@ -54,7 +54,7 @@ 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.textcomposer.ElementRichTextEditorStyle import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.compose.EditorStyledText import io.element.android.wysiwyg.compose.EditorStyledText
import io.element.android.wysiwyg.link.Link import io.element.android.wysiwyg.link.Link

View file

@ -64,7 +64,7 @@ import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH
import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.compose.EditorStyledText import io.element.android.wysiwyg.compose.EditorStyledText
import io.element.android.wysiwyg.link.Link import io.element.android.wysiwyg.link.Link

View file

@ -52,7 +52,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvent import io.element.android.libraries.voiceplayer.api.VoiceMessageEvent
import io.element.android.libraries.voiceplayer.api.VoiceMessageState import io.element.android.libraries.voiceplayer.api.VoiceMessageState
import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider

View file

@ -24,8 +24,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.focused import androidx.compose.ui.semantics.focused
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -227,7 +229,11 @@ private fun ContentInitial(
Text( Text(
modifier = Modifier modifier = Modifier
.clickable { onLearnMoreClick() } .clickable { onLearnMoreClick() }
.padding(vertical = 4.dp, horizontal = 16.dp), .padding(vertical = 4.dp, horizontal = 16.dp)
.semantics {
// Note: there is no Role.Link, so we use Role.Button for better accessibility support
role = Role.Button
},
text = stringResource(CommonStrings.action_learn_more), text = stringResource(CommonStrings.action_learn_more),
style = ElementTheme.typography.fontBodyLgMedium style = ElementTheme.typography.fontBodyLgMedium
) )

View file

@ -11,7 +11,6 @@ package io.element.android.libraries.architecture
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -88,11 +87,9 @@ inline fun <reified NavTarget : Any> BaseFlowNode<NavTarget>.OverlayView(
@Composable @Composable
inline fun <reified NavTarget : Any> BaseFlowNode<NavTarget>.BackstackWithOverlayBox( inline fun <reified NavTarget : Any> BaseFlowNode<NavTarget>.BackstackWithOverlayBox(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit = {},
) { ) {
Box(modifier = modifier) { Box(modifier = modifier) {
BackstackView() BackstackView()
OverlayView() OverlayView()
content()
} }
} }

View file

@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
@ -148,7 +150,11 @@ private fun TitleAndDescription(
text = title, text = title,
style = ElementTheme.typography.fontBodyLgMedium, style = ElementTheme.typography.fontBodyLgMedium,
color = titleColor, color = titleColor,
modifier = Modifier.weight(1f), modifier = Modifier
.weight(1f)
.semantics {
heading()
},
) )
if (trailingContent != null) { if (trailingContent != null) {
Spacer(Modifier.width(12.dp)) Spacer(Modifier.width(12.dp))

View file

@ -17,6 +17,8 @@ import androidx.compose.material3.LocalTextStyle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
@ -48,6 +50,9 @@ fun ListSectionHeader(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Text( Text(
modifier = Modifier.semantics {
heading()
},
text = title, text = title,
style = ElementTheme.typography.fontBodyLgMedium, style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textPrimary, color = ElementTheme.colors.textPrimary,

View file

@ -12,13 +12,12 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -28,7 +27,6 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue 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.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@ -91,33 +89,31 @@ fun MediaPlayerControllerView(
.widthIn(max = 480.dp), .widthIn(max = 480.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
val bgColor = if (state.isPlaying) { val colors = if (state.isPlaying) {
ElementTheme.colors.bgCanvasDefault IconButtonDefaults.iconButtonColors(
containerColor = ElementTheme.colors.bgCanvasDefault,
contentColor = ElementTheme.colors.iconPrimary,
)
} else { } else {
ElementTheme.colors.textPrimary IconButtonDefaults.iconButtonColors(
containerColor = ElementTheme.colors.iconPrimary,
contentColor = ElementTheme.colors.iconOnSolidPrimary,
)
} }
Box( IconButton(
modifier = Modifier modifier = Modifier
.size(36.dp) .size(36.dp),
.background( onClick = onTogglePlay,
color = bgColor, colors = colors,
shape = CircleShape,
)
.clip(CircleShape)
.clickable { onTogglePlay() }
.padding(8.dp),
contentAlignment = Alignment.Center,
) { ) {
if (state.isPlaying) { if (state.isPlaying) {
Icon( Icon(
imageVector = CompoundIcons.PauseSolid(), imageVector = CompoundIcons.PauseSolid(),
tint = ElementTheme.colors.iconPrimary,
contentDescription = stringResource(CommonStrings.a11y_pause) contentDescription = stringResource(CommonStrings.a11y_pause)
) )
} else { } else {
Icon( Icon(
imageVector = CompoundIcons.PlaySolid(), imageVector = CompoundIcons.PlaySolid(),
tint = ElementTheme.colors.iconOnSolidPrimary,
contentDescription = stringResource(CommonStrings.a11y_play) contentDescription = stringResource(CommonStrings.a11y_play)
) )
} }

View file

@ -120,6 +120,52 @@ fun MediaViewerView(
Scaffold( Scaffold(
modifier, modifier,
containerColor = Color.Transparent, containerColor = Color.Transparent,
topBar = {
AnimatedVisibility(
visible = showOverlay,
enter = fadeIn(),
exit = fadeOut(),
) {
when (currentData) {
is MediaViewerPageData.MediaViewerData -> {
MediaViewerTopBar(
data = currentData,
canShowInfo = state.canShowInfo,
onBackClick = onBackClick,
onShareClick = {
state.eventSink(MediaViewerEvent.Share(currentData))
},
onSaveClick = {
state.eventSink(MediaViewerEvent.SaveOnDisk(currentData))
},
onInfoClick = {
state.eventSink(MediaViewerEvent.OpenInfo(currentData))
},
)
}
else -> {
TopAppBar(
title = {
if (currentData is MediaViewerPageData.Loading) {
Text(
modifier = Modifier.semantics {
heading()
},
text = stringResource(id = CommonStrings.common_loading_more),
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textPrimary,
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = bgCanvasWithTransparency,
),
navigationIcon = { BackButton(onClick = onBackClick) },
)
}
}
}
},
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
) { ) {
val pagerState = rememberPagerState(state.currentIndex, 0f) { val pagerState = rememberPagerState(state.currentIndex, 0f) {
@ -186,69 +232,22 @@ fun MediaViewerView(
isUserSelected = (state.listData[page] as? MediaViewerPageData.MediaViewerData)?.eventId == state.initiallySelectedEventId, isUserSelected = (state.listData[page] as? MediaViewerPageData.MediaViewerData)?.eventId == state.initiallySelectedEventId,
) )
// Bottom bar // Bottom bar
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) { AnimatedVisibility(
Box( visible = showOverlay,
modifier = Modifier.fillMaxSize() enter = fadeIn(),
) { exit = fadeOut(),
MediaViewerBottomBar( modifier = Modifier.align(Alignment.BottomCenter),
modifier = Modifier.align(Alignment.BottomCenter), ) {
showDivider = dataForPage.mediaInfo.mimeType.isMimeTypeVideo(), MediaViewerBottomBar(
caption = dataForPage.mediaInfo.caption, showDivider = dataForPage.mediaInfo.mimeType.isMimeTypeVideo(),
onHeightChange = { bottomPaddingInPixels = it }, caption = dataForPage.mediaInfo.caption,
) onHeightChange = { bottomPaddingInPixels = it },
} )
} }
} }
} }
} }
} }
// Top bar
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
Box(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding()
) {
when (currentData) {
is MediaViewerPageData.MediaViewerData -> {
MediaViewerTopBar(
data = currentData,
canShowInfo = state.canShowInfo,
onBackClick = onBackClick,
onShareClick = {
state.eventSink(MediaViewerEvent.Share(currentData))
},
onSaveClick = {
state.eventSink(MediaViewerEvent.SaveOnDisk(currentData))
},
onInfoClick = {
state.eventSink(MediaViewerEvent.OpenInfo(currentData))
},
)
}
else -> {
TopAppBar(
title = {
if (currentData is MediaViewerPageData.Loading) {
Text(
modifier = Modifier.semantics {
heading()
},
text = stringResource(id = CommonStrings.common_loading_more),
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textPrimary,
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = bgCanvasWithTransparency,
),
navigationIcon = { BackButton(onClick = onBackClick) },
)
}
}
}
}
} }
when (val bottomSheetState = state.mediaBottomSheetState) { when (val bottomSheetState = state.mediaBottomSheetState) {
@ -373,11 +372,12 @@ private fun MediaViewerPage(
isUserSelected = isUserSelected, isUserSelected = isUserSelected,
audioFocus = audioFocus, audioFocus = audioFocus,
) )
ThumbnailView( if (showThumbnail) {
mediaInfo = data.mediaInfo, ThumbnailView(
thumbnailSource = data.thumbnailSource, mediaInfo = data.mediaInfo,
isVisible = showThumbnail, thumbnailSource = data.thumbnailSource,
) )
}
if (showError) { if (showError) {
ErrorView( ErrorView(
errorMessage = stringResource(id = CommonStrings.error_unknown), errorMessage = stringResource(id = CommonStrings.error_unknown),
@ -603,7 +603,6 @@ private val maxCaptionHeightLandscape = 128.dp
@Composable @Composable
private fun ThumbnailView( private fun ThumbnailView(
thumbnailSource: MediaSource?, thumbnailSource: MediaSource?,
isVisible: Boolean,
mediaInfo: MediaInfo, mediaInfo: MediaInfo,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -611,21 +610,19 @@ private fun ThumbnailView(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (isVisible) { val mediaRequestData = MediaRequestData(
val mediaRequestData = MediaRequestData( source = thumbnailSource,
source = thumbnailSource, kind = MediaRequestData.Kind.File(mediaInfo.filename, mediaInfo.mimeType)
kind = MediaRequestData.Kind.File(mediaInfo.filename, mediaInfo.mimeType) )
) val alpha = if (LocalInspectionMode.current) 0.1f else 1f
val alpha = if (LocalInspectionMode.current) 0.1f else 1f AsyncImage(
AsyncImage( modifier = Modifier
modifier = Modifier .fillMaxSize()
.fillMaxSize() .alpha(alpha),
.alpha(alpha), model = mediaRequestData,
model = mediaRequestData, contentScale = ContentScale.Fit,
contentScale = ContentScale.Fit, contentDescription = null,
contentDescription = null, )
)
}
} }
} }

View file

@ -679,7 +679,7 @@ private fun TextInputBox(
.align(Alignment.CenterEnd), .align(Alignment.CenterEnd),
imageVector = CompoundIcons.InfoSolid(), imageVector = CompoundIcons.InfoSolid(),
tint = ElementTheme.colors.iconCriticalPrimary, tint = ElementTheme.colors.iconCriticalPrimary,
contentDescription = null, contentDescription = stringResource(CommonStrings.a11y_info),
) )
if (showBottomSheet) { if (showBottomSheet) {
CaptionWarningBottomSheet( CaptionWarningBottomSheet(

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.ui.utils.a11y
import android.app.Activity
import android.app.Application
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun hasExternalKeyboard(): Boolean {
val activity = requireNotNull(LocalActivity.current)
var hasExternalKeyboard by remember { mutableStateOf(activity.resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS) }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
DisposableEffect(Unit) {
val callback = object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {
// We do not have access to onActivityConfigurationChanged, so update the value when tha Activity is resumed
hasExternalKeyboard = activity.resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS
}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
}
activity.registerActivityLifecycleCallbacks(callback)
onDispose {
activity.unregisterActivityLifecycleCallbacks(callback)
}
}
}
return hasExternalKeyboard
}

View file

@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
package io.element.android.libraries.ui.utils.time package io.element.android.libraries.ui.utils.a11y
import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2119838c9649710465dc1b8610550ce101f3016a1a6511c3f6dadc715fb75862 oid sha256:33d583fac967f383a3d3535c4ac38aaccdcbf4a1d48323ff375a239dbce81838
size 82975 size 83494

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4c7dea18de5eabe820fe8670cd09d7b68160a97f31e4f1c474c790d829854ef7 oid sha256:b918ce7162d95c873f0029a657ee07fca2e34926e4c3cdb39eaeb10123a08721
size 25291 size 25340

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:98b39ded83aac1bb5befba6749c08a328b6855bcbd491c1a2f669c848ce72c31 oid sha256:478c8ee2d55bb2a9f99b8d83c6e0eb0b316237ff1a60a6a735b5eb06f7ca083e
size 22982 size 23029

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0f7aa184c28a4281a30a443b208bf3f64d9a0f85ad135ef50c999d40362c67d4 oid sha256:d62cf7beeba92ee47193a871becad07f50d27e6f6fa0b85f6c620c3f1b04b6ce
size 24670 size 24697

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1e13b61fcb56fbd64064bc35628957cb68507efb7fdc25280d6dbf1e6477addc oid sha256:d684c9ed8b2c38cdf7017194ca656ca8da161ab0a40ef3270c6e3988d8e7f144
size 22637 size 22664

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:dbd0574dba895e157ff5b69ab23f3b389d280538810b457d2860ae5d77331705 oid sha256:2b55fce1bf0d6b764aaed8eed0fbc1416c5aa1c5b6160775a641dfa10addb227
size 8256 size 8231

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7e87c0990dcaf2511a2a0573a04bf14c6a9a7d24e0291fed18aa3a9ea28da572 oid sha256:4ca666990cb421602e21be0dbf4ec075e8d3e94f9835464923bd31d2414d9d57
size 7543 size 7594

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1ed14962b63afa972c68a53d2366b9f00906bab2f3436220bb28643fcb1d56cb oid sha256:01726f688e5149a460cb1004dff0c7700302a3fe7179829603625dd369b20e78
size 7668 size 7659

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a50093fb47b3b17a2ccd1aeb2a10fb6b5d368add8f7d458e50f0cc8643128b7c oid sha256:8c30a496415f6c00f5ce3a01c775c78994992efd68b2548ecbb5b5ffac054cc1
size 8053 size 8041

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a45336123e1eec5ea4b21c6932c1c11d6024685f736454e585a0fb6e6a85ccb2 oid sha256:10f58df934f372a3b8c68c8e0c4ef61e5bfef242d65daa8f34f4999249bf9e8d
size 7701 size 7799

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:174f9f97fbce115d36fc5124d3301bd148444acdf24bdc933b5a3fb0754c883d oid sha256:6d452b48ef65df5257eb708553d667bee46b9d83b1c29a07119c54e342cf1bc2
size 7545 size 7527

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ed8342b56d749db4d862aaa82d5d312089494965c2879adf18e8fd0c94f8525f oid sha256:f10ca2d461e4078e46455e007eca2d1e9c9a20dbb6bc24c681fa5164e5f50efd
size 13476 size 13531

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:04a62974dd81e0786127501b8778f9d302db38a27c984d1b403884942d43accd oid sha256:da386980ce6a45e102727715b5d50f219e39dcb40a513abdfaab9fa725b8ea41
size 13209 size 13260

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:041669475888f2f0c3b2d34502ae72098547340d8c2422ea2a674d38eb6a6241 oid sha256:d265f0c3fe5bc7f5a6e6d6cfa022ba16ca770b65eeb9a742ba250ff55c4b066a
size 207620 size 207625

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c56b22d79924d1f463f01428d5be1c69c8068b94a953160fae59a9f6faa112ad oid sha256:f6870b9c1a5aad4257aa4bd7b13d0be5bea281778061d71c711495aadfaafdb8
size 206047 size 206057

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e72ef42ad838837ce2427af5677931f5db68f876eccc297894c7a2cdd437cffa oid sha256:094c1de97c33431390f3ff71d73019d9af9a62ed57ba98c94e53f1f9680851fa
size 210729 size 210728

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:58a323c4f06745a877ea546870fbe69ec927aea50584001b3871cad833fca1d4 oid sha256:2a17d0f4bc47305b9c8a3866f70cdddb16fcb5ba10c8007c8739a329025e846a
size 211343 size 211342

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:55671428c52d37c3f63f8b3410817d4e893e2f4327b31b227bc7b7d9ff84b884 oid sha256:b593f6827598052cbde0e580dcd43b92fb098f6755779b95a60bb691d9ad5003
size 134680 size 134711

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f7bee65567cced59131d471b5c3795347a2ef52cf64ff74b47f1c983a0b4a36f oid sha256:c0249f33aca3e50713c87a3826e71985991f0996998132c42a374c6169800023
size 130718 size 130728

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f3cea16c42488306fe29fd22362a0d37c1eeb0fbb1db48f26ad7ff18f6b196ae oid sha256:43ab4873fc5fc812bf18af50cbe620c83b273ee70305b5ea06a7aeabdf8dbc93
size 137605 size 137637

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:58655e1846dbe73c287073ac909ee6c881a0ad800e8852653116fdbfd043b9a0 oid sha256:33048dafab286cf0e5c84040c034d93a42d69c234edd53132d918a4a1c135ece
size 137881 size 137908