mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 08:09:12 +01:00
Show upload progress for the first backup on the chat list.
This commit is contained in:
committed by
Alex Hart
parent
4c43bf2228
commit
2f6baf8743
@@ -46,13 +46,11 @@ object ArchiveUploadProgress {
|
||||
|
||||
private val TAG = Log.tag(ArchiveUploadProgress::class)
|
||||
|
||||
private val PROGRESS_NONE = ArchiveUploadProgressState(
|
||||
state = ArchiveUploadProgressState.State.None
|
||||
)
|
||||
|
||||
private val _progress: MutableSharedFlow<Unit> = MutableSharedFlow(replay = 1)
|
||||
|
||||
private var uploadProgress: ArchiveUploadProgressState = SignalStore.backup.archiveUploadState ?: PROGRESS_NONE
|
||||
private var uploadProgress: ArchiveUploadProgressState = SignalStore.backup.archiveUploadState ?: ArchiveUploadProgressState(
|
||||
state = ArchiveUploadProgressState.State.None
|
||||
)
|
||||
|
||||
private val attachmentProgress: MutableMap<AttachmentId, AttachmentProgressDetails> = ConcurrentHashMap()
|
||||
|
||||
@@ -76,7 +74,11 @@ object ArchiveUploadProgress {
|
||||
|
||||
if (!SignalStore.backup.backsUpMedia) {
|
||||
Log.i(TAG, "Doesn't upload media. Done!")
|
||||
return@map PROGRESS_NONE
|
||||
SignalStore.backup.finishedInitialBackup = true
|
||||
return@map uploadProgress.copy(
|
||||
state = ArchiveUploadProgressState.State.None,
|
||||
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone
|
||||
)
|
||||
}
|
||||
|
||||
val pendingMediaUploadBytes = SignalDatabase.attachments.getPendingArchiveUploadBytes() - attachmentProgress.values.sumOf { it.bytesUploaded }
|
||||
@@ -87,7 +89,12 @@ object ArchiveUploadProgress {
|
||||
Log.i(TAG, "We uploaded media as part of the backup. We should enqueue another backup now to ensure that CDN info is properly written.")
|
||||
BackupMessagesJob.enqueue()
|
||||
}
|
||||
return@map PROGRESS_NONE
|
||||
SignalStore.backup.finishedInitialBackup = true
|
||||
return@map uploadProgress.copy(
|
||||
state = ArchiveUploadProgressState.State.None,
|
||||
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone,
|
||||
mediaUploadedBytes = uploadProgress.mediaTotalBytes
|
||||
)
|
||||
}
|
||||
|
||||
// It's possible that new attachments may be pending upload after we start a backup.
|
||||
@@ -96,7 +103,7 @@ object ArchiveUploadProgress {
|
||||
// the progress bar may occasionally be including media that is not actually referenced in the active backup file.
|
||||
val totalMediaUploadBytes = max(uploadProgress.mediaTotalBytes, pendingMediaUploadBytes)
|
||||
|
||||
ArchiveUploadProgressState(
|
||||
uploadProgress.copy(
|
||||
state = ArchiveUploadProgressState.State.UploadMedia,
|
||||
mediaUploadedBytes = totalMediaUploadBytes - pendingMediaUploadBytes,
|
||||
mediaTotalBytes = totalMediaUploadBytes
|
||||
@@ -109,9 +116,13 @@ object ArchiveUploadProgress {
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
val inProgress
|
||||
get() = uploadProgress.state != ArchiveUploadProgressState.State.None
|
||||
get() = uploadProgress.state != ArchiveUploadProgressState.State.None && uploadProgress.state != ArchiveUploadProgressState.State.UserCanceled
|
||||
|
||||
fun begin() {
|
||||
if (!SignalStore.backup.finishedInitialBackup) {
|
||||
SignalStore.backup.uploadBannerVisible = true
|
||||
}
|
||||
|
||||
updateState(overrideCancel = true) {
|
||||
ArchiveUploadProgressState(
|
||||
state = ArchiveUploadProgressState.State.Export
|
||||
@@ -124,6 +135,8 @@ object ArchiveUploadProgress {
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
SignalStore.backup.uploadBannerVisible = false
|
||||
|
||||
updateState {
|
||||
ArchiveUploadProgressState(
|
||||
state = ArchiveUploadProgressState.State.UserCanceled
|
||||
@@ -213,6 +226,7 @@ object ArchiveUploadProgress {
|
||||
|
||||
fun onMessageBackupFinishedEarly() {
|
||||
resetState()
|
||||
SignalStore.backup.finishedInitialBackup = true
|
||||
}
|
||||
|
||||
fun onMainBackupFileUploadFailure() {
|
||||
@@ -224,7 +238,12 @@ object ArchiveUploadProgress {
|
||||
if (shouldRevertToUploadMedia) {
|
||||
onAttachmentSectionStarted(SignalDatabase.attachments.getPendingArchiveUploadBytes())
|
||||
} else {
|
||||
updateState { PROGRESS_NONE }
|
||||
updateState {
|
||||
it.copy(
|
||||
state = ArchiveUploadProgressState.State.None,
|
||||
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ private const val NONE = -1
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun BackupStatusBanner(
|
||||
fun ArchiveRestoreStatusBanner(
|
||||
data: ArchiveRestoreProgressState,
|
||||
onBannerClick: () -> Unit = {},
|
||||
onActionClick: (ArchiveRestoreProgressState) -> Unit = {},
|
||||
@@ -311,46 +311,46 @@ private fun ArchiveRestoreProgressState.actionResource(): Int {
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun BackupStatusBannerPreview() {
|
||||
private fun ArchiveRestoreStatusBannerPreview() {
|
||||
Previews.Preview {
|
||||
Column {
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.CALCULATING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 1024.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.CANCELING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 200.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.WAITING_FOR_WIFI, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.WAITING_FOR_INTERNET, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.NONE, restoreStatus = RestoreStatus.FINISHED, remainingRestoreSize = 0.mebiBytes, totalRestoreSize = 0.mebiBytes, totalToRestoreThisRun = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.NOT_ENOUGH_DISK_SPACE, remainingRestoreSize = 500.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.status
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.DropdownMenus
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
|
||||
|
||||
/**
|
||||
* Displays a "heads up" widget containing information about the current
|
||||
* status of the user's backup.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ArchiveUploadStatusBannerView(
|
||||
state: ArchiveUploadStatusBannerViewState,
|
||||
emitter: (ArchiveUploadStatusBannerViewEvents) -> Unit = {},
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
val iconRes: Int = when (state) {
|
||||
ArchiveUploadStatusBannerViewState.CreatingBackupFile -> R.drawable.symbol_backup_light
|
||||
is ArchiveUploadStatusBannerViewState.Uploading -> R.drawable.symbol_backup_light
|
||||
is ArchiveUploadStatusBannerViewState.PausedMissingWifi -> R.drawable.symbol_backup_light
|
||||
is ArchiveUploadStatusBannerViewState.PausedNoInternet -> R.drawable.symbol_backup_light
|
||||
is ArchiveUploadStatusBannerViewState.Finished -> R.drawable.symbol_check_circle_24
|
||||
}
|
||||
|
||||
val iconColor: Color = when (state) {
|
||||
ArchiveUploadStatusBannerViewState.CreatingBackupFile -> BackupsIconColors.Normal.foreground
|
||||
is ArchiveUploadStatusBannerViewState.Uploading -> BackupsIconColors.Normal.foreground
|
||||
is ArchiveUploadStatusBannerViewState.PausedMissingWifi -> BackupsIconColors.Warning.foreground
|
||||
is ArchiveUploadStatusBannerViewState.PausedNoInternet -> BackupsIconColors.Warning.foreground
|
||||
is ArchiveUploadStatusBannerViewState.Finished -> BackupsIconColors.Success.foreground
|
||||
}
|
||||
|
||||
val title: String = when (state) {
|
||||
ArchiveUploadStatusBannerViewState.CreatingBackupFile -> stringResource(R.string.BackupStatus__status_creating_backup)
|
||||
is ArchiveUploadStatusBannerViewState.Uploading -> stringResource(R.string.BackupStatus__uploading_backup)
|
||||
is ArchiveUploadStatusBannerViewState.PausedMissingWifi -> stringResource(R.string.BackupStatus__upload_paused)
|
||||
is ArchiveUploadStatusBannerViewState.PausedNoInternet -> stringResource(R.string.BackupStatus__upload_paused)
|
||||
is ArchiveUploadStatusBannerViewState.Finished -> stringResource(R.string.BackupStatus__upload_complete)
|
||||
}
|
||||
|
||||
val status: String? = when (state) {
|
||||
ArchiveUploadStatusBannerViewState.CreatingBackupFile -> null
|
||||
is ArchiveUploadStatusBannerViewState.Uploading -> stringResource(R.string.BackupStatus__status_size_of_size, state.completedSize, state.totalSize)
|
||||
is ArchiveUploadStatusBannerViewState.PausedMissingWifi -> stringResource(R.string.BackupStatus__status_waiting_for_wifi)
|
||||
is ArchiveUploadStatusBannerViewState.PausedNoInternet -> stringResource(R.string.BackupStatus__status_no_internet)
|
||||
is ArchiveUploadStatusBannerViewState.Finished -> state.uploadedSize
|
||||
}
|
||||
|
||||
val showIndeterminateProgress: Boolean = when (state) {
|
||||
ArchiveUploadStatusBannerViewState.CreatingBackupFile -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val progress: Float? = when (state) {
|
||||
is ArchiveUploadStatusBannerViewState.Uploading -> state.progress
|
||||
else -> null
|
||||
}
|
||||
|
||||
val actionLabelRes: Int? = when (state) {
|
||||
is ArchiveUploadStatusBannerViewState.PausedMissingWifi -> R.string.BackupStatus__resume
|
||||
else -> null
|
||||
}
|
||||
|
||||
val showDismiss: Boolean = when (state) {
|
||||
is ArchiveUploadStatusBannerViewState.Finished -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val dropdownAvailable: Boolean = when (state) {
|
||||
is ArchiveUploadStatusBannerViewState.Finished -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
val menuController = remember { DropdownMenus.MenuController() }
|
||||
|
||||
Box(modifier = Modifier.padding(contentPadding)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.border(1.dp, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.38f), shape = RoundedCornerShape(12.dp))
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 48.dp)
|
||||
.combinedClickable(
|
||||
onClick = { emitter(ArchiveUploadStatusBannerViewEvents.BannerClicked) },
|
||||
onLongClick = { menuController.show() }
|
||||
)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = null,
|
||||
tint = iconColor,
|
||||
modifier = Modifier
|
||||
.padding(start = 4.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
.padding(end = 20.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
|
||||
status?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(end = 12.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showIndeterminateProgress) {
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = 3.dp,
|
||||
strokeCap = StrokeCap.Round,
|
||||
modifier = Modifier
|
||||
.size(24.dp, 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
progress?.let {
|
||||
CircularProgressIndicator(
|
||||
progress = { it },
|
||||
strokeWidth = 3.dp,
|
||||
strokeCap = StrokeCap.Round,
|
||||
modifier = Modifier
|
||||
.size(24.dp, 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (showDismiss) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_x_24),
|
||||
contentDescription = stringResource(R.string.Material3SearchToolbar__close),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = ripple(bounded = false),
|
||||
onClick = { emitter(ArchiveUploadStatusBannerViewEvents.HideClicked) }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (dropdownAvailable) {
|
||||
Box(modifier = Modifier.align(Alignment.BottomEnd)) {
|
||||
DropdownMenus.Menu(
|
||||
controller = menuController,
|
||||
offsetX = 0.dp,
|
||||
offsetY = 10.dp
|
||||
) {
|
||||
DropdownMenus.ItemWithIcon(
|
||||
menuController = menuController,
|
||||
drawableResId = R.drawable.symbol_visible_slash,
|
||||
stringResId = R.string.BackupStatus__hide,
|
||||
onClick = { emitter(ArchiveUploadStatusBannerViewEvents.HideClicked) }
|
||||
)
|
||||
DropdownMenus.ItemWithIcon(
|
||||
menuController = menuController,
|
||||
drawableResId = R.drawable.symbol_x_circle_24,
|
||||
stringResId = R.string.BackupStatus__cancel_upload,
|
||||
onClick = { emitter(ArchiveUploadStatusBannerViewEvents.CancelClicked) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun ArchiveUploadStatusBannerViewPreview() {
|
||||
Previews.Preview {
|
||||
Column {
|
||||
ArchiveUploadStatusBannerView(
|
||||
state = ArchiveUploadStatusBannerViewState.CreatingBackupFile
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
ArchiveUploadStatusBannerView(
|
||||
state = ArchiveUploadStatusBannerViewState.Uploading(
|
||||
completedSize = "224 MB",
|
||||
totalSize = "1 GB",
|
||||
progress = 0.22f
|
||||
)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
ArchiveUploadStatusBannerView(
|
||||
state = ArchiveUploadStatusBannerViewState.Uploading(
|
||||
completedSize = "0 B",
|
||||
totalSize = "1 GB",
|
||||
progress = 0f
|
||||
)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
ArchiveUploadStatusBannerView(
|
||||
state = ArchiveUploadStatusBannerViewState.PausedMissingWifi
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
ArchiveUploadStatusBannerView(
|
||||
state = ArchiveUploadStatusBannerViewState.PausedNoInternet
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
ArchiveUploadStatusBannerView(
|
||||
state = ArchiveUploadStatusBannerViewState.Finished(uploadedSize = "1 GB")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.status
|
||||
|
||||
sealed interface ArchiveUploadStatusBannerViewEvents {
|
||||
data object BannerClicked : ArchiveUploadStatusBannerViewEvents
|
||||
data object CancelClicked : ArchiveUploadStatusBannerViewEvents
|
||||
data object HideClicked : ArchiveUploadStatusBannerViewEvents
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.status
|
||||
|
||||
/**
|
||||
* Renderable state for [ArchiveUploadStatusBannerView].
|
||||
* Each state carries only the data it needs; rendering logic lives in the composable.
|
||||
*/
|
||||
sealed interface ArchiveUploadStatusBannerViewState {
|
||||
/** Creating the intiial backup file. */
|
||||
data object CreatingBackupFile : ArchiveUploadStatusBannerViewState
|
||||
|
||||
/** Actively uploading media with progress. */
|
||||
data class Uploading(
|
||||
val completedSize: String,
|
||||
val totalSize: String,
|
||||
val progress: Float
|
||||
) : ArchiveUploadStatusBannerViewState
|
||||
|
||||
/** Restore paused because Wi-Fi is required. */
|
||||
data object PausedMissingWifi : ArchiveUploadStatusBannerViewState
|
||||
|
||||
/** Restore paused because there is no internet connection. */
|
||||
data object PausedNoInternet : ArchiveUploadStatusBannerViewState
|
||||
|
||||
/** Restore completed successfully. */
|
||||
data class Finished(
|
||||
val uploadedSize: String
|
||||
) : ArchiveUploadStatusBannerViewState
|
||||
}
|
||||
Reference in New Issue
Block a user