Improve TextFieldDialog (#4512)
* Extract TextFieldDialog to its own file (no other change). * Add TextFieldDialogPreview Enhance TextFieldDialog * Let RoomMembersModerationView use TextFieldDialog * Update screenshots * Konsist. * Add modifier parameter. --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
f0cc6e315e
commit
c65b54d3b7
35 changed files with 291 additions and 264 deletions
|
|
@ -1,14 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members.moderation
|
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
|
|
||||||
data class ConfirmingWithReason(
|
|
||||||
val reason: String,
|
|
||||||
) : AsyncAction.Confirming
|
|
||||||
|
|
@ -12,8 +12,10 @@ import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
|
||||||
sealed interface RoomMembersModerationEvents {
|
sealed interface RoomMembersModerationEvents {
|
||||||
data class SelectRoomMember(val roomMember: RoomMember) : RoomMembersModerationEvents
|
data class SelectRoomMember(val roomMember: RoomMember) : RoomMembersModerationEvents
|
||||||
data class KickUser(val reason: String, val needsConfirmation: Boolean) : RoomMembersModerationEvents
|
data object KickUser : RoomMembersModerationEvents
|
||||||
data class BanUser(val reason: String, val needsConfirmation: Boolean) : RoomMembersModerationEvents
|
data class DoKickUser(val reason: String) : RoomMembersModerationEvents
|
||||||
|
data object BanUser : RoomMembersModerationEvents
|
||||||
|
data class DoBanUser(val reason: String) : RoomMembersModerationEvents
|
||||||
data class UnbanUser(val userId: UserId) : RoomMembersModerationEvents
|
data class UnbanUser(val userId: UserId) : RoomMembersModerationEvents
|
||||||
data object Reset : RoomMembersModerationEvents
|
data object Reset : RoomMembersModerationEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,24 +96,22 @@ class RoomMembersModerationPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is RoomMembersModerationEvents.KickUser -> {
|
is RoomMembersModerationEvents.KickUser -> {
|
||||||
if (event.needsConfirmation) {
|
kickUserAsyncAction.value = AsyncAction.ConfirmingNoParams
|
||||||
kickUserAsyncAction.value = ConfirmingWithReason(event.reason)
|
}
|
||||||
} else {
|
is RoomMembersModerationEvents.DoKickUser -> {
|
||||||
selectedMember?.let {
|
selectedMember?.let {
|
||||||
coroutineScope.kickUser(it.userId, event.reason, kickUserAsyncAction)
|
coroutineScope.kickUser(it.userId, event.reason, kickUserAsyncAction)
|
||||||
}
|
|
||||||
selectedMember = null
|
|
||||||
}
|
}
|
||||||
|
selectedMember = null
|
||||||
}
|
}
|
||||||
is RoomMembersModerationEvents.BanUser -> {
|
is RoomMembersModerationEvents.BanUser -> {
|
||||||
if (event.needsConfirmation) {
|
banUserAsyncAction.value = AsyncAction.ConfirmingNoParams
|
||||||
banUserAsyncAction.value = ConfirmingWithReason(event.reason)
|
}
|
||||||
} else {
|
is RoomMembersModerationEvents.DoBanUser -> {
|
||||||
selectedMember?.let {
|
selectedMember?.let {
|
||||||
coroutineScope.banUser(it.userId, event.reason, banUserAsyncAction)
|
coroutineScope.banUser(it.userId, event.reason, banUserAsyncAction)
|
||||||
}
|
|
||||||
selectedMember = null
|
|
||||||
}
|
}
|
||||||
|
selectedMember = null
|
||||||
}
|
}
|
||||||
is RoomMembersModerationEvents.UnbanUser -> {
|
is RoomMembersModerationEvents.UnbanUser -> {
|
||||||
// We are already confirming when we are reaching this point
|
// We are already confirming when we are reaching this point
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,7 @@ class RoomMembersModerationStateProvider : PreviewParameterProvider<RoomMembersM
|
||||||
),
|
),
|
||||||
aRoomMembersModerationState(
|
aRoomMembersModerationState(
|
||||||
selectedRoomMember = anAlice(),
|
selectedRoomMember = anAlice(),
|
||||||
kickUserAsyncAction = ConfirmingWithReason(""),
|
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
||||||
),
|
|
||||||
aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = anAlice(),
|
|
||||||
kickUserAsyncAction = ConfirmingWithReason("A reason"),
|
|
||||||
),
|
),
|
||||||
aRoomMembersModerationState(
|
aRoomMembersModerationState(
|
||||||
selectedRoomMember = anAlice(),
|
selectedRoomMember = anAlice(),
|
||||||
|
|
@ -51,11 +47,7 @@ class RoomMembersModerationStateProvider : PreviewParameterProvider<RoomMembersM
|
||||||
),
|
),
|
||||||
aRoomMembersModerationState(
|
aRoomMembersModerationState(
|
||||||
selectedRoomMember = anAlice(),
|
selectedRoomMember = anAlice(),
|
||||||
banUserAsyncAction = ConfirmingWithReason(""),
|
banUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
||||||
),
|
|
||||||
aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = anAlice(),
|
|
||||||
banUserAsyncAction = ConfirmingWithReason("A reason"),
|
|
||||||
),
|
),
|
||||||
aRoomMembersModerationState(
|
aRoomMembersModerationState(
|
||||||
selectedRoomMember = anAlice(),
|
selectedRoomMember = anAlice(),
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,8 @@ import io.element.android.libraries.designsystem.components.async.rememberAsyncI
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
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.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ListDialog
|
import io.element.android.libraries.designsystem.components.dialogs.TextFieldDialog
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.components.list.TextFieldListItem
|
|
||||||
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.IconSource
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
|
|
@ -74,10 +73,10 @@ fun RoomMembersModerationView(
|
||||||
onDisplayMemberProfile(action.userId)
|
onDisplayMemberProfile(action.userId)
|
||||||
}
|
}
|
||||||
is ModerationAction.KickUser -> {
|
is ModerationAction.KickUser -> {
|
||||||
state.eventSink(RoomMembersModerationEvents.KickUser(reason = "", needsConfirmation = true))
|
state.eventSink(RoomMembersModerationEvents.KickUser)
|
||||||
}
|
}
|
||||||
is ModerationAction.BanUser -> {
|
is ModerationAction.BanUser -> {
|
||||||
state.eventSink(RoomMembersModerationEvents.BanUser(reason = "", needsConfirmation = true))
|
state.eventSink(RoomMembersModerationEvents.BanUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -90,45 +89,19 @@ fun RoomMembersModerationView(
|
||||||
|
|
||||||
when (val action = state.kickUserAsyncAction) {
|
when (val action = state.kickUserAsyncAction) {
|
||||||
is AsyncAction.Confirming -> {
|
is AsyncAction.Confirming -> {
|
||||||
if (action is ConfirmingWithReason) {
|
TextFieldDialog(
|
||||||
ListDialog(
|
title = stringResource(R.string.screen_room_member_list_kick_member_confirmation_title),
|
||||||
title = stringResource(R.string.screen_room_member_list_kick_member_confirmation_title),
|
submitText = stringResource(R.string.screen_room_member_list_kick_member_confirmation_action),
|
||||||
submitText = stringResource(R.string.screen_room_member_list_kick_member_confirmation_action),
|
onSubmit = { reason ->
|
||||||
onSubmit = {
|
state.eventSink(RoomMembersModerationEvents.DoKickUser(reason = reason))
|
||||||
state.eventSink(
|
},
|
||||||
RoomMembersModerationEvents.KickUser(
|
onDismissRequest = { state.eventSink(RoomMembersModerationEvents.Reset) },
|
||||||
reason = action.reason,
|
placeholder = stringResource(id = CommonStrings.common_reason),
|
||||||
needsConfirmation = false,
|
label = stringResource(id = CommonStrings.common_reason),
|
||||||
)
|
withBorder = true,
|
||||||
)
|
content = stringResource(R.string.screen_room_member_list_kick_member_confirmation_description),
|
||||||
},
|
value = "",
|
||||||
applyPaddingToContents = true,
|
)
|
||||||
onDismissRequest = { state.eventSink(RoomMembersModerationEvents.Reset) },
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.screen_room_member_list_kick_member_confirmation_description),
|
|
||||||
style = ElementTheme.materialTypography.bodyMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
TextFieldListItem(
|
|
||||||
placeholder = stringResource(id = CommonStrings.common_reason),
|
|
||||||
label = stringResource(id = CommonStrings.common_reason),
|
|
||||||
withBorder = true,
|
|
||||||
text = action.reason,
|
|
||||||
onTextChange = { newText ->
|
|
||||||
state.eventSink(
|
|
||||||
RoomMembersModerationEvents.KickUser(
|
|
||||||
reason = newText,
|
|
||||||
needsConfirmation = true,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is AsyncAction.Loading -> {
|
is AsyncAction.Loading -> {
|
||||||
LaunchedEffect(action) {
|
LaunchedEffect(action) {
|
||||||
|
|
@ -156,45 +129,19 @@ fun RoomMembersModerationView(
|
||||||
|
|
||||||
when (val action = state.banUserAsyncAction) {
|
when (val action = state.banUserAsyncAction) {
|
||||||
is AsyncAction.Confirming -> {
|
is AsyncAction.Confirming -> {
|
||||||
if (action is ConfirmingWithReason) {
|
TextFieldDialog(
|
||||||
ListDialog(
|
title = stringResource(R.string.screen_room_member_list_ban_member_confirmation_title),
|
||||||
title = stringResource(R.string.screen_room_member_list_ban_member_confirmation_title),
|
submitText = stringResource(R.string.screen_room_member_list_ban_member_confirmation_action),
|
||||||
submitText = stringResource(R.string.screen_room_member_list_ban_member_confirmation_action),
|
onSubmit = { reason ->
|
||||||
onSubmit = {
|
state.eventSink(RoomMembersModerationEvents.DoBanUser(reason = reason))
|
||||||
state.eventSink(
|
},
|
||||||
RoomMembersModerationEvents.BanUser(
|
onDismissRequest = { state.eventSink(RoomMembersModerationEvents.Reset) },
|
||||||
reason = action.reason,
|
placeholder = stringResource(id = CommonStrings.common_reason),
|
||||||
needsConfirmation = false,
|
label = stringResource(id = CommonStrings.common_reason),
|
||||||
)
|
withBorder = true,
|
||||||
)
|
content = stringResource(R.string.screen_room_member_list_ban_member_confirmation_description),
|
||||||
},
|
value = "",
|
||||||
applyPaddingToContents = true,
|
)
|
||||||
onDismissRequest = { state.eventSink(RoomMembersModerationEvents.Reset) },
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.screen_room_member_list_ban_member_confirmation_description),
|
|
||||||
style = ElementTheme.materialTypography.bodyMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
TextFieldListItem(
|
|
||||||
placeholder = stringResource(id = CommonStrings.common_reason),
|
|
||||||
label = stringResource(id = CommonStrings.common_reason),
|
|
||||||
withBorder = true,
|
|
||||||
text = action.reason,
|
|
||||||
onTextChange = { newText ->
|
|
||||||
state.eventSink(
|
|
||||||
RoomMembersModerationEvents.BanUser(
|
|
||||||
reason = newText,
|
|
||||||
needsConfirmation = true,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is AsyncAction.Loading -> {
|
is AsyncAction.Loading -> {
|
||||||
LaunchedEffect(action) {
|
LaunchedEffect(action) {
|
||||||
|
|
|
||||||
|
|
@ -173,15 +173,11 @@ class RoomMembersModerationPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.KickUser(reason = "", needsConfirmation = true))
|
awaitItem().eventSink(RoomMembersModerationEvents.KickUser)
|
||||||
val confirmingState = awaitItem()
|
val confirmingState = awaitItem()
|
||||||
assertThat(confirmingState.kickUserAsyncAction).isEqualTo(ConfirmingWithReason(""))
|
assertThat(confirmingState.kickUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
// Change the reason
|
|
||||||
confirmingState.eventSink(RoomMembersModerationEvents.KickUser(reason = A_REASON, needsConfirmation = true))
|
|
||||||
val confirmingWithReasonState = awaitItem()
|
|
||||||
assertThat(confirmingWithReasonState.kickUserAsyncAction).isEqualTo(ConfirmingWithReason(A_REASON))
|
|
||||||
// Confirm
|
// Confirm
|
||||||
confirmingState.eventSink(RoomMembersModerationEvents.KickUser(reason = A_REASON, needsConfirmation = false))
|
confirmingState.eventSink(RoomMembersModerationEvents.DoKickUser(reason = A_REASON))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.actions).isEmpty()
|
assertThat(loadingState.actions).isEmpty()
|
||||||
|
|
@ -215,15 +211,11 @@ class RoomMembersModerationPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.BanUser(reason = "", needsConfirmation = true))
|
awaitItem().eventSink(RoomMembersModerationEvents.BanUser)
|
||||||
val confirmingState = awaitItem()
|
val confirmingState = awaitItem()
|
||||||
assertThat(confirmingState.banUserAsyncAction).isEqualTo(ConfirmingWithReason(""))
|
assertThat(confirmingState.banUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
// Change the reason
|
|
||||||
confirmingState.eventSink(RoomMembersModerationEvents.BanUser(reason = A_REASON, needsConfirmation = true))
|
|
||||||
val confirmingWithReasonState = awaitItem()
|
|
||||||
assertThat(confirmingWithReasonState.banUserAsyncAction).isEqualTo(ConfirmingWithReason(A_REASON))
|
|
||||||
// Confirm
|
// Confirm
|
||||||
confirmingState.eventSink(RoomMembersModerationEvents.BanUser(reason = A_REASON, needsConfirmation = false))
|
confirmingState.eventSink(RoomMembersModerationEvents.DoBanUser(reason = A_REASON))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val loadingItem = awaitItem()
|
val loadingItem = awaitItem()
|
||||||
assertThat(loadingItem.actions).isEmpty()
|
assertThat(loadingItem.actions).isEmpty()
|
||||||
|
|
@ -314,7 +306,7 @@ class RoomMembersModerationPresenterTest {
|
||||||
val initialItem = awaitItem()
|
val initialItem = awaitItem()
|
||||||
// Kick user and fail
|
// Kick user and fail
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.KickUser(reason = "", needsConfirmation = false))
|
awaitItem().eventSink(RoomMembersModerationEvents.DoKickUser(reason = ""))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
|
@ -324,7 +316,7 @@ class RoomMembersModerationPresenterTest {
|
||||||
|
|
||||||
// Ban user and fail
|
// Ban user and fail
|
||||||
initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.BanUser(reason = "", needsConfirmation = false))
|
awaitItem().eventSink(RoomMembersModerationEvents.DoBanUser(reason = ""))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.test.performTextInput
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.features.roomdetails.impl.R
|
import io.element.android.features.roomdetails.impl.R
|
||||||
import io.element.android.features.roomdetails.impl.members.anAlice
|
import io.element.android.features.roomdetails.impl.members.anAlice
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.test.A_REASON
|
import io.element.android.libraries.matrix.test.A_REASON
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
@ -94,7 +95,7 @@ class RoomMembersModerationViewTest {
|
||||||
rule.clickOn(R.string.screen_room_member_list_manage_member_remove)
|
rule.clickOn(R.string.screen_room_member_list_manage_member_remove)
|
||||||
// Give time for the bottom sheet to animate
|
// Give time for the bottom sheet to animate
|
||||||
rule.mainClock.advanceTimeBy(1_000)
|
rule.mainClock.advanceTimeBy(1_000)
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.KickUser(reason = "", needsConfirmation = true))
|
eventsRecorder.assertSingle(RoomMembersModerationEvents.KickUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -103,7 +104,7 @@ class RoomMembersModerationViewTest {
|
||||||
val roomMember = anAlice()
|
val roomMember = anAlice()
|
||||||
val state = aRoomMembersModerationState(
|
val state = aRoomMembersModerationState(
|
||||||
selectedRoomMember = roomMember,
|
selectedRoomMember = roomMember,
|
||||||
kickUserAsyncAction = ConfirmingWithReason(A_REASON),
|
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
)
|
)
|
||||||
rule.setRoomMembersModerationView(
|
rule.setRoomMembersModerationView(
|
||||||
|
|
@ -115,19 +116,21 @@ class RoomMembersModerationViewTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `confirming 'Remove member' reason edition emits the expected event`() {
|
fun `confirming 'Remove member' reason edition then validation emits the expected event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
||||||
val roomMember = anAlice()
|
val roomMember = anAlice()
|
||||||
val state = aRoomMembersModerationState(
|
val state = aRoomMembersModerationState(
|
||||||
selectedRoomMember = roomMember,
|
selectedRoomMember = roomMember,
|
||||||
kickUserAsyncAction = ConfirmingWithReason(A_REASON),
|
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
)
|
)
|
||||||
rule.setRoomMembersModerationView(
|
rule.setRoomMembersModerationView(
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
rule.onNodeWithText(A_REASON).performTextInput("z")
|
val reason = rule.activity.getString(CommonStrings.common_reason)
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.KickUser(reason = "z$A_REASON", needsConfirmation = true))
|
rule.onNodeWithText(reason).performTextInput(A_REASON)
|
||||||
|
rule.clickOn(R.string.screen_room_member_list_kick_member_confirmation_action)
|
||||||
|
eventsRecorder.assertSingle(RoomMembersModerationEvents.DoKickUser(reason = A_REASON))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -136,7 +139,7 @@ class RoomMembersModerationViewTest {
|
||||||
val roomMember = anAlice()
|
val roomMember = anAlice()
|
||||||
val state = aRoomMembersModerationState(
|
val state = aRoomMembersModerationState(
|
||||||
selectedRoomMember = roomMember,
|
selectedRoomMember = roomMember,
|
||||||
kickUserAsyncAction = ConfirmingWithReason(A_REASON),
|
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
)
|
)
|
||||||
rule.setRoomMembersModerationView(
|
rule.setRoomMembersModerationView(
|
||||||
|
|
@ -144,7 +147,7 @@ class RoomMembersModerationViewTest {
|
||||||
)
|
)
|
||||||
// Note: the string key semantics is not perfect here :/
|
// Note: the string key semantics is not perfect here :/
|
||||||
rule.clickOn(R.string.screen_room_member_list_kick_member_confirmation_action)
|
rule.clickOn(R.string.screen_room_member_list_kick_member_confirmation_action)
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.KickUser(reason = A_REASON, needsConfirmation = false))
|
eventsRecorder.assertSingle(RoomMembersModerationEvents.DoKickUser(reason = ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Config(qualifiers = "h1024dp")
|
@Config(qualifiers = "h1024dp")
|
||||||
|
|
@ -168,7 +171,7 @@ class RoomMembersModerationViewTest {
|
||||||
rule.clickOn(R.string.screen_room_member_list_manage_member_remove_confirmation_ban)
|
rule.clickOn(R.string.screen_room_member_list_manage_member_remove_confirmation_ban)
|
||||||
// Give time for the bottom sheet to animate
|
// Give time for the bottom sheet to animate
|
||||||
rule.mainClock.advanceTimeBy(1_000)
|
rule.mainClock.advanceTimeBy(1_000)
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser(reason = "", needsConfirmation = true))
|
eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -177,7 +180,7 @@ class RoomMembersModerationViewTest {
|
||||||
val roomMember = anAlice()
|
val roomMember = anAlice()
|
||||||
val state = aRoomMembersModerationState(
|
val state = aRoomMembersModerationState(
|
||||||
selectedRoomMember = roomMember,
|
selectedRoomMember = roomMember,
|
||||||
banUserAsyncAction = ConfirmingWithReason(A_REASON),
|
banUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
)
|
)
|
||||||
rule.setRoomMembersModerationView(
|
rule.setRoomMembersModerationView(
|
||||||
|
|
@ -194,14 +197,16 @@ class RoomMembersModerationViewTest {
|
||||||
val roomMember = anAlice()
|
val roomMember = anAlice()
|
||||||
val state = aRoomMembersModerationState(
|
val state = aRoomMembersModerationState(
|
||||||
selectedRoomMember = roomMember,
|
selectedRoomMember = roomMember,
|
||||||
banUserAsyncAction = ConfirmingWithReason(A_REASON),
|
banUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
)
|
)
|
||||||
rule.setRoomMembersModerationView(
|
rule.setRoomMembersModerationView(
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
rule.onNodeWithText(A_REASON).performTextInput("z")
|
val reason = rule.activity.getString(CommonStrings.common_reason)
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser(reason = "z$A_REASON", needsConfirmation = true))
|
rule.onNodeWithText(reason).performTextInput(A_REASON)
|
||||||
|
rule.clickOn(R.string.screen_room_member_list_ban_member_confirmation_action)
|
||||||
|
eventsRecorder.assertSingle(RoomMembersModerationEvents.DoBanUser(reason = A_REASON))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -210,7 +215,7 @@ class RoomMembersModerationViewTest {
|
||||||
val roomMember = anAlice()
|
val roomMember = anAlice()
|
||||||
val state = aRoomMembersModerationState(
|
val state = aRoomMembersModerationState(
|
||||||
selectedRoomMember = roomMember,
|
selectedRoomMember = roomMember,
|
||||||
banUserAsyncAction = ConfirmingWithReason(A_REASON),
|
banUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
)
|
)
|
||||||
rule.setRoomMembersModerationView(
|
rule.setRoomMembersModerationView(
|
||||||
|
|
@ -218,7 +223,7 @@ class RoomMembersModerationViewTest {
|
||||||
)
|
)
|
||||||
// Note: the string key semantics is not perfect here :/
|
// Note: the string key semantics is not perfect here :/
|
||||||
rule.clickOn(R.string.screen_room_member_list_ban_member_confirmation_action)
|
rule.clickOn(R.string.screen_room_member_list_ban_member_confirmation_action)
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser(reason = A_REASON, needsConfirmation = false))
|
eventsRecorder.assertSingle(RoomMembersModerationEvents.DoBanUser(reason = ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components.dialogs
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.designsystem.components.list.TextFieldListItem
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TextFieldDialog(
|
||||||
|
title: String,
|
||||||
|
onSubmit: (String) -> Unit,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
value: String?,
|
||||||
|
placeholder: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
validation: (String?) -> Boolean = { true },
|
||||||
|
onValidationErrorMessage: String? = null,
|
||||||
|
autoSelectOnDisplay: Boolean = true,
|
||||||
|
maxLines: Int = 1,
|
||||||
|
content: String? = null,
|
||||||
|
label: String? = null,
|
||||||
|
withBorder: Boolean = false,
|
||||||
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
|
submitText: String = stringResource(CommonStrings.action_ok),
|
||||||
|
) {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
var textFieldContents by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||||
|
mutableStateOf(
|
||||||
|
TextFieldValue(
|
||||||
|
value.orEmpty(),
|
||||||
|
selection = TextRange(value.orEmpty().length)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var error by rememberSaveable { mutableStateOf(if (!validation(value.orEmpty())) onValidationErrorMessage else null) }
|
||||||
|
var canRequestFocus by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val canSubmit by remember { derivedStateOf { validation(textFieldContents.text) } }
|
||||||
|
ListDialog(
|
||||||
|
title = title,
|
||||||
|
onSubmit = { onSubmit(textFieldContents.text) },
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
enabled = canSubmit,
|
||||||
|
applyPaddingToContents = content.isNullOrEmpty().not(),
|
||||||
|
submitText = submitText,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
if (content != null) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = content,
|
||||||
|
style = ElementTheme.materialTypography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
TextFieldListItem(
|
||||||
|
placeholder = placeholder.orEmpty(),
|
||||||
|
label = label,
|
||||||
|
withBorder = withBorder,
|
||||||
|
text = textFieldContents,
|
||||||
|
onTextChange = {
|
||||||
|
error = if (!validation(it.text)) onValidationErrorMessage else null
|
||||||
|
textFieldContents = it
|
||||||
|
},
|
||||||
|
error = error,
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = KeyboardActions(onAny = {
|
||||||
|
if (validation(textFieldContents.text)) {
|
||||||
|
onSubmit(textFieldContents.text)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
maxLines = maxLines,
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
canRequestFocus = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoSelectOnDisplay && canRequestFocus) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TextFieldDialogPreview() = ElementPreview {
|
||||||
|
TextFieldDialog(
|
||||||
|
title = "Title",
|
||||||
|
value = "",
|
||||||
|
placeholder = "Placeholder",
|
||||||
|
onSubmit = {},
|
||||||
|
onDismissRequest = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TextFieldDialogWithBorderPreview() = ElementPreview {
|
||||||
|
TextFieldDialog(
|
||||||
|
title = "Title",
|
||||||
|
content = "Some content",
|
||||||
|
onSubmit = {},
|
||||||
|
onDismissRequest = {},
|
||||||
|
value = "Value",
|
||||||
|
placeholder = "Placeholder",
|
||||||
|
label = "Label",
|
||||||
|
withBorder = true,
|
||||||
|
onValidationErrorMessage = "Error message",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TextFieldDialogWithErrorPreview() = ElementPreview {
|
||||||
|
TextFieldDialog(
|
||||||
|
title = "Title",
|
||||||
|
content = "Some content",
|
||||||
|
onSubmit = {},
|
||||||
|
validation = { false },
|
||||||
|
onDismissRequest = {},
|
||||||
|
value = "Value",
|
||||||
|
placeholder = "Placeholder",
|
||||||
|
label = "Label",
|
||||||
|
withBorder = true,
|
||||||
|
onValidationErrorMessage = "Error message",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -70,6 +70,8 @@ fun TextFieldListItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
error: String? = null,
|
error: String? = null,
|
||||||
maxLines: Int = 1,
|
maxLines: Int = 1,
|
||||||
|
withBorder: Boolean = false,
|
||||||
|
label: String? = null,
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
) {
|
) {
|
||||||
|
|
@ -79,12 +81,17 @@ fun TextFieldListItem(
|
||||||
value = text,
|
value = text,
|
||||||
onValueChange = { onTextChange(it) },
|
onValueChange = { onTextChange(it) },
|
||||||
placeholder = placeholder?.let { @Composable { Text(it) } },
|
placeholder = placeholder?.let { @Composable { Text(it) } },
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
label = label?.let { @Composable { Text(it) } },
|
||||||
disabledBorderColor = Color.Transparent,
|
colors = if (withBorder) {
|
||||||
errorBorderColor = Color.Transparent,
|
OutlinedTextFieldDefaults.colors()
|
||||||
focusedBorderColor = Color.Transparent,
|
} else {
|
||||||
unfocusedBorderColor = Color.Transparent,
|
OutlinedTextFieldDefaults.colors(
|
||||||
),
|
disabledBorderColor = Color.Transparent,
|
||||||
|
errorBorderColor = Color.Transparent,
|
||||||
|
focusedBorderColor = Color.Transparent,
|
||||||
|
unfocusedBorderColor = Color.Transparent,
|
||||||
|
)
|
||||||
|
},
|
||||||
isError = error != null,
|
isError = error != null,
|
||||||
supportingText = error?.let { @Composable { Text(it) } },
|
supportingText = error?.let { @Composable { Text(it) } },
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
|
|
|
||||||
|
|
@ -7,24 +7,15 @@
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.components.preferences
|
package io.element.android.libraries.designsystem.components.preferences
|
||||||
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import io.element.android.libraries.designsystem.components.dialogs.TextFieldDialog
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.text.TextRange
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ListDialog
|
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.components.list.TextFieldListItem
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
@ -74,58 +65,3 @@ fun PreferenceTextField(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun TextFieldDialog(
|
|
||||||
title: String,
|
|
||||||
onSubmit: (String) -> Unit,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
value: String?,
|
|
||||||
placeholder: String?,
|
|
||||||
validation: (String?) -> Boolean = { true },
|
|
||||||
onValidationErrorMessage: String? = null,
|
|
||||||
autoSelectOnDisplay: Boolean = true,
|
|
||||||
maxLines: Int = 1,
|
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
|
||||||
) {
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
|
||||||
var textFieldContents by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
|
||||||
mutableStateOf(TextFieldValue(value.orEmpty(), selection = TextRange(value.orEmpty().length)))
|
|
||||||
}
|
|
||||||
var error by rememberSaveable { mutableStateOf<String?>(null) }
|
|
||||||
var canRequestFocus by rememberSaveable { mutableStateOf(false) }
|
|
||||||
val canSubmit by remember { derivedStateOf { validation(textFieldContents.text) } }
|
|
||||||
ListDialog(
|
|
||||||
title = title,
|
|
||||||
onSubmit = { onSubmit(textFieldContents.text) },
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
enabled = canSubmit,
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
TextFieldListItem(
|
|
||||||
placeholder = placeholder.orEmpty(),
|
|
||||||
text = textFieldContents,
|
|
||||||
onTextChange = {
|
|
||||||
error = if (!validation(it.text)) onValidationErrorMessage else null
|
|
||||||
textFieldContents = it
|
|
||||||
},
|
|
||||||
error = error,
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = KeyboardActions(onAny = {
|
|
||||||
if (validation(textFieldContents.text)) {
|
|
||||||
onSubmit(textFieldContents.text)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
maxLines = maxLines,
|
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
|
||||||
)
|
|
||||||
canRequestFocus = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoSelectOnDisplay && canRequestFocus) {
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,8 @@ class KonsistPreviewTest {
|
||||||
"TextComposerSimpleNotEncryptedPreview",
|
"TextComposerSimpleNotEncryptedPreview",
|
||||||
"TextComposerVoicePreview",
|
"TextComposerVoicePreview",
|
||||||
"TextComposerVoiceNotEncryptedPreview",
|
"TextComposerVoiceNotEncryptedPreview",
|
||||||
|
"TextFieldDialogWithBorderPreview",
|
||||||
|
"TextFieldDialogWithErrorPreview",
|
||||||
"TimelineImageWithCaptionRowPreview",
|
"TimelineImageWithCaptionRowPreview",
|
||||||
"TimelineItemEventRowForDirectRoomPreview",
|
"TimelineItemEventRowForDirectRoomPreview",
|
||||||
"TimelineItemEventRowShieldPreview",
|
"TimelineItemEventRowShieldPreview",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f83d60c7e1d963ccef24668c219bd9df36494dc8bd8f57af7aa63a3b73ee6882
|
oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650
|
||||||
size 7545
|
size 3642
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:b323796ccdf27e5c445aa02e3f7b73f65df961dd88f3ee480efcba14d8038281
|
|
||||||
size 19426
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650
|
|
||||||
size 3642
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:501ebaae1e09a4f7853b574b788ddb3a73cd470d37d5dbef1f46b2967761e612
|
oid sha256:2d06a58bb3b3fe7b01740cc04ed3128d24a13717be84cb4a70ced228d2256b41
|
||||||
size 31207
|
size 9435
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2d06a58bb3b3fe7b01740cc04ed3128d24a13717be84cb4a70ced228d2256b41
|
oid sha256:39b34e0940f38ab0d66ba68b61c07ba70a51acfb54f995eedce800231ea6a2c5
|
||||||
size 9435
|
size 28257
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:39b34e0940f38ab0d66ba68b61c07ba70a51acfb54f995eedce800231ea6a2c5
|
oid sha256:571207325446195a58251e7ece1ecaabc8a782cbb526315abe2a82f21e6441eb
|
||||||
size 28257
|
size 9006
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:96e0f2e3c32eedf2c85e243661a7947d28cbfe2eee7df81b239c5331770e7f7c
|
oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650
|
||||||
size 29835
|
size 3642
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:571207325446195a58251e7ece1ecaabc8a782cbb526315abe2a82f21e6441eb
|
oid sha256:f83d60c7e1d963ccef24668c219bd9df36494dc8bd8f57af7aa63a3b73ee6882
|
||||||
size 9006
|
size 7545
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650
|
oid sha256:b323796ccdf27e5c445aa02e3f7b73f65df961dd88f3ee480efcba14d8038281
|
||||||
size 3642
|
size 19426
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ba213b12d181b04695b9271670cb7d96ce83dfccc3c2031ee002ba1cf180cc8d
|
oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd
|
||||||
size 6408
|
size 3659
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f9def02bcb5784681dc26327791510a9a25f0772644cdae363d7a4b90e69067c
|
|
||||||
size 17479
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd
|
|
||||||
size 3659
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:83e72d7332d7574dd2f4ea42b9fa18995359720b18105f08bfde1f7f14cf8725
|
oid sha256:273043114950e000953fbede780f3ad8adcce9d3d579edea2cad58f8e3c48881
|
||||||
size 29080
|
size 8175
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:273043114950e000953fbede780f3ad8adcce9d3d579edea2cad58f8e3c48881
|
oid sha256:19483b4fae4026098273e6e4c941592074f08b3e7f9fa44e759bdab9daf2b27a
|
||||||
size 8175
|
size 26233
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:19483b4fae4026098273e6e4c941592074f08b3e7f9fa44e759bdab9daf2b27a
|
oid sha256:2b0d51e2c5b255fb2f94959734293d4b7807f002acb0217c84856f0002297293
|
||||||
size 26233
|
size 7636
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0c36a1bfdfdf92ac180436b8c4aef3d017911119f10f02e2a940b5ab242ac324
|
oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd
|
||||||
size 27856
|
size 3659
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2b0d51e2c5b255fb2f94959734293d4b7807f002acb0217c84856f0002297293
|
oid sha256:ba213b12d181b04695b9271670cb7d96ce83dfccc3c2031ee002ba1cf180cc8d
|
||||||
size 7636
|
size 6408
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd
|
oid sha256:f9def02bcb5784681dc26327791510a9a25f0772644cdae363d7a4b90e69067c
|
||||||
size 3659
|
size 17479
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1be0efd7f33ee57088b04aedf6c63cb8092abc0356a31636fe028e31d542e28d
|
||||||
|
size 15671
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e7609a1d51c1be29b0d117dd968bd44b250d02e82e1630fa062da4cd280bb950
|
||||||
|
size 13934
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c4697eda8d2fb160b2cade0be583148ec0b2e55f35fe44929f98feb4c8535377
|
||||||
|
size 17761
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2774d56a1e788567c19df9a714f1399968e75d2df6c3a4f404e3c03a30286567
|
||||||
|
size 15951
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3257be6487ff2f412564bb8f188860e39b0325156533ad519c9d6852000a173a
|
||||||
|
size 13119
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:baf396f36cdc0ca5e01842f70fe0b93166fa82c11ad981c54a529f59c90b5111
|
||||||
|
size 11469
|
||||||
Loading…
Add table
Add a link
Reference in a new issue