Merge branch 'develop' into feature/bma/mediaForward
# Conflicts: # appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt
This commit is contained in:
commit
cc050a1f26
43 changed files with 399 additions and 190 deletions
|
|
@ -23,8 +23,6 @@ import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appnav.di.RoomGraphFactory
|
import io.element.android.appnav.di.RoomGraphFactory
|
||||||
import io.element.android.appnav.room.RoomNavigationTarget
|
import io.element.android.appnav.room.RoomNavigationTarget
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode.Inputs
|
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode.NavTarget
|
|
||||||
import io.element.android.features.forward.api.ForwardEntryPoint
|
import io.element.android.features.forward.api.ForwardEntryPoint
|
||||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
|
|
@ -158,6 +156,9 @@ class JoinedRoomLoadedFlowNode(
|
||||||
NavTarget.RoomNotificationSettings -> {
|
NavTarget.RoomNotificationSettings -> {
|
||||||
createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomNotificationSettings)
|
createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomNotificationSettings)
|
||||||
}
|
}
|
||||||
|
NavTarget.RoomMemberList -> {
|
||||||
|
createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomMemberList)
|
||||||
|
}
|
||||||
NavTarget.Space -> {
|
NavTarget.Space -> {
|
||||||
createSpaceNode(buildContext)
|
createSpaceNode(buildContext)
|
||||||
}
|
}
|
||||||
|
|
@ -189,6 +190,10 @@ class JoinedRoomLoadedFlowNode(
|
||||||
override fun onOpenDetails() {
|
override fun onOpenDetails() {
|
||||||
backstack.push(NavTarget.RoomDetails)
|
backstack.push(NavTarget.RoomDetails)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onOpenMemberList() {
|
||||||
|
backstack.push(NavTarget.RoomMemberList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return spaceEntryPoint.nodeBuilder(this, buildContext)
|
return spaceEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.inputs(SpaceEntryPoint.Inputs(roomId = inputs.room.roomId))
|
.inputs(SpaceEntryPoint.Inputs(roomId = inputs.room.roomId))
|
||||||
|
|
@ -240,6 +245,9 @@ class JoinedRoomLoadedFlowNode(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object RoomDetails : NavTarget
|
data object RoomDetails : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object RoomMemberList : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class RoomMemberDetails(val userId: UserId) : NavTarget
|
data class RoomMemberDetails(val userId: UserId) : NavTarget
|
||||||
|
|
||||||
|
|
@ -256,17 +264,17 @@ class JoinedRoomLoadedFlowNode(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initialElement(plugins: List<Plugin>): NavTarget {
|
private fun initialElement(plugins: List<Plugin>): JoinedRoomLoadedFlowNode.NavTarget {
|
||||||
val input = plugins.filterIsInstance<Inputs>().single()
|
val input = plugins.filterIsInstance<JoinedRoomLoadedFlowNode.Inputs>().single()
|
||||||
return when (input.initialElement) {
|
return when (input.initialElement) {
|
||||||
is RoomNavigationTarget.Root -> {
|
is RoomNavigationTarget.Root -> {
|
||||||
if (input.room.roomInfoFlow.value.isSpace) {
|
if (input.room.roomInfoFlow.value.isSpace) {
|
||||||
NavTarget.Space
|
JoinedRoomLoadedFlowNode.NavTarget.Space
|
||||||
} else {
|
} else {
|
||||||
NavTarget.Messages(input.initialElement.eventId)
|
JoinedRoomLoadedFlowNode.NavTarget.Messages(input.initialElement.eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RoomNavigationTarget.Details -> NavTarget.RoomDetails
|
RoomNavigationTarget.Details -> JoinedRoomLoadedFlowNode.NavTarget.RoomDetails
|
||||||
RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings
|
RoomNavigationTarget.NotificationSettings -> JoinedRoomLoadedFlowNode.NavTarget.RoomNotificationSettings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,5 @@ sealed interface ChangeRolesEvent {
|
||||||
data class UserSelectionToggled(val matrixUser: MatrixUser) : ChangeRolesEvent
|
data class UserSelectionToggled(val matrixUser: MatrixUser) : ChangeRolesEvent
|
||||||
data object Save : ChangeRolesEvent
|
data object Save : ChangeRolesEvent
|
||||||
data object Exit : ChangeRolesEvent
|
data object Exit : ChangeRolesEvent
|
||||||
data object CancelExit : ChangeRolesEvent
|
data object CloseDialog : ChangeRolesEvent
|
||||||
data object ClearError : ChangeRolesEvent
|
|
||||||
data object CancelSave : ChangeRolesEvent
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,20 +69,19 @@ class ChangeRolesPresenter(
|
||||||
val selectedUsers = remember {
|
val selectedUsers = remember {
|
||||||
mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf())
|
mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf())
|
||||||
}
|
}
|
||||||
val exitState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val saveState: MutableState<AsyncAction<Boolean>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
val saveState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
|
||||||
val usersWithRole = produceState<ImmutableList<MatrixUser>>(initialValue = persistentListOf()) {
|
val usersWithRole = produceState<ImmutableList<MatrixUser>>(initialValue = persistentListOf()) {
|
||||||
room.usersWithRole(role).map { members -> members.map { it.toMatrixUser() } }
|
room.usersWithRole(role).map { members -> members.map { it.toMatrixUser() } }
|
||||||
.onEach { users ->
|
.onEach { users ->
|
||||||
val previous = value
|
val previous = value
|
||||||
value = users.toImmutableList()
|
value = users.toImmutableList()
|
||||||
// Users who were selected but didn't have the role, so their role change was pending
|
// Users who were selected but didn't have the role, so their role change was pending
|
||||||
val toAdd = selectedUsers.value.filter { user -> users.none { it.userId == user.userId } && previous.none { it.userId == user.userId } }
|
val toAdd = selectedUsers.value.filter { user -> users.none { it.userId == user.userId } && previous.none { it.userId == user.userId } }
|
||||||
// Users who no longer have the role
|
// Users who no longer have the role
|
||||||
val toRemove = previous.filter { user -> users.none { it.userId == user.userId } }.toSet()
|
val toRemove = previous.filter { user -> users.none { it.userId == user.userId } }.toSet()
|
||||||
selectedUsers.value = (users + toAdd - toRemove).toImmutableList()
|
selectedUsers.value = (users + toAdd - toRemove).toImmutableList()
|
||||||
}
|
}
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
val roomMemberState by room.membersStateFlow.collectAsState()
|
val roomMemberState by room.membersStateFlow.collectAsState()
|
||||||
|
|
@ -147,22 +146,16 @@ class ChangeRolesPresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ChangeRolesEvent.ClearError -> {
|
|
||||||
saveState.value = AsyncAction.Uninitialized
|
|
||||||
}
|
|
||||||
is ChangeRolesEvent.Exit -> {
|
is ChangeRolesEvent.Exit -> {
|
||||||
exitState.value = if (exitState.value.isUninitialized() && hasPendingChanges) {
|
saveState.value = if (saveState.value.isUninitialized() && hasPendingChanges) {
|
||||||
// Has pending changes, confirm exit
|
// Has pending changes, confirm exit
|
||||||
AsyncAction.ConfirmingNoParams
|
AsyncAction.ConfirmingCancellation
|
||||||
} else {
|
} else {
|
||||||
// No pending changes, exit immediately
|
// No pending changes, exit immediately
|
||||||
AsyncAction.Success(Unit)
|
AsyncAction.Success(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ChangeRolesEvent.CancelExit -> {
|
is ChangeRolesEvent.CloseDialog -> {
|
||||||
exitState.value = AsyncAction.Uninitialized
|
|
||||||
}
|
|
||||||
is ChangeRolesEvent.CancelSave -> {
|
|
||||||
saveState.value = AsyncAction.Uninitialized
|
saveState.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -174,7 +167,6 @@ class ChangeRolesPresenter(
|
||||||
searchResults = searchResults,
|
searchResults = searchResults,
|
||||||
selectedUsers = selectedUsers.value,
|
selectedUsers = selectedUsers.value,
|
||||||
hasPendingChanges = hasPendingChanges,
|
hasPendingChanges = hasPendingChanges,
|
||||||
exitState = exitState.value,
|
|
||||||
savingState = saveState.value,
|
savingState = saveState.value,
|
||||||
canChangeMemberRole = ::canChangeMemberRole,
|
canChangeMemberRole = ::canChangeMemberRole,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
|
|
@ -198,7 +190,7 @@ class ChangeRolesPresenter(
|
||||||
private fun CoroutineScope.save(
|
private fun CoroutineScope.save(
|
||||||
usersWithRole: ImmutableList<MatrixUser>,
|
usersWithRole: ImmutableList<MatrixUser>,
|
||||||
selectedUsers: MutableState<ImmutableList<MatrixUser>>,
|
selectedUsers: MutableState<ImmutableList<MatrixUser>>,
|
||||||
saveState: MutableState<AsyncAction<Unit>>,
|
saveState: MutableState<AsyncAction<Boolean>>,
|
||||||
) = launch {
|
) = launch {
|
||||||
saveState.value = AsyncAction.Loading
|
saveState.value = AsyncAction.Loading
|
||||||
|
|
||||||
|
|
@ -221,7 +213,7 @@ class ChangeRolesPresenter(
|
||||||
saveState.value = AsyncAction.Failure(it)
|
saveState.value = AsyncAction.Failure(it)
|
||||||
}
|
}
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
saveState.value = AsyncAction.Success(Unit)
|
saveState.value = AsyncAction.Success(true)
|
||||||
// Asynchronously reload the room members
|
// Asynchronously reload the room members
|
||||||
launch { room.updateMembers() }
|
launch { room.updateMembers() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,7 @@ data class ChangeRolesState(
|
||||||
val searchResults: SearchBarResultState<MembersByRole>,
|
val searchResults: SearchBarResultState<MembersByRole>,
|
||||||
val selectedUsers: ImmutableList<MatrixUser>,
|
val selectedUsers: ImmutableList<MatrixUser>,
|
||||||
val hasPendingChanges: Boolean,
|
val hasPendingChanges: Boolean,
|
||||||
val exitState: AsyncAction<Unit>,
|
val savingState: AsyncAction<Boolean>,
|
||||||
val savingState: AsyncAction<Unit>,
|
|
||||||
val canChangeMemberRole: (UserId) -> Boolean,
|
val canChangeMemberRole: (UserId) -> Boolean,
|
||||||
val eventSink: (ChangeRolesEvent) -> Unit,
|
val eventSink: (ChangeRolesEvent) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
@ -36,10 +35,10 @@ data class MembersByRole(
|
||||||
val members: ImmutableList<RoomMember>,
|
val members: ImmutableList<RoomMember>,
|
||||||
) {
|
) {
|
||||||
constructor(members: List<RoomMember>) : this(
|
constructor(members: List<RoomMember>) : this(
|
||||||
owners = members.filter { it.role is RoomMember.Role.Owner }.sorted(),
|
owners = members.filter { it.role is RoomMember.Role.Owner }.sorted(),
|
||||||
admins = members.filter { it.role == RoomMember.Role.Admin }.sorted(),
|
admins = members.filter { it.role == RoomMember.Role.Admin }.sorted(),
|
||||||
moderators = members.filter { it.role == RoomMember.Role.Moderator }.sorted(),
|
moderators = members.filter { it.role == RoomMember.Role.Moderator }.sorted(),
|
||||||
members = members.filter { it.role == RoomMember.Role.User }.sorted(),
|
members = members.filter { it.role == RoomMember.Role.User }.sorted(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun isEmpty() = owners.isEmpty() && admins.isEmpty() && moderators.isEmpty() && members.isEmpty()
|
fun isEmpty() = owners.isEmpty() && admins.isEmpty() && moderators.isEmpty() && members.isEmpty()
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,10 @@ class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
|
||||||
searchResults = SearchBarResultState.Results(MembersByRole(aRoomMemberList().take(1).toImmutableList())),
|
searchResults = SearchBarResultState.Results(MembersByRole(aRoomMemberList().take(1).toImmutableList())),
|
||||||
selectedUsers = aMatrixUserList().take(1).toImmutableList(),
|
selectedUsers = aMatrixUserList().take(1).toImmutableList(),
|
||||||
),
|
),
|
||||||
aChangeRolesStateWithSelectedUsers().copy(exitState = AsyncAction.ConfirmingNoParams),
|
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.ConfirmingCancellation),
|
||||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.ConfirmingNoParams),
|
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.ConfirmingNoParams),
|
||||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Loading),
|
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Loading),
|
||||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Success(Unit)),
|
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Success(true)),
|
||||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Failure(Exception("boom"))),
|
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Failure(Exception("boom"))),
|
||||||
aChangeRolesStateWithOwners(),
|
aChangeRolesStateWithOwners(),
|
||||||
aChangeRolesStateWithOwners().copy(role = RoomMember.Role.Owner(isCreator = false)),
|
aChangeRolesStateWithOwners().copy(role = RoomMember.Role.Owner(isCreator = false)),
|
||||||
|
|
@ -55,8 +55,7 @@ internal fun aChangeRolesState(
|
||||||
searchResults: SearchBarResultState<MembersByRole> = SearchBarResultState.NoResultsFound(),
|
searchResults: SearchBarResultState<MembersByRole> = SearchBarResultState.NoResultsFound(),
|
||||||
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
||||||
hasPendingChanges: Boolean = false,
|
hasPendingChanges: Boolean = false,
|
||||||
exitState: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
savingState: AsyncAction<Boolean> = AsyncAction.Uninitialized,
|
||||||
savingState: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
|
||||||
canRemoveMember: (UserId) -> Boolean = { true },
|
canRemoveMember: (UserId) -> Boolean = { true },
|
||||||
eventSink: (ChangeRolesEvent) -> Unit = {},
|
eventSink: (ChangeRolesEvent) -> Unit = {},
|
||||||
) = ChangeRolesState(
|
) = ChangeRolesState(
|
||||||
|
|
@ -66,7 +65,6 @@ internal fun aChangeRolesState(
|
||||||
searchResults = searchResults,
|
searchResults = searchResults,
|
||||||
selectedUsers = selectedUsers,
|
selectedUsers = selectedUsers,
|
||||||
hasPendingChanges = hasPendingChanges,
|
hasPendingChanges = hasPendingChanges,
|
||||||
exitState = exitState,
|
|
||||||
savingState = savingState,
|
savingState = savingState,
|
||||||
canChangeMemberRole = canRemoveMember,
|
canChangeMemberRole = canRemoveMember,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -41,7 +40,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
||||||
|
|
@ -52,7 +50,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||||
|
|
@ -172,61 +169,59 @@ fun ChangeRolesView(
|
||||||
AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), asyncIndicatorState)
|
AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), asyncIndicatorState)
|
||||||
|
|
||||||
AsyncActionView(
|
AsyncActionView(
|
||||||
async = state.exitState,
|
async = state.savingState,
|
||||||
onSuccess = { latestNavigateUp() },
|
onSuccess = { changeSaved ->
|
||||||
confirmationDialog = {
|
if (changeSaved) {
|
||||||
ConfirmationDialog(
|
|
||||||
title = stringResource(CommonStrings.dialog_unsaved_changes_title),
|
|
||||||
content = stringResource(CommonStrings.dialog_unsaved_changes_description_android),
|
|
||||||
onSubmitClick = { state.eventSink(ChangeRolesEvent.Exit) },
|
|
||||||
onDismiss = { state.eventSink(ChangeRolesEvent.CancelExit) }
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onErrorDismiss = { /* Cannot happen */ },
|
|
||||||
)
|
|
||||||
|
|
||||||
when (state.savingState) {
|
|
||||||
is AsyncAction.Confirming -> {
|
|
||||||
when (state.role) {
|
|
||||||
is RoomMember.Role.Owner -> {
|
|
||||||
ConfirmationDialog(
|
|
||||||
title = stringResource(R.string.screen_room_change_role_confirm_change_owners_title),
|
|
||||||
content = stringResource(R.string.screen_room_change_role_confirm_change_owners_description),
|
|
||||||
submitText = stringResource(CommonStrings.action_continue),
|
|
||||||
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
|
|
||||||
onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) },
|
|
||||||
destructiveSubmit = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is RoomMember.Role.Admin -> {
|
|
||||||
ConfirmationDialog(
|
|
||||||
title = stringResource(R.string.screen_room_change_role_confirm_add_admin_title),
|
|
||||||
content = stringResource(R.string.screen_room_change_role_confirm_add_admin_description),
|
|
||||||
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
|
|
||||||
onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> Unit // No confirmation needed for Moderator or User roles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is AsyncAction.Loading -> {
|
|
||||||
ProgressDialog()
|
|
||||||
}
|
|
||||||
is AsyncAction.Failure -> {
|
|
||||||
ErrorDialog(
|
|
||||||
content = stringResource(CommonStrings.error_unknown),
|
|
||||||
onSubmit = { state.eventSink(ChangeRolesEvent.ClearError) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is AsyncAction.Success -> {
|
|
||||||
LaunchedEffect(state.savingState) {
|
|
||||||
asyncIndicatorState.enqueue(durationMs = AsyncIndicator.DURATION_SHORT) {
|
asyncIndicatorState.enqueue(durationMs = AsyncIndicator.DURATION_SHORT) {
|
||||||
AsyncIndicator.Custom(text = stringResource(CommonStrings.common_saved_changes))
|
AsyncIndicator.Custom(text = stringResource(CommonStrings.common_saved_changes))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
latestNavigateUp()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
else -> Unit
|
confirmationDialog = { confirming ->
|
||||||
}
|
when (confirming) {
|
||||||
|
is AsyncAction.ConfirmingCancellation -> {
|
||||||
|
ConfirmationDialog(
|
||||||
|
title = stringResource(CommonStrings.dialog_unsaved_changes_title),
|
||||||
|
content = stringResource(CommonStrings.dialog_unsaved_changes_description_android),
|
||||||
|
onSubmitClick = { state.eventSink(ChangeRolesEvent.Exit) },
|
||||||
|
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
when (state.role) {
|
||||||
|
is RoomMember.Role.Owner -> {
|
||||||
|
ConfirmationDialog(
|
||||||
|
title = stringResource(R.string.screen_room_change_role_confirm_change_owners_title),
|
||||||
|
content = stringResource(R.string.screen_room_change_role_confirm_change_owners_description),
|
||||||
|
submitText = stringResource(CommonStrings.action_continue),
|
||||||
|
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
|
||||||
|
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) },
|
||||||
|
destructiveSubmit = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is RoomMember.Role.Admin -> {
|
||||||
|
ConfirmationDialog(
|
||||||
|
title = stringResource(R.string.screen_room_change_role_confirm_add_admin_title),
|
||||||
|
content = stringResource(R.string.screen_room_change_role_confirm_add_admin_description),
|
||||||
|
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
|
||||||
|
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// No confirmation needed for Moderator or User roles
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorMessage = {
|
||||||
|
stringResource(CommonStrings.error_unknown)
|
||||||
|
},
|
||||||
|
onErrorDismiss = {
|
||||||
|
state.eventSink(ChangeRolesEvent.CloseDialog)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ class ChangeRolesPresenterTest {
|
||||||
assertThat(searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
assertThat(searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||||
assertThat(selectedUsers).isEmpty()
|
assertThat(selectedUsers).isEmpty()
|
||||||
assertThat(hasPendingChanges).isFalse()
|
assertThat(hasPendingChanges).isFalse()
|
||||||
assertThat(exitState).isEqualTo(AsyncAction.Uninitialized)
|
|
||||||
assertThat(savingState).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
|
@ -266,7 +265,7 @@ class ChangeRolesPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Exit will display success if no pending changes`() = runTest {
|
fun `present - Exit will display success false if no pending changes`() = runTest {
|
||||||
val room = FakeJoinedRoom().apply {
|
val room = FakeJoinedRoom().apply {
|
||||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
||||||
|
|
@ -278,15 +277,15 @@ class ChangeRolesPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.hasPendingChanges).isFalse()
|
assertThat(initialState.hasPendingChanges).isFalse()
|
||||||
assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(initialState.savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
|
||||||
initialState.eventSink(ChangeRolesEvent.Exit)
|
initialState.eventSink(ChangeRolesEvent.Exit)
|
||||||
assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Success(Unit))
|
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - CancelExit will remove exit confirmation`() = runTest {
|
fun `present - CloseDialog will remove exit confirmation`() = runTest {
|
||||||
val room = FakeJoinedRoom().apply {
|
val room = FakeJoinedRoom().apply {
|
||||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
||||||
|
|
@ -298,16 +297,16 @@ class ChangeRolesPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.hasPendingChanges).isFalse()
|
assertThat(initialState.hasPendingChanges).isFalse()
|
||||||
assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(initialState.savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
|
||||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||||
|
|
||||||
awaitItem().eventSink(ChangeRolesEvent.Exit)
|
awaitItem().eventSink(ChangeRolesEvent.Exit)
|
||||||
val confirmingState = awaitItem()
|
val confirmingState = awaitItem()
|
||||||
assertThat(confirmingState.exitState).isEqualTo(AsyncAction.ConfirmingNoParams)
|
assertThat(confirmingState.savingState).isEqualTo(AsyncAction.ConfirmingCancellation)
|
||||||
|
|
||||||
confirmingState.eventSink(ChangeRolesEvent.CancelExit)
|
confirmingState.eventSink(ChangeRolesEvent.CloseDialog)
|
||||||
assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,7 +323,7 @@ class ChangeRolesPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.hasPendingChanges).isFalse()
|
assertThat(initialState.hasPendingChanges).isFalse()
|
||||||
assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(initialState.savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
|
||||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||||
val updatedState = awaitItem()
|
val updatedState = awaitItem()
|
||||||
|
|
@ -332,10 +331,10 @@ class ChangeRolesPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
updatedState.eventSink(ChangeRolesEvent.Exit)
|
updatedState.eventSink(ChangeRolesEvent.Exit)
|
||||||
assertThat(awaitItem().exitState).isEqualTo(AsyncAction.ConfirmingNoParams)
|
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.ConfirmingCancellation)
|
||||||
|
|
||||||
updatedState.eventSink(ChangeRolesEvent.Exit)
|
updatedState.eventSink(ChangeRolesEvent.Exit)
|
||||||
assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Success(Unit))
|
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -367,12 +366,12 @@ class ChangeRolesPresenterTest {
|
||||||
assertThat(loadingState.savingState).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(loadingState.savingState).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit))
|
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - CancelSave will remove the confirmation dialog`() = runTest {
|
fun `present - CloseDialog will remove the confirmation dialog`() = runTest {
|
||||||
val room = FakeJoinedRoom().apply {
|
val room = FakeJoinedRoom().apply {
|
||||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
||||||
|
|
@ -391,7 +390,7 @@ class ChangeRolesPresenterTest {
|
||||||
val confirmingState = awaitItem()
|
val confirmingState = awaitItem()
|
||||||
assertThat(confirmingState.savingState).isEqualTo(AsyncAction.ConfirmingNoParams)
|
assertThat(confirmingState.savingState).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
|
|
||||||
confirmingState.eventSink(ChangeRolesEvent.CancelSave)
|
confirmingState.eventSink(ChangeRolesEvent.CloseDialog)
|
||||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -426,7 +425,7 @@ class ChangeRolesPresenterTest {
|
||||||
assertThat(loadingState.savingState).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(loadingState.savingState).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit))
|
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(true))
|
||||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.ChangeMemberRole, RoomModeration.Role.Moderator))
|
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.ChangeMemberRole, RoomModeration.Role.Moderator))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -504,13 +503,13 @@ class ChangeRolesPresenterTest {
|
||||||
assertThat(loadingState.savingState).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(loadingState.savingState).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit))
|
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(true))
|
||||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.ChangeMemberRole, RoomModeration.Role.User))
|
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.ChangeMemberRole, RoomModeration.Role.User))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Save can handle failures and ClearError clears them`() = runTest {
|
fun `present - Save can handle failures and CloseDialog clears them`() = runTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
updateUserRoleResult = { Result.failure(IllegalStateException("Failed")) }
|
updateUserRoleResult = { Result.failure(IllegalStateException("Failed")) }
|
||||||
).apply {
|
).apply {
|
||||||
|
|
@ -534,7 +533,7 @@ class ChangeRolesPresenterTest {
|
||||||
val failedState = awaitItem()
|
val failedState = awaitItem()
|
||||||
assertThat(failedState.savingState).isInstanceOf(AsyncAction.Failure::class.java)
|
assertThat(failedState.savingState).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
|
||||||
failedState.eventSink(ChangeRolesEvent.ClearError)
|
failedState.eventSink(ChangeRolesEvent.CloseDialog)
|
||||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ class ChangeRolesViewTest {
|
||||||
rule.setChangeRolesContent(
|
rule.setChangeRolesContent(
|
||||||
state = aChangeRolesState(
|
state = aChangeRolesState(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
exitState = AsyncAction.ConfirmingNoParams,
|
savingState = AsyncAction.ConfirmingCancellation,
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -151,14 +151,14 @@ class ChangeRolesViewTest {
|
||||||
rule.setChangeRolesContent(
|
rule.setChangeRolesContent(
|
||||||
state = aChangeRolesState(
|
state = aChangeRolesState(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
exitState = AsyncAction.ConfirmingNoParams,
|
savingState = AsyncAction.ConfirmingCancellation,
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
rule.clickOn(CommonStrings.action_cancel)
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
|
|
||||||
eventsRecorder.assertSingle(ChangeRolesEvent.CancelExit)
|
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -209,7 +209,7 @@ class ChangeRolesViewTest {
|
||||||
|
|
||||||
rule.clickOn(CommonStrings.action_cancel)
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
|
|
||||||
eventsRecorder.assertSingle(ChangeRolesEvent.ClearError)
|
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -225,7 +225,7 @@ class ChangeRolesViewTest {
|
||||||
|
|
||||||
rule.clickOn(CommonStrings.action_ok)
|
rule.clickOn(CommonStrings.action_ok)
|
||||||
|
|
||||||
eventsRecorder.assertSingle(ChangeRolesEvent.ClearError)
|
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,9 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object RoomDetails : InitialTarget
|
data object RoomDetails : InitialTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object RoomMemberList : InitialTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class RoomMemberDetails(val roomMemberId: UserId) : InitialTarget
|
data class RoomMemberDetails(val roomMemberId: UserId) : InitialTarget
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,4 +44,5 @@ internal fun InitialTarget.toNavTarget() = when (this) {
|
||||||
is InitialTarget.RoomDetails -> NavTarget.RoomDetails
|
is InitialTarget.RoomDetails -> NavTarget.RoomDetails
|
||||||
is InitialTarget.RoomMemberDetails -> NavTarget.RoomMemberDetails(roomMemberId)
|
is InitialTarget.RoomMemberDetails -> NavTarget.RoomMemberDetails(roomMemberId)
|
||||||
is InitialTarget.RoomNotificationSettings -> NavTarget.RoomNotificationSettings(showUserDefinedSettingStyle = true)
|
is InitialTarget.RoomNotificationSettings -> NavTarget.RoomNotificationSettings(showUserDefinedSettingStyle = true)
|
||||||
|
InitialTarget.RoomMemberList -> NavTarget.RoomMemberList
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ sealed interface RoomDetailsEditEvents {
|
||||||
data class HandleAvatarAction(val action: AvatarAction) : RoomDetailsEditEvents
|
data class HandleAvatarAction(val action: AvatarAction) : RoomDetailsEditEvents
|
||||||
data class UpdateRoomName(val name: String) : RoomDetailsEditEvents
|
data class UpdateRoomName(val name: String) : RoomDetailsEditEvents
|
||||||
data class UpdateRoomTopic(val topic: String) : RoomDetailsEditEvents
|
data class UpdateRoomTopic(val topic: String) : RoomDetailsEditEvents
|
||||||
|
data object OnBackPress : RoomDetailsEditEvents
|
||||||
data object Save : RoomDetailsEditEvents
|
data object Save : RoomDetailsEditEvents
|
||||||
data object CancelSaveChanges : RoomDetailsEditEvents
|
data object CloseDialog : RoomDetailsEditEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,7 @@ class RoomDetailsEditNode(
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
RoomDetailsEditView(
|
RoomDetailsEditView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClick = ::navigateUp,
|
onDone = ::navigateUp,
|
||||||
onRoomEditSuccess = ::navigateUp,
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,13 @@ class RoomDetailsEditPresenter(
|
||||||
|
|
||||||
is RoomDetailsEditEvents.UpdateRoomName -> roomRawNameEdited = event.name
|
is RoomDetailsEditEvents.UpdateRoomName -> roomRawNameEdited = event.name
|
||||||
is RoomDetailsEditEvents.UpdateRoomTopic -> roomTopicEdited = event.topic
|
is RoomDetailsEditEvents.UpdateRoomTopic -> roomTopicEdited = event.topic
|
||||||
RoomDetailsEditEvents.CancelSaveChanges -> saveAction.value = AsyncAction.Uninitialized
|
RoomDetailsEditEvents.CloseDialog -> saveAction.value = AsyncAction.Uninitialized
|
||||||
|
RoomDetailsEditEvents.OnBackPress -> if (saveButtonEnabled.not() || saveAction.value == AsyncAction.ConfirmingCancellation) {
|
||||||
|
// No changes to save or already confirming exit without saving
|
||||||
|
saveAction.value = AsyncAction.Success(Unit)
|
||||||
|
} else {
|
||||||
|
saveAction.value = AsyncAction.ConfirmingCancellation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ open class RoomDetailsEditStateProvider : PreviewParameterProvider<RoomDetailsEd
|
||||||
aRoomDetailsEditState(canChangeName = false, canChangeTopic = true, canChangeAvatar = false, saveButtonEnabled = false),
|
aRoomDetailsEditState(canChangeName = false, canChangeTopic = true, canChangeAvatar = false, saveButtonEnabled = false),
|
||||||
aRoomDetailsEditState(saveAction = AsyncAction.Loading),
|
aRoomDetailsEditState(saveAction = AsyncAction.Loading),
|
||||||
aRoomDetailsEditState(saveAction = AsyncAction.Failure(RuntimeException("Whelp"))),
|
aRoomDetailsEditState(saveAction = AsyncAction.Failure(RuntimeException("Whelp"))),
|
||||||
|
aRoomDetailsEditState(saveAction = AsyncAction.ConfirmingCancellation),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.edit
|
package io.element.android.features.roomdetails.impl.edit
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
|
@ -30,11 +31,13 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
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.roomdetails.impl.R
|
import io.element.android.features.roomdetails.impl.R
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
|
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
|
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -50,8 +53,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomDetailsEditView(
|
fun RoomDetailsEditView(
|
||||||
state: RoomDetailsEditState,
|
state: RoomDetailsEditState,
|
||||||
onBackClick: () -> Unit,
|
onDone: () -> Unit,
|
||||||
onRoomEditSuccess: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
@ -62,12 +64,21 @@ fun RoomDetailsEditView(
|
||||||
isAvatarActionsSheetVisible.value = true
|
isAvatarActionsSheetVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BackHandler {
|
||||||
|
state.eventSink(RoomDetailsEditEvents.OnBackPress)
|
||||||
|
}
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier.clearFocusOnTap(focusManager),
|
modifier = modifier.clearFocusOnTap(focusManager),
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
titleStr = stringResource(id = R.string.screen_room_details_edit_room_title),
|
titleStr = stringResource(id = R.string.screen_room_details_edit_room_title),
|
||||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
navigationIcon = {
|
||||||
|
BackButton(
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(RoomDetailsEditEvents.OnBackPress)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
actions = {
|
actions = {
|
||||||
TextButton(
|
TextButton(
|
||||||
text = stringResource(CommonStrings.action_save),
|
text = stringResource(CommonStrings.action_save),
|
||||||
|
|
@ -126,14 +137,12 @@ fun RoomDetailsEditView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AvatarActionBottomSheet(
|
AvatarActionBottomSheet(
|
||||||
actions = state.avatarActions,
|
actions = state.avatarActions,
|
||||||
isVisible = isAvatarActionsSheetVisible.value,
|
isVisible = isAvatarActionsSheetVisible.value,
|
||||||
onDismiss = { isAvatarActionsSheetVisible.value = false },
|
onDismiss = { isAvatarActionsSheetVisible.value = false },
|
||||||
onSelectAction = { state.eventSink(RoomDetailsEditEvents.HandleAvatarAction(it)) }
|
onSelectAction = { state.eventSink(RoomDetailsEditEvents.HandleAvatarAction(it)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
AsyncActionView(
|
AsyncActionView(
|
||||||
async = state.saveAction,
|
async = state.saveAction,
|
||||||
progressDialog = {
|
progressDialog = {
|
||||||
|
|
@ -141,9 +150,19 @@ fun RoomDetailsEditView(
|
||||||
progressText = stringResource(R.string.screen_room_details_updating_room),
|
progressText = stringResource(R.string.screen_room_details_updating_room),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onSuccess = { onRoomEditSuccess() },
|
confirmationDialog = {
|
||||||
|
if (state.saveAction == AsyncAction.ConfirmingCancellation) {
|
||||||
|
ConfirmationDialog(
|
||||||
|
title = stringResource(CommonStrings.dialog_unsaved_changes_title),
|
||||||
|
content = stringResource(CommonStrings.dialog_unsaved_changes_description_android),
|
||||||
|
onSubmitClick = { state.eventSink(RoomDetailsEditEvents.OnBackPress) },
|
||||||
|
onDismiss = { state.eventSink(RoomDetailsEditEvents.CloseDialog) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess = { onDone() },
|
||||||
errorMessage = { stringResource(R.string.screen_room_details_edition_error) },
|
errorMessage = { stringResource(R.string.screen_room_details_edition_error) },
|
||||||
onErrorDismiss = { state.eventSink(RoomDetailsEditEvents.CancelSaveChanges) }
|
onErrorDismiss = { state.eventSink(RoomDetailsEditEvents.CloseDialog) }
|
||||||
)
|
)
|
||||||
|
|
||||||
PermissionsView(
|
PermissionsView(
|
||||||
|
|
@ -156,7 +175,6 @@ fun RoomDetailsEditView(
|
||||||
internal fun RoomDetailsEditViewPreview(@PreviewParameter(RoomDetailsEditStateProvider::class) state: RoomDetailsEditState) = ElementPreview {
|
internal fun RoomDetailsEditViewPreview(@PreviewParameter(RoomDetailsEditStateProvider::class) state: RoomDetailsEditState) = ElementPreview {
|
||||||
RoomDetailsEditView(
|
RoomDetailsEditView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClick = {},
|
onDone = {},
|
||||||
onRoomEditSuccess = {},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
||||||
),
|
),
|
||||||
banned = persistentListOf(),
|
banned = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
|
moderationState = aRoomMemberModerationState(canBan = true)
|
||||||
),
|
),
|
||||||
aRoomMemberListState(roomMembers = AsyncData.Loading()),
|
aRoomMemberListState(roomMembers = AsyncData.Loading()),
|
||||||
aRoomMemberListState().copy(canInvite = true),
|
aRoomMemberListState().copy(canInvite = true),
|
||||||
|
|
|
||||||
|
|
@ -649,11 +649,88 @@ class RoomDetailsEditPresenterTest {
|
||||||
initialState.eventSink(RoomDetailsEditEvents.Save)
|
initialState.eventSink(RoomDetailsEditEvents.Save)
|
||||||
skipItems(3)
|
skipItems(3)
|
||||||
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
initialState.eventSink(RoomDetailsEditEvents.CancelSaveChanges)
|
initialState.eventSink(RoomDetailsEditEvents.CloseDialog)
|
||||||
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - leave without saving - cancel`() = runTest {
|
||||||
|
val room = aJoinedRoom(
|
||||||
|
displayName = "Name",
|
||||||
|
canSendStateResult = { _, _ -> Result.success(true) }
|
||||||
|
)
|
||||||
|
val deleteCallback = lambdaRecorder<Uri?, Unit> {}
|
||||||
|
val presenter = createRoomDetailsEditPresenter(
|
||||||
|
room = room,
|
||||||
|
temporaryUriDeleter = FakeTemporaryUriDeleter(deleteCallback),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitFirstItem()
|
||||||
|
assertThat(initialState.saveButtonEnabled).isFalse()
|
||||||
|
// Once a change is made, the save button is enabled
|
||||||
|
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name edited"))
|
||||||
|
awaitItem().apply {
|
||||||
|
assertThat(saveButtonEnabled).isTrue()
|
||||||
|
eventSink(RoomDetailsEditEvents.OnBackPress)
|
||||||
|
}
|
||||||
|
awaitItem().apply {
|
||||||
|
assertThat(saveAction).isEqualTo(AsyncAction.ConfirmingCancellation)
|
||||||
|
eventSink(RoomDetailsEditEvents.CloseDialog)
|
||||||
|
}
|
||||||
|
awaitItem().apply {
|
||||||
|
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - leave no changes, no confirmation`() = runTest {
|
||||||
|
val room = aJoinedRoom(
|
||||||
|
displayName = "Name",
|
||||||
|
canSendStateResult = { _, _ -> Result.success(true) }
|
||||||
|
)
|
||||||
|
val presenter = createRoomDetailsEditPresenter(
|
||||||
|
room = room,
|
||||||
|
temporaryUriDeleter = FakeTemporaryUriDeleter {},
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitFirstItem()
|
||||||
|
assertThat(initialState.saveButtonEnabled).isFalse()
|
||||||
|
initialState.eventSink(RoomDetailsEditEvents.OnBackPress)
|
||||||
|
assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - leave without saving - confirm`() = runTest {
|
||||||
|
val room = aJoinedRoom(
|
||||||
|
displayName = "Name",
|
||||||
|
canSendStateResult = { _, _ -> Result.success(true) }
|
||||||
|
)
|
||||||
|
val presenter = createRoomDetailsEditPresenter(
|
||||||
|
room = room,
|
||||||
|
temporaryUriDeleter = FakeTemporaryUriDeleter({}),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitFirstItem()
|
||||||
|
assertThat(initialState.saveButtonEnabled).isFalse()
|
||||||
|
// Once a change is made, the save button is enabled
|
||||||
|
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name edited"))
|
||||||
|
awaitItem().apply {
|
||||||
|
assertThat(saveButtonEnabled).isTrue()
|
||||||
|
eventSink(RoomDetailsEditEvents.OnBackPress)
|
||||||
|
}
|
||||||
|
awaitItem().apply {
|
||||||
|
assertThat(saveAction).isEqualTo(AsyncAction.ConfirmingCancellation)
|
||||||
|
eventSink(RoomDetailsEditEvents.OnBackPress)
|
||||||
|
}
|
||||||
|
awaitItem().apply {
|
||||||
|
assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun saveAndAssertFailure(
|
private suspend fun saveAndAssertFailure(
|
||||||
room: JoinedRoom,
|
room: JoinedRoom,
|
||||||
event: RoomDetailsEditEvents,
|
event: RoomDetailsEditEvents,
|
||||||
|
|
|
||||||
|
|
@ -38,17 +38,41 @@ class RoomDetailsEditViewTest {
|
||||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on back invoke back callback`() {
|
fun `clicking on back emits the expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>(expectEvents = false)
|
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>()
|
||||||
ensureCalledOnce { callback ->
|
rule.setRoomDetailsEditView(
|
||||||
rule.setRoomDetailsEditView(
|
aRoomDetailsEditState(
|
||||||
aRoomDetailsEditState(
|
eventSink = eventsRecorder
|
||||||
eventSink = eventsRecorder
|
),
|
||||||
),
|
)
|
||||||
onBackClick = callback,
|
rule.pressBack()
|
||||||
)
|
eventsRecorder.assertSingle(RoomDetailsEditEvents.OnBackPress)
|
||||||
rule.pressBack()
|
}
|
||||||
}
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on OK when confirming exit emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>()
|
||||||
|
rule.setRoomDetailsEditView(
|
||||||
|
aRoomDetailsEditState(
|
||||||
|
saveAction = AsyncAction.ConfirmingCancellation,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_ok)
|
||||||
|
eventsRecorder.assertSingle(RoomDetailsEditEvents.OnBackPress)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on cancel when confirming exit emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>()
|
||||||
|
rule.setRoomDetailsEditView(
|
||||||
|
aRoomDetailsEditState(
|
||||||
|
saveAction = AsyncAction.ConfirmingCancellation,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
|
eventsRecorder.assertSingle(RoomDetailsEditEvents.CloseDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -60,7 +84,7 @@ class RoomDetailsEditViewTest {
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
saveAction = AsyncAction.Success(Unit)
|
saveAction = AsyncAction.Success(Unit)
|
||||||
),
|
),
|
||||||
onRoomEdited = callback,
|
onDone = callback,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -209,20 +233,18 @@ class RoomDetailsEditViewTest {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
rule.clickOn(CommonStrings.action_ok)
|
rule.clickOn(CommonStrings.action_ok)
|
||||||
eventsRecorder.assertSingle(RoomDetailsEditEvents.CancelSaveChanges)
|
eventsRecorder.assertSingle(RoomDetailsEditEvents.CloseDialog)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDetailsEditView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDetailsEditView(
|
||||||
state: RoomDetailsEditState,
|
state: RoomDetailsEditState,
|
||||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
onDone: () -> Unit = EnsureNeverCalled(),
|
||||||
onRoomEdited: () -> Unit = EnsureNeverCalled(),
|
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
RoomDetailsEditView(
|
RoomDetailsEditView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClick = onBackClick,
|
onDone = onDone,
|
||||||
onRoomEditSuccess = onRoomEdited,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,5 +33,7 @@ interface SpaceEntryPoint : FeatureEntryPoint {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onOpenRoom(roomId: RoomId, viaParameters: List<String>)
|
fun onOpenRoom(roomId: RoomId, viaParameters: List<String>)
|
||||||
fun onOpenDetails()
|
fun onOpenDetails()
|
||||||
|
|
||||||
|
fun onOpenMemberList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,10 @@ class SpaceFlowNode(
|
||||||
callback.onOpenDetails()
|
callback.onOpenDetails()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onOpenMemberList() {
|
||||||
|
callback.onOpenMemberList()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onLeaveSpace() {
|
override fun onLeaveSpace() {
|
||||||
backstack.push(NavTarget.Leave)
|
backstack.push(NavTarget.Leave)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ class SpaceNode(
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onOpenRoom(roomId: RoomId, viaParameters: List<String>)
|
fun onOpenRoom(roomId: RoomId, viaParameters: List<String>)
|
||||||
fun onOpenDetails()
|
fun onOpenDetails()
|
||||||
|
|
||||||
|
fun onOpenMemberList()
|
||||||
fun onLeaveSpace()
|
fun onLeaveSpace()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,6 +85,9 @@ class SpaceNode(
|
||||||
onShareSpace = {
|
onShareSpace = {
|
||||||
onShareRoom(context)
|
onShareRoom(context)
|
||||||
},
|
},
|
||||||
|
onViewMembersClick = {
|
||||||
|
callback.onOpenMemberList()
|
||||||
|
},
|
||||||
acceptDeclineInviteView = {
|
acceptDeclineInviteView = {
|
||||||
acceptDeclineInviteView.Render(
|
acceptDeclineInviteView.Render(
|
||||||
state = state.acceptDeclineInviteState,
|
state = state.acceptDeclineInviteState,
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ fun SpaceView(
|
||||||
onShareSpace: () -> Unit,
|
onShareSpace: () -> Unit,
|
||||||
onLeaveSpaceClick: () -> Unit,
|
onLeaveSpaceClick: () -> Unit,
|
||||||
onDetailsClick: () -> Unit,
|
onDetailsClick: () -> Unit,
|
||||||
|
onViewMembersClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
acceptDeclineInviteView: @Composable () -> Unit,
|
acceptDeclineInviteView: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -89,7 +90,8 @@ fun SpaceView(
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
onLeaveSpaceClick = onLeaveSpaceClick,
|
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||||
onShareSpace = onShareSpace,
|
onShareSpace = onShareSpace,
|
||||||
onDetailsClick = onDetailsClick
|
onDetailsClick = onDetailsClick,
|
||||||
|
onViewMembersClick = onViewMembersClick,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
|
|
@ -256,6 +258,7 @@ private fun SpaceViewTopBar(
|
||||||
onLeaveSpaceClick: () -> Unit,
|
onLeaveSpaceClick: () -> Unit,
|
||||||
onDetailsClick: () -> Unit,
|
onDetailsClick: () -> Unit,
|
||||||
onShareSpace: () -> Unit,
|
onShareSpace: () -> Unit,
|
||||||
|
onViewMembersClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
|
@ -304,6 +307,20 @@ private fun SpaceViewTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
showMenu = false
|
||||||
|
onViewMembersClick()
|
||||||
|
},
|
||||||
|
text = { Text(stringResource(id = CommonStrings.screen_space_menu_action_members)) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.User(),
|
||||||
|
tint = ElementTheme.colors.iconSecondary,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
showMenu = false
|
showMenu = false
|
||||||
|
|
@ -344,10 +361,10 @@ private fun SpaceAvatarAndNameRow(
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 8.dp)
|
.padding(horizontal = 8.dp)
|
||||||
.semantics {
|
.semantics {
|
||||||
heading()
|
heading()
|
||||||
},
|
},
|
||||||
text = name ?: stringResource(CommonStrings.common_no_space_name),
|
text = name ?: stringResource(CommonStrings.common_no_space_name),
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
fontStyle = FontStyle.Italic.takeIf { name == null },
|
fontStyle = FontStyle.Italic.takeIf { name == null },
|
||||||
|
|
@ -403,6 +420,7 @@ internal fun SpaceViewPreview(
|
||||||
onLeaveSpaceClick = {},
|
onLeaveSpaceClick = {},
|
||||||
acceptDeclineInviteView = {},
|
acceptDeclineInviteView = {},
|
||||||
onDetailsClick = {},
|
onDetailsClick = {},
|
||||||
|
onViewMembersClick = {},
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ class DefaultSpaceEntryPointTest {
|
||||||
val callback = object : SpaceEntryPoint.Callback {
|
val callback = object : SpaceEntryPoint.Callback {
|
||||||
override fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) = lambdaError()
|
override fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) = lambdaError()
|
||||||
override fun onOpenDetails() = lambdaError()
|
override fun onOpenDetails() = lambdaError()
|
||||||
|
override fun onOpenMemberList() = lambdaError()
|
||||||
}
|
}
|
||||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||||
.inputs(nodeInputs)
|
.inputs(nodeInputs)
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
|
||||||
onShareSpace: () -> Unit = EnsureNeverCalled(),
|
onShareSpace: () -> Unit = EnsureNeverCalled(),
|
||||||
onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(),
|
onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(),
|
||||||
onDetailsClick: () -> Unit = EnsureNeverCalled(),
|
onDetailsClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onViewMembersClick: () -> Unit = EnsureNeverCalled(),
|
||||||
acceptDeclineInviteView: @Composable () -> Unit = {},
|
acceptDeclineInviteView: @Composable () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
|
|
@ -150,6 +151,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
|
||||||
onShareSpace = onShareSpace,
|
onShareSpace = onShareSpace,
|
||||||
onLeaveSpaceClick = onLeaveSpaceClick,
|
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||||
onDetailsClick = onDetailsClick,
|
onDetailsClick = onDetailsClick,
|
||||||
|
onViewMembersClick = onViewMembersClick,
|
||||||
acceptDeclineInviteView = acceptDeclineInviteView,
|
acceptDeclineInviteView = acceptDeclineInviteView,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ telephoto = "0.18.0"
|
||||||
haze = "1.6.10"
|
haze = "1.6.10"
|
||||||
|
|
||||||
# Dependency analysis
|
# Dependency analysis
|
||||||
dependencyAnalysis = "3.2.0"
|
dependencyAnalysis = "3.4.0"
|
||||||
|
|
||||||
# DI
|
# DI
|
||||||
metro = "0.7.2"
|
metro = "0.7.2"
|
||||||
|
|
@ -171,7 +171,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
|
||||||
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
|
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
|
||||||
# All new features should not be implemented in the pull request that upgrades the version, developers should
|
# All new features should not be implemented in the pull request that upgrades the version, developers should
|
||||||
# only fix API breaks and may add some TODOs.
|
# only fix API breaks and may add some TODOs.
|
||||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.10.13"
|
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.10.28"
|
||||||
|
|
||||||
# Others
|
# Others
|
||||||
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
||||||
|
|
@ -213,7 +213,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0"
|
||||||
posthog = "com.posthog:posthog-android:3.25.0"
|
posthog = "com.posthog:posthog-android:3.25.0"
|
||||||
sentry = "io.sentry:sentry-android:8.24.0"
|
sentry = "io.sentry:sentry-android:8.24.0"
|
||||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
|
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.29.2"
|
||||||
|
|
||||||
# Emojibase
|
# Emojibase
|
||||||
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.4.3"
|
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.4.3"
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ sealed interface AsyncAction<out T> {
|
||||||
|
|
||||||
data object ConfirmingNoParams : Confirming
|
data object ConfirmingNoParams : Confirming
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User cancels the action, use this object to ask for confirmation.
|
||||||
|
*/
|
||||||
|
data object ConfirmingCancellation : Confirming
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an operation that is currently ongoing.
|
* Represents an operation that is currently ongoing.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -17,4 +17,6 @@ sealed class QrLoginException : Exception() {
|
||||||
data object SlidingSyncNotAvailable : QrLoginException()
|
data object SlidingSyncNotAvailable : QrLoginException()
|
||||||
data object OtherDeviceNotSignedIn : QrLoginException()
|
data object OtherDeviceNotSignedIn : QrLoginException()
|
||||||
data object Unknown : QrLoginException()
|
data object Unknown : QrLoginException()
|
||||||
|
data object CheckCodeAlreadySent : QrLoginException()
|
||||||
|
data object CheckCodeCannotBeSent : QrLoginException()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -289,10 +289,13 @@ class RustMatrixAuthenticationService(
|
||||||
qrCodeData = sdkQrCodeLoginData,
|
qrCodeData = sdkQrCodeLoginData,
|
||||||
)
|
)
|
||||||
client.loginWithQrCode(
|
client.loginWithQrCode(
|
||||||
qrCodeData = qrCodeData.rustQrCodeData,
|
|
||||||
oidcConfiguration = oidcConfiguration,
|
oidcConfiguration = oidcConfiguration,
|
||||||
progressListener = progressListener,
|
).use {
|
||||||
)
|
it.scan(
|
||||||
|
qrCodeData = qrCodeData.rustQrCodeData,
|
||||||
|
progressListener = progressListener,
|
||||||
|
)
|
||||||
|
}
|
||||||
// Ensure that the user is not already logged in with the same account
|
// Ensure that the user is not already logged in with the same account
|
||||||
ensureNotAlreadyLoggedIn(client)
|
ensureNotAlreadyLoggedIn(client)
|
||||||
val sessionData = client.session()
|
val sessionData = client.session()
|
||||||
|
|
|
||||||
|
|
@ -42,5 +42,7 @@ object QrErrorMapper {
|
||||||
is RustHumanQrLoginException.Unknown -> QrLoginException.Unknown
|
is RustHumanQrLoginException.Unknown -> QrLoginException.Unknown
|
||||||
is RustHumanQrLoginException.OidcMetadataInvalid -> QrLoginException.OidcMetadataInvalid
|
is RustHumanQrLoginException.OidcMetadataInvalid -> QrLoginException.OidcMetadataInvalid
|
||||||
is RustHumanQrLoginException.SlidingSyncNotAvailable -> QrLoginException.SlidingSyncNotAvailable
|
is RustHumanQrLoginException.SlidingSyncNotAvailable -> QrLoginException.SlidingSyncNotAvailable
|
||||||
|
is RustHumanQrLoginException.CheckCodeAlreadySent -> QrLoginException.CheckCodeAlreadySent
|
||||||
|
is RustHumanQrLoginException.CheckCodeCannotBeSent -> QrLoginException.CheckCodeCannotBeSent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ internal fun ComposerDraft.into(): RustComposerDraft {
|
||||||
return RustComposerDraft(
|
return RustComposerDraft(
|
||||||
plainText = plainText,
|
plainText = plainText,
|
||||||
htmlText = htmlText,
|
htmlText = htmlText,
|
||||||
draftType = draftType.into()
|
draftType = draftType.into(),
|
||||||
|
// TODO add media attachments to the draft
|
||||||
|
attachments = emptyList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,6 @@ object NotificationIdProvider {
|
||||||
return getOffset(sessionId) + FALLBACK_NOTIFICATION_ID
|
return getOffset(sessionId) + FALLBACK_NOTIFICATION_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCallNotificationId(sessionId: SessionId): Int {
|
|
||||||
return getOffset(sessionId) + ROOM_CALL_NOTIFICATION_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getForegroundServiceNotificationId(type: ForegroundServiceType): Int {
|
fun getForegroundServiceNotificationId(type: ForegroundServiceType): Int {
|
||||||
return type.id * 10 + FOREGROUND_SERVICE_NOTIFICATION_ID
|
return type.id * 10 + FOREGROUND_SERVICE_NOTIFICATION_ID
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +45,6 @@ object NotificationIdProvider {
|
||||||
private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
|
private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
|
||||||
private const val ROOM_EVENT_NOTIFICATION_ID = 2
|
private const val ROOM_EVENT_NOTIFICATION_ID = 2
|
||||||
private const val ROOM_INVITATION_NOTIFICATION_ID = 3
|
private const val ROOM_INVITATION_NOTIFICATION_ID = 3
|
||||||
private const val ROOM_CALL_NOTIFICATION_ID = 3
|
|
||||||
|
|
||||||
private const val FOREGROUND_SERVICE_NOTIFICATION_ID = 4
|
private const val FOREGROUND_SERVICE_NOTIFICATION_ID = 4
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv
|
||||||
import io.element.android.libraries.push.impl.workmanager.SyncNotificationWorkManagerRequest
|
import io.element.android.libraries.push.impl.workmanager.SyncNotificationWorkManagerRequest
|
||||||
import io.element.android.libraries.push.impl.workmanager.WorkerDataConverter
|
import io.element.android.libraries.push.impl.workmanager.WorkerDataConverter
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
|
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
|
||||||
|
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -49,6 +50,7 @@ class DefaultNotificationResolverQueue(
|
||||||
private val workManagerScheduler: WorkManagerScheduler,
|
private val workManagerScheduler: WorkManagerScheduler,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val workerDataConverter: WorkerDataConverter,
|
private val workerDataConverter: WorkerDataConverter,
|
||||||
|
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
) : NotificationResolverQueue {
|
) : NotificationResolverQueue {
|
||||||
companion object {
|
companion object {
|
||||||
private const val BATCH_WINDOW_MS = 250L
|
private const val BATCH_WINDOW_MS = 250L
|
||||||
|
|
@ -100,6 +102,7 @@ class DefaultNotificationResolverQueue(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
notificationEventRequests = requests,
|
notificationEventRequests = requests,
|
||||||
workerDataConverter = workerDataConverter,
|
workerDataConverter = workerDataConverter,
|
||||||
|
buildVersionSdkIntProvider = buildVersionSdkIntProvider,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import io.element.android.libraries.push.impl.notifications.NotificationResolver
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
|
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
|
||||||
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
|
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
|
||||||
import io.element.android.libraries.workmanager.api.di.WorkerKey
|
import io.element.android.libraries.workmanager.api.di.WorkerKey
|
||||||
|
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -39,7 +40,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class FetchNotificationsWorker(
|
class FetchNotificationsWorker(
|
||||||
@Assisted workerParams: WorkerParameters,
|
@Assisted workerParams: WorkerParameters,
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val eventResolver: NotifiableEventResolver,
|
private val eventResolver: NotifiableEventResolver,
|
||||||
private val queue: NotificationResolverQueue,
|
private val queue: NotificationResolverQueue,
|
||||||
|
|
@ -47,6 +48,7 @@ class FetchNotificationsWorker(
|
||||||
private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
|
private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
private val workerDataConverter: WorkerDataConverter,
|
private val workerDataConverter: WorkerDataConverter,
|
||||||
|
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
) : CoroutineWorker(context, workerParams) {
|
) : CoroutineWorker(context, workerParams) {
|
||||||
override suspend fun doWork(): Result = withContext(coroutineDispatchers.io) {
|
override suspend fun doWork(): Result = withContext(coroutineDispatchers.io) {
|
||||||
Timber.d("FetchNotificationsWorker started")
|
Timber.d("FetchNotificationsWorker started")
|
||||||
|
|
@ -88,6 +90,7 @@ class FetchNotificationsWorker(
|
||||||
sessionId = failedSessionId,
|
sessionId = failedSessionId,
|
||||||
notificationEventRequests = requestsToRetry,
|
notificationEventRequests = requestsToRetry,
|
||||||
workerDataConverter = workerDataConverter,
|
workerDataConverter = workerDataConverter,
|
||||||
|
buildVersionSdkIntProvider = buildVersionSdkIntProvider,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.workmanager
|
package io.element.android.libraries.push.impl.workmanager
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.OutOfQuotaPolicy
|
import androidx.work.OutOfQuotaPolicy
|
||||||
import androidx.work.WorkRequest
|
import androidx.work.WorkRequest
|
||||||
|
|
@ -15,6 +16,7 @@ import io.element.android.libraries.push.api.push.NotificationEventRequest
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerRequest
|
import io.element.android.libraries.workmanager.api.WorkManagerRequest
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
|
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
|
||||||
import io.element.android.libraries.workmanager.api.workManagerTag
|
import io.element.android.libraries.workmanager.api.workManagerTag
|
||||||
|
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -24,6 +26,7 @@ class SyncNotificationWorkManagerRequest(
|
||||||
private val sessionId: SessionId,
|
private val sessionId: SessionId,
|
||||||
private val notificationEventRequests: List<NotificationEventRequest>,
|
private val notificationEventRequests: List<NotificationEventRequest>,
|
||||||
private val workerDataConverter: WorkerDataConverter,
|
private val workerDataConverter: WorkerDataConverter,
|
||||||
|
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
) : WorkManagerRequest {
|
) : WorkManagerRequest {
|
||||||
override fun build(): Result<WorkRequest> {
|
override fun build(): Result<WorkRequest> {
|
||||||
if (notificationEventRequests.isEmpty()) {
|
if (notificationEventRequests.isEmpty()) {
|
||||||
|
|
@ -36,7 +39,14 @@ class SyncNotificationWorkManagerRequest(
|
||||||
return Result.success(
|
return Result.success(
|
||||||
OneTimeWorkRequestBuilder<FetchNotificationsWorker>()
|
OneTimeWorkRequestBuilder<FetchNotificationsWorker>()
|
||||||
.setInputData(data)
|
.setInputData(data)
|
||||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
.apply {
|
||||||
|
// Expedited workers aren't needed on Android 12 or lower:
|
||||||
|
// They force displaying a foreground sync notification for no good reason, since they sync almost immediately anyway
|
||||||
|
// See https://developer.android.com/develop/background-work/background-tasks/persistent/getting-started/define-work#backwards-compat
|
||||||
|
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
||||||
|
setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||||
|
}
|
||||||
|
}
|
||||||
.setTraceTag(workManagerTag(sessionId, WorkManagerRequestType.NOTIFICATION_SYNC))
|
.setTraceTag(workManagerTag(sessionId, WorkManagerRequestType.NOTIFICATION_SYNC))
|
||||||
// TODO investigate using this instead of the resolver queue
|
// TODO investigate using this instead of the resolver queue
|
||||||
// .setInputMerger()
|
// .setInputMerger()
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushSto
|
||||||
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret
|
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerRequest
|
import io.element.android.libraries.workmanager.api.WorkManagerRequest
|
||||||
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
|
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
|
||||||
|
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||||
import io.element.android.tests.testutils.lambda.any
|
import io.element.android.tests.testutils.lambda.any
|
||||||
|
|
@ -717,6 +718,7 @@ class DefaultPushHandlerTest {
|
||||||
workManagerScheduler = workManagerScheduler,
|
workManagerScheduler = workManagerScheduler,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
workerDataConverter = WorkerDataConverter(DefaultJsonProvider()),
|
workerDataConverter = WorkerDataConverter(DefaultJsonProvider()),
|
||||||
|
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(33),
|
||||||
),
|
),
|
||||||
appCoroutineScope = backgroundScope,
|
appCoroutineScope = backgroundScope,
|
||||||
fallbackNotificationFactory = FallbackNotificationFactory(
|
fallbackNotificationFactory = FallbackNotificationFactory(
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import io.element.android.libraries.push.test.notifications.FakeNotificationReso
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerRequest
|
import io.element.android.libraries.workmanager.api.WorkManagerRequest
|
||||||
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
|
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
|
||||||
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
|
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
|
||||||
|
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
|
@ -176,6 +177,7 @@ class FetchNotificationWorkerTest {
|
||||||
syncOnNotifiableEvent = syncOnNotifiableEvent,
|
syncOnNotifiableEvent = syncOnNotifiableEvent,
|
||||||
coroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
workerDataConverter = WorkerDataConverter(DefaultJsonProvider()),
|
workerDataConverter = WorkerDataConverter(DefaultJsonProvider()),
|
||||||
|
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(33),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun TestScope.createWorkerParams(
|
private fun TestScope.createWorkerParams(
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,17 @@ import io.element.android.libraries.push.api.push.NotificationEventRequest
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotificationEventRequest
|
import io.element.android.libraries.push.impl.notifications.fixtures.aNotificationEventRequest
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
|
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
|
||||||
import io.element.android.libraries.workmanager.api.workManagerTag
|
import io.element.android.libraries.workmanager.api.workManagerTag
|
||||||
|
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class SyncNotificationWorkManagerRequestTest {
|
class SyncNotificationWorkManagerRequestTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build - success`() = runTest {
|
fun `build - success API 33`() = runTest {
|
||||||
val request = createSyncNotificationWorkManagerRequest(
|
val request = createSyncNotificationWorkManagerRequest(
|
||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
notificationEventRequests = listOf(aNotificationEventRequest())
|
notificationEventRequests = listOf(aNotificationEventRequest()),
|
||||||
|
sdkVersion = 33,
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = request.build()
|
val result = request.build()
|
||||||
|
|
@ -33,11 +35,31 @@ class SyncNotificationWorkManagerRequestTest {
|
||||||
result.getOrNull()!!.run {
|
result.getOrNull()!!.run {
|
||||||
assertThat(this).isInstanceOf(OneTimeWorkRequest::class.java)
|
assertThat(this).isInstanceOf(OneTimeWorkRequest::class.java)
|
||||||
assertThat(workSpec.input.hasKeyWithValueOfType<String>("requests")).isTrue()
|
assertThat(workSpec.input.hasKeyWithValueOfType<String>("requests")).isTrue()
|
||||||
|
// True in API 33+
|
||||||
assertThat(workSpec.expedited).isTrue()
|
assertThat(workSpec.expedited).isTrue()
|
||||||
assertThat(workSpec.traceTag).isEqualTo(workManagerTag(A_SESSION_ID, WorkManagerRequestType.NOTIFICATION_SYNC))
|
assertThat(workSpec.traceTag).isEqualTo(workManagerTag(A_SESSION_ID, WorkManagerRequestType.NOTIFICATION_SYNC))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `build - success API 32 and lower`() = runTest {
|
||||||
|
val request = createSyncNotificationWorkManagerRequest(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
notificationEventRequests = listOf(aNotificationEventRequest()),
|
||||||
|
sdkVersion = 32,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = request.build()
|
||||||
|
assertThat(result.isSuccess).isTrue()
|
||||||
|
result.getOrNull()!!.run {
|
||||||
|
assertThat(this).isInstanceOf(OneTimeWorkRequest::class.java)
|
||||||
|
assertThat(workSpec.input.hasKeyWithValueOfType<String>("requests")).isTrue()
|
||||||
|
// False before API 33
|
||||||
|
assertThat(workSpec.expedited).isFalse()
|
||||||
|
assertThat(workSpec.traceTag).isEqualTo(workManagerTag(A_SESSION_ID, WorkManagerRequestType.NOTIFICATION_SYNC))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `build - empty list of requests fails`() = runTest {
|
fun `build - empty list of requests fails`() = runTest {
|
||||||
val request = createSyncNotificationWorkManagerRequest(
|
val request = createSyncNotificationWorkManagerRequest(
|
||||||
|
|
@ -64,9 +86,11 @@ class SyncNotificationWorkManagerRequestTest {
|
||||||
private fun createSyncNotificationWorkManagerRequest(
|
private fun createSyncNotificationWorkManagerRequest(
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
notificationEventRequests: List<NotificationEventRequest>,
|
notificationEventRequests: List<NotificationEventRequest>,
|
||||||
workerDataConverter: WorkerDataConverter = WorkerDataConverter(DefaultJsonProvider())
|
workerDataConverter: WorkerDataConverter = WorkerDataConverter(DefaultJsonProvider()),
|
||||||
|
sdkVersion: Int = 33,
|
||||||
) = SyncNotificationWorkManagerRequest(
|
) = SyncNotificationWorkManagerRequest(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
notificationEventRequests = notificationEventRequests,
|
notificationEventRequests = notificationEventRequests,
|
||||||
workerDataConverter = workerDataConverter,
|
workerDataConverter = workerDataConverter,
|
||||||
|
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(sdkVersion),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,9 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
includeBuild("plugins")
|
includeBuild("plugins")
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
@ -18,14 +16,17 @@ pluginManagement {
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
maven {
|
maven {
|
||||||
url = URI("https://www.jitpack.io")
|
url = uri("https://www.jitpack.io")
|
||||||
content {
|
content {
|
||||||
includeModule("com.github.matrix-org", "matrix-analytics-events")
|
includeModule("com.github.matrix-org", "matrix-analytics-events")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url = uri("https://repo1.maven.org/maven2/")
|
||||||
|
}
|
||||||
flatDir {
|
flatDir {
|
||||||
dirs("libraries/matrix/libs")
|
dirs("libraries/matrix/libs")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2cd78ece31258d6ee07eabc282f935e6228893d52d24e00e0bbd129138ed8c31
|
||||||
|
size 30836
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2bf7bc223f5d481ec8bd1f2a6711751a3bb82ac8f0d856cb5f6c8f4ee7ef50e1
|
||||||
|
size 28891
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:37940d14ca14d65256ea220cfd7f0685ba2d100425f65af4d4cffe35be4b69fb
|
oid sha256:0cd12c68415f61b198696a9f9d8b19da5e6ced287e015071a8a850100238862b
|
||||||
size 45485
|
size 49992
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4c45c582fb6698e70b376f832b9564946a2271a6b3532886239dfd75d8bb9755
|
oid sha256:be897269967951ea0ddb0cc209005d07d4904531a8ab9d865b6e622c9ba98e18
|
||||||
size 45409
|
size 49747
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue