Display a notice if Matrix ID isn't resolved (#461)
Display a notice if Matrix ID isn't resolved If we can't get the profile of a user after an mxid was searched for, show a warning under their ID to say the invite probably won't be delivered. Closes #424
This commit is contained in:
parent
4dbeaa3390
commit
473bfd1e23
89 changed files with 503 additions and 216 deletions
1
changelog.d/424.feature
Normal file
1
changelog.d/424.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
[Create and join rooms] Show a notice for MXIDs that don't resolve when searching for users to invite
|
||||
|
|
@ -22,40 +22,49 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
|
||||
@Composable
|
||||
fun SearchMultipleUsersResultItem(
|
||||
matrixUser: MatrixUser,
|
||||
searchResult: UserSearchResult,
|
||||
isUserSelected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onCheckedChange: (Boolean) -> Unit = {},
|
||||
) {
|
||||
CheckableMatrixUserRow(
|
||||
checked = isUserSelected,
|
||||
modifier = modifier,
|
||||
matrixUser = matrixUser,
|
||||
avatarSize = AvatarSize.Custom(36.dp),
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
if (searchResult.isUnresolved) {
|
||||
CheckableUnresolvedUserRow(
|
||||
checked = isUserSelected,
|
||||
modifier = modifier,
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.Custom(36.dp)),
|
||||
id = searchResult.matrixUser.userId.value,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
} else {
|
||||
CheckableMatrixUserRow(
|
||||
checked = isUserSelected,
|
||||
modifier = modifier,
|
||||
matrixUser = searchResult.matrixUser,
|
||||
avatarSize = AvatarSize.Custom(36.dp),
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SearchMultipleUsersResultItemLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SearchMultipleUsersResultItemDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Column {
|
||||
SearchMultipleUsersResultItem(matrixUser = aMatrixUser(), isUserSelected = true)
|
||||
SearchMultipleUsersResultItem(matrixUser = aMatrixUser(), isUserSelected = false)
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = false)
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = true)
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = false)
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,39 +17,48 @@
|
|||
package io.element.android.features.createroom.impl.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.UnresolvedUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
|
||||
@Composable
|
||||
fun SearchSingleUserResultItem(
|
||||
matrixUser: MatrixUser,
|
||||
searchResult: UserSearchResult,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
MatrixUserRow(
|
||||
modifier = modifier.clickable(onClick = onClick),
|
||||
matrixUser = matrixUser,
|
||||
avatarSize = AvatarSize.Custom(36.dp),
|
||||
)
|
||||
if (searchResult.isUnresolved) {
|
||||
UnresolvedUserRow(
|
||||
modifier = modifier.clickable(onClick = onClick),
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.Custom(36.dp)),
|
||||
id = searchResult.matrixUser.userId.value,
|
||||
)
|
||||
} else {
|
||||
MatrixUserRow(
|
||||
modifier = modifier.clickable(onClick = onClick),
|
||||
matrixUser = searchResult.matrixUser,
|
||||
avatarSize = AvatarSize.Custom(36.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SearchSingleUserResultItemLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SearchSingleUserResultItemDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview{ ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
SearchSingleUserResultItem(matrixUser = aMatrixUser())
|
||||
Column {
|
||||
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false))
|
||||
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,13 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
|
|||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun SearchUserBar(
|
||||
query: String,
|
||||
state: SearchBarResultState<ImmutableList<MatrixUser>>,
|
||||
state: SearchBarResultState<ImmutableList<UserSearchResult>>,
|
||||
selectedUsers: ImmutableList<MatrixUser>,
|
||||
active: Boolean,
|
||||
isMultiSelectionEnabled: Boolean,
|
||||
|
|
@ -68,26 +69,26 @@ fun SearchUserBar(
|
|||
resultHandler = { users ->
|
||||
LazyColumn {
|
||||
if (isMultiSelectionEnabled) {
|
||||
items(users) { matrixUser ->
|
||||
items(users) { searchResult ->
|
||||
SearchMultipleUsersResultItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
matrixUser = matrixUser,
|
||||
isUserSelected = selectedUsers.find { it.userId == matrixUser.userId } != null,
|
||||
searchResult = searchResult,
|
||||
isUserSelected = selectedUsers.find { it.userId == searchResult.matrixUser.userId } != null,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
onUserSelected(matrixUser)
|
||||
onUserSelected(searchResult.matrixUser)
|
||||
} else {
|
||||
onUserDeselected(matrixUser)
|
||||
onUserDeselected(searchResult.matrixUser)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(users) { matrixUser ->
|
||||
items(users) { searchResult ->
|
||||
SearchSingleUserResultItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
matrixUser = matrixUser,
|
||||
onClick = { onUserSelected(matrixUser) }
|
||||
searchResult = searchResult,
|
||||
onClick = { onUserSelected(searchResult.matrixUser) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
|||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchResults: SearchBarResultState<ImmutableList<MatrixUser>> by remember {
|
||||
var searchResults: SearchBarResultState<ImmutableList<UserSearchResult>> by remember {
|
||||
mutableStateOf(SearchBarResultState.NotSearching())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,11 +18,12 @@ package io.element.android.features.createroom.impl.userlist
|
|||
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class UserListState(
|
||||
val searchQuery: String,
|
||||
val searchResults: SearchBarResultState<ImmutableList<MatrixUser>>,
|
||||
val searchResults: SearchBarResultState<ImmutableList<UserSearchResult>>,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
val isSearchActive: Boolean,
|
||||
val selectionMode: SelectionMode,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.createroom.impl.userlist
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
|
@ -38,14 +39,14 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
|||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
searchResults = SearchBarResultState.Results(aMatrixUserList().map { UserSearchResult(it) }.toImmutableList()),
|
||||
),
|
||||
aUserListState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
searchResults = SearchBarResultState.Results(aMatrixUserList().map { UserSearchResult(it) }.toImmutableList()),
|
||||
),
|
||||
aUserListState().copy(
|
||||
isSearchActive = true,
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@
|
|||
<string name="screen_create_room_add_people_title">"Añadir personas"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Se ha producido un error al intentar iniciar un chat"</string>
|
||||
<string name="screen_create_room_title">"Crear una sala"</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@
|
|||
<string name="screen_create_room_add_people_title">"Aggiungi persone"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Si è verificato un errore durante il tentativo di avviare una chat"</string>
|
||||
<string name="screen_create_room_title">"Crea una stanza"</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -14,4 +14,4 @@
|
|||
<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>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -130,18 +131,18 @@ class DefaultUserListPresenterTests {
|
|||
skipItems(2)
|
||||
|
||||
// When the user repository emits a result, it's copied to the state
|
||||
userRepository.emitResult(listOf(aMatrixUser()))
|
||||
userRepository.emitResult(listOf(UserSearchResult(aMatrixUser())))
|
||||
assertThat(awaitItem().searchResults).isEqualTo(
|
||||
SearchBarResultState.Results(
|
||||
persistentListOf(aMatrixUser())
|
||||
persistentListOf(UserSearchResult(aMatrixUser()))
|
||||
)
|
||||
)
|
||||
|
||||
// When the user repository emits another result, it replaces the previous value
|
||||
userRepository.emitResult(aMatrixUserList())
|
||||
userRepository.emitResult(aMatrixUserList().map { UserSearchResult(it) })
|
||||
assertThat(awaitItem().searchResults).isEqualTo(
|
||||
SearchBarResultState.Results(
|
||||
aMatrixUserList()
|
||||
aMatrixUserList().map { UserSearchResult(it) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,9 +45,9 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs
|
||||
import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection
|
||||
import io.element.android.libraries.architecture.isLoading
|
||||
|
|
@ -67,6 +67,7 @@ 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
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
|
@ -185,7 +186,7 @@ internal fun RoomHeaderSection(
|
|||
|
||||
@Composable
|
||||
internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) {
|
||||
PreferenceCategory(title = stringResource(R.string.screen_room_details_topic_title), modifier = modifier) {
|
||||
PreferenceCategory(title = stringResource(StringR.string.common_topic), modifier = modifier) {
|
||||
Text(
|
||||
roomTopic,
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp),
|
||||
|
|
|
|||
|
|
@ -126,15 +126,16 @@ class RoomInviteMembersPresenter @Inject constructor(
|
|||
userRepository.search(searchQuery).collect {
|
||||
searchResults.value = when {
|
||||
it.isEmpty() -> SearchBarResultState.NoResults()
|
||||
else -> SearchBarResultState.Results(it.map { user ->
|
||||
val existingMembership = joinedMembers.firstOrNull { j -> j.userId == user.userId }?.membership
|
||||
else -> SearchBarResultState.Results(it.map { result ->
|
||||
val existingMembership = joinedMembers.firstOrNull { j -> j.userId == result.matrixUser.userId }?.membership
|
||||
val isJoined = existingMembership == RoomMembershipState.JOIN
|
||||
val isInvited = existingMembership == RoomMembershipState.INVITE
|
||||
InvitableUser(
|
||||
matrixUser = user,
|
||||
isSelected = selectedUsers.value.contains(user) || isJoined || isInvited,
|
||||
matrixUser = result.matrixUser,
|
||||
isSelected = selectedUsers.value.contains(result.matrixUser) || isJoined || isInvited,
|
||||
isAlreadyJoined = isJoined,
|
||||
isAlreadyInvited = isInvited,
|
||||
isUnresolved = result.isUnresolved,
|
||||
)
|
||||
}.toImmutableList())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,4 +35,5 @@ data class InvitableUser(
|
|||
val isSelected: Boolean = false,
|
||||
val isAlreadyJoined: Boolean = false,
|
||||
val isAlreadyInvited: Boolean = false,
|
||||
val isUnresolved: Boolean = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -48,5 +48,19 @@ internal class RoomInviteMembersStateProvider : PreviewParameterProvider<RoomInv
|
|||
)
|
||||
)
|
||||
),
|
||||
RoomInviteMembersState(
|
||||
isSearchActive = true,
|
||||
canInvite = true,
|
||||
searchQuery = "@alice:server.org",
|
||||
selectedUsers = persistentListOf(
|
||||
aMatrixUser("@carol:server.org", "Carol")
|
||||
),
|
||||
searchResults = SearchBarResultState.Results(
|
||||
persistentListOf(
|
||||
InvitableUser(aMatrixUser("@alice:server.org"), isUnresolved = true),
|
||||
InvitableUser(aMatrixUser("@bob:server.org", "Bob")),
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
|
|||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.CheckableUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
|
|
@ -180,22 +181,32 @@ private fun RoomInviteMembersSearchBar(
|
|||
|
||||
LazyColumn {
|
||||
items(results) { invitableUser ->
|
||||
CheckableUserRow(
|
||||
checked = invitableUser.isSelected,
|
||||
enabled = !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined,
|
||||
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.MEDIUM),
|
||||
name = invitableUser.matrixUser.getBestName(),
|
||||
subtext = when {
|
||||
// If they're already invited or joined we show that information
|
||||
invitableUser.isAlreadyJoined -> stringResource(R.string.screen_room_details_already_a_member)
|
||||
invitableUser.isAlreadyInvited -> stringResource(R.string.screen_room_details_already_invited)
|
||||
// Otherwise show the ID, unless that's already used for their name
|
||||
invitableUser.matrixUser.displayName.isNullOrEmpty().not() -> invitableUser.matrixUser.userId.value
|
||||
else -> null
|
||||
},
|
||||
onCheckedChange = { onUserToggled(invitableUser.matrixUser) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
if (invitableUser.isUnresolved && !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined) {
|
||||
CheckableUnresolvedUserRow(
|
||||
checked = invitableUser.isSelected,
|
||||
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.MEDIUM),
|
||||
id = invitableUser.matrixUser.userId.value,
|
||||
onCheckedChange = { onUserToggled(invitableUser.matrixUser) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} else {
|
||||
CheckableUserRow(
|
||||
checked = invitableUser.isSelected,
|
||||
enabled = !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined,
|
||||
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.MEDIUM),
|
||||
name = invitableUser.matrixUser.getBestName(),
|
||||
subtext = when {
|
||||
// If they're already invited or joined we show that information
|
||||
invitableUser.isAlreadyJoined -> stringResource(R.string.screen_room_details_already_a_member)
|
||||
invitableUser.isAlreadyInvited -> stringResource(R.string.screen_room_details_already_invited)
|
||||
// Otherwise show the ID, unless that's already used for their name
|
||||
invitableUser.matrixUser.displayName.isNullOrEmpty().not() -> invitableUser.matrixUser.userId.value
|
||||
else -> null
|
||||
},
|
||||
onCheckedChange = { onUserToggled(invitableUser.matrixUser) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -29,10 +29,12 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
|
|||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -130,7 +132,7 @@ internal class RoomInviteMembersPresenterTest {
|
|||
skipItems(1)
|
||||
|
||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||
repository.emitResult(aMatrixUserList())
|
||||
repository.emitResult(aMatrixUserList().map { UserSearchResult(it) })
|
||||
skipItems(1)
|
||||
|
||||
val resultState = awaitItem()
|
||||
|
|
@ -175,7 +177,7 @@ internal class RoomInviteMembersPresenterTest {
|
|||
skipItems(1)
|
||||
|
||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||
repository.emitResult(aMatrixUserList())
|
||||
repository.emitResult(aMatrixUserList().map { UserSearchResult(it) })
|
||||
skipItems(1)
|
||||
|
||||
val resultState = awaitItem()
|
||||
|
|
@ -202,6 +204,53 @@ internal class RoomInviteMembersPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - performs search and handles unresolved results`() = runTest {
|
||||
val userList = aMatrixUserList()
|
||||
val joinedUser = userList[0]
|
||||
val invitedUser = userList[1]
|
||||
|
||||
val repository = FakeUserRepository()
|
||||
val presenter = RoomInviteMembersPresenter(
|
||||
userRepository = repository,
|
||||
roomMemberListDataSource = createDataSource(FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(
|
||||
aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN),
|
||||
aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE),
|
||||
)))
|
||||
}),
|
||||
coroutineDispatchers = testCoroutineDispatchers()
|
||||
)
|
||||
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query"))
|
||||
skipItems(1)
|
||||
|
||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||
|
||||
val unresolvedUser = UserSearchResult(aMatrixUser(id = A_USER_ID.value), isUnresolved = true)
|
||||
repository.emitResult(listOf(unresolvedUser) + aMatrixUserList().map { UserSearchResult(it) })
|
||||
skipItems(1)
|
||||
|
||||
val resultState = awaitItem()
|
||||
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||
|
||||
val users = resultState.searchResults.users()
|
||||
|
||||
val userWhoShouldBeUnresolved = users.first()
|
||||
assertThat(userWhoShouldBeUnresolved.isUnresolved).isTrue()
|
||||
|
||||
// All other users are neither joined nor invited
|
||||
val otherUsers = users.minus(userWhoShouldBeUnresolved)
|
||||
assertThat(otherUsers.none { it.isUnresolved }).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - toggle users updates selected user state`() = runTest {
|
||||
val repository = FakeUserRepository()
|
||||
|
|
@ -254,7 +303,7 @@ internal class RoomInviteMembersPresenterTest {
|
|||
skipItems(1)
|
||||
|
||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||
repository.emitResult(aMatrixUserList() + selectedUser)
|
||||
repository.emitResult((aMatrixUserList() + selectedUser).map { UserSearchResult(it) })
|
||||
skipItems(2)
|
||||
|
||||
val resultState = awaitItem()
|
||||
|
|
@ -296,7 +345,7 @@ internal class RoomInviteMembersPresenterTest {
|
|||
skipItems(1)
|
||||
|
||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||
repository.emitResult(aMatrixUserList() + selectedUser)
|
||||
repository.emitResult((aMatrixUserList() + selectedUser).map { UserSearchResult(it) })
|
||||
skipItems(2)
|
||||
|
||||
// And then a user is toggled
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.Error
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.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.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
|
||||
@Composable
|
||||
fun UnresolvedUserRow(
|
||||
avatarData: AvatarData,
|
||||
id: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Avatar(avatarData)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp),
|
||||
) {
|
||||
// ID
|
||||
Text(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
text = id,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
|
||||
// Warning
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Error,
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.align(Alignment.Top)
|
||||
.padding(2.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.common_invite_unknown_profile),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckableUnresolvedUserRow(
|
||||
checked: Boolean,
|
||||
avatarData: AvatarData,
|
||||
id: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onCheckedChange: (Boolean) -> Unit = {},
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(role = Role.Checkbox, enabled = enabled) {
|
||||
onCheckedChange(!checked)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
UnresolvedUserRow(
|
||||
modifier = Modifier.weight(1f),
|
||||
avatarData = avatarData,
|
||||
id = id,
|
||||
)
|
||||
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun UnresolvedUserRowPreview() =
|
||||
ElementThemedPreview {
|
||||
val matrixUser = aMatrixUser()
|
||||
UnresolvedUserRow(matrixUser.getAvatarData(), matrixUser.userId.value)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,10 +16,9 @@
|
|||
|
||||
package io.element.android.libraries.usersearch.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface UserRepository {
|
||||
|
||||
suspend fun search(query: String): Flow<List<MatrixUser>>
|
||||
suspend fun search(query: String): Flow<List<UserSearchResult>>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.libraries.usersearch.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
data class UserSearchResult(
|
||||
val matrixUser: MatrixUser,
|
||||
val isUnresolved: Boolean = false,
|
||||
)
|
||||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserListDataSource
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
|
@ -33,24 +34,26 @@ class MatrixUserRepository @Inject constructor(
|
|||
private val dataSource: UserListDataSource
|
||||
) : UserRepository {
|
||||
|
||||
override suspend fun search(query: String): Flow<List<MatrixUser>> = flow {
|
||||
override suspend fun search(query: String): Flow<List<UserSearchResult>> = flow {
|
||||
// Manually add a fake result with the matrixId, if any
|
||||
val isUserId = MatrixPatterns.isUserId(query)
|
||||
if (isUserId) {
|
||||
emit(listOf(MatrixUser(UserId(query))))
|
||||
emit(listOf(UserSearchResult(MatrixUser(UserId(query)))))
|
||||
}
|
||||
|
||||
if (query.length >= MINIMUM_SEARCH_LENGTH) {
|
||||
// Debounce
|
||||
delay(DEBOUNCE_TIME_MILLIS)
|
||||
|
||||
val results = dataSource.search(query).toMutableList()
|
||||
val results = dataSource.search(query).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.userId.value == query }) {
|
||||
val getProfileResult: MatrixUser? = dataSource.getProfile(UserId(query))
|
||||
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
||||
results.add(0, profile)
|
||||
if (isUserId && results.none { it.matrixUser.userId.value == query }) {
|
||||
results.add(
|
||||
0,
|
||||
dataSource.getProfile(UserId(query))
|
||||
?.let { UserSearchResult(it) }
|
||||
?: UserSearchResult(MatrixUser(UserId(query)), isUnresolved = true))
|
||||
}
|
||||
|
||||
emit(results)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
|||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import io.element.android.libraries.usersearch.test.FakeUserListDataSource
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -63,7 +64,7 @@ internal class MatrixUserRepositoryTest {
|
|||
val result = repository.search("some query")
|
||||
|
||||
result.test {
|
||||
assertThat(awaitItem()).isEqualTo(aMatrixUserList())
|
||||
assertThat(awaitItem()).isEqualTo(aMatrixUserList().toUserSearchResults())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
|
@ -76,7 +77,7 @@ internal class MatrixUserRepositoryTest {
|
|||
val result = repository.search(A_USER_ID.value)
|
||||
|
||||
result.test {
|
||||
assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)))
|
||||
assertThat(awaitItem()).isEqualTo(listOf(placeholderResult()))
|
||||
skipItems(1)
|
||||
awaitComplete()
|
||||
}
|
||||
|
|
@ -93,7 +94,7 @@ internal class MatrixUserRepositoryTest {
|
|||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(searchResults)
|
||||
assertThat(awaitItem()).isEqualTo(searchResults.toUserSearchResults())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
|
@ -112,13 +113,13 @@ internal class MatrixUserRepositoryTest {
|
|||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(listOf(userProfile) + searchResults)
|
||||
assertThat(awaitItem()).isEqualTo((listOf(userProfile) + searchResults).toUserSearchResults())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - just shows id if profile can't be loaded`() = runTest {
|
||||
fun `search - returns unresolved user if profile can't be loaded`() = runTest {
|
||||
val searchResults = aMatrixUserListWithoutUserId(A_USER_ID)
|
||||
|
||||
val dataSource = FakeUserListDataSource()
|
||||
|
|
@ -130,11 +131,15 @@ internal class MatrixUserRepositoryTest {
|
|||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)) + searchResults)
|
||||
assertThat(awaitItem()).isEqualTo(listOf(placeholderResult(isUnresolved = true)) + searchResults.toUserSearchResults())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun aMatrixUserListWithoutUserId(userId: UserId) = aMatrixUserList().filterNot { it.userId == userId }
|
||||
|
||||
private fun List<MatrixUser>.toUserSearchResults() = map { UserSearchResult(it) }
|
||||
|
||||
private fun placeholderResult(id: UserId = A_USER_ID, isUnresolved: Boolean = false) = UserSearchResult(MatrixUser(id), isUnresolved = isUnresolved)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
package io.element.android.libraries.usersearch.test
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
|
|
@ -26,14 +26,14 @@ class FakeUserRepository : UserRepository {
|
|||
var providedQuery: String? = null
|
||||
private set
|
||||
|
||||
private val flow = MutableSharedFlow<List<MatrixUser>>()
|
||||
private val flow = MutableSharedFlow<List<UserSearchResult>>()
|
||||
|
||||
override suspend fun search(query: String): Flow<List<MatrixUser>> {
|
||||
override suspend fun search(query: String): Flow<List<UserSearchResult>> {
|
||||
providedQuery = query
|
||||
return flow
|
||||
}
|
||||
|
||||
suspend fun emitResult(result: List<MatrixUser>) {
|
||||
suspend fun emitResult(result: List<UserSearchResult>) {
|
||||
flow.emit(result)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:439a2d9e9497486e86b63fe42402f1c2ee33b15695e47312e18867530d041dad
|
||||
size 96539
|
||||
oid sha256:c1c1eedbab868e0c2501220293608572850e052f685f7076ec939b3f1a9abf27
|
||||
size 4464
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:255b99dea5c1da9d466533d510d7c0abe39a79cdc42b579cfa186176e9c6a49d
|
||||
size 91991
|
||||
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
||||
size 4457
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4bd925ebe0257b400ed0b1cb956848622d1b42d8afcc71f915522c4878aaf94b
|
||||
size 23447
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:12b569ad80bb4a4df89d70d9ae4a92da0d9e96deb477e1e437efbef5ec4a1fdd
|
||||
size 22302
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8e7352789223e57f85c597a591ada117c903e06ed2df49719a98e793d128cd7b
|
||||
size 106579
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:91de9d54216aac2bf2c4721e5e8ff5e74be43105846bdac16230cb7bea7427af
|
||||
size 13777
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:38fbef57e6fb2860d05ab2b864bab16a30ebc98199eb18b19e422f946c52e163
|
||||
size 13096
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:815864d4c929c5f123bc830d367b4f983851412891a23329d03e58e7726e4280
|
||||
size 54198
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:56c23bd0880524cc56b713c93c5358e9f3771291c20f7a149bad00465f3f987a
|
||||
size 20648
|
||||
oid sha256:b13d46300c7cdd3632ee39a6bfce92a5b9bfa44917b32928da313741965addc7
|
||||
size 8677
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b7b91a2d05b975d568116615c286568f376ebead49e25ff17f5aae8b75be0e1f
|
||||
size 28876
|
||||
oid sha256:4d63e57cea65fd8617f19f2775c6cacb58fb1fd933ac26895445b49c8d8638d4
|
||||
size 17363
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f12591531cfdce24378003dda48940c20d8c9cf7bfac73ac1e7713e925bfac4d
|
||||
size 20214
|
||||
oid sha256:255e7caab6e7133239a186a421cf669649133704b15cddebe2dbc4ca9baca372
|
||||
size 8731
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:44a35f28b3a59cc28937fee16eda26c26ef7b7622f929218e40f7537e096b2e8
|
||||
size 28120
|
||||
oid sha256:4d62a75d2faea119344221414f4442bb0682bf74a1bd71ca1cfc5ddfcaea4635
|
||||
size 17342
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:372f239e351215dadf0d5c451b8105e93bc86e338f4e564aa4726d482028ae9c
|
||||
size 396027
|
||||
oid sha256:6e30e5a9ef2371748af9a77806168d5e49185b2e565308f31edb62b94f8060b0
|
||||
size 396024
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:087ef6e2e489e63e1a30793b183915984a32265676fd5a95f02d7ca02821c84b
|
||||
size 132174
|
||||
oid sha256:036b362d18c690ff516de13f45aa5561902772bb4ff5a5c48383e22b27eb2999
|
||||
size 132172
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f5e03b31f020201e8a8bd7ad5962022337441c4e83d15e03e95f83c9fa10eaaf
|
||||
size 98743
|
||||
oid sha256:e4c14c033b2d4961bad5d05ae7eb61e72fc542e497bb09770271980321832607
|
||||
size 98742
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479
|
||||
size 393620
|
||||
oid sha256:bc6306a6c951e34884603f71ceae6c6fdbf32889146b60049f41f9451972dd2e
|
||||
size 393618
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479
|
||||
size 393620
|
||||
oid sha256:bc6306a6c951e34884603f71ceae6c6fdbf32889146b60049f41f9451972dd2e
|
||||
size 393618
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479
|
||||
size 393620
|
||||
oid sha256:bc6306a6c951e34884603f71ceae6c6fdbf32889146b60049f41f9451972dd2e
|
||||
size 393618
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479
|
||||
size 393620
|
||||
oid sha256:bc6306a6c951e34884603f71ceae6c6fdbf32889146b60049f41f9451972dd2e
|
||||
size 393618
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479
|
||||
size 393620
|
||||
oid sha256:bc6306a6c951e34884603f71ceae6c6fdbf32889146b60049f41f9451972dd2e
|
||||
size 393618
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:712e7422e6f0ae9d6093619b156d982b0d4a861dc04c04473d3d0eb5751bd097
|
||||
size 6302
|
||||
oid sha256:8db2a0def5cf23917ababd10121dda386ccc44aa0172810e9ca916fbb63b08d0
|
||||
size 6303
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7e3db033f79ac2c77abb8c41f6331e17ca89702fe00e42fa0b3c3f68b0d331de
|
||||
size 7036
|
||||
oid sha256:417e4325a43fd13637f54fa71536441f2c4b5652e3bb25e2d579ef7d713c4aa9
|
||||
size 7035
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:712e7422e6f0ae9d6093619b156d982b0d4a861dc04c04473d3d0eb5751bd097
|
||||
size 6302
|
||||
oid sha256:8db2a0def5cf23917ababd10121dda386ccc44aa0172810e9ca916fbb63b08d0
|
||||
size 6303
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7e3db033f79ac2c77abb8c41f6331e17ca89702fe00e42fa0b3c3f68b0d331de
|
||||
size 7036
|
||||
oid sha256:417e4325a43fd13637f54fa71536441f2c4b5652e3bb25e2d579ef7d713c4aa9
|
||||
size 7035
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7cdecfdb401b135159f3a4f9001a7739e4ba9a8fd80371b4a68278a07a8834c0
|
||||
size 6161
|
||||
oid sha256:593546b9837061cc2f0e27b94fe8a6fae80c35db62767cd43b97cc77c942c71b
|
||||
size 6163
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:310058557ae80956fe325fc8a58df7b62c45040d707a0f7569f87cbb74cede07
|
||||
size 6836
|
||||
oid sha256:93805e6e1a168295b4263152f569b3b7065e890c11870f7e21f5b0f5cc1b07a6
|
||||
size 6834
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7cdecfdb401b135159f3a4f9001a7739e4ba9a8fd80371b4a68278a07a8834c0
|
||||
size 6161
|
||||
oid sha256:593546b9837061cc2f0e27b94fe8a6fae80c35db62767cd43b97cc77c942c71b
|
||||
size 6163
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:310058557ae80956fe325fc8a58df7b62c45040d707a0f7569f87cbb74cede07
|
||||
size 6836
|
||||
oid sha256:93805e6e1a168295b4263152f569b3b7065e890c11870f7e21f5b0f5cc1b07a6
|
||||
size 6834
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3dc781e3f17f09eb7e0cb8bf753a68ebde850b0b618e8d6bff7c780e365ee778
|
||||
size 11853
|
||||
oid sha256:ff1f9f65252e855037d6008aea0d884ccaec6419412b26e4ad12a32ab583f5ca
|
||||
size 11848
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6d14b21a4723db7ac5ceda97eee44681d5d236007262e0e9e8b5a8d5abf8022b
|
||||
oid sha256:37c370bc41527d7b407679c971a35aee051da93c033c2f195b6c3d6c7b2d885a
|
||||
size 11514
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:121e9eb7013123e5647e351eda7d331ade8032ae5aa56bd1be943ee6c6acad53
|
||||
size 41444
|
||||
oid sha256:f99c5fd192b3d56188dc0a14a522b1ea4a05cdbe9b18f63c4a4c99f0914c0df5
|
||||
size 41443
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e19a24fa27305822af8ec3a4dfac727eb1297ccbbeffec726664c6bc29b1a2a4
|
||||
size 53421
|
||||
oid sha256:8a524ff4463d72a0adca6a13907d95c6fb2cfe8afeba541cfe03c8fb44202d4f
|
||||
size 53427
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9ab84b59725483b7187e91f3df18d1d55decbef7da37e186099027c6c98e3430
|
||||
oid sha256:6c7090f345c722c0c5d5d415723e1770843d36fef48dc750a893320830b9dfd4
|
||||
size 46068
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5b388b6664ea0e5a86d6506a954cbcdc36c39054204739365eb0228b2e1d4fd4
|
||||
size 197778
|
||||
oid sha256:b876f9b49579215a6f0cbb73627e6c5ad19e7b71131bee02a2fe4b7f46432a67
|
||||
size 197787
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dbeca10cc6a489dd20390bbc3bf5f2fa4f726342083621b8161a2a4448622ffc
|
||||
size 198122
|
||||
oid sha256:6d358680e2b08ed4d4f709b9f4d908f5cad84b888a7aa74b49609b09297e416e
|
||||
size 198126
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:23ed23fbc4a47152c4d9b6373111c7de5497900e9ccb3382f99364bde2444032
|
||||
oid sha256:f8f54ea6a7d158e8f7da56648aabf05c8dbc761db8698891d2d3f1ff863b76fb
|
||||
size 51724
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8a715939660ec1a2bea4c5a3e9d0a3b4b0bc45f137fae4c3210476145346a3a9
|
||||
oid sha256:4c2845cd6ef914a8062d3685d62d6b861a3963e09aad13efd5a9ac5e2de1a3ea
|
||||
size 73507
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f3094dab56dd2c46a5c34162e717afcf674823be080959ec5e852227946237ff
|
||||
oid sha256:8dbf4c20504f68324ab8606d84f41390bef28479f5e0f9d76d34c358403b0de8
|
||||
size 42954
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:44534d8513114e666e7faa341a81184420fd84e8c3723c2d17236949f31e2fdb
|
||||
size 55118
|
||||
oid sha256:3defc06e2e3d791261e87048fb019d2da7e8372d35550bcb17aacb2bb62370eb
|
||||
size 55127
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f988cd33ae74aa292f26f0c6f11b5fcec8ff298005423161921e912a5e416c97
|
||||
size 39341
|
||||
oid sha256:824b93fd466fff749da6f6314fecbc1a15d316b3cac201adac0137c28660e121
|
||||
size 39342
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9a65842cd12943594ed28ffba0a958ff076d6c6492533bfc5ffd395b915b2b30
|
||||
size 56467
|
||||
oid sha256:8233a3eafef97e18dcd6b846d78af3e3a6a4fb79b89af276a1a41067f9e9af45
|
||||
size 56472
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f7822ba6ecc9e7cf7bfdb4446b6796dce685d2417f6ed29b6ef5015438e09ace
|
||||
size 40935
|
||||
oid sha256:fbe93775a53dc0e06a019d2fd3c72612dfbf9000709f48b4cdcc14ef72c914ad
|
||||
size 40934
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d3d8fae3d49c61850eef15c762d1f9615a735ca1943e78aa4dc116eaa830c02a
|
||||
size 53182
|
||||
oid sha256:4703dc63b3015c2a2c96e731dbabe55f2cff07c5508853f370d4eeb67e67740a
|
||||
size 53186
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ebc4e13d35c918274f35445db57955780bfdc037d5bd85a98eddb9b3bac42d5e
|
||||
size 45846
|
||||
oid sha256:70a1848c08f7e3ed4fcc2cd0b05344234b2c4a2c26139c256344b102b49d98a1
|
||||
size 45847
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed3f266ce286216deca3747ded2b46bd7ef37c707145d1ae676e8afe227850b2
|
||||
size 199651
|
||||
oid sha256:4fe993ac7679fef189bcbfdf9e82abfb0263686721fcabb4ddeca8d2a3d4e209
|
||||
size 199655
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:685a607ef2c644b8844a1a83bc9427040f3890b4b9f777aa80592c98f0a96d87
|
||||
size 199963
|
||||
oid sha256:fa118c2f28fe2c7bd3a194cf80cb25ece8c61fd15edd407d93ee76816c362fe1
|
||||
size 199975
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d5975a89ee4cf263317079be95b6f6cc199bfbbcf75ac88d622989bfd859c813
|
||||
size 51801
|
||||
oid sha256:022ca7f2b5ce17e0079346778ee3b99b1e235439f88885a2d1bf545cc11864e6
|
||||
size 51800
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0375a01da0c470b8379d2edcfb282b4b55f5cee86dc9da3efbe66fbadf7019a5
|
||||
size 74079
|
||||
oid sha256:c347c51252c8e1305385c6c92936941403c481c792ba3fe10ca4949a1accd051
|
||||
size 74080
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f085dfb5b0147374af21b8df3f4cd2b9930e638188add6f3a4fa94008747c7ee
|
||||
size 42553
|
||||
oid sha256:51120566fc91498097a2e68232f04342ade84d4aee702d5cc06ee5ff4bc17460
|
||||
size 42551
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7ec8c64bb4500bce08725d74753f45db93a369095626064ecdf9a83122aea326
|
||||
size 55285
|
||||
oid sha256:548566862bbb0da3e13b43aee882b28656ea544b9341d6152d4d1f5c526ed3f2
|
||||
size 55292
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:54046d58d89dd8f44e8e2fdb83306682fd6305a32ba91c06c9bcd042e99bbdd3
|
||||
oid sha256:092598b99ed8d53381f80352c357b13b159dd65cb23ef54d277bfa862de28263
|
||||
size 38928
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a96004c7012bd1b83eb395511472a6c93f410f9504656f4e3621d7809eccca9f
|
||||
size 56483
|
||||
oid sha256:0dd943e910de80c7a36db12acb17b0ee116228181ddb58a45c8ec9252e85e435
|
||||
size 56491
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:49759fbb46a966d0e0f1c793545e320e7588ac482399a12ac45762474e6781f8
|
||||
size 45579
|
||||
oid sha256:74d763ff8d44e686ee3a10dcadc0e2c1f02e3332c486d62d00e2333d0c3b60cf
|
||||
size 45578
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cdca9e94c071c20753aa2076f6c3a7ed931a2c1e90a78c29e00f27f7af99dfe8
|
||||
size 45758
|
||||
oid sha256:09b29e2e7bb8c38c978d3b56b04a97fc96d752154bb78fddd13a5cde8051a048
|
||||
size 45757
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4e47966820a6a77efece51aaac5adbe3dd1dad1880b1feb7da2e86e36b386f7e
|
||||
size 43815
|
||||
oid sha256:a936d98080f35c838adab660ef9b9287fc380d1358335c54e3da56230476721f
|
||||
size 43810
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bc3fc4c97a43c5786a75328e35828ed0e0cfd74ffab673832053ecc11db9b5e0
|
||||
size 44852
|
||||
oid sha256:43042ccb540b5fa3b8d5d9e273bd1155258d0dc12875b95c7a364492b3985da2
|
||||
size 44854
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7f82a51b6b6ed4ac030c4602da9da2629010169e998aaba16f9e05c652f6010d
|
||||
size 45283
|
||||
oid sha256:16cf470ec6ff50ee469db7bdc2cbfc1e32c5bcd7835c12c7313aafa495a44825
|
||||
size 45282
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5d18843f61267ae7385c23113be5c5c3d4376d95a843eef126ed670877f69b80
|
||||
size 42281
|
||||
oid sha256:e6cc29403f3b3e146e9a637973320f5bd6bf73feb7b042f8844337817e551113
|
||||
size 42280
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c820bd324df729db08710d1a6c17ca34a451a3bb94da2206fc57d6b4efe91e2f
|
||||
size 60019
|
||||
oid sha256:b53d55b5085673ac3a0a7663f7ff68e7d510fe352f3931b49c7e75e4c3767b93
|
||||
size 60007
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:da2f9f87d89382b4b18d39a477a8e86f98067e2326adae4def8374b5bf09f316
|
||||
size 57588
|
||||
oid sha256:a761318f3dfbc2ce6e777cfabc19eeb2f89100ae7631660f4b4d7550cd947c84
|
||||
size 57580
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:628f1f00dca9d15faabd8288a3c54b1b8581a380cf14edf364f0dee9ecc187d5
|
||||
oid sha256:147786f2cfcf7674147253cca5e41b4af96587a27216076a1c0802c81b0b1b46
|
||||
size 180117
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:827bccb0fdd3106fb24a95d665b4da2cfc15de48e8f508ae809c9f75d6d1bbf0
|
||||
size 178915
|
||||
oid sha256:789a979f7207531b4b9ba488ae2de52e6046809e788baf6690e1383a933cf624
|
||||
size 178916
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cb1178f063eafb21ba01b93310d84335afbc4e6ad1fc99d1776578fe3f7eaa07
|
||||
size 49237
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ca9a896e2dd3cf47792467f052f2d45b555d99ccbd91436ef4c03dfca66836ab
|
||||
size 47542
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:33e3ba31ada4522ce7c9b6adafd4c1c37df1927017e4bba8bf3f418a352a0cc9
|
||||
size 129286
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4bd094880b88662dd76409e4a728eda3736700c59c0721b87ba2568ee4bc0782
|
||||
size 39296
|
||||
Loading…
Add table
Add a link
Reference in a new issue