Detect RTL text in events and adjust the text rendering (#6994)

* Add `TextDirection.detect` to check the overall LTR or RTL direction of texts

* Calculate if the contents of events have a different direction than the layout to reverse the layout direction if needed.

LTR content, RTL text -> mismatched direction, reverse.
RTL content, LTR text -> also mismatched, reversed too.

* Measure text in `measureLastTextLine` based on its direction, not the layout's,
to avoid some issues with incorrectly calculated free space.

* Tweak the paddings in `TimelineEventTimestampView` a bit if the layout and text directions don't match

* Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa
2026-06-29 15:11:49 +02:00
committed by GitHub
parent 46460e1c27
commit 6068aeaf01
34 changed files with 293 additions and 77 deletions
@@ -43,6 +43,7 @@ fun TimelineEventTimestampView(
event: TimelineItem.Event,
eventSink: (TimelineEvent.TimelineItemEvent) -> Unit,
modifier: Modifier = Modifier,
isLayoutDirectionMismatched: Boolean = false,
) {
val formattedTime = event.sentTime
val hasError = event.failedToSend
@@ -77,9 +78,16 @@ fun TimelineEventTimestampView(
else -> Modifier
}
}
val padding = if (!isLayoutDirectionMismatched) {
PaddingValues(start = TimelineEventTimestampViewDefaults.spacing)
} else {
PaddingValues(end = TimelineEventTimestampViewDefaults.spacing)
}
Row(
modifier = Modifier
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
.padding(padding)
// For a better click target, make the corners rounded
.clip(RoundedCornerShape(8.dp))
.then(clickableModifier)
@@ -39,7 +39,9 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.res.pluralStringResource
@@ -49,9 +51,12 @@ import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.constraintlayout.compose.ConstrainScope
@@ -68,12 +73,14 @@ import io.element.android.features.messages.impl.timeline.components.receipt.Rea
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
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.aTimelineItemImageContent
@@ -91,6 +98,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.modifiers.niceClickable
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewWithExtraLargeHeight
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
import io.element.android.libraries.designsystem.swipe.SwipeableActionsState
@@ -106,6 +114,7 @@ import io.element.android.libraries.matrix.api.timeline.item.EmbeddedEventInfo
import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
@@ -122,7 +131,9 @@ import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonPlurals
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.libraries.ui.utils.text.detect
import io.element.android.wysiwyg.link.Link
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.roundToInt
@@ -649,23 +660,44 @@ private fun MessageEventBubbleContent(
.padding(horizontal = 4.dp, vertical = 2.dp)
)
}
TimestampPosition.Aligned ->
ContentAvoidingLayout(
modifier = modifier,
// The spacing is negative to make the content overlap the empty space at the start of the timestamp
spacing = (-4).dp,
overlayOffset = DpOffset(0.dp, -1.dp),
shrinkContent = canShrinkContent,
content = { content(this::onContentLayoutChange) },
overlay = {
TimelineEventTimestampView(
event = event,
eventSink = eventSink,
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 4.dp)
)
TimestampPosition.Aligned -> @Composable {
val originalLayoutDirection = LocalLayoutDirection.current
// Detect if the direction of the text content (if any) does not match the layout direction, to place the content and timestamp correctly
val contentDirection = if (event.content is TimelineItemTextContent) {
remember(event.content.body) {
when (TextDirection.detect(event.content.body)) {
TextDirection.Ltr, TextDirection.ContentOrLtr -> LayoutDirection.Ltr
TextDirection.Rtl, TextDirection.ContentOrRtl -> LayoutDirection.Rtl
else -> originalLayoutDirection
}
}
)
} else {
originalLayoutDirection
}
CompositionLocalProvider(LocalLayoutDirection provides contentDirection) {
ContentAvoidingLayout(
modifier = modifier,
// The spacing is negative to make the content overlap the empty space at the start of the timestamp
spacing = (-4).dp,
overlayOffset = DpOffset(0.dp, -1.dp),
shrinkContent = canShrinkContent,
content = { content(this::onContentLayoutChange) },
overlay = {
// Use the original layout direction for the timestamp
CompositionLocalProvider(LocalLayoutDirection provides originalLayoutDirection) {
TimelineEventTimestampView(
event = event,
eventSink = eventSink,
isLayoutDirectionMismatched = originalLayoutDirection != contentDirection,
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
)
}
}
TimestampPosition.Below ->
Column(modifier) {
content {}
@@ -879,6 +911,149 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview {
}
}
@PreviewWithExtraLargeHeight
@Composable
internal fun TimelineItemEventRowRtlContentPreview() = ElementPreview {
Column {
Text(
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray)
.padding(8.dp),
text = "LTR layout direction",
textAlign = TextAlign.Center,
style = ElementTheme.typography.fontHeadingMdBold,
)
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
sequenceOf(false, true).forEach { isMine ->
ATimelineItemEventRow(
event = aTimelineItemEvent(
senderDisplayName = "Sender with a super long name that should ellipsize",
isMine = isMine,
content = aTimelineItemTextContent(
body = "ظَة وَدَاع يَسْتَغْرِب فِيهَ"
),
timelineItemReactions = TimelineItemReactions(persistentListOf()),
groupPosition = TimelineItemGroupPosition.First,
)
)
ATimelineItemEventRow(
event = aTimelineItemEvent(
senderDisplayName = "Sender with a super long name that should ellipsize",
isMine = isMine,
content = aTimelineItemTextContent(
body = "ظَة وَدَاع يَسْتَغْرِب فِيهَ",
isEdited = true,
),
timelineItemReactions = TimelineItemReactions(persistentListOf()),
groupPosition = TimelineItemGroupPosition.Middle,
messageShield = MessageShield.UnknownDevice(isCritical = true),
)
)
ATimelineItemEventRow(
event = aTimelineItemEvent(
senderDisplayName = "Sender with a super long name that should ellipsize",
isMine = isMine,
content = aTimelineItemTextContent(
body = "ظَة وَدَاع \nيَسْتَغْرِب فِيهَ"
),
timelineItemReactions = TimelineItemReactions(persistentListOf()),
groupPosition = TimelineItemGroupPosition.Middle,
)
)
ATimelineItemEventRow(
event = aTimelineItemEvent(
senderDisplayName = "Sender with a super long name that should ellipsize",
isMine = isMine,
content = aTimelineItemTextContent(
body = "ظَة وَدَاع يَسْتَغْرِب فِيهَا اَلشَّاعِر أَنْ لَا يَبْكِي مِنْ أَلَم اَلْفِرَاق،" +
" وَيَصِف حَالَة اَلْمُودِعِينَ"
),
timelineItemReactions = TimelineItemReactions(persistentListOf()),
groupPosition = TimelineItemGroupPosition.Last,
)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray)
.padding(8.dp),
text = "RTL layout direction",
textAlign = TextAlign.Center,
style = ElementTheme.typography.fontHeadingMdBold,
)
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
sequenceOf(false, true).forEach { isMine ->
ATimelineItemEventRow(
event = aTimelineItemEvent(
senderDisplayName = "Sender with a super long name that should ellipsize",
isMine = isMine,
content = aTimelineItemTextContent(
body = "ظَة وَدَاع يَسْتَغْرِب فِيهَا اَلشَّاعِر أَنْ لَا يَبْكِي مِنْ أَلَم اَلْفِرَاق،" +
" وَيَصِف حَالَة اَلْمُودِعِينَ",
),
timelineItemReactions = TimelineItemReactions(persistentListOf()),
groupPosition = TimelineItemGroupPosition.First,
)
)
ATimelineItemEventRow(
event = aTimelineItemEvent(
senderDisplayName = "Sender with a super long name that should ellipsize",
isMine = isMine,
content = aTimelineItemTextContent(
body = "Testing\nLTR Line\nBreaks.",
),
timelineItemReactions = TimelineItemReactions(persistentListOf()),
groupPosition = TimelineItemGroupPosition.Middle,
)
)
ATimelineItemEventRow(
event = aTimelineItemEvent(
senderDisplayName = "Sender with a super long name that should ellipsize",
isMine = isMine,
content = aTimelineItemTextContent(
body = "Testing a very long LTR text in an RTL layout."
),
timelineItemReactions = TimelineItemReactions(persistentListOf()),
groupPosition = TimelineItemGroupPosition.Middle,
)
)
ATimelineItemEventRow(
event = aTimelineItemEvent(
senderDisplayName = "Sender with a super long name that should ellipsize",
isMine = isMine,
content = aTimelineItemTextContent(
body = "Testing LTR in RTL layout.",
),
timelineItemReactions = TimelineItemReactions(persistentListOf()),
groupPosition = TimelineItemGroupPosition.Last,
messageShield = MessageShield.UnknownDevice(isCritical = true),
)
)
ATimelineItemEventRow(
event = aTimelineItemEvent(
senderDisplayName = "Sender with a super long name that should ellipsize",
isMine = isMine,
content = aTimelineItemTextContent(
body = "Testing LTR in RTL layout.",
isEdited = true,
),
timelineItemReactions = TimelineItemReactions(persistentListOf()),
groupPosition = TimelineItemGroupPosition.Last,
messageShield = MessageShield.UnknownDevice(isCritical = true),
)
)
}
}
}
}
@PreviewsDayNight
@Composable
internal fun ThreadSummaryViewPreview() {
@@ -15,13 +15,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.designsystem.text.roundToPx
import io.element.android.wysiwyg.compose.EditorStyledText
import kotlin.math.max
@@ -149,13 +149,13 @@ object ContentAvoidingLayout {
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
extraWidth: Dp = 0.dp,
): ((TextLayoutResult) -> Unit) {
val layoutDirection = LocalLayoutDirection.current
val extraWidthPx = extraWidth.roundToPx()
return { textLayout: TextLayoutResult ->
// We need to add the external extra width so it's not taken into account as 'free space'
val lastLineWidth = when (layoutDirection) {
LayoutDirection.Ltr -> textLayout.getLineRight(textLayout.lineCount - 1).roundToInt()
LayoutDirection.Rtl -> textLayout.getLineLeft(textLayout.lineCount - 1).roundToInt()
val textDirection = tryOrNull { textLayout.getParagraphDirection(0) }
val lastLineWidth = when (textDirection) {
ResolvedTextDirection.Rtl -> textLayout.getLineLeft(textLayout.lineCount - 1).roundToInt()
else -> textLayout.getLineRight(textLayout.lineCount - 1).roundToInt()
}
val lastLineHeight = textLayout.getLineBottom(textLayout.lineCount - 1).roundToInt()
onContentLayoutChange(
+1
View File
@@ -130,6 +130,7 @@ androidx_compose_material3 = { module = "androidx.compose.material3:material3",
androidx_compose_material3_windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class" }
androidx_compose_material3_adaptive = "androidx.compose.material3:material3-adaptive-android:1.0.0-alpha06"
androidx_compose_ui = { module = "androidx.compose.ui:ui" }
androidx_compose_ui_text = { module = "androidx.compose.ui:ui-text" }
androidx_compose_ui_tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx_compose_ui_tooling_preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx_compose_ui_test_manifest = { module = "androidx.compose.ui:ui-test-manifest" }
+1
View File
@@ -19,6 +19,7 @@ android {
dependencies {
implementation(projects.libraries.androidutils)
implementation(projects.services.toolbox.impl)
implementation(libs.androidx.compose.ui.text)
testCommonDependencies(libs)
}
@@ -0,0 +1,28 @@
/*
* 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.text
import androidx.compose.ui.text.style.TextDirection
/**
* Detect if a text is RTL or not, based on its content.
*/
fun TextDirection.Companion.detect(text: String): TextDirection {
return if (text.any(::isRtlChar)) TextDirection.Rtl else TextDirection.Ltr
}
/**
* Detect if a character is an RTL character or not, based on its Unicode directionality.
*/
fun isRtlChar(char: Char): Boolean = when (Character.getDirectionality(char)) {
Character.DIRECTIONALITY_RIGHT_TO_LEFT,
Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC,
Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING,
Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE -> true
else -> false
}
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:86eac241e9084cc830ff13d231908da2eb0dff246aa5cac64dddaac68a5d5f13
size 295298
oid sha256:4420dfe9f6f95411093eb1c17825a389a31d7a5d5ef146129e5872cdd5d87aff
size 295295
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d6441c572f64d99bc119d649e5e6422fe516b9d097f310ccbd99a9ddded9609f
size 612494
oid sha256:3f358da2b9b0521c14e525bcb2bd41c29c96aacaf5f007546e6d362bde2ee3fb
size 612526
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e3bb005a88e9a4d2bb850478ee8ac2c4f31d32625bd6d2abf455fade461cd399
size 607806
oid sha256:54e71f45db31e8664c42f8c05feb91ae839589e00cb89d12088c15c85fb9f25f
size 607824
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f9e6d26823bf16cb281760fa157685edcfef3937dc7b13a7bd22387d77fc6b9b
size 54561
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:76e5ae44362792d570cd82f3910c8bd0acde509b8418c5892e0385e7d636f042
size 370223
oid sha256:8344427a0fe87cbf0883ff53a9ee4dcf90976a7011f6618d85961df48b2c53a9
size 370228
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2f0d24b8011564ee7a8b2f8c2e6a24eda668ee42d451d862f884acb7926aa06c
size 368529
oid sha256:c00e504593d8eb61b598f05f46ee4d591757975a80c0b4227b240847d2cd4868
size 368510
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:991deacdfb7bc371a2df960a2364ae3b2187e588aa5afe7f39409e367040dd9c
size 363893
oid sha256:a4ac79f3b8fb4e231244bd96b244a4f2526d6ed28d558f9d50734d83bbb74992
size 363909
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2cd357a1fbaa425eca95587bff2f768a45e773f983f75664875653ba1900e534
size 361625
oid sha256:c1e15400fa2c5ee6087316501445c551a3932b65e16de31e68acc5efbd16a559
size 361603
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4c410e853089cc2c4a2c48a00dd9a8babc8095aed72bb4625057444710559c6c
size 368129
oid sha256:41a0e404dea7f52359b24b3ec3926b1405429b165cfed44c63b3922ea6d3cb46
size 368114
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1a92f8af94682facac66d4aacb6e49f345561c15be2f8b689041b78aeaaae831
size 345062
oid sha256:6ca8cf82aa8ab1bdad56a27584b27789402aeb7ad8fadd038280a376fb75a72c
size 345094
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f0c6dafaa41849af00d5097a85bd88e3a054a0a9e41c0466a08e6ff8dd6102b2
size 356857
oid sha256:dce23463c335d01eb67293cfcede5d85e96ab2ba035cc9a188d42e8e6866bc63
size 356831
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2528df873713755f542f61cd794080929586c1bd5f316c0378ce9e837c21d50b
size 397145
oid sha256:426d1c844a8c88281c5fa4c61916d93278fdfd38f8cdf2dc54bf1a821f2de3eb
size 397167
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9daeee1aaa04247da3c8bb5aaae9947aab1ed2d2057995e53d6ebdb63e5cd356
size 355848
oid sha256:ffa68c8398d789444151855496e5df4ffa03218a383db6ea46fd4f979829b823
size 355831
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7b23d3a37774283067a8919b8c332b1bb5a79e90dceb7216b484b985491282e5
size 355389
oid sha256:3ec1b0244dab22414c6719b3309426e13128d0c70fb67dc66cd8a60d42521fd0
size 355411
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a92bcf5885322e11a706071969fb674b1f6e60e021af153aaa125baa4a602ae
size 366285
oid sha256:9a85db2f5958db867578663533f204b2e803372d470da890fd8056ffed0cf46c
size 366254
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:54372845e922d047408dd960d048b9b5eae2263d589117d8e7d197741aa34f32
size 342827
oid sha256:4bee12a6bdb18fea5ac6997671bfcb903296da7278cf69b2456330066ca31dfb
size 342870
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6524cc87257cc9d3f135904f4e14594b56df695e71fe576418d2319c472d710b
size 355285
oid sha256:9e2d8caf5aaae7d3406c703288e491f328552f0acd18e56afd5265b4ceaf1cd1
size 355272
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e642229c58169775fe702769a306bbef22698516ae6f7ac75a761213c3f195fa
size 395446
oid sha256:217ac95bf476c53d2dc6d3d0143b21312bf38b8ab86c8dfd4f9360582b584f9d
size 395476
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5ed24d190f10c079f8cf176404372d40eb25cc78ce595a22febc540315057b86
size 354075
oid sha256:dabad9dcd5b8f0d4bb964a0187ad7c94b023b04552dacd29a077daddfd48d550
size 354052
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e614f4a8657637c584a283d69124236d00fe1251c2b8d739c5688a57a6fd152e
size 353693
oid sha256:756f0cff90ddd61adce2f467c0384d74d430f6d51c091e4980e45bcc9ee6f0fd
size 353705
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:26e14b77fa3c3aeaea58782309e6de8b9047e07945009322990ebf5f4c283309
size 52770
oid sha256:28a95d4b7ebca92c4e0467f3ecd9acdaf7f6257604d190f5d088a9a25e87d637
size 55305
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e5a2e306087a967849f61872b56a83f194d1a76df5723f0bbae6e6e2f4a98c63
size 55725
oid sha256:0ce2519906f1aaf34700ca9ba4bdeaa72030f1f5aff3f7e24352a1435ac67d98
size 53966
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:21aee17ee9a292bb2f0d3d2ec2db565ebb8d5df753454a5a002f033ed5ed39c5
size 3853
oid sha256:e8a3ad99763929d2783147814e44fa9ec7e7efb7b99949e3580f23c38fb5d971
size 9398
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1342b6427f5b2e3075ae3ccee60bf1d3b11afaccf3e152643d6ef2d65c21b54e
size 9108
oid sha256:21aee17ee9a292bb2f0d3d2ec2db565ebb8d5df753454a5a002f033ed5ed39c5
size 3853
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:410a8596d4c70b9a3856d959d6d71ade8e831e8581ff410c9900957a93b3eb50
size 8114
oid sha256:520cae2544153ba45f8fe4b021286c2d22da06aa1b60be7f1b13879723ff994c
size 3664
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:520cae2544153ba45f8fe4b021286c2d22da06aa1b60be7f1b13879723ff994c
size 3664
oid sha256:0f3a0cb5d9bae6725a1c527f3d9575aabf85d3edf63a28d9faa23f4f0b65e2d0
size 7624
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58b9491aaa5554b89d86c62a5c9c44975367538c6baa5562f8dc5768f76d8264
size 8705
oid sha256:4811667dec0f32accc5e2c8a5dd5c0b34017fcff8d1e0376ee7a4708426bca0f
size 9838
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d57df7110371c9224e232b707d6a1b10ce14afcb56c803a78220792f2a0d4cf2
size 11354
oid sha256:ef0dc4d53daeb986f378b6e4d85928b4b6729336c89cf21e642c88c53688a3ab
size 10292