Merge remote-tracking branch 'origin/develop' into misc/cjs/create-join-design-feedback
This commit is contained in:
commit
fb147f78b0
200 changed files with 3116 additions and 353 deletions
|
|
@ -21,13 +21,25 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint.InitialTarget
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsFlowNode.NavTarget
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRoomDetailsEntryPoint @Inject constructor() : RoomDetailsEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List<Plugin>): Node {
|
||||
return parentNode.createNode<RoomDetailsFlowNode>(buildContext, plugins)
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
inputs: RoomDetailsEntryPoint.Inputs,
|
||||
plugins: List<Plugin>
|
||||
): Node {
|
||||
return parentNode.createNode<RoomDetailsFlowNode>(buildContext, plugins + inputs)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun InitialTarget.toNavTarget() = when (this) {
|
||||
is InitialTarget.RoomDetails -> NavTarget.RoomDetails
|
||||
is InitialTarget.RoomMemberDetails -> NavTarget.RoomMemberDetails(roomMemberId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode
|
||||
|
|
@ -44,7 +45,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
) : BackstackNode<RoomDetailsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.RoomDetails,
|
||||
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Inputs>().first().initialElement.toNavTarget(),
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
|
|
@ -95,7 +96,8 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
createNode<RoomInviteMembersNode>(buildContext)
|
||||
}
|
||||
is NavTarget.RoomMemberDetails -> {
|
||||
createNode<RoomMemberDetailsNode>(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMemberId)))
|
||||
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId))
|
||||
createNode<RoomMemberDetailsNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -42,11 +42,11 @@ class RoomMemberDetailsNode @AssistedInject constructor(
|
|||
presenterFactory: RoomMemberDetailsPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
data class Inputs(
|
||||
val roomMemberId: UserId,
|
||||
data class RoomMemberDetailsInput(
|
||||
val roomMemberId: UserId
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs = inputs<Inputs>()
|
||||
private val inputs = inputs<RoomMemberDetailsInput>()
|
||||
private val presenter = presenterFactory.create(inputs.roomMemberId)
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import io.element.android.libraries.core.bool.orFalse
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.ui.room.getRoomMember
|
||||
import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
|||
override fun present(): RoomMemberDetailsState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
|
||||
val roomMember by room.getRoomMember(roomMemberId)
|
||||
val roomMember by room.getRoomMemberAsState(roomMemberId)
|
||||
// the room member is not really live...
|
||||
val isBlocked = remember {
|
||||
mutableStateOf(roomMember?.isIgnored.orFalse())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue