Merge pull request #519 from vector-im/misc/cjs/create-join-design-feedback

Design tweaks for create/join rooms
This commit is contained in:
Chris Smith 2023-06-05 13:24:42 +01:00 committed by GitHub
commit 93456e8d44
183 changed files with 653 additions and 488 deletions

View file

@ -16,15 +16,26 @@
package io.element.android.features.createroom.impl.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.theme.components.Divider
import io.element.android.libraries.designsystem.theme.components.SearchBar
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.user.MatrixUser
@ -49,6 +60,8 @@ fun SearchUserBar(
onUserSelected: (MatrixUser) -> Unit = {},
onUserDeselected: (MatrixUser) -> Unit = {},
) {
val columnState = rememberLazyListState()
SearchBar(
query = query,
onQueryChange = onTextChanged,
@ -59,19 +72,38 @@ fun SearchUserBar(
showBackButton = showBackButton,
contentPrefix = {
if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) {
// We want the selected users to behave a bit like a top bar - when the list below is scrolled, the colour
// should change to indicate elevation.
val elevation = remember {
derivedStateOf {
if (columnState.canScrollBackward) {
4.dp
} else {
0.dp
}
}
}
val appBarContainerColor by animateColorAsState(
targetValue = MaterialTheme.colorScheme.surfaceColorAtElevation(elevation.value),
animationSpec = spring(stiffness = Spring.StiffnessMediumLow)
)
SelectedUsersList(
contentPadding = PaddingValues(16.dp),
selectedUsers = selectedUsers,
autoScroll = true,
onUserRemoved = onUserDeselected,
modifier = Modifier.background(appBarContainerColor)
)
}
},
resultState = state,
resultHandler = { users ->
LazyColumn {
LazyColumn(state = columnState) {
if (isMultiSelectionEnabled) {
items(users) { searchResult ->
itemsIndexed(users) { index, searchResult ->
SearchMultipleUsersResultItem(
modifier = Modifier.fillMaxWidth(),
searchResult = searchResult,
@ -84,14 +116,20 @@ fun SearchUserBar(
}
}
)
if (index < users.lastIndex) {
Divider()
}
}
} else {
items(users) { searchResult ->
itemsIndexed(users) { index, searchResult ->
SearchSingleUserResultItem(
modifier = Modifier.fillMaxWidth(),
searchResult = searchResult,
onClick = { onUserSelected(searchResult.matrixUser) }
)
if (index < users.lastIndex) {
Divider()
}
}
}
}

View file

@ -130,6 +130,7 @@ fun ConfigureRoomView(
)
if (state.config.invites.isNotEmpty()) {
SelectedUsersList(
modifier = Modifier.padding(bottom = 16.dp),
contentPadding = PaddingValues(horizontal = 24.dp),
selectedUsers = state.config.invites,
onUserRemoved = {
@ -226,7 +227,7 @@ fun RoomNameWithAvatar(
LabelledTextField(
label = stringResource(R.string.screen_create_room_room_name_label),
value = roomName,
placeholder = stringResource(R.string.screen_create_room_room_name_placeholder),
placeholder = stringResource(StringR.string.common_room_name_placeholder),
singleLine = true,
onValueChange = onRoomNameChanged,
)
@ -243,7 +244,7 @@ fun RoomTopic(
modifier = modifier,
label = stringResource(R.string.screen_create_room_topic_label),
value = topic,
placeholder = stringResource(R.string.screen_create_room_topic_placeholder),
placeholder = stringResource(StringR.string.common_topic_placeholder),
onValueChange = onTopicChanged,
maxLines = 3,
)

View file

@ -26,14 +26,15 @@ import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
@ -142,7 +143,11 @@ fun CreateRoomRootViewTopBar(
},
actions = {
IconButton(onClick = onClosePressed) {
Icon(imageVector = Icons.Default.Close, contentDescription = stringResource(id = StringR.string.action_close))
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(id = StringR.string.action_close),
tint = MaterialTheme.colorScheme.primary,
)
}
}
)
@ -157,7 +162,7 @@ fun CreateRoomActionButtonsList(
Column(modifier = modifier) {
CreateRoomActionButton(
iconRes = DrawableR.drawable.ic_groups,
text = stringResource(id = StringR.string.action_create_a_room),
text = stringResource(id = R.string.screen_create_room_action_create_room),
onClick = onNewRoomClicked,
)
CreateRoomActionButton(
@ -185,11 +190,16 @@ fun CreateRoomActionButton(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
modifier = Modifier.alpha(0.5f), // FIXME align on Design system theme (removing alpha should be fine)
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.secondary,
resourceId = iconRes,
contentDescription = null,
)
Text(text = text)
Text(
text = text,
fontSize = 16.sp,
fontWeight = FontWeight.Normal
)
}
}

View file

@ -9,9 +9,7 @@
<string name="screen_create_room_public_option_description">"Zprávy nejsou šifrované a může si je přečíst kdokoli. Šifrování můžete povolit později."</string>
<string name="screen_create_room_public_option_title">"Veřejná místnost (kdokoli)"</string>
<string name="screen_create_room_room_name_label">"Název místnosti"</string>
<string name="screen_create_room_room_name_placeholder">"např. Produktový sprint"</string>
<string name="screen_create_room_topic_label">"Téma (nepovinné)"</string>
<string name="screen_create_room_topic_placeholder">"O čem je tato místnost?"</string>
<string name="screen_start_chat_error_starting_chat">"Při pokusu o zahájení chatu došlo k chybě"</string>
<string name="screen_create_room_title">"Vytvořit místnost"</string>
</resources>
</resources>

View file

@ -9,9 +9,7 @@
<string name="screen_create_room_public_option_description">"Nachrichten sind nicht verschlüsselt und jeder kann sie lesen. Du kannst die Verschlüsselung zu einem späteren Zeitpunkt aktivieren."</string>
<string name="screen_create_room_public_option_title">"Öffentlicher Raum (jeder)"</string>
<string name="screen_create_room_room_name_label">"Raumname"</string>
<string name="screen_create_room_room_name_placeholder">"z.B. Produkt-Sprint"</string>
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
<string name="screen_create_room_topic_placeholder">"Worum geht es in diesem Raum?"</string>
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
<string name="screen_create_room_title">"Raum erstellen"</string>
</resources>
</resources>

View file

@ -9,9 +9,7 @@
<string name="screen_create_room_public_option_description">"Les messages ne sont pas chiffrés et n\'importe qui peut les lire. Vous pouvez activer le chiffrement ultérieurement."</string>
<string name="screen_create_room_public_option_title">"Salle publique (nimporte qui)"</string>
<string name="screen_create_room_room_name_label">"Nom de la salle"</string>
<string name="screen_create_room_room_name_placeholder">"Ex: Sprint Produit"</string>
<string name="screen_create_room_topic_label">"Sujet (optionnel)"</string>
<string name="screen_create_room_topic_placeholder">"De quoi parle cette salle ?"</string>
<string name="screen_start_chat_error_starting_chat">"Une erreur s\'est produite lors de la tentative de démarrage d\'une discussion"</string>
<string name="screen_create_room_title">"Créer une salle"</string>
</resources>
</resources>

View file

@ -9,9 +9,7 @@
<string name="screen_create_room_public_option_description">"Mesajele nu sunt criptate și oricine le poate citi. Puteți activa criptarea la o dată ulterioară."</string>
<string name="screen_create_room_public_option_title">"Cameră publică (oricine)"</string>
<string name="screen_create_room_room_name_label">"Numele camerei"</string>
<string name="screen_create_room_room_name_placeholder">"e.g. Mici și Cozonaci"</string>
<string name="screen_create_room_topic_label">"Subiect (opțional)"</string>
<string name="screen_create_room_topic_placeholder">"Despre ce este această cameră?"</string>
<string name="screen_start_chat_error_starting_chat">"A apărut o eroare la încercarea începerii conversației"</string>
<string name="screen_create_room_title">"Creați o cameră"</string>
</resources>

View file

@ -9,9 +9,7 @@
<string name="screen_create_room_public_option_description">"Messages are not encrypted and anyone can read them. You can enable encryption at a later date."</string>
<string name="screen_create_room_public_option_title">"Public room (anyone)"</string>
<string name="screen_create_room_room_name_label">"Room name"</string>
<string name="screen_create_room_room_name_placeholder">"e.g. Product Sprint"</string>
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
<string name="screen_create_room_topic_placeholder">"What is this room about?"</string>
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
<string name="screen_create_room_title">"Create a room"</string>
</resources>

View file

@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@ -42,6 +42,7 @@ import io.element.android.libraries.designsystem.components.dialogs.Confirmation
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Divider
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
@ -81,6 +82,9 @@ fun InviteListView(
ConfirmationDialog(
content = stringResource(contentResource, state.declineConfirmationDialog.name),
title = stringResource(titleResource),
submitText = stringResource(StringR.string.action_decline),
cancelText = stringResource(StringR.string.action_cancel),
emphasizeSubmitButton = true,
onSubmitClicked = { state.eventSink(InviteListEvents.ConfirmDeclineInvite) },
onDismiss = { state.eventSink(InviteListEvents.CancelDeclineInvite) }
)
@ -143,14 +147,18 @@ fun InviteListContent(
LazyColumn(
modifier = Modifier.weight(1f)
) {
items(
itemsIndexed(
items = state.inviteList,
) { invite ->
) { index, invite ->
InviteSummaryRow(
invite = invite,
onAcceptClicked = { state.eventSink(InviteListEvents.AcceptInvite(invite)) },
onDeclineClicked = { state.eventSink(InviteListEvents.DeclineInvite(invite)) },
)
if (index != state.inviteList.lastIndex) {
Divider()
}
}
}
}

View file

@ -17,13 +17,13 @@
package io.element.android.features.invitelist.impl.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
@ -31,23 +31,18 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
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.platform.LocalDensity
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@ -64,8 +59,8 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.noFontPadding
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
import kotlinx.collections.immutable.persistentMapOf
import io.element.android.libraries.ui.strings.R as StringR
private val minHeight = 72.dp
@ -104,23 +99,27 @@ internal fun DefaultInviteSummaryRow(
verticalAlignment = Alignment.Top
) {
Avatar(
invite.roomAvatarData,
invite.roomAvatarData.copy(size = AvatarSize.Custom(52.dp)),
)
Column(
modifier = Modifier
.padding(start = 12.dp, end = 4.dp)
.padding(start = 16.dp, end = 4.dp)
.alignByBaseline()
.weight(1f)
) {
val bonusPadding = if (invite.isNew) 12.dp else 0.dp
// Name
Text(
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
fontWeight = FontWeight.Medium,
text = invite.roomName,
color = MaterialTheme.colorScheme.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
overflow = TextOverflow.Ellipsis,
style = noFontPadding,
modifier = Modifier.padding(end = bonusPadding),
)
// ID or Alias
@ -131,7 +130,8 @@ internal fun DefaultInviteSummaryRow(
text = it,
color = MaterialTheme.colorScheme.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(end = bonusPadding),
)
}
@ -145,8 +145,8 @@ internal fun DefaultInviteSummaryRow(
OutlinedButton(
content = { Text(stringResource(StringR.string.action_decline), style = ElementTextStyles.Button) },
onClick = onDeclineClicked,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 7.dp),
modifier = Modifier.weight(1f).heightIn(max = 36.dp),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 0.dp),
)
Spacer(modifier = Modifier.width(12.dp))
@ -154,8 +154,8 @@ internal fun DefaultInviteSummaryRow(
Button(
content = { Text(stringResource(StringR.string.action_accept), style = ElementTextStyles.Button) },
onClick = onAcceptClicked,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 7.dp),
modifier = Modifier.weight(1f).heightIn(max = 36.dp),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 0.dp),
)
}
}
@ -173,45 +173,36 @@ internal fun DefaultInviteSummaryRow(
@Composable
private fun SenderRow(sender: InviteSender) {
Text(
text = buildAnnotatedString {
val placeholder = "$"
val text = stringResource(R.string.screen_invites_invited_you, placeholder)
val nameIndex = text.indexOf(placeholder)
// Text before the placeholder
append(text.take(nameIndex))
// Avatar and display name
appendInlineContent("avatar")
withStyle(SpanStyle(fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary)) {
append(sender.displayName)
}
// Text after the placeholder
append(text.drop(nameIndex + placeholder.length))
},
color = MaterialTheme.colorScheme.secondary,
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(top = 6.dp),
inlineContent = persistentMapOf(
"avatar" to InlineTextContent(
with(LocalDensity.current) {
Placeholder(20.dp.toSp(), 20.dp.toSp(), PlaceholderVerticalAlign.Center)
}
) {
Box(
Modifier
.fillMaxHeight()
.padding(end = 4.dp)
) {
Avatar(
avatarData = sender.avatarData.copy(size = AvatarSize.Custom(16.dp)),
modifier = Modifier.align(Alignment.Center)
)
}
}
) {
Avatar(
avatarData = sender.avatarData.copy(size = AvatarSize.Custom(16.dp)),
)
)
Text(
text = stringResource(R.string.screen_invites_invited_you, sender.displayName, sender.userId.value).let { text ->
val senderNameStart = LocalContext.current.getString(R.string.screen_invites_invited_you).indexOf("%1\$s")
AnnotatedString(
text = text,
spanStyles = listOf(
AnnotatedString.Range(
SpanStyle(
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
),
start = senderNameStart,
end = senderNameStart + sender.displayName.length
)
)
)
},
style = noFontPadding,
color = MaterialTheme.colorScheme.secondary,
fontSize = 14.sp,
fontWeight = FontWeight.Normal,
)
}
}
@Preview

View file

@ -24,7 +24,8 @@ open class InviteListInviteSummaryProvider : PreviewParameterProvider<InviteList
override val values: Sequence<InviteListInviteSummary>
get() = sequenceOf(
aInviteListInviteSummary(),
aInviteListInviteSummary().copy(roomAlias = "#someroom:example.com"),
aInviteListInviteSummary().copy(roomAlias = "#someroom-with-a-long-alias:example.com"),
aInviteListInviteSummary().copy(roomAlias = "#someroom-with-a-long-alias:example.com", isNew = true),
aInviteListInviteSummary().copy(roomName = "Alice", sender = null),
aInviteListInviteSummary().copy(isNew = true)
)
@ -32,9 +33,9 @@ open class InviteListInviteSummaryProvider : PreviewParameterProvider<InviteList
fun aInviteListInviteSummary() = InviteListInviteSummary(
roomId = RoomId("!room1:example.com"),
roomName = "Some room",
roomName = "Some room with a long name that will truncate",
sender = InviteSender(
userId = UserId("@alice:example.org"),
displayName = "Alice"
userId = UserId("@alice-with-a-long-mxid:example.org"),
displayName = "Alice with a long name"
),
)

View file

@ -5,5 +5,4 @@
<string name="screen_invites_decline_direct_chat_message">"Möchten Sie den Chat mit %1$s wirklich ablehnen?"</string>
<string name="screen_invites_decline_direct_chat_title">"Chat ablehnen"</string>
<string name="screen_invites_empty_list">"Keine Einladungen"</string>
<string name="screen_invites_invited_you">"%1$s hat dich eingeladen"</string>
</resources>
</resources>

View file

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_invited_you">"%1$s te invitó."</string>
</resources>
</resources>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_empty_list">"Aucune invitation"</string>
<string name="screen_invites_invited_you">"%1$s vous a invité."</string>
</resources>
</resources>

View file

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_invited_you">"%1$s ti ha invitato"</string>
</resources>
</resources>

View file

@ -5,5 +5,4 @@
<string name="screen_invites_decline_direct_chat_message">"Sigur doriți să refuzați conversațiile cu %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Refuzați conversația"</string>
<string name="screen_invites_empty_list">"Nicio invitație"</string>
<string name="screen_invites_invited_you">"%1$s v-a invitat"</string>
</resources>
</resources>

View file

@ -5,5 +5,5 @@
<string name="screen_invites_decline_direct_chat_message">"Are you sure you want to decline to chat with %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Decline chat"</string>
<string name="screen_invites_empty_list">"No Invites"</string>
<string name="screen_invites_invited_you">"%1$s invited you"</string>
</resources>
<string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</string>
</resources>

View file

@ -26,7 +26,7 @@ open class RoomDetailsEditStateProvider : PreviewParameterProvider<RoomDetailsEd
get() = sequenceOf(
aRoomDetailsEditState(),
aRoomDetailsEditState().copy(roomTopic = ""),
aRoomDetailsEditState().copy(roomAvatarUrl = Uri.EMPTY),
aRoomDetailsEditState().copy(roomAvatarUrl = Uri.parse("example://uri")),
aRoomDetailsEditState().copy(canChangeName = true, canChangeTopic = false, canChangeAvatar = true, saveButtonEnabled = false),
aRoomDetailsEditState().copy(canChangeName = false, canChangeTopic = true, canChangeAvatar = false, saveButtonEnabled = false),
aRoomDetailsEditState().copy(saveAction = Async.Loading()),

View file

@ -145,7 +145,7 @@ fun RoomDetailsEditView(
LabelledTextField(
label = stringResource(id = R.string.screen_room_details_room_name_label),
value = state.roomName,
placeholder = stringResource(id = R.string.screen_room_details_room_name_placeholder),
placeholder = stringResource(StringR.string.common_room_name_placeholder),
singleLine = true,
onValueChange = { state.eventSink(RoomDetailsEditEvents.UpdateRoomName(it)) },
)
@ -160,9 +160,9 @@ fun RoomDetailsEditView(
if (state.canChangeTopic) {
LabelledTextField(
label = stringResource(id = StringR.string.common_topic),
label = stringResource(StringR.string.common_topic),
value = state.roomTopic,
placeholder = stringResource(id = R.string.screen_room_details_topic_placeholder),
placeholder = stringResource(StringR.string.common_topic_placeholder),
maxLines = 10,
onValueChange = { state.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(it)) },
)

View file

@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -40,6 +40,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
import io.element.android.libraries.designsystem.theme.components.Divider
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.SearchBar
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
@ -181,7 +182,7 @@ private fun RoomInviteMembersSearchBar(
)
LazyColumn {
items(results) { invitableUser ->
itemsIndexed(results) { index, invitableUser ->
if (invitableUser.isUnresolved && !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined) {
CheckableUnresolvedUserRow(
checked = invitableUser.isSelected,
@ -208,6 +209,10 @@ private fun RoomInviteMembersSearchBar(
modifier = Modifier.fillMaxWidth()
)
}
if (index < results.lastIndex) {
Divider()
}
}
}
},

View file

@ -164,6 +164,7 @@ private fun LazyListScope.roomMemberListSection(
Text(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
text = headerText(),
fontSize = 16.sp,
style = ElementTextStyles.Regular.callout,
color = MaterialTheme.colorScheme.secondary,
textAlign = TextAlign.Start,

View file

@ -12,10 +12,9 @@
<string name="screen_room_details_edition_error_title">"Unable to update room"</string>
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
<string name="screen_room_details_invite_people_title">"Invite people"</string>
<string name="screen_room_details_room_name_label">"Room name"</string>
<string name="screen_room_details_room_name_placeholder">"e.g. Product Sprint"</string>
<string name="screen_room_details_share_room_title">"Share room"</string>
<string name="screen_room_details_topic_placeholder">"What is this room about?"</string>
<string name="screen_room_details_updating_room">"Updating room…"</string>
<string name="screen_room_member_list_pending_header_title">"Pending"</string>
<string name="screen_room_member_list_room_members_header_title">"Room members"</string>
@ -25,7 +24,6 @@
<string name="screen_dm_details_unblock_alert_action">"Unblock"</string>
<string name="screen_dm_details_unblock_alert_description">"On unblocking the user, you will be able to see all messages by them again."</string>
<string name="screen_dm_details_unblock_user">"Unblock user"</string>
<string name="screen_room_details_invite_people_title">"Invite friends to Element"</string>
<string name="screen_room_details_leave_room_title">"Leave room"</string>
<string name="screen_room_details_people_title">"People"</string>
<string name="screen_room_details_security_title">"Security"</string>

View file

@ -29,10 +29,12 @@ import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
@ -51,16 +53,19 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.leaveroom.api.LeaveRoomView
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
import io.element.android.features.roomlist.impl.components.RoomListTopBar
@ -72,12 +77,13 @@ import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Divider
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.noFontPadding
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.api.core.RoomId
@ -230,37 +236,48 @@ fun RoomListContent(
if (state.invitesState != InvitesState.NoInvites) {
item {
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxSize()) {
TextButton(
content = {
Text(stringResource(StringR.string.action_invites_list))
if (state.invitesState == InvitesState.NewInvites) {
Spacer(Modifier.size(8.dp))
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(MaterialTheme.roomListUnreadIndicator())
)
}
},
onClick = onInvitesClicked,
Row(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.clickable(role = Role.Button, onClick = onInvitesClicked)
.heightIn(min = 48.dp),
) {
Text(
text = stringResource(StringR.string.action_invites_list),
fontSize = 14.sp,
style = noFontPadding,
)
if (state.invitesState == InvitesState.NewInvites) {
Spacer(Modifier.width(8.dp))
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(MaterialTheme.roomListUnreadIndicator())
)
}
Spacer(Modifier.width(16.dp))
}
}
}
items(
itemsIndexed(
items = state.roomList,
contentType = { room -> room.contentType() },
) { room ->
contentType = { _, room -> room.contentType() },
) { index, room ->
RoomSummaryRow(
room = room,
onClick = ::onRoomClicked,
onLongClick = onRoomLongClicked,
)
if (index != state.roomList.lastIndex) {
Divider()
}
}
}
}

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.designsystem
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
@ -25,12 +26,14 @@ import androidx.compose.ui.unit.sp
// TODO Remove
object ElementTextStyles {
@Suppress("DEPRECATION")
val Button = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontWeight = FontWeight.Medium,
lineHeight = 22.sp,
fontStyle = FontStyle.Normal,
textAlign = TextAlign.Center,
platformStyle = PlatformTextStyle(includeFontPadding = false)
)
object Bold {

View file

@ -19,7 +19,8 @@ package io.element.android.libraries.designsystem.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
@ -46,29 +47,10 @@ fun ProgressDialog(
onDismissRequest = onDismiss,
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(8.dp)
)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (!text.isNullOrBlank()) {
Text(
text = text,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(16.dp)
)
}
}
}
ProgressDialogContent(
modifier = modifier,
text = text,
)
}
}
@ -80,22 +62,23 @@ private fun ProgressDialogContent(
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(8.dp)
)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(top = 38.dp, bottom = 32.dp, start = 40.dp, end = 40.dp)
) {
CircularProgressIndicator(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (!text.isNullOrBlank()) {
Spacer(modifier = Modifier.height(22.dp))
Text(
text = text,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(16.dp)
)
}
}

View file

@ -49,7 +49,7 @@ fun Avatar(
val commonModifier = modifier
.size(avatarData.size.dp)
.clip(CircleShape)
if (avatarData.url == null) {
if (avatarData.url.isNullOrBlank()) {
InitialsAvatar(
avatarData = avatarData,
modifier = commonModifier,
@ -72,7 +72,7 @@ private fun ImageAvatar(
AsyncImage(
model = avatarData,
onError = {
Timber.e("TAG", "Error $it\n${it.result}", it.result.throwable)
Timber.e(it.result.throwable, "Error loading avatar $it\n${it.result}")
},
contentDescription = contentDescription,
contentScale = ContentScale.Crop,

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.designsystem.theme
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
@ -107,3 +108,13 @@ val titleMediumDefault: TextStyle = TextStyle(
letterSpacing = 0.5.sp
)
// Temporary style for text that needs to be aligned without weird font padding issues. `includeFontPadding` will default to false in a future version of
// compose, at which point this can be removed.
//
// Ref: https://medium.com/androiddevelopers/fixing-font-padding-in-compose-text-768cd232425b
@Suppress("DEPRECATION")
val noFontPadding: TextStyle = TextStyle(
platformStyle = PlatformTextStyle(
includeFontPadding = false
)
)

View file

@ -27,7 +27,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Text
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@ -35,10 +34,12 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.ui.media.AvatarAction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -62,6 +63,7 @@ fun AvatarActionBottomSheet(
ModalBottomSheetLayout(
modifier = modifier,
sheetState = modalBottomSheetState,
displayHandle = true,
sheetContent = {
AvatarActionBottomSheetContent(
actions = actions,
@ -91,6 +93,7 @@ private fun AvatarActionBottomSheetContent(
headlineContent = {
Text(
text = stringResource(action.titleResId),
fontSize = 16.sp,
color = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
)
},
@ -98,7 +101,7 @@ private fun AvatarActionBottomSheetContent(
Icon(
imageVector = action.icon,
contentDescription = stringResource(action.titleResId),
tint = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
tint = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary,
)
}
)

View file

@ -20,12 +20,14 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@ -79,8 +81,10 @@ fun CheckableUserRow(
)
Checkbox(
modifier = Modifier
.padding(end = 16.dp),
checked = checked,
onCheckedChange = onCheckedChange,
onCheckedChange = null,
enabled = enabled,
)
}

View file

@ -16,9 +16,11 @@
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@ -38,6 +40,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.noFontPadding
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.getBestName
@ -46,7 +49,7 @@ import io.element.android.libraries.matrix.ui.model.getBestName
fun MatrixUserRow(
matrixUser: MatrixUser,
modifier: Modifier = Modifier,
avatarSize: AvatarSize = AvatarSize.MEDIUM,
avatarSize: AvatarSize = AvatarSize.Custom(36.dp),
) = UserRow(
avatarData = matrixUser.getAvatarData(avatarSize),
name = matrixUser.getBestName(),
@ -71,25 +74,29 @@ fun UserRow(
Avatar(avatarData)
Column(
modifier = Modifier
.padding(start = 12.dp),
.padding(start = 12.dp)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween,
) {
// Name
Text(
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
fontWeight = FontWeight.Normal,
text = name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.primary,
style = noFontPadding,
)
// Id
subtext?.let {
Text(
text = subtext,
color = MaterialTheme.colorScheme.secondary,
fontSize = 14.sp,
fontSize = 12.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
overflow = TextOverflow.Ellipsis,
style = noFontPadding,
)
}
}

View file

@ -16,16 +16,20 @@
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -38,7 +42,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
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.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData
@ -51,7 +55,9 @@ fun SelectedUser(
modifier: Modifier = Modifier,
onUserRemoved: (MatrixUser) -> Unit = {},
) {
Box(modifier = modifier.width(56.dp)) {
Box(modifier = modifier
.width(56.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
@ -63,18 +69,23 @@ fun SelectedUser(
style = MaterialTheme.typography.bodyLarge,
)
}
IconButton(
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
.size(20.dp)
.align(Alignment.TopEnd),
onClick = { onUserRemoved(matrixUser) }
.align(Alignment.TopEnd)
.clickable(
indication = rememberRipple(),
interactionSource = remember { MutableInteractionSource() },
onClick = { onUserRemoved(matrixUser) }
),
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(id = StringR.string.action_remove),
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.padding(2.dp)
)
}
}

View file

@ -16,18 +16,27 @@
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@ -35,6 +44,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlin.math.floor
@Composable
fun SelectedUsersList(
@ -56,16 +66,64 @@ fun SelectedUsersList(
}
}
val rowWidth by remember {
derivedStateOf {
lazyListState.layoutInfo.viewportSize.width - lazyListState.layoutInfo.beforeContentPadding
}
}
// Calculate spacing to show between each user. This is at least [minimumSpacing], and will grow to ensure that if the available space is filled with
// users, the last visible user will be precisely half visible. This gives an obvious affordance that there are more entries and the list can be scrolled.
// For efficiency, we assume that all the children are the same width. If they needed to be different sizes we'd have to do this calculation each time
// they needed to be measured.
val minimumSpacing = with(LocalDensity.current) { 24.dp.toPx() }
val userWidth = with(LocalDensity.current) { 56.dp.toPx() }
val userSpacing by remember {
derivedStateOf {
if (rowWidth == 0) {
// The row hasn't yet been measured yet, so we don't know how big it is
minimumSpacing
} else {
val userWidthWithSpacing = userWidth + minimumSpacing
val maxVisibleUsers = rowWidth / userWidthWithSpacing
// Round down the number of visible users to end with a state where one is half visible
val targetFraction = (userWidth / 2) / userWidthWithSpacing
val targetUsers = floor(maxVisibleUsers - targetFraction) + targetFraction
// Work out how much extra spacing we need to reduce the number of users that much, then split it evenly amongst the visible users
val extraSpacing = (maxVisibleUsers - targetUsers) * userWidthWithSpacing
val extraSpacingPerUser = extraSpacing / floor(targetUsers)
minimumSpacing + extraSpacingPerUser
}
}
}
LazyRow(
state = lazyListState,
modifier = modifier,
modifier = modifier
.fillMaxWidth(),
contentPadding = contentPadding,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
items(selectedUsers.toList()) { matrixUser ->
SelectedUser(
matrixUser = matrixUser,
onUserRemoved = onUserRemoved,
itemsIndexed(selectedUsers.toList()) { index, matrixUser ->
Layout(
content = {
SelectedUser(
matrixUser = matrixUser,
onUserRemoved = onUserRemoved,
)
},
measurePolicy = { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
val spacing = if (index == selectedUsers.lastIndex) 0f else userSpacing
layout(
width = (placeable.width + spacing).toInt(),
height = placeable.height
) {
placeable.place(0, 0)
}
}
)
}
}
@ -81,7 +139,23 @@ internal fun SelectedUsersListDarkPreview() = ElementPreviewDark { ContentToPrev
@Composable
private fun ContentToPreview() {
SelectedUsersList(
selectedUsers = aMatrixUserList().take(6).toImmutableList(),
)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
// Two users that will be visible with no scrolling
SelectedUsersList(
selectedUsers = aMatrixUserList().take(2).toImmutableList(),
modifier = Modifier
.width(200.dp)
.border(1.dp, Color.Red)
)
// Multiple users that don't fit, so will be spaced out per the measure policy
for (i in 0..5) {
SelectedUsersList(
selectedUsers = aMatrixUserList().take(6).toImmutableList(),
modifier = Modifier
.width((200 + (i * 20)).dp)
.border(1.dp, Color.Red)
)
}
}
}

View file

@ -17,9 +17,11 @@
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@ -39,10 +41,12 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.theme.components.Checkbox
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.noFontPadding
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.R
@ -62,7 +66,9 @@ fun UnresolvedUserRow(
Avatar(avatarData)
Column(
modifier = Modifier
.padding(start = 12.dp),
.padding(start = 12.dp)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween,
) {
// ID
Text(
@ -72,10 +78,11 @@ fun UnresolvedUserRow(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.primary,
style = noFontPadding,
)
// Warning
Row(modifier = Modifier.fillMaxWidth()) {
Row(modifier = Modifier.fillMaxWidth().padding(top = 3.dp)) {
Icon(
imageVector = Icons.Filled.Error,
contentDescription = "",
@ -121,8 +128,9 @@ fun CheckableUnresolvedUserRow(
)
Checkbox(
modifier = Modifier.padding(end = 16.dp),
checked = checked,
onCheckedChange = onCheckedChange,
onCheckedChange = null,
enabled = enabled,
)
}
@ -142,9 +150,9 @@ internal fun CheckableUnresolvedUserRowPreview() =
ElementThemedPreview {
val matrixUser = aMatrixUser()
Column {
CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(), matrixUser.userId.value)
CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(), matrixUser.userId.value)
CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(), matrixUser.userId.value, enabled = false)
CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(), matrixUser.userId.value, enabled = false)
CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(AvatarSize.Custom(36.dp)), matrixUser.userId.value)
CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(AvatarSize.Custom(36.dp)), matrixUser.userId.value)
CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(AvatarSize.Custom(36.dp)), matrixUser.userId.value, enabled = false)
CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(AvatarSize.Custom(36.dp)), matrixUser.userId.value, enabled = false)
}
}

View file

@ -69,6 +69,7 @@
<string name="common_encryption_enabled">"Encryption enabled"</string>
<string name="common_error">"Error"</string>
<string name="common_file">"File"</string>
<string name="common_file_saved_on_disk_android">"File saved to Downloads"</string>
<string name="common_gif">"GIF"</string>
<string name="common_image">"Image"</string>
<string name="common_invite_unknown_profile">"We cant validate this users Matrix ID. The invite might not be received."</string>
@ -89,6 +90,7 @@
<string name="common_report_a_bug">"Report a bug"</string>
<string name="common_report_submitted">"Report submitted"</string>
<string name="common_room_name">"Room name"</string>
<string name="common_room_name_placeholder">"e.g. your project name"</string>
<string name="common_search_for_someone">"Search for someone"</string>
<string name="common_search_results">"Search results"</string>
<string name="common_security">"Security"</string>
@ -102,6 +104,7 @@
<string name="common_success">"Success"</string>
<string name="common_suggestions">"Suggestions"</string>
<string name="common_topic">"Topic"</string>
<string name="common_topic_placeholder">"What is this room about?"</string>
<string name="common_unable_to_decrypt">"Unable to decrypt"</string>
<string name="common_unable_to_invite_message">"We were unable to successfully send invites to one or more users."</string>
<string name="common_unable_to_invite_title">"Unable to send invite(s)"</string>

View file

@ -21,6 +21,6 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
interface UserListDataSource {
//TODO should probably have a flow
suspend fun search(query: String): List<MatrixUser>
suspend fun search(query: String, count: Long): List<MatrixUser>
suspend fun getProfile(userId: UserId): MatrixUser?
}

View file

@ -28,16 +28,12 @@ import javax.inject.Inject
class MatrixUserListDataSource @Inject constructor(
private val client: MatrixClient
) : UserListDataSource {
override suspend fun search(query: String): List<MatrixUser> {
val res = client.searchUsers(query, MAX_SEARCH_RESULTS)
override suspend fun search(query: String, count: Long): List<MatrixUser> {
val res = client.searchUsers(query, count)
return res.getOrNull()?.results.orEmpty()
}
override suspend fun getProfile(userId: UserId): MatrixUser? {
return client.getProfile(userId).getOrNull()
}
companion object {
private const val MAX_SEARCH_RESULTS = 5L
}
}

View file

@ -45,7 +45,7 @@ class MatrixUserRepository @Inject constructor(
// Debounce
delay(DEBOUNCE_TIME_MILLIS)
val results = dataSource.search(query).map { UserSearchResult(it) }.toMutableList()
val results = dataSource.search(query, MAXIMUM_SEARCH_RESULTS).map { UserSearchResult(it) }.toMutableList()
// If the query is a user ID and the result doesn't contain that user ID, query the profile information explicitly
if (isUserId && results.none { it.matrixUser.userId.value == query }) {
@ -61,7 +61,8 @@ class MatrixUserRepository @Inject constructor(
}
companion object {
private const val DEBOUNCE_TIME_MILLIS = 500L
private const val DEBOUNCE_TIME_MILLIS = 250L
private const val MINIMUM_SEARCH_LENGTH = 3
private const val MAXIMUM_SEARCH_RESULTS = 10L
}
}

View file

@ -47,7 +47,7 @@ internal class MatrixUserListDataSourceTest {
)
val dataSource = MatrixUserListDataSource(matrixClient)
val results = dataSource.search("test")
val results = dataSource.search("test", 2)
Truth.assertThat(results).containsExactly(
aMatrixUserProfile(),
aMatrixUserProfile(userId = A_USER_ID_2)
@ -63,7 +63,7 @@ internal class MatrixUserListDataSourceTest {
)
val dataSource = MatrixUserListDataSource(matrixClient)
val results = dataSource.search("test")
val results = dataSource.search("test", 2)
Truth.assertThat(results).isEmpty()
}

View file

@ -25,7 +25,7 @@ class FakeUserListDataSource : UserListDataSource {
private var searchResult: List<MatrixUser> = emptyList()
private var profile: MatrixUser? = null
override suspend fun search(query: String): List<MatrixUser> = searchResult
override suspend fun search(query: String, count: Long): List<MatrixUser> = searchResult.take(count.toInt())
override suspend fun getProfile(userId: UserId): MatrixUser? = profile

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:95ea8eb76135a05ff1248f87c906861fcde67432bea549f85b27c9fef24a63d7
size 41912
oid sha256:56c70ed35d2183ab9244b2cdcbccaf1979d1a862dd7f672ed7987bfd0ff726c9
size 41511

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:51ee8709e885016afc333ea0e9f3277081dc5703a4c8fcbbe8ab1a5bbefc2e40
size 39885
oid sha256:9d4175df2f74fc5c074a2eea0e7a3f80ffcdfe438aff24db7864010894de8cb5
size 39412

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8e7352789223e57f85c597a591ada117c903e06ed2df49719a98e793d128cd7b
size 106579
oid sha256:ba78d6c4836f018d6c048e6c1f14ed65a2e6ae00380054daad8eaa0fb4e1108a
size 103095

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:815864d4c929c5f123bc830d367b4f983851412891a23329d03e58e7726e4280
size 54198
oid sha256:930409bc725dcb68db252b08d838856056fbbeb178c02ba6831e819cadc37528
size 53181

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5c68cf2cd74c9eae988c2fce68460ce2188566c27206bd8454e8b131d7b65f7e
size 37361
oid sha256:92dd6bad4047f68312190d8923c111435a646cd83f5b52cdbce8c6ce48954dda
size 36975

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fd898673d5b5c20d68cde9fc1294350798b45c131010648fe3be6f366df90c20
size 87816
oid sha256:d941ac36664583640991d6a310edf45b4f87d8285647d6aa661970dba3c365b4
size 83119

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:04af6faf6e7efb06a6abe23cf49f8486a577f7afbf232af847ba9efb6ed62ce7
size 102827
oid sha256:99f4447da79ca0c37c7607fb673b6dfd29cb241ba30420adcc0399db170a5810
size 98150

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d09b939e42eaba34d2f003571ea0eda96502fa04bd54ed6f08041ef1ea3e8552
size 35143
oid sha256:521895f59c73d907fc75a5d270598add33512f0a359274adb0b53f28954bb20b
size 34704

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:93bce38b6b7031ff38d68a8751eb1cf49397803ba634f1ece711cc77b3acc38c
size 84264
oid sha256:12839404b6e9eaf8983ccf3e5773cd8c80174068e76924ad72b7b6429018b0b6
size 79478

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fb8f6e1d2b368d57911410322543a5bb1cbb4abbff76507190abc8d7d1e04a52
size 97755
oid sha256:4115bdacc08b7c5bdb4c2859d131eaa0e84748d3f42e2de388ffef72131851a2
size 93105

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:906e041d043f2bf604da80766d04b93a75e52b955d6db3496dcf3ae456acca13
size 60877
oid sha256:0d7a3255202968ab6a467b698ca41154b4564882ac3e45578ea36060a8c97583
size 61586

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:befde6b8663f65b9ad0e6789ca21802ceca164f911f0de18a795e9c81b7c4fe5
size 100278
oid sha256:0bab25b90e451021883fcd388030ac054729d1ea28a2b4e9b6041f5a103f0a7b
size 99914

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4032529e222fb9d14d4dc1fd215c8aae5d63326809df7cec449011d7904a60d2
size 58355
oid sha256:1a3a36f97c7c22d4113570ce8413de29c1d62e96d1d979851a66f051ca229e08
size 59148

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2bf49968f5b07ca6798c4c8670a9e173ef3a511c6c60ed8d55d652bb042b8a8b
size 96370
oid sha256:b28cfcb8e238559bd7775507397307fef4e192bb9e1615a0b08f048e73a5e6dd
size 96054

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:67c951e661c54a12d318f874c115768b30806f08e8e6e59bfedc107a968927d7
size 21742
oid sha256:dffe981836e1070a47cb815275752526e2e81a26ccc1658de8c503016f699afb
size 21292

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b13d46300c7cdd3632ee39a6bfce92a5b9bfa44917b32928da313741965addc7
size 8677
oid sha256:88f8d524d6a22a05b013c8cdc73b909a4ca41938d0fdb41923381c2045e6b37f
size 8990

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58877acda18e78027ae01b4f627ad8ad2df731b865c3e8d534479c41def14c16
size 21086
oid sha256:1345fb49ea9a7ea7e31086af10755c222c2093ea59c62cf4c6ce291b98c08e5b
size 20965

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:255e7caab6e7133239a186a421cf669649133704b15cddebe2dbc4ca9baca372
size 8731
oid sha256:f6ff5a856c3af2b85848a80e7d832ebbf737e7d5cf06105d5a01c1e6b3f1eeb7
size 9115

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:732ea7939a85e877771298e082df8d39e49881c97e4a9085c7887341ce85e502
size 22271
oid sha256:7e20ba9c18742b91164f5d0bdbbfe41b3960ab67bf921c2d305cfd72fbe57e52
size 33100

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5edcc6a91850f6217d850d5db6a539f356ee9e60fe662f46dbf4a65614450450
size 25857
oid sha256:3dfa909234ae27da447b12391d53657afd23ef0dd17cff4c8bb55ef1b9811432
size 38347

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9f35a7a94121ce8a082c5e73434108e6fd73e6ef204c8e5ab887aacc2721e715
size 16980
oid sha256:7c1b9dc41e7f4abce90208ce21b2537d8522e1467afdc702735f2d3ab79ed265
size 38320

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:77bcd34155ae0e32c3b38f4be655da52fb5c5da5fc9383a7379e80ed4519e7f5
size 22565
oid sha256:d9f210f49c4b9a17b01fffe23229afd6dc7909a50ff79732c4d7f169f6c6c558
size 18098

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57ed6d0c0051a91172e0acc4136337899740de50dc30aa83d8d82835ea2bd97f
size 21479
oid sha256:d0741f10b79b3ba739a7d2f160e61df8ea30c7e90d5b03db66cf0fadae39637d
size 31877

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08f1465c078449fcd080455f58b98becd522ae22be308c4549a03c82d7eff03e
size 25145
oid sha256:f9e818b80358ed35dfe0b1b1d26b8e64a137aa6f2fbce02385cb3cc7900bd852
size 37293

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c57ccb1f27270234abaa6765c330a13dd98b81e291d9c50e67b14e2c3633832
size 16192
oid sha256:18dd6a307f1990b95cddba7e5ae199fc2c9746fdc49231ceb320558707c1ec1c
size 37252

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bbddd7abaae52b60131036140bd28bbe0b3254ea45297ea1ddfb14e71679149c
size 21789
oid sha256:7c0e24c4fc71151ae8dc04a6213ecc15e0d25dbe6b1684d177086572aeff84c5
size 17289

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc812d7b9418a67040d8bd9a007444d611c00108662064baf79ae5805432f081
size 56695
oid sha256:4bddb1e4c74494ed89812a31e35c8d056a3109043fa817c1dbae13d522e56566
size 66641

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4031fc3820333ed5b28f2356792b957bc38cba1559b599b0ac0fef66074252e0
size 43572
oid sha256:4d6e5e784a6ee50cbf767bc0da43b493e910ee2a7ace9890419916d23c1e11a3
size 45461

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8936f72285c8a0f483080a4ff007703e40f187abfe0f67bb3ae33eb57901a1af
size 44022
oid sha256:f74cb8516eaf4d8f0e289f685d520bc130df21cbfc4fbc68eed001c4138c83b2
size 45907

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6dd33c2e92caac62963ccc75c068aa2b27037fe075b48823d2a8b75c88b4f661
size 39408
oid sha256:360c2c69a3d03538f6f61847757a15e88051b38195cadcd33f9cf2e57638b3f1
size 41761

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6dd33c2e92caac62963ccc75c068aa2b27037fe075b48823d2a8b75c88b4f661
size 39408
oid sha256:360c2c69a3d03538f6f61847757a15e88051b38195cadcd33f9cf2e57638b3f1
size 41761

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e87dd78bba7e3fd682e647258d989e5d720b7a0e879d698f96611b98867cdaaf
size 55009
oid sha256:75f493a62614704c956b8be9351532bb431cc69e400adc9f1d536579b6dc731c
size 64226

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dbd348146e2161e9b2a57af31d0ad48c44dd5f0274fbebfd885ea628235e08f9
size 42903
oid sha256:e96dae6cc767476bd60f23079776600449414a9423976b85c91c348ed065d7ae
size 44877

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9c7eb8bd040fb4321ecf28b5e00619f5356f23e5b07f59feb3894dc6620b9643
size 43335
oid sha256:fd0bb44da30359d50cf5754768d58d75bf4489c9d6fe9c4d5c0d2f3fd9020541
size 45302

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:63e2e25f5e92c1890dd23ccc8f0e9c352587c0ac1190c012e430c21b7b8d728e
size 38709
oid sha256:9654f73bd95d1b0d03dc6ea44d13c4910fb8e64a3c86644a11d5bc8591a4be8a
size 41312

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:63e2e25f5e92c1890dd23ccc8f0e9c352587c0ac1190c012e430c21b7b8d728e
size 38709
oid sha256:9654f73bd95d1b0d03dc6ea44d13c4910fb8e64a3c86644a11d5bc8591a4be8a
size 41312

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c37ec529bcec1a58e53fa21dd467fba2c3667a177dd9a49c7eb086b01e0c29a2
size 9613
oid sha256:ee403297e15204e165ec5dce90e3e93e95e8e1b2e2cbd03489b91c4c9a6de083
size 10360

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7bb86dbb18a135602034c3e40c8f070b54051669522f0655235e5d4e2c3830b6
size 8584
oid sha256:33237fb8f262176e6f291a8ded90ab56600113c93bed188f6d4d01df950df53e
size 9286

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ff5b9b385af291a3de025a4449f01d80078df3f447c2dea0873f7cade56b7ae7
size 39733
oid sha256:ac39dda453fbdf0f5cc3c6df2d9436a171c141c8be2ca2e2d2a2c7ee5d16b36f
size 39650

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ff9b924eacf7746fd91d5008c7ef2be5c2870d311d8bb629cda2551609f6c3a
size 43339
oid sha256:49ef94a7bd404fe5982f7319aa1c4f6b5d29d11675f53e192af85b4efb8c3517
size 43267

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9f460575d657933a81e13a914570687d81869144ac327f10b503fbe22c8b3a59
size 43344
oid sha256:47942f38dd2afb38d0e1174bd155bb708f645b231b250525920f5abaa7074308
size 43217

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:494167e5d7f96bf16b16f5b7c53b304dacf4015b31ce0cd51bfe9e05802664df
size 48831
oid sha256:b006f7768bbc48cf68e5d897920a14894496d0b2b84a1839a7158b45edea9a75
size 48757

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e3e09d6d5a4c0ea8a0dd8ab2ac2b55bfd0fcfbd3f130098adfff5c42500b5013
size 42442
oid sha256:12702b2d71193e376f15df6f1a029531868b9651b70c40d2d738646f15980704
size 42362

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58b326bad20c368430626a15ca98a8c7af66597262bc42e5de585485f261b3c5
size 42396
oid sha256:a942ed49efc175776994cf58d511af3be8f878b30ed5b34ab3ae0d600f2e0db1
size 42316

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ad6c6bdc7d4e4c405231c2c8944613ff151eb309e00821ac7eda107e853ed456
size 37891
oid sha256:d692cd9030e6193691e676de62f68a9241cebf220a1f6c7986e095370e25c547
size 37839

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4cfd887060df1a1b7e12e3f47010e813678fec822595d90b19c51af99b42ab16
size 41714
oid sha256:716e20f80dd634feb58ee26247defdd8e390d268e9ec5d9767ad7b574d7ca2bc
size 41581

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ede9377d3fbceae95a10e02cb1c033f9bbd53901137ef88ca52fdd4041737c70
size 41695
oid sha256:9658c925b80565109772111972a495d437bda9ae97a9d9327d92a44ff0a1b9d1
size 41521

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a976609ec6f5ce83388242db7a43ff6c062f84011ce7d2263e5ff8d23c64a96
size 47912
oid sha256:f0511bf277f5194a6d38cf13416c2b2a1c834bf216c511243351d2f9bfe1b7e3
size 47857

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e0a065c5a4c0af748524b0600e1b5233023cdff0eaae8a2039649323e8a19fe3
size 40635
oid sha256:e248fa1785749f3a590576ed6f3c6899ad0042f71a80a4afdc7336981c7419a3
size 40589

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dfa33ed990249e2d3e0097521bc51a3028bf51537736839a1301b54f99ad473b
size 40598
oid sha256:903df8ff5d3816a85e16a3ec46b2a98e742ba0fc0a317df829b0fca57f33d9aa
size 40553

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc30ecdc5f33bc07ebf3b8376078cfa2c0b3ef23c7e112b9e6e5726b42cc3fcf
size 31578
oid sha256:558f8f3b34e3a94a62460cca37396f94b0caa8197b7fbd49ce894382392f1899
size 31512

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc30ecdc5f33bc07ebf3b8376078cfa2c0b3ef23c7e112b9e6e5726b42cc3fcf
size 31578
oid sha256:558f8f3b34e3a94a62460cca37396f94b0caa8197b7fbd49ce894382392f1899
size 31512

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c638430bd598a44e76ca0c00ea2b0ff4e9dc047cea19e83b742765ba15ef1607
size 33317
oid sha256:c4469faa61e68d951ed13b9cf324d11a99a1b041fb096bade2cd5d11c8185acc
size 33232

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc6250ce3b15adb28fcada6698fdb556d391ea3a6beeded279fc5f30c71418e3
size 33333
oid sha256:81125c72229ddb56f3c3e8b826de42f947f8739c3f372cad99917ee7d4db3b5c
size 33318

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:085658106ddaecf65fea877669bba466ec947b0b05e77791a6ae48c9a83e1128
size 29611
oid sha256:cff519b372c22b623f72407e7c4113cfa24dae31fc517aea5de75bea9b4d61bf
size 29616

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c638430bd598a44e76ca0c00ea2b0ff4e9dc047cea19e83b742765ba15ef1607
size 33317
oid sha256:c4469faa61e68d951ed13b9cf324d11a99a1b041fb096bade2cd5d11c8185acc
size 33232

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:05de0afa4939c0ff67a9a613434f9f482a492f52c1ff29158865eec542c9a639
size 25340
oid sha256:78b278d94f46995b7d11719677b72c0804219b1c36072fd8e2ad6dfdb9de8aba
size 25313

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e2ef182ba3721fa37c014dff57b49cbb7ee23d6becbc4ee60d81d44aff11a198
size 30523
oid sha256:ef34f0de8599a2c950690d1ef1d4c8f68419b137594177ed09cfe3715deb3767
size 30490

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e2ef182ba3721fa37c014dff57b49cbb7ee23d6becbc4ee60d81d44aff11a198
size 30523
oid sha256:ef34f0de8599a2c950690d1ef1d4c8f68419b137594177ed09cfe3715deb3767
size 30490

Some files were not shown because too many files have changed in this diff Show more