Merge pull request #912 from vector-im/feature/dla_custom_reactions_layout

Reactions layout improvements
This commit is contained in:
David Langley 2023-07-21 11:00:43 +01:00 committed by GitHub
commit 4c87138a48
66 changed files with 372 additions and 196 deletions

View file

@ -117,6 +117,7 @@ private fun TextContent(
.height(reactionEmojiLineHeight.toDp()), .height(reactionEmojiLineHeight.toDp()),
text = text, text = text,
style = ElementTheme.typography.fontBodyMdRegular, style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.materialColors.primary
) )
@Composable @Composable
@ -126,7 +127,7 @@ private fun IconContent(
) = Icon( ) = Icon(
imageVector = imageVector, imageVector = imageVector,
contentDescription = stringResource(id = R.string.screen_room_timeline_add_reaction), contentDescription = stringResource(id = R.string.screen_room_timeline_add_reaction),
tint = MaterialTheme.colorScheme.secondary, tint = ElementTheme.materialColors.secondary,
modifier = modifier modifier = modifier
.size(reactionEmojiLineHeight.toDp()) .size(reactionEmojiLineHeight.toDp())
) )

View file

@ -291,7 +291,7 @@ private fun TimelineItemEventRowContent(
if (event.reactionsState.reactions.isNotEmpty()) { if (event.reactionsState.reactions.isNotEmpty()) {
TimelineItemReactions( TimelineItemReactions(
reactionsState = event.reactionsState, reactionsState = event.reactionsState,
mainAxisAlignment = if (event.isMine) FlowMainAxisAlignment.End else FlowMainAxisAlignment.Start, isOutgoing = event.isMine,
onReactionClicked = onReactionClicked, onReactionClicked = onReactionClicked,
onMoreReactionsClicked = { onMoreReactionsClicked(event) }, onMoreReactionsClicked = { onMoreReactionsClicked(event) },
modifier = Modifier modifier = Modifier

View file

@ -0,0 +1,208 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddReaction
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import io.element.android.features.messages.impl.R
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
/**
* A flow layout for reactions that will show a collapse/expand button when the layout wraps over a defined number of rows.
* It displays an add more button when there are greater than 0 reactions and always displays the reaction and add more button
* on the same row (moving them both to a new row if necessary).
* @param expandButton The expand button
* @param addMoreButton The add more button
* @param modifier The modifier to apply to this layout
* @param itemSpacing The horizontal spacing between items
* @param rowSpacing The vertical spacing between rows
* @param expanded Whether the layout should display in expanded or collapsed state
* @param rowsBeforeCollapsible The number of rows before the collapse/expand button is shown
* @param reactions The reaction buttons
*/
@Composable
fun TimelineItemReactionsLayout(
expandButton: @Composable () -> Unit,
addMoreButton: @Composable () -> Unit,
modifier: Modifier = Modifier,
itemSpacing: Dp = 0.dp,
rowSpacing: Dp = 0.dp,
expanded: Boolean = false,
rowsBeforeCollapsible: Int? = 2,
reactions: @Composable () -> Unit,
) {
SubcomposeLayout(modifier) { constraints ->
// Given the placeables and returns a structure representing
// how they should wrap on to multiple rows given the constraints max width.
fun calculateRows(measurables: List<Placeable>): List<List<Placeable>> {
val rows = mutableListOf<List<Placeable>>()
var currentRow = mutableListOf<Placeable>()
var rowX = 0
measurables.forEach { placeable ->
val horizontalSpacing = if (currentRow.isEmpty()) 0 else itemSpacing.toPx().toInt()
// If the current view does not fit on this row bump to the next
if (rowX + placeable.width > constraints.maxWidth) {
rows.add(currentRow)
currentRow = mutableListOf()
rowX = 0
}
rowX += horizontalSpacing + placeable.width
currentRow.add(placeable)
}
// If there are items in the current row remember to append it to the returned value
if (currentRow.size > 0) {
rows.add(currentRow)
}
return rows
}
// Used to render the collapsed state, this takes the rows inputted and adds the extra button to the last row,
// removing only as many trailing reactions as needed to make space for it.
fun replaceTrailingItemsWithButtons(rowsIn: List<List<Placeable>>, expandButton: Placeable, addMoreButton: Placeable): List<List<Placeable>> {
val rows = rowsIn.toMutableList()
val lastRow = rows.last()
val buttonsWidth = expandButton.width + itemSpacing.toPx().toInt() + addMoreButton.width
var rowX = 0
lastRow.forEachIndexed { i, placeable ->
val horizontalSpacing = if (i == 0) 0 else itemSpacing.toPx().toInt()
rowX += placeable.width + horizontalSpacing
if (rowX > (constraints.maxWidth - (buttonsWidth + horizontalSpacing))) {
val lastRowWithButton = lastRow.take(i) + listOf(expandButton, addMoreButton)
rows[rows.size - 1] = lastRowWithButton
return rows
}
}
val lastRowWithButton = lastRow + listOf(expandButton, addMoreButton)
rows[rows.size - 1] = lastRowWithButton
return rows
}
// To prevent the add more and expand buttons from wrapping on to separate lines.
// If there is one item on the last line, it moves the expand button down.
fun ensureCollapseAndAddMoreButtonsAreOnTheSameRow(rowsIn: List<List<Placeable>>): List<List<Placeable>> {
val lastRow = rowsIn.last().toMutableList()
if (lastRow.size != 1) {
return rowsIn
}
val rows = rowsIn.toMutableList()
val secondLastRow = rows[rows.size - 2].toMutableList()
val expandButtonPlaceable = secondLastRow.removeLast()
lastRow.add(0, expandButtonPlaceable)
rows[rows.size - 2] = secondLastRow
rows[rows.size - 1] = lastRow
return rows
}
/// Given a list of rows place them in the layout.
fun layoutRows(rows: List<List<Placeable>>): MeasureResult {
var width = 0
var height = 0
val placeables = rows.mapIndexed { i, row ->
var rowX = 0
var rowHeight = 0
val verticalSpacing = if (i == 0) 0 else rowSpacing.toPx().toInt()
val rowWithPoints = row.mapIndexed { j, placeable ->
val horizontalSpacing = if (j == 0) 0 else itemSpacing.toPx().toInt()
val point = IntOffset(rowX + horizontalSpacing, height + verticalSpacing)
rowX += placeable.width + horizontalSpacing
rowHeight = maxOf(rowHeight, placeable.height)
Pair(placeable, point)
}
height += rowHeight + verticalSpacing
width = maxOf(width, rowX)
rowWithPoints
}.flatten()
return layout(width = width, height = height) {
placeables.forEach {
val (placeable, origin) = it
placeable.placeRelative(origin.x, origin.y)
}
}
}
val reactionsPlaceables = subcompose(0, reactions).map { it.measure(constraints) }
if (reactionsPlaceables.isEmpty()) {
return@SubcomposeLayout layoutRows(listOf())
}
val addMorePlaceable = subcompose(1, addMoreButton).first().measure(constraints)
val expandPlaceable = subcompose(2, expandButton).first().measure(constraints)
// Calculate the layout of the rows with the reactions button and add more button
val reactionsAndAddMore = calculateRows(reactionsPlaceables + listOf(addMorePlaceable))
// If we have extended beyond the defined number of rows we are showing the expand/collapse ui
if (rowsBeforeCollapsible?.let { reactionsAndAddMore.size > it } == true) {
if (expanded) {
// Show all subviews with the add more button at the end
var reactionsAndButtons = calculateRows(reactionsPlaceables + listOf(expandPlaceable, addMorePlaceable))
reactionsAndButtons = ensureCollapseAndAddMoreButtonsAreOnTheSameRow(reactionsAndButtons)
layoutRows(reactionsAndButtons)
} else {
// Truncate to `rowsBeforeCollapsible` number of rows and replace the reactions at the end of the last row with the buttons
val collapsedRows = reactionsAndAddMore.take(rowsBeforeCollapsible)
val collapsedRowsWithButtons = replaceTrailingItemsWithButtons(collapsedRows, expandPlaceable, addMorePlaceable)
layoutRows(collapsedRowsWithButtons)
}
} else {
// Otherwise we are just showing all items without the expand button
layoutRows(reactionsAndAddMore)
}
}
}
@DayNightPreviews
@Composable
internal fun TimelineItemReactionsLayoutPreview() = ElementPreview {
TimelineItemReactionsLayout(
expanded = false,
expandButton = {
MessagesReactionButton(
content = MessagesReactionsButtonContent.Text(
text = stringResource(id = R.string.screen_room_timeline_less_reactions)
),
onClick = { },
)
},
addMoreButton = {
MessagesReactionButton(
content = MessagesReactionsButtonContent.Icon(Icons.Outlined.AddReaction),
onClick = {}
)
},
reactions = {
io.element.android.features.messages.impl.timeline.aTimelineItemReactions(count = 18).reactions.forEach {
MessagesReactionButton(
content = MessagesReactionsButtonContent.Reaction(
it
),
onClick = {}
)
}
}
)
}

View file

@ -19,18 +19,16 @@ package io.element.android.features.messages.impl.timeline.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddReaction import androidx.compose.material.icons.outlined.AddReaction
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowMainAxisAlignment
import com.google.accompanist.flowlayout.FlowRow
import io.element.android.features.messages.impl.R import io.element.android.features.messages.impl.R
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
@ -38,162 +36,119 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemReac
import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
/**
* The maximum number of items that can be displayed before some items will be hidden
*
* TODO The threshold should be based on the number of rows, rather than items.
* Once items would spill onto a third row, they should be hidden.
* Note this could be particularly worthwhile to handle reactions that are
* longer than a single character (as annotation keys are free text).
*/
private const val COLLAPSE_ITEMS_THRESHOLD = 8
@Composable @Composable
fun TimelineItemReactions( fun TimelineItemReactions(
reactionsState: TimelineItemReactions, reactionsState: TimelineItemReactions,
mainAxisAlignment: FlowMainAxisAlignment, isOutgoing: Boolean,
onReactionClicked: (emoji: String) -> Unit, onReactionClicked: (emoji: String) -> Unit,
onMoreReactionsClicked: () -> Unit, onMoreReactionsClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var expanded: Boolean by rememberSaveable { mutableStateOf(false) } var expanded: Boolean by rememberSaveable { mutableStateOf(false) }
val reactions by remember(reactionsState, expanded) { // In LTR languages we want an incoming message's reactions to be LRT and outgoing to be RTL.
derivedStateOf { // For RTL languages it should be the opposite.
val numToDisplay = if (expanded) { val reactionsLayoutDirection = if (!isOutgoing) LocalLayoutDirection.current
reactionsState.reactions.count() else if (LocalLayoutDirection.current == LayoutDirection.Ltr)
} else { LayoutDirection.Rtl
COLLAPSE_ITEMS_THRESHOLD else
} LayoutDirection.Ltr
reactionsState.reactions.take(numToDisplay).toPersistentList()
} CompositionLocalProvider(LocalLayoutDirection provides reactionsLayoutDirection) {
TimelineItemReactionsView(
modifier = modifier,
reactions = reactionsState.reactions,
expanded = expanded,
onReactionClick = onReactionClicked,
onMoreReactionsClick = onMoreReactionsClicked,
onToggleExpandClick = { expanded = !expanded },
)
} }
val expandableState by remember {
derivedStateOf {
if (expanded) {
ExpandableState.Expanded
} else {
val hiddenItems = reactionsState.reactions.count() - reactions.count()
if (hiddenItems > 0) {
ExpandableState.Collapsed(hidden = hiddenItems)
} else {
ExpandableState.None
}
}
}
}
TimelineItemReactionsView(
modifier = modifier,
reactions = reactions,
expandableState = expandableState,
mainAxisAlignment = mainAxisAlignment,
onReactionClick = onReactionClicked,
onMoreReactionsClick = onMoreReactionsClicked,
onExpandClick = { expanded = true },
onCollapseClick = { expanded = false }
)
}
private sealed class ExpandableState {
object None: ExpandableState()
data class Collapsed(val hidden: Int): ExpandableState()
object Expanded : ExpandableState()
} }
@Composable @Composable
private fun TimelineItemReactionsView( private fun TimelineItemReactionsView(
reactions: ImmutableList<AggregatedReaction>, reactions: ImmutableList<AggregatedReaction>,
expandableState: ExpandableState, expanded: Boolean,
mainAxisAlignment: FlowMainAxisAlignment,
onReactionClick: (emoji: String) -> Unit, onReactionClick: (emoji: String) -> Unit,
onMoreReactionsClick: () -> Unit, onMoreReactionsClick: () -> Unit,
onExpandClick: () -> Unit, onToggleExpandClick: () -> Unit,
onCollapseClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) = ) = TimelineItemReactionsLayout(
FlowRow( modifier = modifier,
modifier = modifier, itemSpacing = 4.dp,
mainAxisSpacing = 4.dp, rowSpacing = 4.dp,
crossAxisSpacing = 4.dp, expanded = expanded,
mainAxisAlignment = mainAxisAlignment, expandButton = {
) { MessagesReactionButton(
content = MessagesReactionsButtonContent.Text(
text = stringResource(id = if (expanded) R.string.screen_room_reactions_show_less else R.string.screen_room_reactions_show_more)
),
onClick = onToggleExpandClick,
)
},
addMoreButton = {
MessagesReactionButton(
content = MessagesReactionsButtonContent.Icon(Icons.Outlined.AddReaction),
onClick = onMoreReactionsClick
)
},
reactions = {
reactions.forEach { reaction -> reactions.forEach { reaction ->
MessagesReactionButton( MessagesReactionButton(
content = MessagesReactionsButtonContent.Reaction(reaction = reaction), content = MessagesReactionsButtonContent.Reaction(reaction = reaction),
onClick = { onReactionClick(reaction.key) } onClick = { onReactionClick(reaction.key) }
) )
} }
when (expandableState) {
ExpandableState.Expanded ->
MessagesReactionButton(
content = MessagesReactionsButtonContent.Text(
text = stringResource(id = R.string.screen_room_timeline_less_reactions)
),
onClick = onCollapseClick,
)
is ExpandableState.Collapsed -> {
val hidden = expandableState.hidden
MessagesReactionButton(
content = MessagesReactionsButtonContent.Text(
text = pluralStringResource(id = R.plurals.screen_room_timeline_more_reactions, hidden, hidden)
),
onClick = onExpandClick,
)
}
ExpandableState.None -> {
// No expand or collapse action available
}
}
MessagesReactionButton(
content = MessagesReactionsButtonContent.Icon(Icons.Outlined.AddReaction),
onClick = onMoreReactionsClick
)
} }
)
@DayNightPreviews @DayNightPreviews
@Composable @Composable
fun TimelineItemReactionsViewPreview() = ElementPreview { fun TimelineItemReactionsViewPreview() = ElementPreview {
ContentToPreview( ContentToPreview(
reactions = aTimelineItemReactions(count = 1).reactions, reactions = aTimelineItemReactions(count = 1).reactions
expandableState = ExpandableState.None,
) )
} }
@DayNightPreviews @DayNightPreviews
@Composable @Composable
fun TimelineItemReactionsViewCollapsedPreview() = ElementPreview { fun TimelineItemReactionsViewFewPreview() = ElementPreview {
ContentToPreview( ContentToPreview(
reactions = aTimelineItemReactions(count = 3).reactions, reactions = aTimelineItemReactions(count = 3).reactions
expandableState = ExpandableState.Collapsed(hidden = 7),
) )
} }
@DayNightPreviews @DayNightPreviews
@Composable @Composable
fun TimelineItemReactionsViewExpandedPreview() = ElementPreview { fun TimelineItemReactionsViewIncomingPreview() = ElementPreview {
ContentToPreview( ContentToPreview(
reactions = aTimelineItemReactions(count = 10).reactions, reactions = aTimelineItemReactions(count = 18).reactions
expandableState = ExpandableState.Expanded, )
}
@DayNightPreviews
@Composable
fun TimelineItemReactionsViewOutgoingPreview() = ElementPreview {
ContentToPreview(
reactions = aTimelineItemReactions(count = 18).reactions,
isOutgoing = true
) )
} }
@Composable @Composable
private fun ContentToPreview( private fun ContentToPreview(
reactions: ImmutableList<AggregatedReaction>, reactions: ImmutableList<AggregatedReaction>,
expandableState: ExpandableState isOutgoing: Boolean = false
) { ) {
TimelineItemReactionsView( TimelineItemReactions(
reactions = reactions, reactionsState = TimelineItemReactions(
expandableState = expandableState, reactions
mainAxisAlignment = FlowMainAxisAlignment.Center, ),
onReactionClick = {}, isOutgoing = isOutgoing,
onMoreReactionsClick = {}, onReactionClicked = {},
onExpandClick = {}, onMoreReactionsClicked = {},
onCollapseClick = {}
) )
} }

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:724bc9b0db8f581119201a8db3c405c9a7e7261cccea45a354fd37a5e33fcb41 oid sha256:6c73a80923ec5b269bbb5f5108dde9617dac39e9c51f9199ab9229c4dd7ca2b0
size 11141 size 11532

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:109a1a9c6359a9fd7fe4258f022e845c9b8142f9e590a03e5c2efa9273cb572f oid sha256:9eee3a5873231554cc0335fe7b552a62459f35e4ffb8c43b91a9746b34314bdc
size 10742 size 11171

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:38c6d3f4a47ed89c3deb4150024b49c0a533a859f21a1592a82309b8c7316ea4 oid sha256:85f9960cebee2e04d09f3ef17bd81e9adf9f463b24edae73a56d6d0c0a09ce48
size 152242 size 152228

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f82840398e396e038ad68a5fe855394a0088c413e7aade339998b8ed63fb1c09 oid sha256:39226828ae899c8b765827cad36f9966ec80c0a39ed406c1257224a56255af9b
size 157273 size 157243

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7c5ef7519aef3badeda3983b2ec5474f31404bc06b4b46b9829848dc7884fe1d oid sha256:88bdceab1a4e44f971ce507b5b6aeb5513657dfe6c4d61ec5483e215d83254d5
size 81749 size 81534

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c9e9b769422e4714e87ace97058e5a8f064e3b927e3a7c6042494de8381d6b09 oid sha256:461683659d023323184049e122d02e4c63f217157e0bb3165f50be833d19ba7e
size 85856 size 85517

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4970830b042e7b6588c03bf632ae3ade5b39de7339e27618ee341cbd9861c0e9 oid sha256:c00bf46d1ef6337bad18f6454cf4601141ed2190c6a697ddc46d5a156d4997ca
size 129351 size 127950

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4aba48e03e8424e5ff131c22fd5a606280745dd318f5822e2167f3b39794ff41 oid sha256:88cd76c95c31061bf4e9e7528ed5d66a8b49813d868f21b2d2615e77318a9706
size 134569 size 133068

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c81a4a854304b62bd356ae9a6d588918d11bf1df0aaa209910fa2fa970f47cb8
size 26684

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f32e1f9f7c10672b3a35d6c0c3e3a9347b92ef3fe62be40bc378a5225da5d3d6
size 26285

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4e892d14d11fb576228418acd216468438f41a631a073e80638675ec48a91ed6
size 12334

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:64c2e18e97bba9ff6ae6bb24a17fe567352cd49f1b5905065b7de8854369e22f
size 12209

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6d0d29cc0acc0d9009c6fd6aebccf72d6244cec8ec3e3bf3bd445e7cced52561
size 26004

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dbfe5166cb1df7be35f2d3f114f73fb4425a10f4eb5bd24a87cafe3ddd76e873
size 25719

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d5f4241f356099cc2f2f55e557218c8872cd0f25f582ba75db254c11613090db
size 26026

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf149f6a8c5c8d5d4591b9f6cfe8612a1af2b1cb101b0d1f699945bd711890b4
size 25685

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:54c12970e3563de958f88e4c538dd368f9810266060627393256a91741f7c6cf oid sha256:cfc4f0cc8e252cdc9200286cf330c4bbdf3cd66ced2c23364cca680aca79954d
size 53340 size 53293

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3b7ae3084cb9d1ecee2e4db49c228516bdd7352683e797edf737c8d216922dec oid sha256:01c7f94b759eea738fb42baa4b8b835c27d48e3fbfdd40d0a925080db8196a80
size 65601 size 65563

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:dd034d439c08793e0dfd59f6bd5dcd88c06ded6cbe98172bf3cf296888e6d575 oid sha256:e4360b6f5be55e5b468e735f83b6263ae3b0f1a3f7c0084e6dc3c07396723abd
size 51244 size 51206

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d9d835cb1a420117b4d967181e2ca0fad71ba243d6a17bea08b82cae41f6b8e2 oid sha256:0a58bb336bbf56b05584d73fa7fd3bd2e2fe97c82829a01988343d4c3b4c77ab
size 68760 size 68727

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4ad09278ae2ebb8171adba96d9f6e91d0cc4f120b0b2368087796dadb37eeb87 oid sha256:ff2c7a701945b72afc6731c9da722e0bf178879f0bd80bc40da7ebb1d6e90d28
size 58539 size 58505

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:082206122d4e6d9e6171b3b2444c576ff7bd47fb3946d8d98e2812654f39cd40 oid sha256:4591b08ec297c0b024870e60ac18b03aadeaa8ff20a726dd991b18a80ec353ef
size 73641 size 73607

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:dc695bfc589e8e5a852eeb9b2828ce968d17519bb5be957cf2734a7d6d9cc356 oid sha256:6586d62bdbd3d4331105f561669c5383d578ad8af559270a26c7d9ff741b473b
size 89482 size 89412

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9a243d53d10ca2eaa249b22af8a9fddf1a8ccf60db4f3ad9374e31cf494fe878 oid sha256:8981ec8fd5e68e6e332f98ee2c95fc0f902b9b912a05201cad1c6a9feb5dbc89
size 54909 size 54868

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3fdd478be89b47fcaf9725ca14c1e775087f1ccf2a266f3476a537bbc2b29922 oid sha256:a5e787ce25a3542e398bec3aadd2b9b923c81dee4f3fbf8936c1c15e5aa1816b
size 67476 size 67450

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1a3b5bbcdd1593e81384b335045bdcb0b3e01782868993e9f6437a15ca39dbca oid sha256:1a8af5ff868170d946d03db115271e7ea9c0a15db35bf6815bf95cc4e4f7fa81
size 51380 size 51357

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7e26db0a0a8d767d6e732c1d2742cba3c3475d761b0c3017ff01cee7e3d362a5 oid sha256:438bc82c007c9cd72228f47681981e833645c591c0a48e8a5cb70bd51721d35a
size 62773 size 62756

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3d5cae7d73178aecc12fbf5f1f27c9e66608b90d462deec862a881b403469e93 oid sha256:a0926170ca4277e47f9ae49f5092c195cee2a84d67259ba8dc3aa3539d4faecf
size 49475 size 49454

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fe9456f4446104142221e28167a578a2b5cac772dc35aeb14352c0d422dd6fb0 oid sha256:68ce3ada02e8ed6cb1e257e49d8c703bd0ff3401bf0e520fa617ed1ea91c6756
size 65726 size 65691

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cad3b5ce023d890fd4a5ff0f5bbabb678bc6d5c76d3476e8053673c06f360c8b oid sha256:c902787fb2d84a0e92832020cdd447fc039264fc5add63abea32241251c92bf7
size 56102 size 56102

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:41a6c2dd81698696708802276b615a78ff22cc7cb8ae2d4b8d8cc862bfe44a24 oid sha256:bc956e4fdab7bf15d71e10585947f9f5c24bc1d9e4eaa071de9b747fae879f53
size 70856 size 70883

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:167f8368e0b94aa05e5d1cc30055e3b0c2c910752d719092ef2d34aae09dc832 oid sha256:fdf1b66ffe0b7042356d22aa835ceea2c5b697c4ce62287b5ec040f528210216
size 84649 size 84635

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:789ee5ccad8356198cdc0634b4e9a65ed44be2d26e7ce83a8662598c1bd8d4c2 oid sha256:e438a2e542780851a7b570407e02f125e191d23b9c5299d9457baad78523de84
size 52765 size 52740

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7b40a2e5d60a906d7c35c3ece1854671f5b319b00ad077322d094cbf906c07f7 oid sha256:17f11b2cb93b8736a2d1224b7ad70c7e3a1a45bcb7e5ceb60475dba734e6efec
size 64803 size 64787

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7c724bc77185a9ceec2cf092cb1d7865b13718d5320bd7ac4850bb85590f05b2 oid sha256:de7f21b5fcc1235a80ea2e7f9e26a93503ea2d0c0793a89db8657979afb33a7d
size 52294 size 52267

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a303894134ed06348b609e41cf109dcafcd994c3dffabc6d9ab436fe92605245 oid sha256:3d368f7cc84b5a8850577fac658aaeb89a1c83a2890b7fd393577c9cde919069
size 53710 size 53689

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7523ec0d6defd7074af0c804fdde64fb8d421f6cfa729bc1c4f9858bd87c42d0 oid sha256:56440b44cf610b0baaf7401bebb8ba2f4504b5dd36cb54fe1669d5deec6d1674
size 52554 size 52530

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1f48089147f7e089abfa64254143a80857ccc9f840aa60403e1aabc67e2b6d51 oid sha256:9387ab3e62a9e10721efdea3a6abcdf29db82a04ebdb3c4cefcad2a20ab9b9b8
size 55458 size 55386

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7ff682ee8363d450bb76db72ea06deea87fa47692ce319b7dff315d2a10dfb6a oid sha256:c0e3bd4e37bb665df997f189e5c2dc763c7f703b78384c63737675bd764fe7a8
size 51033 size 51174

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e84edf8adf1a89153dd45272d36e04561d66f2ea765ad9edecfd8d750ba99f97 oid sha256:ade57e690313eaaeddc7b21b9a2661026fca70e6af1b09aa331bba3d1b1bbaf3
size 54237 size 54240

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0bc9521bd1576d47ca6f643adf43ce3638d40328207d908ec863c72503d34f24 oid sha256:89520fd2999582229ace8fd9304644fc74ce01230a4024b90b47ce1cd61eb564
size 55682 size 55678

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c2209c3cc4e7de32ed92b3b0da4616b54d19091d43d21c0e4013728450d0d3f7 oid sha256:19420aad469fad8a218a0a8cd0f7cfbb9593d7ceb799478a7ad9b84d9c24602f
size 54595 size 54598

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d7bdd0ca39534b31c9d421e28337712b1cf2aaf841a766fcc6bdaf996c756bfa oid sha256:e224a21a1ae913066530e8be81760795b10be388cb7fa92fe399b1e11367d7af
size 57524 size 57423

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ba349f81d5c417c612cae1263504ea5b4e83dc606ec20c3942368e8992b87ad4 oid sha256:cac4b5bdb9f225c5313f36caef190e3f698b734db79707928572c942fed85603
size 52886 size 52960