Show progress dialog while we are sending invites in a room (#5342)
* Add `InvitePeopleState.sendInvitesAction` Keep track of the progress on sending invites with a new state property. * Keep `RoomInviteMembersView` open until invites are sent * Sync strings from localazy * extend `ProgressDialog` to support custom content For my current design, a simple text element is insufficient. I extend `ProgressDialog` to give more flexibility over the content of the dialog. * Show progress dialog while invites are being sent * Add new ProgressDialog previews to the naming exceptions list * Update screenshots --------- Co-authored-by: ElementBot <android@element.io> Co-authored-by: Jorge Martín <jorgem@element.io>
This commit is contained in:
parent
3af4405ee3
commit
120c30e076
22 changed files with 189 additions and 19 deletions
|
|
@ -7,8 +7,11 @@
|
|||
|
||||
package io.element.android.features.invitepeople.api
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
interface InvitePeopleState {
|
||||
val canInvite: Boolean
|
||||
val isSearchActive: Boolean
|
||||
val sendInvitesAction: AsyncAction<Unit>
|
||||
val eventSink: (InvitePeopleEvents) -> Unit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,28 +8,33 @@
|
|||
package io.element.android.features.invitepeople.api
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
class InvitePeopleStateProvider : PreviewParameterProvider<InvitePeopleState> {
|
||||
override val values: Sequence<InvitePeopleState>
|
||||
get() = sequenceOf(
|
||||
aPreviewInvitePeopleState(),
|
||||
aPreviewInvitePeopleState(canInvite = true),
|
||||
aPreviewInvitePeopleState(isSearchActive = true)
|
||||
aPreviewInvitePeopleState(isSearchActive = true),
|
||||
aPreviewInvitePeopleState(sendInvitesAction = AsyncAction.Loading),
|
||||
)
|
||||
}
|
||||
|
||||
private data class PreviewInvitePeopleState(
|
||||
override val canInvite: Boolean,
|
||||
override val isSearchActive: Boolean,
|
||||
override val sendInvitesAction: AsyncAction<Unit>,
|
||||
override val eventSink: (InvitePeopleEvents) -> Unit,
|
||||
) : InvitePeopleState
|
||||
|
||||
private fun aPreviewInvitePeopleState(
|
||||
canInvite: Boolean = false,
|
||||
isSearchActive: Boolean = false,
|
||||
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (InvitePeopleEvents) -> Unit = {},
|
||||
) = PreviewInvitePeopleState(
|
||||
canInvite = canInvite,
|
||||
isSearchActive = isSearchActive,
|
||||
sendInvitesAction = sendInvitesAction,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@ import dev.zacsweers.metro.Inject
|
|||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.map
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
|
@ -73,6 +75,8 @@ class DefaultInvitePeoplePresenter(
|
|||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
||||
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
|
||||
val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) {
|
||||
if (joinedRoom == null) {
|
||||
val result = matrixClient.getJoinedRoom(roomId)
|
||||
|
|
@ -116,7 +120,7 @@ class DefaultInvitePeoplePresenter(
|
|||
}
|
||||
is InvitePeopleEvents.SendInvites -> {
|
||||
room.dataOrNull()?.let {
|
||||
sessionCoroutineScope.sendInvites(it, selectedUsers.value)
|
||||
sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction)
|
||||
}
|
||||
}
|
||||
is InvitePeopleEvents.CloseSearch -> {
|
||||
|
|
@ -128,12 +132,13 @@ class DefaultInvitePeoplePresenter(
|
|||
|
||||
return DefaultInvitePeopleState(
|
||||
room = room.map { },
|
||||
canInvite = selectedUsers.value.isNotEmpty(),
|
||||
canInvite = selectedUsers.value.isNotEmpty() && !sendInvitesAction.value.isLoading(),
|
||||
selectedUsers = selectedUsers.value,
|
||||
searchQuery = searchQuery,
|
||||
isSearchActive = searchActive,
|
||||
searchResults = searchResults.value,
|
||||
showSearchLoader = showSearchLoader.value,
|
||||
sendInvitesAction = sendInvitesAction.value,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
|
@ -141,16 +146,21 @@ class DefaultInvitePeoplePresenter(
|
|||
private fun CoroutineScope.sendInvites(
|
||||
room: JoinedRoom,
|
||||
selectedUsers: List<MatrixUser>,
|
||||
sendInvitesAction: MutableState<AsyncAction<Unit>>,
|
||||
) = launch {
|
||||
val anyInviteFailed = selectedUsers
|
||||
.map { room.inviteUserById(it.userId) }
|
||||
.any { it.isFailure }
|
||||
sendInvitesAction.runUpdatingState {
|
||||
val anyInviteFailed = selectedUsers
|
||||
.map { room.inviteUserById(it.userId) }
|
||||
.any { it.isFailure }
|
||||
|
||||
if (anyInviteFailed) {
|
||||
appErrorStateService.showError(
|
||||
titleRes = CommonStrings.common_unable_to_invite_title,
|
||||
bodyRes = CommonStrings.common_unable_to_invite_message,
|
||||
)
|
||||
if (anyInviteFailed) {
|
||||
appErrorStateService.showError(
|
||||
titleRes = CommonStrings.common_unable_to_invite_title,
|
||||
bodyRes = CommonStrings.common_unable_to_invite_message,
|
||||
)
|
||||
}
|
||||
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.invitepeople.impl
|
|||
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
|
@ -22,5 +23,6 @@ data class DefaultInvitePeopleState(
|
|||
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
override val isSearchActive: Boolean,
|
||||
override val sendInvitesAction: AsyncAction<Unit>,
|
||||
override val eventSink: (InvitePeopleEvents) -> Unit
|
||||
) : InvitePeopleState
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.features.invitepeople.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
|
@ -68,6 +69,11 @@ internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider<Defau
|
|||
showSearchLoader = true,
|
||||
),
|
||||
aDefaultInvitePeopleState(room = AsyncData.Failure(Exception("Room not found"))),
|
||||
aDefaultInvitePeopleState(
|
||||
canInvite = false,
|
||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||
sendInvitesAction = AsyncAction.Loading,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -93,6 +99,7 @@ private fun aDefaultInvitePeopleState(
|
|||
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
||||
isSearchActive: Boolean = false,
|
||||
showSearchLoader: Boolean = false,
|
||||
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
): DefaultInvitePeopleState {
|
||||
return DefaultInvitePeopleState(
|
||||
room = room,
|
||||
|
|
@ -102,6 +109,7 @@ private fun aDefaultInvitePeopleState(
|
|||
selectedUsers = selectedUsers,
|
||||
isSearchActive = isSearchActive,
|
||||
showSearchLoader = showSearchLoader,
|
||||
sendInvitesAction = sendInvitesAction,
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -409,10 +409,23 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||
// Send invites
|
||||
initialState.eventSink(InvitePeopleEvents.SendInvites)
|
||||
|
||||
// Can't invite in the loading state
|
||||
awaitItem().run {
|
||||
assertThat(sendInvitesAction.isLoading()).isTrue()
|
||||
assertThat(canInvite).isFalse()
|
||||
}
|
||||
|
||||
delay(1_000)
|
||||
inviteUserResult.assertions().isCalledOnce().with(
|
||||
value(selectedUser.userId)
|
||||
)
|
||||
|
||||
// Can invite again once the action is finished
|
||||
awaitItem().run {
|
||||
assertThat(sendInvitesAction.isReady()).isTrue()
|
||||
assertThat(canInvite).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -445,6 +458,13 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||
// Send invites
|
||||
initialState.eventSink(InvitePeopleEvents.SendInvites)
|
||||
|
||||
// Can't invite in the loading state
|
||||
awaitItem().run {
|
||||
assertThat(sendInvitesAction.isLoading()).isTrue()
|
||||
assertThat(canInvite).isFalse()
|
||||
}
|
||||
|
||||
delay(1_000)
|
||||
inviteUserResult.assertions().isCalledOnce().with(
|
||||
value(selectedUser.userId)
|
||||
|
|
@ -455,6 +475,12 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
value(CommonStrings.common_unable_to_invite_title),
|
||||
value(CommonStrings.common_unable_to_invite_message)
|
||||
)
|
||||
|
||||
// Can invite again once the action is finished
|
||||
awaitItem().run {
|
||||
assertThat(sendInvitesAction.isReady()).isTrue()
|
||||
assertThat(canInvite).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.features.roomdetails.impl.invite
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
|
|
@ -49,11 +50,18 @@ class RoomInviteMembersNode(
|
|||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = invitePeoplePresenter.present()
|
||||
|
||||
// Once invites have been sent successfully, close the Invite view.
|
||||
LaunchedEffect(state.sendInvitesAction) {
|
||||
if (state.sendInvitesAction.isReady()) {
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
RoomInviteMembersView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onBackClick = { navigateUp() },
|
||||
onDone = { navigateUp() }
|
||||
onBackClick = { navigateUp() }
|
||||
) {
|
||||
invitePeopleRenderer.Render(state, Modifier)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,22 +8,29 @@
|
|||
package io.element.android.features.roomdetails.impl.invite
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleStateProvider
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
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.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -32,7 +39,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
fun RoomInviteMembersView(
|
||||
state: InvitePeopleState,
|
||||
onBackClick: () -> Unit,
|
||||
onDone: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
invitePeopleView: @Composable () -> Unit,
|
||||
) {
|
||||
|
|
@ -49,7 +55,6 @@ fun RoomInviteMembersView(
|
|||
},
|
||||
onSubmitClick = {
|
||||
state.eventSink(InvitePeopleEvents.SendInvites)
|
||||
onDone()
|
||||
},
|
||||
canSend = state.canInvite,
|
||||
)
|
||||
|
|
@ -64,6 +69,10 @@ fun RoomInviteMembersView(
|
|||
invitePeopleView()
|
||||
}
|
||||
}
|
||||
|
||||
if (state.sendInvitesAction.isLoading()) {
|
||||
InviteProgressDialog()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -86,6 +95,24 @@ private fun RoomInviteMembersTopBar(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InviteProgressDialog() {
|
||||
ProgressDialog {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.screen_room_details_invite_people_preparing),
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.screen_room_details_invite_people_dont_close),
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RoomInviteMembersViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview {
|
||||
|
|
@ -93,6 +120,5 @@ internal fun RoomInviteMembersViewPreview(@PreviewParameter(InvitePeopleStatePro
|
|||
state = state,
|
||||
invitePeopleView = {},
|
||||
onBackClick = {},
|
||||
onDone = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@
|
|||
<string name="screen_room_details_error_loading_notification_settings">"An error occurred when loading notification settings."</string>
|
||||
<string name="screen_room_details_error_muting">"Failed muting this room, please try again."</string>
|
||||
<string name="screen_room_details_error_unmuting">"Failed unmuting this room, please try again."</string>
|
||||
<string name="screen_room_details_invite_people_dont_close">"Don\'t close the app until finished."</string>
|
||||
<string name="screen_room_details_invite_people_preparing">"Preparing invitations…"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invite people"</string>
|
||||
<string name="screen_room_details_leave_conversation_title">"Leave conversation"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Leave room"</string>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,18 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
|
|||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* A progress dialog, with a spinner, and optional text content.
|
||||
*
|
||||
* @param modifier
|
||||
* @param text Optional text to show under the spinner.
|
||||
* @param type
|
||||
* @param properties
|
||||
* @param showCancelButton
|
||||
* @param onDismissRequest
|
||||
* @param content Optional additional content to show under the spinner, and above the cancel button (if shown). If both `text` and `content` are supplied,
|
||||
* `text` is shown above `content`.
|
||||
*/
|
||||
@Composable
|
||||
fun ProgressDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -46,6 +58,7 @@ fun ProgressDialog(
|
|||
properties: DialogProperties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
|
||||
showCancelButton: Boolean = false,
|
||||
onDismissRequest: () -> Unit = {},
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
|
|
@ -75,7 +88,8 @@ fun ProgressDialog(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +110,8 @@ private fun ProgressDialogContent(
|
|||
CircularProgressIndicator(
|
||||
color = ElementTheme.colors.iconPrimary
|
||||
)
|
||||
}
|
||||
},
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
@ -118,6 +133,7 @@ private fun ProgressDialogContent(
|
|||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
content()
|
||||
if (showCancelButton) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Box(
|
||||
|
|
@ -138,7 +154,7 @@ private fun ProgressDialogContent(
|
|||
@Composable
|
||||
internal fun ProgressDialogContentPreview() = ElementThemedPreview {
|
||||
DialogPreview {
|
||||
ProgressDialogContent(text = "test dialog content", showCancelButton = true)
|
||||
ProgressDialogContent(text = "test dialog content", showCancelButton = true, content = {})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,3 +163,34 @@ internal fun ProgressDialogContentPreview() = ElementThemedPreview {
|
|||
internal fun ProgressDialogPreview() = ElementPreview {
|
||||
ProgressDialog(text = "test dialog content", showCancelButton = true)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ProgressDialogWithContentPreview() = ElementPreview {
|
||||
ProgressDialog(showCancelButton = true) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Heading",
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Subtext",
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ProgressDialogWithTextAndContentPreview() = ElementPreview {
|
||||
ProgressDialog(text = "Text Content") {
|
||||
Text(
|
||||
text = "blah blah",
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -304,6 +304,7 @@ Reason: %1$s."</string>
|
|||
<string name="common_sent">"Sent"</string>
|
||||
<string name="common_sentence_delimiter">". "</string>
|
||||
<string name="common_server_not_supported">"Server not supported"</string>
|
||||
<string name="common_server_unreachable">"Server unreachable"</string>
|
||||
<string name="common_server_url">"Server URL"</string>
|
||||
<string name="common_settings">"Settings"</string>
|
||||
<string name="common_shared_location">"Shared location"</string>
|
||||
|
|
|
|||
|
|
@ -114,6 +114,8 @@ class KonsistPreviewTest {
|
|||
"PollContentViewDisclosedPreview",
|
||||
"PollContentViewEndedPreview",
|
||||
"PollContentViewUndisclosedPreview",
|
||||
"ProgressDialogWithContentPreview",
|
||||
"ProgressDialogWithTextAndContentPreview",
|
||||
"ReadReceiptBottomSheetPreview",
|
||||
"RoomMemberListViewBannedPreview",
|
||||
"SasEmojisPreview",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7415b3286707130c0218a8d1e82a9d9c61c8eec8df346653dd67626d0dd7709c
|
||||
size 10038
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:39c67413ca997982b0fcd32831ccedee1d7a0763619784b8758b194df6c7ff81
|
||||
size 9639
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:94341fe8aa52b4551ef8a73cf298aaa1c4352ae0d4c83990c04c4c6e491b64e8
|
||||
size 21444
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:00a961c76598dc98a0d9c7a45781865984c51bb440bb1605c381e9496ca1789f
|
||||
size 21971
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a809a8792d519f93df9a15c57c06d3fd4f3cb7c84245e80cf61ff07ab1c5d620
|
||||
size 20363
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:433e3467cfa0c0f4e737ec15a1c41d6bbc36d4d2c2dd9f2f7f22bb457208f280
|
||||
size 19180
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7b637e70da73ea0fca81adb861abf69fb2092d35493417fb29103f5acebf6fa0
|
||||
size 11706
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:440b6f0e84c9fccfbbc997a40e1e8459df1962218fa4fdceb7ea8ecc8c96090b
|
||||
size 11442
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b3212b09963d5fa9b89b1ebfd8f904b3f436399299e67f560f39620d063eb997
|
||||
size 11408
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ae69faa0c95f4ae4b7b410da438f0e3992571c21837e644e63621fa1f73e297a
|
||||
size 11220
|
||||
Loading…
Add table
Add a link
Reference in a new issue