Browse Source

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
feature/fga/small_timeline_improvements
Chris Smith 1 year ago committed by GitHub
parent
commit
473bfd1e23
  1. 1
      changelog.d/424.feature
  2. 45
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt
  3. 39
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt
  4. 19
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt
  5. 4
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt
  6. 3
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt
  7. 5
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt
  8. 9
      features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt
  9. 5
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt
  10. 9
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt
  11. 1
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt
  12. 14
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt
  13. 43
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt
  14. 57
      features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt
  15. 150
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt
  16. 3
      libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt
  17. 24
      libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserSearchResult.kt
  18. 17
      libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt
  19. 17
      libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepositoryTest.kt
  20. 8
      libraries/usersearch/test/src/main/kotlin/io/element/android/libraries/usersearch/test/FakeUserRepository.kt
  21. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
  22. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
  23. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemDarkPreview_0_null,NEXUS_5,1.0,en].png
  24. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemLightPreview_0_null,NEXUS_5,1.0,en].png
  25. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemPreview_0_null,NEXUS_5,1.0,en].png
  26. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemDarkPreview_0_null,NEXUS_5,1.0,en].png
  27. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemLightPreview_0_null,NEXUS_5,1.0,en].png
  28. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemPreview_0_null,NEXUS_5,1.0,en].png
  29. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
  30. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
  31. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
  32. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
  33. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
  34. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
  35. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
  36. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
  37. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
  38. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
  39. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
  40. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
  41. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png
  42. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png
  43. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png
  44. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png
  45. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png
  46. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png
  47. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png
  48. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png
  49. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png
  50. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png
  51. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
  52. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
  53. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png
  54. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
  55. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
  56. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
  57. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png
  58. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png
  59. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png
  60. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png
  61. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png
  62. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
  63. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
  64. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png
  65. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
  66. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
  67. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
  68. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png
  69. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png
  70. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png
  71. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png
  72. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png
  73. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
  74. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
  75. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
  76. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
  77. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
  78. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
  79. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png
  80. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png
  81. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
  82. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
  83. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_6,NEXUS_5,1.0,en].png
  84. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_6,NEXUS_5,1.0,en].png
  85. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_CheckableUnresolvedUserRowPreview_0_null,NEXUS_5,1.0,en].png
  86. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_UnresolvedUserRowPreview_0_null,NEXUS_5,1.0,en].png

1
changelog.d/424.feature

@ -0,0 +1 @@
[Create and join rooms] Show a notice for MXIDs that don't resolve when searching for users to invite

45
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt

@ -22,40 +22,49 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow 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.components.aMatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.usersearch.api.UserSearchResult
@Composable @Composable
fun SearchMultipleUsersResultItem( fun SearchMultipleUsersResultItem(
matrixUser: MatrixUser, searchResult: UserSearchResult,
isUserSelected: Boolean, isUserSelected: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onCheckedChange: (Boolean) -> Unit = {}, onCheckedChange: (Boolean) -> Unit = {},
) { ) {
CheckableMatrixUserRow( if (searchResult.isUnresolved) {
checked = isUserSelected, CheckableUnresolvedUserRow(
modifier = modifier, checked = isUserSelected,
matrixUser = matrixUser, modifier = modifier,
avatarSize = AvatarSize.Custom(36.dp), avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.Custom(36.dp)),
onCheckedChange = onCheckedChange, id = searchResult.matrixUser.userId.value,
) onCheckedChange = onCheckedChange,
)
} else {
CheckableMatrixUserRow(
checked = isUserSelected,
modifier = modifier,
matrixUser = searchResult.matrixUser,
avatarSize = AvatarSize.Custom(36.dp),
onCheckedChange = onCheckedChange,
)
}
} }
@Preview @Preview
@Composable @Composable
internal fun SearchMultipleUsersResultItemLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview { ContentToPreview() }
@Preview
@Composable
internal fun SearchMultipleUsersResultItemDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {
Column { Column {
SearchMultipleUsersResultItem(matrixUser = aMatrixUser(), isUserSelected = true) SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = false)
SearchMultipleUsersResultItem(matrixUser = aMatrixUser(), 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)
} }
} }

39
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt

@ -17,39 +17,48 @@
package io.element.android.features.createroom.impl.components package io.element.android.features.createroom.impl.components
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.MatrixUserRow 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.components.aMatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.usersearch.api.UserSearchResult
@Composable @Composable
fun SearchSingleUserResultItem( fun SearchSingleUserResultItem(
matrixUser: MatrixUser, searchResult: UserSearchResult,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
MatrixUserRow( if (searchResult.isUnresolved) {
modifier = modifier.clickable(onClick = onClick), UnresolvedUserRow(
matrixUser = matrixUser, modifier = modifier.clickable(onClick = onClick),
avatarSize = AvatarSize.Custom(36.dp), 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 @Preview
@Composable @Composable
internal fun SearchSingleUserResultItemLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview{ ContentToPreview() }
@Preview
@Composable
internal fun SearchSingleUserResultItemDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {
SearchSingleUserResultItem(matrixUser = aMatrixUser()) Column {
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false))
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true))
}
} }

19
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt

@ -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.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.SelectedUsersList import io.element.android.libraries.matrix.ui.components.SelectedUsersList
import io.element.android.libraries.ui.strings.R import io.element.android.libraries.ui.strings.R
import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Composable @Composable
fun SearchUserBar( fun SearchUserBar(
query: String, query: String,
state: SearchBarResultState<ImmutableList<MatrixUser>>, state: SearchBarResultState<ImmutableList<UserSearchResult>>,
selectedUsers: ImmutableList<MatrixUser>, selectedUsers: ImmutableList<MatrixUser>,
active: Boolean, active: Boolean,
isMultiSelectionEnabled: Boolean, isMultiSelectionEnabled: Boolean,
@ -68,26 +69,26 @@ fun SearchUserBar(
resultHandler = { users -> resultHandler = { users ->
LazyColumn { LazyColumn {
if (isMultiSelectionEnabled) { if (isMultiSelectionEnabled) {
items(users) { matrixUser -> items(users) { searchResult ->
SearchMultipleUsersResultItem( SearchMultipleUsersResultItem(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
matrixUser = matrixUser, searchResult = searchResult,
isUserSelected = selectedUsers.find { it.userId == matrixUser.userId } != null, isUserSelected = selectedUsers.find { it.userId == searchResult.matrixUser.userId } != null,
onCheckedChange = { checked -> onCheckedChange = { checked ->
if (checked) { if (checked) {
onUserSelected(matrixUser) onUserSelected(searchResult.matrixUser)
} else { } else {
onUserDeselected(matrixUser) onUserDeselected(searchResult.matrixUser)
} }
} }
) )
} }
} else { } else {
items(users) { matrixUser -> items(users) { searchResult ->
SearchSingleUserResultItem( SearchSingleUserResultItem(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
matrixUser = matrixUser, searchResult = searchResult,
onClick = { onUserSelected(matrixUser) } onClick = { onUserSelected(searchResult.matrixUser) }
) )
} }
} }

4
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt

@ -30,8 +30,8 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.di.SessionScope 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.UserRepository
import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -56,7 +56,7 @@ class DefaultUserListPresenter @AssistedInject constructor(
var isSearchActive by rememberSaveable { mutableStateOf(false) } var isSearchActive by rememberSaveable { mutableStateOf(false) }
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList()) val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
var searchQuery by rememberSaveable { mutableStateOf("") } var searchQuery by rememberSaveable { mutableStateOf("") }
var searchResults: SearchBarResultState<ImmutableList<MatrixUser>> by remember { var searchResults: SearchBarResultState<ImmutableList<UserSearchResult>> by remember {
mutableStateOf(SearchBarResultState.NotSearching()) mutableStateOf(SearchBarResultState.NotSearching())
} }

3
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt

@ -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.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class UserListState( data class UserListState(
val searchQuery: String, val searchQuery: String,
val searchResults: SearchBarResultState<ImmutableList<MatrixUser>>, val searchResults: SearchBarResultState<ImmutableList<UserSearchResult>>,
val selectedUsers: ImmutableList<MatrixUser>, val selectedUsers: ImmutableList<MatrixUser>,
val isSearchActive: Boolean, val isSearchActive: Boolean,
val selectionMode: SelectionMode, val selectionMode: SelectionMode,

5
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt

@ -19,6 +19,7 @@ package io.element.android.features.createroom.impl.userlist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.ui.components.aMatrixUserList 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.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -38,14 +39,14 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
isSearchActive = true, isSearchActive = true,
searchQuery = "@someone:matrix.org", searchQuery = "@someone:matrix.org",
selectedUsers = aListOfSelectedUsers(), selectedUsers = aListOfSelectedUsers(),
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()), searchResults = SearchBarResultState.Results(aMatrixUserList().map { UserSearchResult(it) }.toImmutableList()),
), ),
aUserListState().copy( aUserListState().copy(
isSearchActive = true, isSearchActive = true,
searchQuery = "@someone:matrix.org", searchQuery = "@someone:matrix.org",
selectionMode = SelectionMode.Multiple, selectionMode = SelectionMode.Multiple,
selectedUsers = aListOfSelectedUsers(), selectedUsers = aListOfSelectedUsers(),
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()), searchResults = SearchBarResultState.Results(aMatrixUserList().map { UserSearchResult(it) }.toImmutableList()),
), ),
aUserListState().copy( aUserListState().copy(
isSearchActive = true, isSearchActive = true,

9
features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt

@ -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.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList 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.libraries.usersearch.test.FakeUserRepository
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -130,18 +131,18 @@ class DefaultUserListPresenterTests {
skipItems(2) skipItems(2)
// When the user repository emits a result, it's copied to the state // 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( assertThat(awaitItem().searchResults).isEqualTo(
SearchBarResultState.Results( SearchBarResultState.Results(
persistentListOf(aMatrixUser()) persistentListOf(UserSearchResult(aMatrixUser()))
) )
) )
// When the user repository emits another result, it replaces the previous value // 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( assertThat(awaitItem().searchResults).isEqualTo(
SearchBarResultState.Results( SearchBarResultState.Results(
aMatrixUserList() aMatrixUserList().map { UserSearchResult(it) }
) )
) )
} }

5
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt

@ -45,9 +45,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.features.leaveroom.api.LeaveRoomView
import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs
import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection 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.RoomMemberHeaderSection
import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection
import io.element.android.libraries.architecture.isLoading 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.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.ui.strings.R as StringR
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
@ -185,7 +186,7 @@ internal fun RoomHeaderSection(
@Composable @Composable
internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) { 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( Text(
roomTopic, roomTopic,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp), modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp),

9
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt

@ -126,15 +126,16 @@ class RoomInviteMembersPresenter @Inject constructor(
userRepository.search(searchQuery).collect { userRepository.search(searchQuery).collect {
searchResults.value = when { searchResults.value = when {
it.isEmpty() -> SearchBarResultState.NoResults() it.isEmpty() -> SearchBarResultState.NoResults()
else -> SearchBarResultState.Results(it.map { user -> else -> SearchBarResultState.Results(it.map { result ->
val existingMembership = joinedMembers.firstOrNull { j -> j.userId == user.userId }?.membership val existingMembership = joinedMembers.firstOrNull { j -> j.userId == result.matrixUser.userId }?.membership
val isJoined = existingMembership == RoomMembershipState.JOIN val isJoined = existingMembership == RoomMembershipState.JOIN
val isInvited = existingMembership == RoomMembershipState.INVITE val isInvited = existingMembership == RoomMembershipState.INVITE
InvitableUser( InvitableUser(
matrixUser = user, matrixUser = result.matrixUser,
isSelected = selectedUsers.value.contains(user) || isJoined || isInvited, isSelected = selectedUsers.value.contains(result.matrixUser) || isJoined || isInvited,
isAlreadyJoined = isJoined, isAlreadyJoined = isJoined,
isAlreadyInvited = isInvited, isAlreadyInvited = isInvited,
isUnresolved = result.isUnresolved,
) )
}.toImmutableList()) }.toImmutableList())
} }

1
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt

@ -35,4 +35,5 @@ data class InvitableUser(
val isSelected: Boolean = false, val isSelected: Boolean = false,
val isAlreadyJoined: Boolean = false, val isAlreadyJoined: Boolean = false,
val isAlreadyInvited: Boolean = false, val isAlreadyInvited: Boolean = false,
val isUnresolved: Boolean = false,
) )

14
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt

@ -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")),
)
)
),
) )
} }

43
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt

@ -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.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.matrix.api.user.MatrixUser 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.CheckableUserRow
import io.element.android.libraries.matrix.ui.components.SelectedUsersList import io.element.android.libraries.matrix.ui.components.SelectedUsersList
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
@ -180,22 +181,32 @@ private fun RoomInviteMembersSearchBar(
LazyColumn { LazyColumn {
items(results) { invitableUser -> items(results) { invitableUser ->
CheckableUserRow( if (invitableUser.isUnresolved && !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined) {
checked = invitableUser.isSelected, CheckableUnresolvedUserRow(
enabled = !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined, checked = invitableUser.isSelected,
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.MEDIUM), avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.MEDIUM),
name = invitableUser.matrixUser.getBestName(), id = invitableUser.matrixUser.userId.value,
subtext = when { onCheckedChange = { onUserToggled(invitableUser.matrixUser) },
// If they're already invited or joined we show that information modifier = Modifier.fillMaxWidth()
invitableUser.isAlreadyJoined -> stringResource(R.string.screen_room_details_already_a_member) )
invitableUser.isAlreadyInvited -> stringResource(R.string.screen_room_details_already_invited) } else {
// Otherwise show the ID, unless that's already used for their name CheckableUserRow(
invitableUser.matrixUser.displayName.isNullOrEmpty().not() -> invitableUser.matrixUser.userId.value checked = invitableUser.isSelected,
else -> null enabled = !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined,
}, avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.MEDIUM),
onCheckedChange = { onUserToggled(invitableUser.matrixUser) }, name = invitableUser.matrixUser.getBestName(),
modifier = Modifier.fillMaxWidth() 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()
)
}
} }
} }
}, },

57
features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt

@ -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.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState 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.A_USER_ID_2
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom 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.aMatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList 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.libraries.usersearch.test.FakeUserRepository
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -130,7 +132,7 @@ internal class RoomInviteMembersPresenterTest {
skipItems(1) skipItems(1)
assertThat(repository.providedQuery).isEqualTo("some query") assertThat(repository.providedQuery).isEqualTo("some query")
repository.emitResult(aMatrixUserList()) repository.emitResult(aMatrixUserList().map { UserSearchResult(it) })
skipItems(1) skipItems(1)
val resultState = awaitItem() val resultState = awaitItem()
@ -175,7 +177,7 @@ internal class RoomInviteMembersPresenterTest {
skipItems(1) skipItems(1)
assertThat(repository.providedQuery).isEqualTo("some query") assertThat(repository.providedQuery).isEqualTo("some query")
repository.emitResult(aMatrixUserList()) repository.emitResult(aMatrixUserList().map { UserSearchResult(it) })
skipItems(1) skipItems(1)
val resultState = awaitItem() 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 @Test
fun `present - toggle users updates selected user state`() = runTest { fun `present - toggle users updates selected user state`() = runTest {
val repository = FakeUserRepository() val repository = FakeUserRepository()
@ -254,7 +303,7 @@ internal class RoomInviteMembersPresenterTest {
skipItems(1) skipItems(1)
assertThat(repository.providedQuery).isEqualTo("some query") assertThat(repository.providedQuery).isEqualTo("some query")
repository.emitResult(aMatrixUserList() + selectedUser) repository.emitResult((aMatrixUserList() + selectedUser).map { UserSearchResult(it) })
skipItems(2) skipItems(2)
val resultState = awaitItem() val resultState = awaitItem()
@ -296,7 +345,7 @@ internal class RoomInviteMembersPresenterTest {
skipItems(1) skipItems(1)
assertThat(repository.providedQuery).isEqualTo("some query") assertThat(repository.providedQuery).isEqualTo("some query")
repository.emitResult(aMatrixUserList() + selectedUser) repository.emitResult((aMatrixUserList() + selectedUser).map { UserSearchResult(it) })
skipItems(2) skipItems(2)
// And then a user is toggled // And then a user is toggled

150
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt

@ -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)
}
}

3
libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt

@ -16,10 +16,9 @@
package io.element.android.libraries.usersearch.api package io.element.android.libraries.usersearch.api
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface UserRepository { interface UserRepository {
suspend fun search(query: String): Flow<List<MatrixUser>> suspend fun search(query: String): Flow<List<UserSearchResult>>
} }

24
libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserSearchResult.kt

@ -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,
)

17
libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt

@ -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.matrix.api.user.MatrixUser
import io.element.android.libraries.usersearch.api.UserListDataSource import io.element.android.libraries.usersearch.api.UserListDataSource
import io.element.android.libraries.usersearch.api.UserRepository import io.element.android.libraries.usersearch.api.UserRepository
import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@ -33,24 +34,26 @@ class MatrixUserRepository @Inject constructor(
private val dataSource: UserListDataSource private val dataSource: UserListDataSource
) : UserRepository { ) : 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 // Manually add a fake result with the matrixId, if any
val isUserId = MatrixPatterns.isUserId(query) val isUserId = MatrixPatterns.isUserId(query)
if (isUserId) { if (isUserId) {
emit(listOf(MatrixUser(UserId(query)))) emit(listOf(UserSearchResult(MatrixUser(UserId(query)))))
} }
if (query.length >= MINIMUM_SEARCH_LENGTH) { if (query.length >= MINIMUM_SEARCH_LENGTH) {
// Debounce // Debounce
delay(DEBOUNCE_TIME_MILLIS) 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 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 }) { if (isUserId && results.none { it.matrixUser.userId.value == query }) {
val getProfileResult: MatrixUser? = dataSource.getProfile(UserId(query)) results.add(
val profile = getProfileResult ?: MatrixUser(UserId(query)) 0,
results.add(0, profile) dataSource.getProfile(UserId(query))
?.let { UserSearchResult(it) }
?: UserSearchResult(MatrixUser(UserId(query)), isUnresolved = true))
} }
emit(results) emit(results)

17
libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepositoryTest.kt

@ -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_ID
import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.ui.components.aMatrixUserList 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 io.element.android.libraries.usersearch.test.FakeUserListDataSource
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -63,7 +64,7 @@ internal class MatrixUserRepositoryTest {
val result = repository.search("some query") val result = repository.search("some query")
result.test { result.test {
assertThat(awaitItem()).isEqualTo(aMatrixUserList()) assertThat(awaitItem()).isEqualTo(aMatrixUserList().toUserSearchResults())
awaitComplete() awaitComplete()
} }
} }
@ -76,7 +77,7 @@ internal class MatrixUserRepositoryTest {
val result = repository.search(A_USER_ID.value) val result = repository.search(A_USER_ID.value)
result.test { result.test {
assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID))) assertThat(awaitItem()).isEqualTo(listOf(placeholderResult()))
skipItems(1) skipItems(1)
awaitComplete() awaitComplete()
} }
@ -93,7 +94,7 @@ internal class MatrixUserRepositoryTest {
result.test { result.test {
skipItems(1) skipItems(1)
assertThat(awaitItem()).isEqualTo(searchResults) assertThat(awaitItem()).isEqualTo(searchResults.toUserSearchResults())
awaitComplete() awaitComplete()
} }
} }
@ -112,13 +113,13 @@ internal class MatrixUserRepositoryTest {
result.test { result.test {
skipItems(1) skipItems(1)
assertThat(awaitItem()).isEqualTo(listOf(userProfile) + searchResults) assertThat(awaitItem()).isEqualTo((listOf(userProfile) + searchResults).toUserSearchResults())
awaitComplete() awaitComplete()
} }
} }
@Test @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 searchResults = aMatrixUserListWithoutUserId(A_USER_ID)
val dataSource = FakeUserListDataSource() val dataSource = FakeUserListDataSource()
@ -130,11 +131,15 @@ internal class MatrixUserRepositoryTest {
result.test { result.test {
skipItems(1) skipItems(1)
assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)) + searchResults) assertThat(awaitItem()).isEqualTo(listOf(placeholderResult(isUnresolved = true)) + searchResults.toUserSearchResults())
awaitComplete() awaitComplete()
} }
} }
private fun aMatrixUserListWithoutUserId(userId: UserId) = aMatrixUserList().filterNot { it.userId == userId } 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)
} }

8
libraries/usersearch/test/src/main/kotlin/io/element/android/libraries/usersearch/test/FakeUserRepository.kt

@ -16,8 +16,8 @@
package io.element.android.libraries.usersearch.test 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.UserRepository
import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -26,14 +26,14 @@ class FakeUserRepository : UserRepository {
var providedQuery: String? = null var providedQuery: String? = null
private set 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 providedQuery = query
return flow return flow
} }
suspend fun emitResult(result: List<MatrixUser>) { suspend fun emitResult(result: List<UserSearchResult>) {
flow.emit(result) flow.emit(result)
} }

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemDarkPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemLightPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemDarkPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemLightPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_6,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_6,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_CheckableUnresolvedUserRowPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_UnresolvedUserRowPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save