Add an empty state for the space screen if the user can modify its graph (#6064)
* Add an empty state for the space screen if the user can modify its graph. It adds a new 'create room' button that allows you to open the create room screen with some preset values. * When computing the editable spaces in `ConfigureRoomPresenter`, also set up the initial selected parent space if possible * Use `Builder` pattern for `CreateRoomEntryPoint` * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
fc8afab1bb
commit
6a9d084a50
31 changed files with 248 additions and 111 deletions
|
|
@ -484,7 +484,10 @@ class LoggedInFlowNode(
|
||||||
backstack.replace(NavTarget.Room(roomIdOrAlias = RoomIdOrAlias.Id(roomId), serverNames = emptyList()))
|
backstack.replace(NavTarget.Room(roomIdOrAlias = RoomIdOrAlias.Id(roomId), serverNames = emptyList()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createRoomEntryPoint.createNode(isSpace = true, parentNode = this, buildContext = buildContext, callback = callback)
|
createRoomEntryPoint
|
||||||
|
.builder(parentNode = this, buildContext = buildContext, callback = callback)
|
||||||
|
.setIsSpace(true)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
is NavTarget.SecureBackup -> {
|
is NavTarget.SecureBackup -> {
|
||||||
secureBackupEntryPoint.createNode(
|
secureBackupEntryPoint.createNode(
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,13 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
interface CreateRoomEntryPoint : FeatureEntryPoint {
|
interface CreateRoomEntryPoint : FeatureEntryPoint {
|
||||||
fun createNode(
|
interface Builder {
|
||||||
isSpace: Boolean,
|
fun setIsSpace(isSpace: Boolean): Builder
|
||||||
parentNode: Node,
|
fun setParentSpace(parentSpaceId: RoomId): Builder
|
||||||
buildContext: BuildContext,
|
fun build(): Node
|
||||||
callback: Callback,
|
}
|
||||||
): Node
|
|
||||||
|
fun builder(parentNode: Node, buildContext: BuildContext, callback: Callback): Builder
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onRoomCreated(roomId: RoomId)
|
fun onRoomCreated(roomId: RoomId)
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ class CreateRoomFlowNode(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
) : BaseFlowNode<CreateRoomFlowNode.NavTarget>(
|
) : BaseFlowNode<CreateRoomFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.ConfigureRoom(isSpace = plugins.filterIsInstance<Inputs>().first().isSpace),
|
initialElement = initialElementFromInputs(plugins.filterIsInstance<Inputs>().first()),
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
|
|
@ -46,7 +46,8 @@ class CreateRoomFlowNode(
|
||||||
) {
|
) {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val isSpace: Boolean
|
val isSpace: Boolean,
|
||||||
|
val parentSpaceId: RoomId?,
|
||||||
) : NodeInputs, Parcelable
|
) : NodeInputs, Parcelable
|
||||||
|
|
||||||
private val callback: CreateRoomEntryPoint.Callback = callback()
|
private val callback: CreateRoomEntryPoint.Callback = callback()
|
||||||
|
|
@ -54,7 +55,7 @@ class CreateRoomFlowNode(
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
is NavTarget.ConfigureRoom -> {
|
is NavTarget.ConfigureRoom -> {
|
||||||
val inputs = ConfigureRoomNode.Inputs(isSpace = navTarget.isSpace)
|
val inputs = ConfigureRoomNode.Inputs(isSpace = navTarget.isSpace, parentSpaceId = navTarget.parentSpaceId)
|
||||||
val callback = object : ConfigureRoomNode.Callback {
|
val callback = object : ConfigureRoomNode.Callback {
|
||||||
override fun onCreateRoomSuccess(roomId: RoomId) {
|
override fun onCreateRoomSuccess(roomId: RoomId) {
|
||||||
backstack.replace(NavTarget.AddPeople(roomId))
|
backstack.replace(NavTarget.AddPeople(roomId))
|
||||||
|
|
@ -81,9 +82,14 @@ class CreateRoomFlowNode(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ConfigureRoom(val isSpace: Boolean) : NavTarget
|
data class ConfigureRoom(val isSpace: Boolean, val parentSpaceId: RoomId?) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AddPeople(val roomId: RoomId) : NavTarget
|
data class AddPeople(val roomId: RoomId) : NavTarget
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initialElementFromInputs(inputs: CreateRoomFlowNode.Inputs) = CreateRoomFlowNode.NavTarget.ConfigureRoom(
|
||||||
|
isSpace = inputs.isSpace,
|
||||||
|
parentSpaceId = inputs.parentSpaceId,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,35 @@ import dev.zacsweers.metro.ContributesBinding
|
||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultCreateRoomEntryPoint : CreateRoomEntryPoint {
|
class DefaultCreateRoomEntryPoint : CreateRoomEntryPoint {
|
||||||
override fun createNode(
|
class Builder(
|
||||||
isSpace: Boolean,
|
private val parentNode: Node,
|
||||||
parentNode: Node,
|
private val buildContext: BuildContext,
|
||||||
buildContext: BuildContext,
|
private val callback: CreateRoomEntryPoint.Callback,
|
||||||
callback: CreateRoomEntryPoint.Callback,
|
) : CreateRoomEntryPoint.Builder {
|
||||||
): Node {
|
private var isSpace = false
|
||||||
val inputs = CreateRoomFlowNode.Inputs(isSpace)
|
private var parentSpaceId: RoomId? = null
|
||||||
return parentNode.createNode<CreateRoomFlowNode>(buildContext, listOf(inputs, callback))
|
|
||||||
|
override fun setIsSpace(isSpace: Boolean): Builder {
|
||||||
|
this.isSpace = isSpace
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setParentSpace(parentSpaceId: RoomId): Builder {
|
||||||
|
this.parentSpaceId = parentSpaceId
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun build(): Node {
|
||||||
|
val inputs = CreateRoomFlowNode.Inputs(isSpace = isSpace, parentSpaceId = parentSpaceId)
|
||||||
|
return parentNode.createNode<CreateRoomFlowNode>(buildContext, listOf(inputs, callback))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun builder(parentNode: Node, buildContext: BuildContext, callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.Builder {
|
||||||
|
return Builder(parentNode, buildContext, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,12 @@ class ConfigureRoomNode(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val isSpace: Boolean,
|
val isSpace: Boolean,
|
||||||
|
val parentSpaceId: RoomId?,
|
||||||
) : NodeInputs, Parcelable
|
) : NodeInputs, Parcelable
|
||||||
|
|
||||||
private val inputs = inputs<Inputs>()
|
private val inputs = inputs<Inputs>()
|
||||||
|
|
||||||
private val presenter = presenterFactory.create(inputs.isSpace)
|
private val presenter = presenterFactory.create(inputs.isSpace, inputs.parentSpaceId)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class ConfigureRoomPresenter(
|
class ConfigureRoomPresenter(
|
||||||
@Assisted private val isSpace: Boolean,
|
@Assisted private val isSpace: Boolean,
|
||||||
|
@Assisted private val initialParentSpaceId: RoomId?,
|
||||||
private val dataStore: CreateRoomConfigStore,
|
private val dataStore: CreateRoomConfigStore,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val mediaPickerProvider: PickerProvider,
|
private val mediaPickerProvider: PickerProvider,
|
||||||
|
|
@ -75,7 +76,7 @@ class ConfigureRoomPresenter(
|
||||||
) : Presenter<ConfigureRoomState> {
|
) : Presenter<ConfigureRoomState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(isSpace: Boolean): ConfigureRoomPresenter
|
fun create(isSpace: Boolean, parentSpaceId: RoomId?): ConfigureRoomPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
|
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
|
||||||
|
|
@ -122,6 +123,9 @@ class ConfigureRoomPresenter(
|
||||||
} else {
|
} else {
|
||||||
persistentListOf()
|
persistentListOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val parentSpace = spaces.find { it.roomId == initialParentSpaceId }
|
||||||
|
parentSpace?.let { dataStore.setParentSpace(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(cameraPermissionState.permissionGranted) {
|
LaunchedEffect(cameraPermissionState.permissionGranted) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
<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.
|
||||||
|
|
|
||||||
|
|
@ -532,6 +532,7 @@ class ConfigureRoomPresenterTest {
|
||||||
|
|
||||||
private fun createConfigureRoomPresenter(
|
private fun createConfigureRoomPresenter(
|
||||||
isSpace: Boolean = false,
|
isSpace: Boolean = false,
|
||||||
|
initialParenSpaceId: RoomId? = null,
|
||||||
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
|
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
|
||||||
dataStore: CreateRoomConfigStore = CreateRoomConfigStore(roomAliasHelper),
|
dataStore: CreateRoomConfigStore = CreateRoomConfigStore(roomAliasHelper),
|
||||||
matrixClient: MatrixClient = createMatrixClient(),
|
matrixClient: MatrixClient = createMatrixClient(),
|
||||||
|
|
@ -543,6 +544,7 @@ class ConfigureRoomPresenterTest {
|
||||||
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
||||||
) = ConfigureRoomPresenter(
|
) = ConfigureRoomPresenter(
|
||||||
isSpace = isSpace,
|
isSpace = isSpace,
|
||||||
|
initialParentSpaceId = initialParenSpaceId,
|
||||||
dataStore = dataStore,
|
dataStore = dataStore,
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
mediaPickerProvider = pickerProvider,
|
mediaPickerProvider = pickerProvider,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ 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.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
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 org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -36,15 +37,16 @@ class DefaultCreateRoomEntryPointTest {
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val buildContext = BuildContext.root(null)
|
||||||
|
|
||||||
val callback = object : CreateRoomEntryPoint.Callback {
|
val callback = object : CreateRoomEntryPoint.Callback {
|
||||||
override fun onRoomCreated(roomId: RoomId) = lambdaError()
|
override fun onRoomCreated(roomId: RoomId) = lambdaError()
|
||||||
}
|
}
|
||||||
val result = entryPoint.createNode(
|
val result = entryPoint
|
||||||
isSpace = false,
|
.builder(parentNode, buildContext, callback)
|
||||||
parentNode = parentNode,
|
.setIsSpace(true)
|
||||||
buildContext = BuildContext.root(null),
|
.setParentSpace(A_ROOM_ID)
|
||||||
callback = callback,
|
.build()
|
||||||
)
|
|
||||||
assertThat(result.plugins).contains(callback)
|
assertThat(result.plugins).contains(callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,6 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.features.createroom.api)
|
implementation(projects.features.createroom.api)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.tests.testutils)
|
implementation(projects.tests.testutils)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,19 @@ package io.element.android.features.createroom.api
|
||||||
|
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakeCreateRoomEntryPoint : CreateRoomEntryPoint {
|
class FakeCreateRoomEntryPoint : CreateRoomEntryPoint {
|
||||||
override fun createNode(
|
class Builder : CreateRoomEntryPoint.Builder {
|
||||||
isSpace: Boolean,
|
override fun setIsSpace(isSpace: Boolean): Builder = this
|
||||||
|
override fun setParentSpace(parentSpaceId: RoomId): Builder = this
|
||||||
|
override fun build(): Node = lambdaError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun builder(
|
||||||
parentNode: Node,
|
parentNode: Node,
|
||||||
buildContext: BuildContext,
|
buildContext: BuildContext,
|
||||||
callback: CreateRoomEntryPoint.Callback,
|
callback: CreateRoomEntryPoint.Callback,
|
||||||
): Node = lambdaError()
|
): Builder = lambdaError()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ dependencies {
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
|
implementation(projects.features.createroom.api)
|
||||||
implementation(projects.features.invite.api)
|
implementation(projects.features.invite.api)
|
||||||
implementation(projects.libraries.previewutils)
|
implementation(projects.libraries.previewutils)
|
||||||
implementation(projects.features.securityandprivacy.api)
|
implementation(projects.features.securityandprivacy.api)
|
||||||
|
|
@ -49,5 +50,6 @@ dependencies {
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
|
testImplementation(projects.features.createroom.test)
|
||||||
testImplementation(projects.features.invite.test)
|
testImplementation(projects.features.invite.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
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.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
|
||||||
|
|
@ -49,6 +50,7 @@ class SpaceFlowNode(
|
||||||
room: JoinedRoom,
|
room: JoinedRoom,
|
||||||
spaceService: SpaceService,
|
spaceService: SpaceService,
|
||||||
graphFactory: SpaceFlowGraph.Factory,
|
graphFactory: SpaceFlowGraph.Factory,
|
||||||
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
) : BaseFlowNode<SpaceFlowNode.NavTarget>(
|
) : BaseFlowNode<SpaceFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Root,
|
initialElement = NavTarget.Root,
|
||||||
|
|
@ -71,6 +73,9 @@ class SpaceFlowNode(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Leave : NavTarget
|
data object Leave : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object CreateRoom : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object AddRoom : NavTarget
|
data object AddRoom : NavTarget
|
||||||
}
|
}
|
||||||
|
|
@ -116,6 +121,10 @@ class SpaceFlowNode(
|
||||||
backstack.push(NavTarget.Leave)
|
backstack.push(NavTarget.Leave)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateRoom() {
|
||||||
|
backstack.push(NavTarget.CreateRoom)
|
||||||
|
}
|
||||||
|
|
||||||
override fun navigateToAddRoom() {
|
override fun navigateToAddRoom() {
|
||||||
backstack.push(NavTarget.AddRoom)
|
backstack.push(NavTarget.AddRoom)
|
||||||
}
|
}
|
||||||
|
|
@ -140,6 +149,21 @@ class SpaceFlowNode(
|
||||||
}
|
}
|
||||||
createNode<SpaceSettingsFlowNode>(buildContext, listOf(callback))
|
createNode<SpaceSettingsFlowNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
|
is NavTarget.CreateRoom -> {
|
||||||
|
val callback = object : CreateRoomEntryPoint.Callback {
|
||||||
|
override fun onRoomCreated(roomId: RoomId) {
|
||||||
|
callback.navigateToRoom(roomId, emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createRoomEntryPoint
|
||||||
|
.builder(
|
||||||
|
parentNode = this,
|
||||||
|
buildContext = buildContext,
|
||||||
|
callback = callback,
|
||||||
|
)
|
||||||
|
.setParentSpace(spaceRoomList.roomId)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
NavTarget.AddRoom -> {
|
NavTarget.AddRoom -> {
|
||||||
val callback = object : AddRoomToSpaceNode.Callback {
|
val callback = object : AddRoomToSpaceNode.Callback {
|
||||||
override fun onFinish() {
|
override fun onFinish() {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ class SpaceNode(
|
||||||
fun navigateToRoomMemberList()
|
fun navigateToRoomMemberList()
|
||||||
fun startLeaveSpaceFlow()
|
fun startLeaveSpaceFlow()
|
||||||
fun navigateToAddRoom()
|
fun navigateToAddRoom()
|
||||||
|
|
||||||
|
fun onCreateRoom()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
|
|
@ -105,6 +107,7 @@ class SpaceNode(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onCreateRoomClick = callback::onCreateRoom,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
|
||||||
spaceInfo = aSpaceInfo(),
|
spaceInfo = aSpaceInfo(),
|
||||||
children = aListOfSpaceRooms(),
|
children = aListOfSpaceRooms(),
|
||||||
joiningRooms = setOf(RoomId("!spaceId0:example.com")),
|
joiningRooms = setOf(RoomId("!spaceId0:example.com")),
|
||||||
hasMoreToLoad = false
|
hasMoreToLoad = true,
|
||||||
),
|
),
|
||||||
aSpaceState(
|
aSpaceState(
|
||||||
topicViewerState = TopicViewerState.Shown(topic = "Space description goes here." + LoremIpsum(20).values.first()),
|
topicViewerState = TopicViewerState.Shown(topic = "Space description goes here." + LoremIpsum(20).values.first()),
|
||||||
|
|
@ -71,7 +71,7 @@ fun aSpaceState(
|
||||||
joiningRooms: Set<RoomId> = emptySet(),
|
joiningRooms: Set<RoomId> = emptySet(),
|
||||||
joinActions: Map<RoomId, AsyncAction<Unit>> = joiningRooms.associateWith { AsyncAction.Loading },
|
joinActions: Map<RoomId, AsyncAction<Unit>> = joiningRooms.associateWith { AsyncAction.Loading },
|
||||||
hideInvitesAvatar: Boolean = false,
|
hideInvitesAvatar: Boolean = false,
|
||||||
hasMoreToLoad: Boolean = true,
|
hasMoreToLoad: Boolean = false,
|
||||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||||
topicViewerState: TopicViewerState = TopicViewerState.Hidden,
|
topicViewerState: TopicViewerState = TopicViewerState.Hidden,
|
||||||
canAccessSpaceSettings: Boolean = true,
|
canAccessSpaceSettings: Boolean = true,
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,9 @@ import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.space.impl.R
|
import io.element.android.features.space.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
|
||||||
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||||
import io.element.android.libraries.designsystem.components.SimpleModalBottomSheet
|
import io.element.android.libraries.designsystem.components.SimpleModalBottomSheet
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
|
|
@ -65,6 +67,7 @@ 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.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.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.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
|
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
|
||||||
|
|
@ -72,6 +75,7 @@ import io.element.android.libraries.designsystem.theme.components.DropdownMenuIt
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
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.IconButton
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
|
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.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
|
||||||
|
|
@ -98,6 +102,7 @@ fun SpaceView(
|
||||||
onLeaveSpaceClick: () -> Unit,
|
onLeaveSpaceClick: () -> Unit,
|
||||||
onSettingsClick: () -> Unit,
|
onSettingsClick: () -> Unit,
|
||||||
onViewMembersClick: () -> Unit,
|
onViewMembersClick: () -> Unit,
|
||||||
|
onCreateRoomClick: () -> Unit,
|
||||||
onAddRoomClick: () -> Unit,
|
onAddRoomClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
acceptDeclineInviteView: @Composable () -> Unit,
|
acceptDeclineInviteView: @Composable () -> Unit,
|
||||||
|
|
@ -161,7 +166,8 @@ fun SpaceView(
|
||||||
},
|
},
|
||||||
onTopicClick = { topic ->
|
onTopicClick = { topic ->
|
||||||
state.eventSink(SpaceEvents.ShowTopicViewer(topic))
|
state.eventSink(SpaceEvents.ShowTopicViewer(topic))
|
||||||
}
|
},
|
||||||
|
onCreateRoomClick = onCreateRoomClick,
|
||||||
)
|
)
|
||||||
JoinFailuresEffect(
|
JoinFailuresEffect(
|
||||||
hasAnyFailure = state.hasAnyJoinFailures,
|
hasAnyFailure = state.hasAnyJoinFailures,
|
||||||
|
|
@ -234,6 +240,7 @@ private fun SpaceViewContent(
|
||||||
state: SpaceState,
|
state: SpaceState,
|
||||||
onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
|
onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
|
||||||
onTopicClick: (String) -> Unit,
|
onTopicClick: (String) -> Unit,
|
||||||
|
onCreateRoomClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LazyColumn(modifier.fillMaxSize()) {
|
LazyColumn(modifier.fillMaxSize()) {
|
||||||
|
|
@ -259,61 +266,90 @@ private fun SpaceViewContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
itemsIndexed(
|
|
||||||
items = state.children,
|
if (state.children.isEmpty() && state.canEditSpaceGraph && !state.hasMoreToLoad) {
|
||||||
key = { _, spaceRoom -> spaceRoom.roomId }
|
item {
|
||||||
) { index, spaceRoom ->
|
EmptySpaceView(onCreateRoomClick = onCreateRoomClick)
|
||||||
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
}
|
||||||
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
|
} else {
|
||||||
val isSelected = state.isSelected(spaceRoom.roomId)
|
itemsIndexed(
|
||||||
val showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites && !state.isManageMode
|
items = state.children,
|
||||||
SpaceRoomItemView(
|
key = { _, spaceRoom -> spaceRoom.roomId }
|
||||||
spaceRoom = spaceRoom,
|
) { index, spaceRoom ->
|
||||||
showUnreadIndicator = showUnreadIndicator,
|
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
||||||
hideAvatars = isInvitation && state.hideInvitesAvatar,
|
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
|
||||||
onClick = {
|
val isSelected = state.isSelected(spaceRoom.roomId)
|
||||||
onRoomClick(spaceRoom)
|
val showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites && !state.isManageMode
|
||||||
},
|
SpaceRoomItemView(
|
||||||
onLongClick = {
|
spaceRoom = spaceRoom,
|
||||||
// TODO
|
showUnreadIndicator = showUnreadIndicator,
|
||||||
},
|
hideAvatars = isInvitation && state.hideInvitesAvatar,
|
||||||
trailingAction = if (state.isManageMode) {
|
onClick = {
|
||||||
{
|
onRoomClick(spaceRoom)
|
||||||
Checkbox(
|
},
|
||||||
checked = isSelected,
|
onLongClick = {
|
||||||
onCheckedChange = null,
|
// TODO
|
||||||
|
},
|
||||||
|
trailingAction = if (state.isManageMode) {
|
||||||
|
{
|
||||||
|
Checkbox(
|
||||||
|
checked = isSelected,
|
||||||
|
onCheckedChange = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
|
||||||
|
state.eventSink(SpaceEvents.Join(spaceRoom))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomAction = if (state.isManageMode) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
spaceRoom.inviteButtons(
|
||||||
|
onAcceptClick = {
|
||||||
|
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
|
||||||
|
},
|
||||||
|
onDeclineClick = {
|
||||||
|
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
)
|
||||||
spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
|
if (index != state.children.lastIndex) {
|
||||||
state.eventSink(SpaceEvents.Join(spaceRoom))
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
bottomAction = if (state.isManageMode) {
|
|
||||||
null
|
if (state.hasMoreToLoad) {
|
||||||
} else {
|
item {
|
||||||
spaceRoom.inviteButtons(
|
LoadingMoreIndicator(eventSink = state.eventSink)
|
||||||
onAcceptClick = {
|
|
||||||
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
|
|
||||||
},
|
|
||||||
onDeclineClick = {
|
|
||||||
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
if (index != state.children.lastIndex) {
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state.hasMoreToLoad) {
|
|
||||||
item {
|
|
||||||
LoadingMoreIndicator(eventSink = state.eventSink)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptySpaceView(onCreateRoomClick: () -> Unit) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.padding(bottom = 24.dp),
|
||||||
|
) {
|
||||||
|
IconTitleSubtitleMolecule(
|
||||||
|
title = stringResource(R.string.screen_space_empty_state_title),
|
||||||
|
subTitle = null,
|
||||||
|
iconStyle = BigIcon.Style.Default(CompoundIcons.Room()),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.padding(top = 40.dp, start = 24.dp, end = 24.dp, bottom = 24.dp),
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
text = stringResource(R.string.screen_space_add_room_action),
|
||||||
|
leadingIcon = IconSource.Vector(CompoundIcons.Plus()),
|
||||||
|
onClick = onCreateRoomClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LoadingMoreIndicator(
|
private fun LoadingMoreIndicator(
|
||||||
eventSink: (SpaceEvents) -> Unit,
|
eventSink: (SpaceEvents) -> Unit,
|
||||||
|
|
@ -611,6 +647,7 @@ internal fun SpaceViewPreview(
|
||||||
acceptDeclineInviteView = {},
|
acceptDeclineInviteView = {},
|
||||||
onSettingsClick = {},
|
onSettingsClick = {},
|
||||||
onViewMembersClick = {},
|
onViewMembersClick = {},
|
||||||
|
onCreateRoomClick = {},
|
||||||
onAddRoomClick = {},
|
onAddRoomClick = {},
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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.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
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -43,7 +44,8 @@ class DefaultSpaceEntryPointTest {
|
||||||
spaceRoomListResult = { _: RoomId -> FakeSpaceRoomList(A_ROOM_ID) }
|
spaceRoomListResult = { _: RoomId -> FakeSpaceRoomList(A_ROOM_ID) }
|
||||||
),
|
),
|
||||||
room = FakeJoinedRoom(),
|
room = FakeJoinedRoom(),
|
||||||
graphFactory = FakeSpaceFlowGraph.Factory
|
graphFactory = FakeSpaceFlowGraph.Factory,
|
||||||
|
createRoomEntryPoint = FakeCreateRoomEntryPoint(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val callback = object : SpaceEntryPoint.Callback {
|
val callback = object : SpaceEntryPoint.Callback {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.space.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
|
|
@ -30,6 +31,7 @@ import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.pressBack
|
import io.element.android.tests.testutils.pressBack
|
||||||
import io.element.android.tests.testutils.pressBackKey
|
import io.element.android.tests.testutils.pressBackKey
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -200,6 +202,22 @@ class SpaceViewTest {
|
||||||
rule.clickOn(CommonStrings.action_remove, inDialog = true)
|
rule.clickOn(CommonStrings.action_remove, inDialog = true)
|
||||||
eventsRecorder.assertSingle(SpaceEvents.ConfirmRoomRemoval)
|
eventsRecorder.assertSingle(SpaceEvents.ConfirmRoomRemoval)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking create room button calls the expected callback`() {
|
||||||
|
val onCreateRoomClick = lambdaRecorder<Unit> { }
|
||||||
|
rule.setSpaceView(
|
||||||
|
aSpaceState(
|
||||||
|
children = emptyList(),
|
||||||
|
hasMoreToLoad = false,
|
||||||
|
isManageMode = true,
|
||||||
|
canManageRooms = true,
|
||||||
|
),
|
||||||
|
onCreateRoomClick = onCreateRoomClick,
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_space_add_room_action)
|
||||||
|
onCreateRoomClick.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceView(
|
||||||
|
|
@ -210,6 +228,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
|
||||||
onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(),
|
onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(),
|
||||||
onSettingsClick: () -> Unit = EnsureNeverCalled(),
|
onSettingsClick: () -> Unit = EnsureNeverCalled(),
|
||||||
onViewMembersClick: () -> Unit = EnsureNeverCalled(),
|
onViewMembersClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onCreateRoomClick: () -> Unit = EnsureNeverCalled(),
|
||||||
onAddRoomClick: () -> Unit = EnsureNeverCalled(),
|
onAddRoomClick: () -> Unit = EnsureNeverCalled(),
|
||||||
acceptDeclineInviteView: @Composable () -> Unit = {},
|
acceptDeclineInviteView: @Composable () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
|
@ -224,6 +243,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
|
||||||
onViewMembersClick = onViewMembersClick,
|
onViewMembersClick = onViewMembersClick,
|
||||||
onAddRoomClick = onAddRoomClick,
|
onAddRoomClick = onAddRoomClick,
|
||||||
acceptDeclineInviteView = acceptDeclineInviteView,
|
acceptDeclineInviteView = acceptDeclineInviteView,
|
||||||
|
onCreateRoomClick = onCreateRoomClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,12 +80,14 @@ class StartChatFlowNode(
|
||||||
navigator.onRoomCreated(roomId.toRoomIdOrAlias(), emptyList())
|
navigator.onRoomCreated(roomId.toRoomIdOrAlias(), emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createRoomEntryPoint.createNode(
|
createRoomEntryPoint
|
||||||
isSpace = false,
|
.builder(
|
||||||
parentNode = this,
|
parentNode = this,
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
callback = callback,
|
callback = callback,
|
||||||
)
|
)
|
||||||
|
.setIsSpace(false)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
NavTarget.JoinByAddress -> {
|
NavTarget.JoinByAddress -> {
|
||||||
createNode<JoinRoomByAddressNode>(buildContext = buildContext, plugins = listOf(navigator))
|
createNode<JoinRoomByAddressNode>(buildContext = buildContext, plugins = listOf(navigator))
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e59a9e2ae6ef36f28e61534b5639314cc840953df51bb1660e77e8d565865357
|
oid sha256:6444cece1e3f5dfc94591380837fb8aa8caecdc4a87881485278c036accf2007
|
||||||
size 32998
|
size 40411
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b854ce2b0618ebcbc88eff9952d6869bceeb8b43f9eccb8f8e5feef225e0a4c2
|
oid sha256:70e98028aafc62aeb86e69db82dce9b9bb0250d47339a81e8e832a0600d21217
|
||||||
size 33181
|
size 40588
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e1cf063ee5c9fbc50a53445050780ba239d4fb0fd1e9903a578eab8dd3bfc257
|
oid sha256:a6a950c626b57f2624ce8929f61dfa2fcdc25036e0b3ebb5ec87a00e7531aea8
|
||||||
size 33496
|
size 40898
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:525059397001897f705630b8ac5a661439a502f2e623fdca252f9e86f97133e4
|
oid sha256:778488da7192f2c3f98367d9d424783a42343a4bff8b7616f6175952526a8d30
|
||||||
size 58181
|
size 57766
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:59af097a2152b235c7df83aa76eea880ceef65edebebecb86f00745ec39712f4
|
oid sha256:dbe04b9b4274183626f94d7014df9fc6462114226670161ffa3bb94b214b027e
|
||||||
size 35937
|
size 34283
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a5151bbd3effcd6a46724a0cfb13730e845a3b8ba489826146c76d0a797403e0
|
oid sha256:b86c4e7011f15079e01e7aa8e8c26b94cffb4ef451b3374d3834641336d3d036
|
||||||
size 36502
|
size 34853
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:47c8bdf4d153ecebe749ae3cacfe4c9a1c59ac836106fb0903dca80305aedac2
|
oid sha256:9b221b06e7533c21ed4393f388630c7119a1e7087b3a33de6c2cf498b01bbe65
|
||||||
size 32412
|
size 39544
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:33917297cb1c6d38e5b955757de50f4ff6b73844bb87e8e88d0885daa01b266b
|
oid sha256:dd9b3d59af7fc710b7ef8f16ed961086693aa685de93658e2df85e3edbd7f787
|
||||||
size 32556
|
size 39689
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:930f763051533ea3aa4032e483e15e0c1ccb1d213d2e59bebe7f934517b500ba
|
oid sha256:db27594444024da1818bbb089fac602b0937ad786072ad3542aa822e5a29c8c4
|
||||||
size 32868
|
size 40008
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:dd673c2cf628285836848a732d3972a17421a2394464a3a77e7a49e4c5a862f1
|
oid sha256:21ec87536d892719f5343c0aacde2ed4be1a7d0987eff72675b03b4be5038f78
|
||||||
size 56636
|
size 56278
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b459fa8aa195dcf85995ac51c2f079e9d2505efede138bcef94c0a5049e1f271
|
oid sha256:8c01095702c3ab147349f9b60729b534ec1763bc5c04e43b6d571a52a115e10a
|
||||||
size 35266
|
size 33634
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:36fd123fcff07169723857c99e4e375fe9bbb8193b2a6e97508343ea025d5787
|
oid sha256:ae1a096a6de24c2c6517455cd632291ad9d3d45e16df02fa75c33f278e44b26e
|
||||||
size 35782
|
size 34149
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue