Add special flow for leaving a space as the last owner (#6112)
* When the user is in a v12 room, use different UI to select the last owner when leaving - Add `LeaveSpaceRoom.areCreatorsPrivileged` to detect when this is happening. - Import new strings. - Build the new UI. - Attach it to a change member roles screen navigation. * Don't display the `isLastOwner` UI if the user is the only joined one in the room * Rename `LeaveSpaceState.isLastOwner` to `.needsOwnerChange`. This way, it's easier to understand the difference with the passed `LeaveSpaceRoom.isLastOwner` value * Add a test for the new check of user not being the last joined member * Fix paddings in `LeaveSpaceView` * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
e34b15823e
commit
63f24f0ae1
36 changed files with 212 additions and 114 deletions
|
|
@ -7,8 +7,6 @@
|
||||||
<string name="screen_create_room_name_placeholder">"Add name…"</string>
|
<string name="screen_create_room_name_placeholder">"Add name…"</string>
|
||||||
<string name="screen_create_room_new_room_title">"New room"</string>
|
<string name="screen_create_room_new_room_title">"New room"</string>
|
||||||
<string name="screen_create_room_new_space_title">"New space"</string>
|
<string name="screen_create_room_new_space_title">"New space"</string>
|
||||||
<string name="screen_create_room_parent_space_home_description">"(no space)"</string>
|
|
||||||
<string name="screen_create_room_parent_space_home_title">"Home"</string>
|
|
||||||
<string name="screen_create_room_private_option_description">"Only people invited can join."</string>
|
<string name="screen_create_room_private_option_description">"Only people invited can join."</string>
|
||||||
<string name="screen_create_room_private_option_title">"Private"</string>
|
<string name="screen_create_room_private_option_title">"Private"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Anyone can find this room.
|
<string name="screen_create_room_public_option_description">"Anyone can find this room.
|
||||||
|
|
|
||||||
|
|
@ -52,4 +52,5 @@ dependencies {
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.features.createroom.test)
|
testImplementation(projects.features.createroom.test)
|
||||||
testImplementation(projects.features.invite.test)
|
testImplementation(projects.features.invite.test)
|
||||||
|
testImplementation(projects.features.rolesandpermissions.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,13 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
|
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint
|
||||||
|
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType
|
||||||
import io.element.android.features.space.api.SpaceEntryPoint
|
import io.element.android.features.space.api.SpaceEntryPoint
|
||||||
import io.element.android.features.space.impl.addroom.AddRoomToSpaceNode
|
import io.element.android.features.space.impl.addroom.AddRoomToSpaceNode
|
||||||
import io.element.android.features.space.impl.di.SpaceFlowGraph
|
import io.element.android.features.space.impl.di.SpaceFlowGraph
|
||||||
|
|
@ -38,10 +41,15 @@ import io.element.android.libraries.architecture.callback
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||||
import io.element.android.libraries.matrix.api.spaces.loadAllIncrementally
|
import io.element.android.libraries.matrix.api.spaces.loadAllIncrementally
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
|
|
@ -49,10 +57,12 @@ import kotlinx.parcelize.Parcelize
|
||||||
class SpaceFlowNode(
|
class SpaceFlowNode(
|
||||||
@Assisted val buildContext: BuildContext,
|
@Assisted val buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
room: JoinedRoom,
|
private val room: JoinedRoom,
|
||||||
spaceService: SpaceService,
|
spaceService: SpaceService,
|
||||||
graphFactory: SpaceFlowGraph.Factory,
|
graphFactory: SpaceFlowGraph.Factory,
|
||||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
|
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
|
||||||
|
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||||
) : BaseFlowNode<SpaceFlowNode.NavTarget>(
|
) : BaseFlowNode<SpaceFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Root,
|
initialElement = NavTarget.Root,
|
||||||
|
|
@ -80,6 +90,9 @@ class SpaceFlowNode(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object AddRoom : NavTarget
|
data object AddRoom : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object ChangeOwners : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
|
|
@ -105,6 +118,10 @@ class SpaceFlowNode(
|
||||||
override fun navigateToRolesAndPermissions() {
|
override fun navigateToRolesAndPermissions() {
|
||||||
backstack.push(NavTarget.Settings(SpaceSettingsFlowNode.NavTarget.RolesAndPermissions))
|
backstack.push(NavTarget.Settings(SpaceSettingsFlowNode.NavTarget.RolesAndPermissions))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun navigateToChooseOwners() {
|
||||||
|
backstack.replace(NavTarget.ChangeOwners)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createNode<LeaveSpaceNode>(buildContext, listOf(callback))
|
createNode<LeaveSpaceNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
|
|
@ -177,6 +194,29 @@ class SpaceFlowNode(
|
||||||
}
|
}
|
||||||
createNode<AddRoomToSpaceNode>(buildContext, listOf(callback))
|
createNode<AddRoomToSpaceNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
|
NavTarget.ChangeOwners -> {
|
||||||
|
val node = changeRoomMemberRolesEntryPoint.createNode(
|
||||||
|
parentNode = this,
|
||||||
|
buildContext = buildContext,
|
||||||
|
room = room,
|
||||||
|
listType = ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving,
|
||||||
|
)
|
||||||
|
|
||||||
|
val completionProxy = node as ChangeRoomMemberRolesEntryPoint.NodeProxy
|
||||||
|
sessionCoroutineScope.launch {
|
||||||
|
val changedOwners = withContext(NonCancellable) {
|
||||||
|
completionProxy.waitForCompletion()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedOwners) {
|
||||||
|
backstack.replace(NavTarget.Leave)
|
||||||
|
} else {
|
||||||
|
backstack.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ class LeaveSpaceNode(
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun closeLeaveSpaceFlow()
|
fun closeLeaveSpaceFlow()
|
||||||
fun navigateToRolesAndPermissions()
|
fun navigateToRolesAndPermissions()
|
||||||
|
fun navigateToChooseOwners()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(room.roomId)
|
private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(room.roomId)
|
||||||
|
|
@ -57,6 +58,7 @@ class LeaveSpaceNode(
|
||||||
state = state,
|
state = state,
|
||||||
onCancel = callback::closeLeaveSpaceFlow,
|
onCancel = callback::closeLeaveSpaceFlow,
|
||||||
onRolesAndPermissionsClick = callback::navigateToRolesAndPermissions,
|
onRolesAndPermissionsClick = callback::navigateToRolesAndPermissions,
|
||||||
|
onChooseOwnersClick = callback::navigateToChooseOwners,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ class LeaveSpacePresenter(
|
||||||
SelectableSpaceRoom(
|
SelectableSpaceRoom(
|
||||||
spaceRoom = room.spaceRoom,
|
spaceRoom = room.spaceRoom,
|
||||||
isLastOwner = room.isLastOwner,
|
isLastOwner = room.isLastOwner,
|
||||||
|
joinedMembersCount = room.spaceRoom.numJoinedMembers,
|
||||||
isSelected = selectedRoomIds.contains(room.spaceRoom.roomId),
|
isSelected = selectedRoomIds.contains(room.spaceRoom.roomId),
|
||||||
)
|
)
|
||||||
}.toImmutableList()
|
}.toImmutableList()
|
||||||
|
|
@ -130,9 +131,11 @@ class LeaveSpacePresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val currentSpaceToLeave = leaveSpaceRooms.dataOrNull()?.current
|
||||||
return LeaveSpaceState(
|
return LeaveSpaceState(
|
||||||
spaceName = leaveSpaceRooms.dataOrNull()?.current?.spaceRoom?.displayName,
|
spaceName = currentSpaceToLeave?.spaceRoom?.displayName,
|
||||||
isLastOwner = leaveSpaceRooms.dataOrNull()?.current?.isLastOwner == true,
|
needsOwnerChange = currentSpaceToLeave?.let { it.spaceRoom.numJoinedMembers > 1 && it.isLastOwner } == true,
|
||||||
|
areCreatorsPrivileged = currentSpaceToLeave?.areCreatorsPrivileged == true,
|
||||||
selectableSpaceRooms = selectableSpaceRooms,
|
selectableSpaceRooms = selectableSpaceRooms,
|
||||||
leaveSpaceAction = leaveSpaceAction.value,
|
leaveSpaceAction = leaveSpaceAction.value,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
data class LeaveSpaceState(
|
data class LeaveSpaceState(
|
||||||
val spaceName: String?,
|
val spaceName: String?,
|
||||||
val isLastOwner: Boolean,
|
val needsOwnerChange: Boolean,
|
||||||
|
val areCreatorsPrivileged: Boolean,
|
||||||
val selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>>,
|
val selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>>,
|
||||||
val leaveSpaceAction: AsyncAction<Unit>,
|
val leaveSpaceAction: AsyncAction<Unit>,
|
||||||
val eventSink: (LeaveSpaceEvents) -> Unit,
|
val eventSink: (LeaveSpaceEvents) -> Unit,
|
||||||
|
|
@ -25,7 +26,7 @@ data class LeaveSpaceState(
|
||||||
private val selectableRooms: ImmutableList<SelectableSpaceRoom>
|
private val selectableRooms: ImmutableList<SelectableSpaceRoom>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val partition = rooms.partition { it.isLastOwner }
|
val partition = rooms.partition { it.isLastOwner && it.joinedMembersCount > 1 }
|
||||||
lastAdminRooms = partition.first.toImmutableList()
|
lastAdminRooms = partition.first.toImmutableList()
|
||||||
selectableRooms = partition.second.toImmutableList()
|
selectableRooms = partition.second.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
@ -33,12 +34,12 @@ data class LeaveSpaceState(
|
||||||
/**
|
/**
|
||||||
* True if we should show the quick action to select/deselect all rooms.
|
* True if we should show the quick action to select/deselect all rooms.
|
||||||
*/
|
*/
|
||||||
val showQuickAction = isLastOwner.not() && selectableRooms.isNotEmpty()
|
val showQuickAction = needsOwnerChange.not() && selectableRooms.isNotEmpty()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if we should show the leave button.
|
* True if we should show the leave button.
|
||||||
*/
|
*/
|
||||||
val showLeaveButton = isLastOwner.not() && selectableSpaceRooms is AsyncData.Success
|
val showLeaveButton = needsOwnerChange.not() && selectableSpaceRooms is AsyncData.Success
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if there all the selectable rooms are selected.
|
* True if there all the selectable rooms are selected.
|
||||||
|
|
|
||||||
|
|
@ -109,17 +109,23 @@ class LeaveSpaceStateProvider : PreviewParameterProvider<LeaveSpaceState> {
|
||||||
aLeaveSpaceState(
|
aLeaveSpaceState(
|
||||||
isLastOwner = true,
|
isLastOwner = true,
|
||||||
),
|
),
|
||||||
|
aLeaveSpaceState(
|
||||||
|
isLastOwner = true,
|
||||||
|
areCreatorsPrivileged = true,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aLeaveSpaceState(
|
fun aLeaveSpaceState(
|
||||||
spaceName: String? = "Space name",
|
spaceName: String? = "Space name",
|
||||||
isLastOwner: Boolean = false,
|
isLastOwner: Boolean = false,
|
||||||
|
areCreatorsPrivileged: Boolean = false,
|
||||||
selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>> = AsyncData.Uninitialized,
|
selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>> = AsyncData.Uninitialized,
|
||||||
leaveSpaceAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
leaveSpaceAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
) = LeaveSpaceState(
|
) = LeaveSpaceState(
|
||||||
spaceName = spaceName,
|
spaceName = spaceName,
|
||||||
isLastOwner = isLastOwner,
|
needsOwnerChange = isLastOwner,
|
||||||
|
areCreatorsPrivileged = areCreatorsPrivileged,
|
||||||
selectableSpaceRooms = selectableSpaceRooms,
|
selectableSpaceRooms = selectableSpaceRooms,
|
||||||
leaveSpaceAction = leaveSpaceAction,
|
leaveSpaceAction = leaveSpaceAction,
|
||||||
eventSink = { }
|
eventSink = { }
|
||||||
|
|
@ -128,9 +134,11 @@ fun aLeaveSpaceState(
|
||||||
fun aSelectableSpaceRoom(
|
fun aSelectableSpaceRoom(
|
||||||
spaceRoom: SpaceRoom = aSpaceRoom(),
|
spaceRoom: SpaceRoom = aSpaceRoom(),
|
||||||
isLastOwner: Boolean = false,
|
isLastOwner: Boolean = false,
|
||||||
|
joinedMembersCount: Int = 2,
|
||||||
isSelected: Boolean = false,
|
isSelected: Boolean = false,
|
||||||
) = SelectableSpaceRoom(
|
) = SelectableSpaceRoom(
|
||||||
spaceRoom = spaceRoom,
|
spaceRoom = spaceRoom,
|
||||||
isLastOwner = isLastOwner,
|
isLastOwner = isLastOwner,
|
||||||
|
joinedMembersCount = joinedMembersCount,
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,13 @@ package io.element.android.features.space.impl.leave
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.imePadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
|
@ -40,6 +39,7 @@ import io.element.android.features.space.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
import io.element.android.libraries.designsystem.components.BigIcon
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
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.AsyncFailure
|
import io.element.android.libraries.designsystem.components.async.AsyncFailure
|
||||||
|
|
@ -54,7 +54,6 @@ import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
|
@ -71,30 +70,42 @@ fun LeaveSpaceView(
|
||||||
state: LeaveSpaceState,
|
state: LeaveSpaceState,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
onRolesAndPermissionsClick: () -> Unit,
|
onRolesAndPermissionsClick: () -> Unit,
|
||||||
|
onChooseOwnersClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
HeaderFooterPage(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
contentPadding = PaddingValues(bottom = 14.dp),
|
||||||
topBar = {
|
topBar = {
|
||||||
LeaveSpaceHeader(
|
TopAppBar(
|
||||||
state = state,
|
navigationIcon = {
|
||||||
onBackClick = onCancel,
|
BackButton(onClick = onCancel)
|
||||||
|
},
|
||||||
|
title = {},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = ElementTheme.colors.bgCanvasDefault,
|
header = {
|
||||||
) { padding ->
|
LeaveSpaceHeader(state = state)
|
||||||
Column(
|
},
|
||||||
modifier = Modifier
|
footer = {
|
||||||
.padding(padding)
|
LeaveSpaceButtons(
|
||||||
.imePadding()
|
showLeaveButton = state.showLeaveButton,
|
||||||
.consumeWindowInsets(padding)
|
selectedRoomsCount = state.selectedRoomsCount,
|
||||||
.fillMaxSize()
|
onLeaveSpace = {
|
||||||
) {
|
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||||
LazyColumn(
|
},
|
||||||
modifier = Modifier
|
onCancel = onCancel,
|
||||||
.weight(1f),
|
showRolesAndPermissionsButton = state.needsOwnerChange && !state.areCreatorsPrivileged,
|
||||||
) {
|
showChooseOwnersButton = state.needsOwnerChange && state.areCreatorsPrivileged,
|
||||||
if (state.isLastOwner.not()) {
|
onChooseOwnersButtonClick = onChooseOwnersClick,
|
||||||
|
onRolesAndPermissionsClick = onRolesAndPermissionsClick,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
if (state.needsOwnerChange.not()) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.padding(top = 20.dp),
|
||||||
|
) {
|
||||||
when (state.selectableSpaceRooms) {
|
when (state.selectableSpaceRooms) {
|
||||||
is AsyncData.Success -> {
|
is AsyncData.Success -> {
|
||||||
// List rooms where the user is the only admin
|
// List rooms where the user is the only admin
|
||||||
|
|
@ -125,18 +136,8 @@ fun LeaveSpaceView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LeaveSpaceButtons(
|
|
||||||
showLeaveButton = state.showLeaveButton,
|
|
||||||
selectedRoomsCount = state.selectedRoomsCount,
|
|
||||||
onLeaveSpace = {
|
|
||||||
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
|
||||||
},
|
|
||||||
onCancel = onCancel,
|
|
||||||
showRolesAndPermissionsButton = state.isLastOwner,
|
|
||||||
onRolesAndPermissionsClick = onRolesAndPermissionsClick,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
AsyncActionView(
|
AsyncActionView(
|
||||||
async = state.leaveSpaceAction,
|
async = state.leaveSpaceAction,
|
||||||
|
|
@ -149,25 +150,27 @@ fun LeaveSpaceView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun LeaveSpaceHeader(
|
private fun LeaveSpaceHeader(
|
||||||
state: LeaveSpaceState,
|
state: LeaveSpaceState,
|
||||||
onBackClick: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
TopAppBar(
|
|
||||||
navigationIcon = {
|
|
||||||
BackButton(onClick = onBackClick)
|
|
||||||
},
|
|
||||||
title = {},
|
|
||||||
)
|
|
||||||
IconTitleSubtitleMolecule(
|
IconTitleSubtitleMolecule(
|
||||||
modifier = Modifier.padding(top = 0.dp, bottom = 8.dp, start = 24.dp, end = 24.dp),
|
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp, start = 24.dp, end = 24.dp),
|
||||||
iconStyle = BigIcon.Style.AlertSolid,
|
iconStyle = BigIcon.Style.AlertSolid,
|
||||||
title = stringResource(
|
title = if (state.needsOwnerChange) {
|
||||||
if (state.isLastOwner) R.string.screen_leave_space_title_last_admin else R.string.screen_leave_space_title,
|
if (state.areCreatorsPrivileged) {
|
||||||
state.spaceName ?: stringResource(CommonStrings.common_space)
|
stringResource(R.string.screen_leave_space_title_last_owner)
|
||||||
),
|
} else {
|
||||||
|
stringResource(R.string.screen_leave_space_title_last_admin, state.spaceName ?: stringResource(CommonStrings.common_space))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.screen_leave_space_title, state.spaceName ?: stringResource(CommonStrings.common_space))
|
||||||
|
},
|
||||||
subTitle =
|
subTitle =
|
||||||
if (state.isLastOwner) {
|
if (state.needsOwnerChange) {
|
||||||
stringResource(R.string.screen_leave_space_subtitle_last_admin)
|
if (state.areCreatorsPrivileged) {
|
||||||
|
stringResource(R.string.screen_leave_space_subtitle_last_owner, state.spaceName ?: stringResource(CommonStrings.common_space))
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.screen_leave_space_subtitle_last_admin)
|
||||||
|
}
|
||||||
} else if (state.selectableSpaceRooms is AsyncData.Success && state.selectableSpaceRooms.data.isNotEmpty()) {
|
} else if (state.selectableSpaceRooms is AsyncData.Success && state.selectableSpaceRooms.data.isNotEmpty()) {
|
||||||
if (state.hasOnlyLastAdminRoom) {
|
if (state.hasOnlyLastAdminRoom) {
|
||||||
stringResource(R.string.screen_leave_space_subtitle_only_last_admin)
|
stringResource(R.string.screen_leave_space_subtitle_only_last_admin)
|
||||||
|
|
@ -216,10 +219,12 @@ private fun LeaveSpaceButtons(
|
||||||
onLeaveSpace: () -> Unit,
|
onLeaveSpace: () -> Unit,
|
||||||
showRolesAndPermissionsButton: Boolean,
|
showRolesAndPermissionsButton: Boolean,
|
||||||
onRolesAndPermissionsClick: () -> Unit,
|
onRolesAndPermissionsClick: () -> Unit,
|
||||||
|
showChooseOwnersButton: Boolean,
|
||||||
|
onChooseOwnersButtonClick: () -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ButtonColumnMolecule(
|
ButtonColumnMolecule(
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(top = 16.dp)
|
||||||
) {
|
) {
|
||||||
if (showLeaveButton) {
|
if (showLeaveButton) {
|
||||||
val text = if (selectedRoomsCount > 0) {
|
val text = if (selectedRoomsCount > 0) {
|
||||||
|
|
@ -243,6 +248,14 @@ private fun LeaveSpaceButtons(
|
||||||
leadingIcon = IconSource.Vector(CompoundIcons.Settings()),
|
leadingIcon = IconSource.Vector(CompoundIcons.Settings()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (showChooseOwnersButton) {
|
||||||
|
Button(
|
||||||
|
text = stringResource(R.string.screen_leave_space_choose_owners_action),
|
||||||
|
onClick = onChooseOwnersButtonClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
destructive = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(CommonStrings.action_cancel),
|
text = stringResource(CommonStrings.action_cancel),
|
||||||
|
|
@ -262,6 +275,7 @@ private fun SpaceItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = 66.dp)
|
.heightIn(min = 66.dp)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
.toggleable(
|
.toggleable(
|
||||||
value = selectableSpaceRoom.isSelected,
|
value = selectableSpaceRoom.isSelected,
|
||||||
role = Role.Checkbox,
|
role = Role.Checkbox,
|
||||||
|
|
@ -276,9 +290,9 @@ private fun SpaceItem(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
avatarData = room.getAvatarData(AvatarSize.LeaveSpaceRoom),
|
avatarData = room.getAvatarData(AvatarSize.LeaveSpaceRoom),
|
||||||
avatarType = if (room.isSpace) AvatarType.Space() else AvatarType.Room(),
|
avatarType = if (room.isSpace) AvatarType.Space() else AvatarType.Room(),
|
||||||
)
|
)
|
||||||
|
|
@ -358,5 +372,6 @@ internal fun LeaveSpaceViewPreview(
|
||||||
state = state,
|
state = state,
|
||||||
onCancel = {},
|
onCancel = {},
|
||||||
onRolesAndPermissionsClick = {},
|
onRolesAndPermissionsClick = {},
|
||||||
|
onChooseOwnersClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,6 @@ import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
data class SelectableSpaceRoom(
|
data class SelectableSpaceRoom(
|
||||||
val spaceRoom: SpaceRoom,
|
val spaceRoom: SpaceRoom,
|
||||||
val isLastOwner: Boolean,
|
val isLastOwner: Boolean,
|
||||||
|
val joinedMembersCount: Int,
|
||||||
val isSelected: Boolean,
|
val isSelected: Boolean,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_leave_space_choose_owners_action">"Choose owners"</string>
|
||||||
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
||||||
<plurals name="screen_leave_space_submit">
|
<plurals name="screen_leave_space_submit">
|
||||||
<item quantity="one">"Leave %1$d room and space"</item>
|
<item quantity="one">"Leave %1$d room and space"</item>
|
||||||
|
|
@ -7,9 +8,11 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_leave_space_subtitle">"Select the rooms you’d like to leave which you\'re not the only administrator for:"</string>
|
<string name="screen_leave_space_subtitle">"Select the rooms you’d like to leave which you\'re not the only administrator for:"</string>
|
||||||
<string name="screen_leave_space_subtitle_last_admin">"You need to assign another admin for this space before you can leave."</string>
|
<string name="screen_leave_space_subtitle_last_admin">"You need to assign another admin for this space before you can leave."</string>
|
||||||
|
<string name="screen_leave_space_subtitle_last_owner">"You are the only owner of %1$s. You need to transfer ownership to someone else before you leave."</string>
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"You will not be removed from the following room(s) because you\'re the only administrator:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"You will not be removed from the following room(s) because you\'re the only administrator:"</string>
|
||||||
<string name="screen_leave_space_title">"Leave %1$s?"</string>
|
<string name="screen_leave_space_title">"Leave %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"You are the only admin for %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"You are the only admin for %1$s"</string>
|
||||||
|
<string name="screen_leave_space_title_last_owner">"Transfer ownership"</string>
|
||||||
<string name="screen_space_add_room_action">"Room"</string>
|
<string name="screen_space_add_room_action">"Room"</string>
|
||||||
<string name="screen_space_add_rooms_room_access_description">"Adding a room will not affect the room access. To change the access go to Room settings > Security & privacy."</string>
|
<string name="screen_space_add_rooms_room_access_description">"Adding a room will not affect the room access. To change the access go to Room settings > Security & privacy."</string>
|
||||||
<string name="screen_space_empty_state_title">"Add your first room"</string>
|
<string name="screen_space_empty_state_title">"Add your first room"</string>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.changeroommemberroles.test.FakeChangeRoomMemberRolesEntryPoint
|
||||||
import io.element.android.features.createroom.api.FakeCreateRoomEntryPoint
|
import io.element.android.features.createroom.api.FakeCreateRoomEntryPoint
|
||||||
import io.element.android.features.space.api.SpaceEntryPoint
|
import io.element.android.features.space.api.SpaceEntryPoint
|
||||||
import io.element.android.features.space.impl.di.FakeSpaceFlowGraph
|
import io.element.android.features.space.impl.di.FakeSpaceFlowGraph
|
||||||
|
|
@ -22,6 +23,7 @@ import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.node.TestParentNode
|
import io.element.android.tests.testutils.node.TestParentNode
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -33,7 +35,7 @@ class DefaultSpaceEntryPointTest {
|
||||||
val mainDispatcherRule = MainDispatcherRule()
|
val mainDispatcherRule = MainDispatcherRule()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test node builder`() {
|
fun `test node builder`() = runTest {
|
||||||
val entryPoint = DefaultSpaceEntryPoint()
|
val entryPoint = DefaultSpaceEntryPoint()
|
||||||
val nodeInputs = SpaceEntryPoint.Inputs(A_ROOM_ID)
|
val nodeInputs = SpaceEntryPoint.Inputs(A_ROOM_ID)
|
||||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||||
|
|
@ -46,6 +48,8 @@ class DefaultSpaceEntryPointTest {
|
||||||
room = FakeJoinedRoom(),
|
room = FakeJoinedRoom(),
|
||||||
graphFactory = FakeSpaceFlowGraph.Factory,
|
graphFactory = FakeSpaceFlowGraph.Factory,
|
||||||
createRoomEntryPoint = FakeCreateRoomEntryPoint(),
|
createRoomEntryPoint = FakeCreateRoomEntryPoint(),
|
||||||
|
changeRoomMemberRolesEntryPoint = FakeChangeRoomMemberRolesEntryPoint(),
|
||||||
|
sessionCoroutineScope = backgroundScope,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val callback = object : SpaceEntryPoint.Callback {
|
val callback = object : SpaceEntryPoint.Callback {
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,6 @@ import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class LeaveSpacePresenterTest {
|
class LeaveSpacePresenterTest {
|
||||||
private val aSpace = aSpaceRoom(
|
|
||||||
roomId = A_SPACE_ID,
|
|
||||||
displayName = A_SPACE_NAME,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = createLeaveSpacePresenter(
|
val presenter = createLeaveSpacePresenter(
|
||||||
|
|
@ -44,7 +39,7 @@ class LeaveSpacePresenterTest {
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
assertThat(state.spaceName).isNull()
|
assertThat(state.spaceName).isNull()
|
||||||
assertThat(state.isLastOwner).isFalse()
|
assertThat(state.needsOwnerChange).isFalse()
|
||||||
assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
|
assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
|
||||||
assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
|
@ -87,7 +82,7 @@ class LeaveSpacePresenterTest {
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.spaceName).isEqualTo(A_SPACE_NAME)
|
assertThat(finalState.spaceName).isEqualTo(A_SPACE_NAME)
|
||||||
assertThat(finalState.isLastOwner).isTrue()
|
assertThat(finalState.needsOwnerChange).isTrue()
|
||||||
// The current state is not in the sub room list
|
// The current state is not in the sub room list
|
||||||
assertThat(finalState.selectableSpaceRooms.dataOrNull()!!).isEmpty()
|
assertThat(finalState.selectableSpaceRooms.dataOrNull()!!).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
@ -145,8 +140,8 @@ class LeaveSpacePresenterTest {
|
||||||
roomsResult = {
|
roomsResult = {
|
||||||
Result.success(
|
Result.success(
|
||||||
listOf(
|
listOf(
|
||||||
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID), isLastOwner = false),
|
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID), isLastOwner = false, areCreatorsPrivileged = false),
|
||||||
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID_2), isLastOwner = true),
|
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID_2), isLastOwner = true, areCreatorsPrivileged = false),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -157,7 +152,7 @@ class LeaveSpacePresenterTest {
|
||||||
skipItems(3)
|
skipItems(3)
|
||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
assertThat(state.spaceName).isNull()
|
assertThat(state.spaceName).isNull()
|
||||||
assertThat(state.isLastOwner).isFalse()
|
assertThat(state.needsOwnerChange).isFalse()
|
||||||
val data = state.selectableSpaceRooms.dataOrNull()!!
|
val data = state.selectableSpaceRooms.dataOrNull()!!
|
||||||
assertThat(data.size).isEqualTo(2)
|
assertThat(data.size).isEqualTo(2)
|
||||||
// Only one room is selectable as the user is the last admin in the other one
|
// Only one room is selectable as the user is the last admin in the other one
|
||||||
|
|
@ -232,6 +227,20 @@ class LeaveSpacePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - needsOwnerChange is false if user is the last joined member`() = runTest {
|
||||||
|
val presenter = createLeaveSpacePresenter(
|
||||||
|
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||||
|
roomsResult = { Result.success(listOf(aLeaveSpaceRoom(spaceRoom = aSpaceRoom(numJoinedMembers = 1), isLastOwner = true))) },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(3)
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat(state.needsOwnerChange).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createLeaveSpacePresenter(
|
private fun createLeaveSpacePresenter(
|
||||||
leaveSpaceHandle: LeaveSpaceHandle = FakeLeaveSpaceHandle(),
|
leaveSpaceHandle: LeaveSpaceHandle = FakeLeaveSpaceHandle(),
|
||||||
): LeaveSpacePresenter {
|
): LeaveSpacePresenter {
|
||||||
|
|
@ -241,13 +250,18 @@ class LeaveSpacePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val aSpace = aSpaceRoom(
|
||||||
|
roomId = A_SPACE_ID,
|
||||||
|
displayName = A_SPACE_NAME,
|
||||||
|
numJoinedMembers = 2,
|
||||||
|
)
|
||||||
|
|
||||||
private fun aLeaveSpaceRoom(
|
private fun aLeaveSpaceRoom(
|
||||||
spaceRoom: SpaceRoom = aSpaceRoom(
|
spaceRoom: SpaceRoom = aSpace,
|
||||||
roomId = A_SPACE_ID,
|
|
||||||
displayName = A_SPACE_NAME,
|
|
||||||
),
|
|
||||||
isLastOwner: Boolean = false,
|
isLastOwner: Boolean = false,
|
||||||
|
areCreatorsPrivileged: Boolean = false,
|
||||||
) = LeaveSpaceRoom(
|
) = LeaveSpaceRoom(
|
||||||
spaceRoom = spaceRoom,
|
spaceRoom = spaceRoom,
|
||||||
isLastOwner = isLastOwner,
|
isLastOwner = isLastOwner,
|
||||||
|
areCreatorsPrivileged = areCreatorsPrivileged,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,5 @@ package io.element.android.libraries.matrix.api.spaces
|
||||||
data class LeaveSpaceRoom(
|
data class LeaveSpaceRoom(
|
||||||
val spaceRoom: SpaceRoom,
|
val spaceRoom: SpaceRoom,
|
||||||
val isLastOwner: Boolean,
|
val isLastOwner: Boolean,
|
||||||
|
val areCreatorsPrivileged: Boolean,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ class RustLeaveSpaceHandle(
|
||||||
LeaveSpaceRoom(
|
LeaveSpaceRoom(
|
||||||
spaceRoom = spaceRoomMapper.map(leaveSpaceRoom.spaceRoom),
|
spaceRoom = spaceRoomMapper.map(leaveSpaceRoom.spaceRoom),
|
||||||
isLastOwner = leaveSpaceRoom.isLastOwner,
|
isLastOwner = leaveSpaceRoom.isLastOwner,
|
||||||
|
areCreatorsPrivileged = leaveSpaceRoom.areCreatorsPrivileged,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:51d8d0648c4fb58b7dc75ecc60d59a0a7abb5d79aee5f711f163b05f8188af87
|
oid sha256:6a02edc97a1019920c4efa226b3f328ce12c9eb51c7ad29b524354a40d65c0ca
|
||||||
size 13970
|
size 14066
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:420a0602c83e35fc352b2357ac113b61b9c2ea6e4752b1ac75ac5c9f470466d6
|
||||||
|
size 31569
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8f39bbc5e4ca32400555a3c934e18ca195f7d0c3d5d1e69753956a921e9ab017
|
oid sha256:a65ebc58159ab95a2e8b9e7aedb1ce1113b26117c335e6a79c5944d78012d594
|
||||||
size 15873
|
size 15857
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:890519f50978232445ad53ff654ae53ee06a9811c4f93903c9b28edf4b922c04
|
oid sha256:33f4f4c25d57bd0b71fbeb7f983c206c27932a3fdd0a2f8baac4a5d05ae7cd0e
|
||||||
size 44357
|
size 44267
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6e494e7ad96ca581fc24109d30eb45dc94ffc8f00a65e5c437958a723f50a5b3
|
oid sha256:67ce94b2bbe6394230480a65cd57cfa8025052525ec0a477c49714a5b95b046e
|
||||||
size 44247
|
size 44688
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:def5cf57cdc01aecc58da2dc78a693d328db67ad1c67a80fcedcdad00ee658be
|
oid sha256:f6fb03a992094eec5a88ec201f64e7773eebdea831c6eb75aa35e7b543386443
|
||||||
size 35911
|
size 36262
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:be2282d9efed268e05ec6ab9f54b1f24260b4adc5ebc95ffd942bc30aeb075c7
|
oid sha256:7b4967b0a0dc144e43a9a22d62affe132f5000d0fc312537445c90d32a3e6146
|
||||||
size 42642
|
size 42936
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:27d182389507043106053e120a8fb9f6dff6dd525bbb1da0043b74de84cd8309
|
oid sha256:faffcd04f9cac0d4c3ce035c22851066c4d8ea90a547ab3014106256dde95bf1
|
||||||
size 42196
|
size 39677
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:db70500bf818299ebe264a9ce3e600107b0b0d3ad4588c7ddb384b308b0a3f0e
|
oid sha256:031c8b3ceef13ebbadbcd11a31e122e3862b105c3f9f2707664b4cd84b113644
|
||||||
size 40173
|
size 37240
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:969272a4544df5be09e42dadc3ae18b2526ec203947a74b5505c194a8c545351
|
oid sha256:4deec322ce73460029b43e7f76d5ae1b2e1b9b038192367648b5e6dfb8bf9cd9
|
||||||
size 16560
|
size 16761
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:08e629078ddc8fe761305d7e3b1ce22344187a9b6bf4e729b85cfc6c59d2fde2
|
oid sha256:a5eb3f2d8e7a508ed1a6ea894915181180db2442cf6460c8a5a325bd5190ddf9
|
||||||
size 33271
|
size 33648
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c3f33575c7ad3ba99f4e571b6e9810b48c79a23d462b7a2cb0a305f7f7f32307
|
oid sha256:6fd460c22bc9822d0c85b3826753f5eb39670c98688d77890131be97b3b55229
|
||||||
size 13919
|
size 13942
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6e9f06efd39f5e4ea6934bb9509fee07efe7ff77f83e993bfc31aeee8641a55a
|
||||||
|
size 30894
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b27e0253ee16c90b0ff9447366702b72b1abb4d19aa01cc521ff35206888c948
|
oid sha256:c7c2812171bf4a27491aa677235ad4d56d6027a919001ebd8b44832d7c811482
|
||||||
size 15386
|
size 15430
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5a3d199d941e84e0d70e18de23375629dedc9b74bc4bfed2d7353fc85f9fc496
|
oid sha256:ceca65b246352957ec37506af8ac82022bfca136bde7f53db89c5756a8cdff21
|
||||||
size 43143
|
size 43211
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f7e276ca863450e76ddbbc10ff6f22fc17c35bd6414a47a1ce33285a3f46b3c8
|
oid sha256:f76d06da0abb8e63a2ab3d16acfbdb28653505e41e710712151918d888d72c2c
|
||||||
size 42957
|
size 43435
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8cc5d0edd3dc205c3af67390a15697c020160294548ee96fb3d89c2a1e8706e0
|
oid sha256:970ec46da6ab6358315bcea7ff7bd611811750c7e4edb437b804cf62bde9a55c
|
||||||
size 34940
|
size 35267
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c66c9ce57c7679b1f2b32c11789fc6050ee5d7ec251d77f35aefe988fd7d850f
|
oid sha256:0a76a195966879b08238d4882de54aaf998d65e689b304cc586fbb0700408bac
|
||||||
size 41627
|
size 42020
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0648f116f14434da9708075d4a225d4d2b9fb8cdb55a61c217a2200ea5c3eb33
|
oid sha256:fd1f1f8e203bcf4e9c3cd4a4c309fe70d94c67f8017188caa6ecc6e4a4ca5e0f
|
||||||
size 40640
|
size 38019
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3cd90931fcccd226921e51efa78c026dfe5ca230b6ee56cf2236c2f4c5e1f7b5
|
oid sha256:3e00026924d002f0838b172f098d7e44f6462f182c32f4b59a1282834ffe020a
|
||||||
size 38134
|
size 35018
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ae93fc35355b1d16fb7be977bc49d2937da7b60f2ed76e2a184756b900afca17
|
oid sha256:95db09b4af9f4dd225d5410d2a67fd9e2e8736c3ba9c803d6433093d0487b30a
|
||||||
size 16435
|
size 16502
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5fc7f07a583f9dedb887ca83c67543e904d7bab092ebf70e06bc5292bf1e95b3
|
oid sha256:dbc6e69ee6acf4ba8c2753ae731fe2114076c530f9a0572f44dee665e15f4096
|
||||||
size 32414
|
size 32901
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue