diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt
index 5adfe59138e..144875798bc 100644
--- a/app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt
+++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt
@@ -27,6 +27,7 @@ import com.wire.kalium.cells.domain.usecase.AddAttachmentDraftUseCase
import com.wire.kalium.cells.domain.usecase.CreateFolderUseCase
import com.wire.kalium.cells.domain.usecase.DeleteCellAssetUseCase
import com.wire.kalium.cells.domain.usecase.DownloadCellFileUseCase
+import com.wire.kalium.cells.domain.usecase.DownloadCellVersionUseCase
import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase
import com.wire.kalium.cells.domain.usecase.GetCellFileUseCase
import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase
@@ -111,7 +112,7 @@ class CellsModule {
@ViewModelScoped
@Provides
- fun provideDownloadUseCase(cellsScope: CellsScope): DownloadCellFileUseCase = cellsScope.downloadFile
+ fun provideDownloadUseCase(cellsScope: CellsScope): DownloadCellFileUseCase = cellsScope.downloadCellFile
@ViewModelScoped
@Provides
@@ -220,4 +221,9 @@ class CellsModule {
@Provides
fun provideRestoreNodeVersionUseCase(cellsScope: CellsScope): RestoreNodeVersionUseCase =
cellsScope.restoreNodeVersion
+
+ @ViewModelScoped
+ @Provides
+ fun provideDownloadCellVersionUseCase(cellsScope: CellsScope): DownloadCellVersionUseCase =
+ cellsScope.downloadCellVersion
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt
index 7c436830854..28d7b25b924 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt
@@ -184,7 +184,7 @@ import com.wire.android.util.normalizeLink
import com.wire.android.util.serverDate
import com.wire.android.util.ui.PreviewMultipleThemes
import com.wire.android.util.ui.UIText
-import com.wire.android.util.ui.openDownloadFolder
+import com.wire.android.util.openDownloadFolder
import com.wire.kalium.common.error.NetworkFailure
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.conversation.Conversation.TypingIndicatorMode
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt
index 098924dfeaf..5048866d0b7 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt
@@ -75,7 +75,7 @@ import com.wire.android.ui.theme.wireDimensions
import com.wire.android.util.ui.PreviewMultipleThemes
import com.wire.android.util.ui.SnackBarMessageHandler
import com.wire.android.util.ui.UIText
-import com.wire.android.util.ui.openDownloadFolder
+import com.wire.android.util.openDownloadFolder
import com.wire.kalium.logic.data.id.ConversationId
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryScreen.kt
index 5f3daa79915..b10e1d08520 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryScreen.kt
@@ -66,7 +66,7 @@ import com.wire.android.util.permission.rememberWriteStoragePermissionFlow
import com.wire.android.util.startFileShareIntent
import com.wire.android.util.ui.PreviewMultipleThemes
import com.wire.android.util.ui.SnackBarMessageHandler
-import com.wire.android.util.ui.openDownloadFolder
+import com.wire.android.util.openDownloadFolder
@OptIn(ExperimentalCoilApi::class)
@WireDestination(
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 9529be9aab8..90f5469d9c4 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -1516,7 +1516,6 @@ registriert. Bitte versuchen Sie es mit einer anderen.
10 Sek
Daten zur Fehlerbehebung
"Dadurch werden anonymisierte Informationen zur Fehlerbehebung lokal gespeichert. "
- "Keine Anwendung zum Öffnen des Download-Ordners gefunden"
Jemand
Hat eine selbstlöschende Nachricht gesendet
Lizenzen
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index ec796779e18..7ac7a78b6bb 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -1418,7 +1418,6 @@ Kérjük, próbálja meg újra.
Hibakeresési információ
Belső hibakeresés
"Ez helyileg tárolja az anonimizált hibaelhárítási információkat. "
- "Nem található alkalmazás a letöltési mappa megnyitásához"
Valaki
Önmegsemmisítő üzenetet küldött
Licenc információ
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 63a5798121f..4aa1f66f56c 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -1098,7 +1098,6 @@ registrato. Sei pregato di riprovare.
Dati di debug
Debug interno
"Questo memorizza localmente le informazioni anonime di risoluzione dei problemi. "
- "Nessun'applicazione trovata per aprire la cartella dei download"
Qualcuno
Ha inviato un messaggio a eliminazione automatica
Informazioni sulla licenza
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 3e8f0ae8a4e..936fbbc949d 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -1358,7 +1358,6 @@ registrado. Tente novamente.
Dados de depuração
Depuração interna
"Isso armazena informações anônimas de solução de problemas localmente. "
- "Nenhum aplicativo encontrado para abrir a pasta de downloads"
Alguém
Enviou uma mensagem de auto-exclusão
Informações de Licença
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 7ea51bdafa8..ce24773f507 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -1658,7 +1658,6 @@
Отладочные данные
Внутренняя отладка
"Это позволяет локально сохранять анонимизированную информацию о неполадках. "
- "Не найдено приложение для открытия папки загрузок"
Кто-то
Отправил самоудаляющееся сообщение
Информация о лицензии
diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml
index f986c5848b6..ccb180d0b43 100644
--- a/app/src/main/res/values-si/strings.xml
+++ b/app/src/main/res/values-si/strings.xml
@@ -1427,7 +1427,6 @@
නිදෝස්කරණ දත්ත
අභ්යන්තර නිදොස්කරණය
"මෙය නිර්නාමික දෝෂ නිරාකරණ තොරතුරු ස්ථානීයව ගබඩා කරයි. "
- "බාගැනීමේ බහාලුම ඇරීමට සුදුසු යෙදුමක් හමු නොවිණි"
යමෙක්
ඉබේ මැකෙන පණිවිඩයක් එවා ඇත
බලපත්රයෙහි තොරතුරු
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 49bd692e4d3..53adc32446f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1624,7 +1624,6 @@ In group conversations, the group admin can overwrite this setting.
Debug data
Internal debugging
"This stores anonymized troubleshooting information locally. "
- "No application found to open downloads folder"
Someone
Sent a self-deleting message
License Information
diff --git a/app/src/main/kotlin/com/wire/android/util/ui/DownloadFolderOpener.kt b/core/ui-common/src/main/kotlin/com/wire/android/util/DownloadFolderOpener.kt
similarity index 79%
rename from app/src/main/kotlin/com/wire/android/util/ui/DownloadFolderOpener.kt
rename to core/ui-common/src/main/kotlin/com/wire/android/util/DownloadFolderOpener.kt
index 8f41eb9505b..5b195a4d2e5 100644
--- a/app/src/main/kotlin/com/wire/android/util/ui/DownloadFolderOpener.kt
+++ b/core/ui-common/src/main/kotlin/com/wire/android/util/DownloadFolderOpener.kt
@@ -1,6 +1,6 @@
/*
* Wire
- * Copyright (C) 2024 Wire Swiss GmbH
+ * Copyright (C) 2025 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,16 +15,19 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
-package com.wire.android.util.ui
+package com.wire.android.util
import android.app.DownloadManager
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.widget.Toast
+import com.wire.android.ui.common.R
-fun openDownloadFolder(context: Context) {
- val errorToastMessage = context.resources.getString(com.wire.android.R.string.label_no_application_found_open_downloads_folder)
+fun openDownloadFolder(
+ context: Context,
+) {
+ val errorToastMessage = context.resources.getString(R.string.label_no_application_found_open_downloads_folder)
try {
context.startActivity(Intent(DownloadManager.ACTION_VIEW_DOWNLOADS))
} catch (e: ActivityNotFoundException) {
diff --git a/core/ui-common/src/main/kotlin/com/wire/android/util/StringUtils.kt b/core/ui-common/src/main/kotlin/com/wire/android/util/StringUtils.kt
index 296c8dcacf2..6b789982c51 100644
--- a/core/ui-common/src/main/kotlin/com/wire/android/util/StringUtils.kt
+++ b/core/ui-common/src/main/kotlin/com/wire/android/util/StringUtils.kt
@@ -47,3 +47,14 @@ fun String.toTitleCase(delimiter: String = " ", separator: String = " "): String
fun String.capitalizeFirstLetter(): String = lowercase().replaceFirstChar(Char::titlecaseChar)
fun String.normalizeFileName(): String = this.replace("/", "")
+
+fun String.addBeforeExtension(insert: String): String {
+ val dotIndex = this.lastIndexOf('.')
+ return if (dotIndex != -1) {
+ val name = this.take(dotIndex)
+ val ext = this.substring(dotIndex)
+ "${name}_$insert$ext"
+ } else {
+ this + insert
+ }
+}
diff --git a/core/ui-common/src/main/res/values-de/strings.xml b/core/ui-common/src/main/res/values-de/strings.xml
index 68d8fa3e08c..bf30dde6901 100644
--- a/core/ui-common/src/main/res/values-de/strings.xml
+++ b/core/ui-common/src/main/res/values-de/strings.xml
@@ -54,4 +54,5 @@
GB
TB
Haken
+ "Keine Anwendung zum Öffnen des Download-Ordners gefunden"
diff --git a/core/ui-common/src/main/res/values-hu/strings.xml b/core/ui-common/src/main/res/values-hu/strings.xml
index bc967ead3f2..ced9cd16ded 100644
--- a/core/ui-common/src/main/res/values-hu/strings.xml
+++ b/core/ui-common/src/main/res/values-hu/strings.xml
@@ -18,4 +18,5 @@
-->
Pipa jel
+ "Nem található alkalmazás a letöltési mappa megnyitásához"
diff --git a/core/ui-common/src/main/res/values-it/strings.xml b/core/ui-common/src/main/res/values-it/strings.xml
index 7d60fd033e6..5a8cd8e7612 100644
--- a/core/ui-common/src/main/res/values-it/strings.xml
+++ b/core/ui-common/src/main/res/values-it/strings.xml
@@ -18,4 +18,5 @@
-->
Segno di spunta
+ "Nessun'applicazione trovata per aprire la cartella dei download"
diff --git a/core/ui-common/src/main/res/values-pt/strings.xml b/core/ui-common/src/main/res/values-pt/strings.xml
index d4b996de5b9..60ccc0f7f86 100644
--- a/core/ui-common/src/main/res/values-pt/strings.xml
+++ b/core/ui-common/src/main/res/values-pt/strings.xml
@@ -18,4 +18,5 @@
-->
Marca de seleção
+ "Nenhum aplicativo encontrado para abrir a pasta de downloads"
diff --git a/core/ui-common/src/main/res/values-ru/strings.xml b/core/ui-common/src/main/res/values-ru/strings.xml
index cbb4a81b0c4..0d0177db174 100644
--- a/core/ui-common/src/main/res/values-ru/strings.xml
+++ b/core/ui-common/src/main/res/values-ru/strings.xml
@@ -61,4 +61,5 @@
GB
TB
Отметка
+ "Не найдено приложение для открытия папки загрузок"
diff --git a/core/ui-common/src/main/res/values-si/strings.xml b/core/ui-common/src/main/res/values-si/strings.xml
index 16c81769eeb..988ad29dd3d 100644
--- a/core/ui-common/src/main/res/values-si/strings.xml
+++ b/core/ui-common/src/main/res/values-si/strings.xml
@@ -18,4 +18,5 @@
-->
හරි ලකුණ
+ "බාගැනීමේ බහාලුම ඇරීමට සුදුසු යෙදුමක් හමු නොවිණි"
diff --git a/core/ui-common/src/main/res/values/strings.xml b/core/ui-common/src/main/res/values/strings.xml
index 0e8cf323cb3..92b118eba05 100644
--- a/core/ui-common/src/main/res/values/strings.xml
+++ b/core/ui-common/src/main/res/values/strings.xml
@@ -58,8 +58,8 @@
Ok
Cancel
No selected date
-
Check mark
+ "No application found to open downloads folder"
B
KB
diff --git a/default.json b/default.json
index 500d8d831c9..86fb3549fbf 100644
--- a/default.json
+++ b/default.json
@@ -72,7 +72,8 @@
"analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438",
"analytics_server_url": "https://wire.count.ly/",
"enable_new_registration": true,
- "use_async_flush_logging": true
+ "use_async_flush_logging": true,
+ "collabora_integration": true
},
"internal": {
"application_id": "com.wire.internal",
diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellScreenContent.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellScreenContent.kt
index f76df14a655..4852da8bd6e 100644
--- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellScreenContent.kt
+++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellScreenContent.kt
@@ -87,7 +87,7 @@ internal fun CellScreenContent(
isSearchResult: Boolean = false,
isFiltering: Boolean = false,
retryEditNodeError: (String) -> Unit = {},
- showVersionHistoryScreen: (String, String) -> Unit = { _, _ -> }
+ showVersionHistoryScreen: (String, String) -> Unit = { _, _ -> },
) {
val context = LocalContext.current
diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/CellVersion.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/CellVersion.kt
index b2ebd5bfd0a..745211335e6 100644
--- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/CellVersion.kt
+++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/CellVersion.kt
@@ -24,7 +24,8 @@ data class CellVersion(
val modifiedAt: String = "",
val modifiedBy: String = "",
val fileSize: String = "",
- val isCurrentVersion: Boolean = false
+ val isCurrentVersion: Boolean = false,
+ val presignedUrl: String? = null
)
data class VersionGroup(
diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionActionBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionActionBottomSheet.kt
index 078703da608..df169cb24fe 100644
--- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionActionBottomSheet.kt
+++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionActionBottomSheet.kt
@@ -38,10 +38,10 @@ import com.wire.android.ui.theme.wireDimensions
@Composable
fun VersionActionBottomSheet(
- sheetState: WireModalSheetState,
+ sheetState: WireModalSheetState>,
onDismiss: () -> Unit,
onRestoreVersionClicked: (String) -> Unit,
- onDownloadVersionClicked: (String) -> Unit,
+ onDownloadVersionClicked: (String, String) -> Unit,
) {
WireModalSheetLayout(
onDismissRequest = onDismiss,
@@ -52,13 +52,13 @@ fun VersionActionBottomSheet(
add {
RestoreVersionModalItem(
title = stringResource(R.string.restore_version_bottom_sheet_item_label),
- onClicked = { onRestoreVersionClicked(state.versionId) },
+ onClicked = { onRestoreVersionClicked(state.second.versionId) },
)
}
add {
DownloadVersionModalItem(
title = stringResource(R.string.download_version_bottom_sheet_item_label),
- onClicked = { onDownloadVersionClicked(state.versionId) },
+ onClicked = { onDownloadVersionClicked(state.second.versionId, state.first) },
)
}
}
@@ -111,10 +111,10 @@ private fun DownloadVersionModalItem(
private fun PreviewCellsOptionsBottomSheet() {
WireTheme {
VersionActionBottomSheet(
- sheetState = rememberWireModalSheetState(WireSheetValue.Expanded(value = CellVersion())),
+ sheetState = rememberWireModalSheetState(WireSheetValue.Expanded(value = "" to CellVersion())),
onDismiss = {},
onRestoreVersionClicked = {},
- onDownloadVersionClicked = {}
+ onDownloadVersionClicked = { _, _ -> }
)
}
}
diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryNavArgs.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryNavArgs.kt
index cfd1a803156..70e3ca879f6 100644
--- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryNavArgs.kt
+++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryNavArgs.kt
@@ -18,6 +18,6 @@
package com.wire.android.feature.cells.ui.versioning
class VersionHistoryNavArgs(
- val uuid: String? = null,
- val fileName: String? = null
+ val uuid: String,
+ val fileName: String
)
diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryScreen.kt
index b01a4e501ad..aea842070ce 100644
--- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryScreen.kt
+++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryScreen.kt
@@ -25,19 +25,24 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import com.wire.android.feature.cells.R
import com.wire.android.feature.cells.ui.common.ErrorScreen
import com.wire.android.feature.cells.ui.common.LoadingScreen
+import com.wire.android.feature.cells.ui.versioning.download.DownloadState
import com.wire.android.feature.cells.ui.versioning.restore.RestoreDialogState
import com.wire.android.feature.cells.ui.versioning.restore.RestoreNodeVersionConfirmationDialog
import com.wire.android.navigation.WireNavigator
@@ -50,10 +55,12 @@ import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.divider.WireDivider
import com.wire.android.ui.common.preview.MultipleThemePreviews
import com.wire.android.ui.common.scaffold.WireScaffold
+import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
import com.wire.android.ui.common.topappbar.NavigationIconType
import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
import com.wire.android.ui.theme.WireTheme
import com.wire.android.ui.theme.wireTypography
+import com.wire.android.util.openDownloadFolder
import com.wire.android.util.ui.toUIText
import kotlinx.coroutines.launch
@@ -67,7 +74,13 @@ fun VersionHistoryScreen(
modifier: Modifier = Modifier,
versionHistoryViewModel: VersionHistoryViewModel = hiltViewModel()
) {
- val optionsBottomSheetState = rememberWireModalSheetState()
+ val optionsBottomSheetState = rememberWireModalSheetState>()
+ val snackbarHostState = LocalSnackbarHostState.current
+ val context = LocalContext.current
+ val downloadState = versionHistoryViewModel.downloadState.value
+ val savedToDownloads = stringResource(R.string.snackbar_download_cell_version_saved_to_downloads_folder_label)
+ val showLabel = stringResource(R.string.snackbar_download_cell_version_show_label)
+ val downloadingLabel = stringResource(R.string.snackbar_download_cell_version_downloading_label)
val scope = rememberCoroutineScope()
VersionHistoryScreenContent(
@@ -81,8 +94,9 @@ fun VersionHistoryScreen(
restoreVersion = {
versionHistoryViewModel.restoreVersion()
},
- downloadVersion = {
+ downloadVersion = { versionId, versionDate ->
optionsBottomSheetState.hide()
+ versionHistoryViewModel.downloadVersion(versionId, versionDate)
},
showRestoreConfirmationDialog = { versionId ->
optionsBottomSheetState.hide()
@@ -91,13 +105,39 @@ fun VersionHistoryScreen(
onDismissRestoreConfirmationDialog = {
versionHistoryViewModel.hideRestoreConfirmationDialog()
},
- onGoToFileClicked = {},
+ onGoToFileClicked = {
+ versionHistoryViewModel.openOnlineEditor()
+ },
onRefresh = {
scope.launch {
versionHistoryViewModel.refreshVersions()
}
}
)
+
+ LaunchedEffect(downloadState) {
+ when (downloadState) {
+ is DownloadState.Downloaded -> {
+ val snackbarResult = snackbarHostState.showSnackbar(
+ message = "\"${downloadState.fileName}\" $savedToDownloads",
+ actionLabel = showLabel,
+ duration = SnackbarDuration.Short,
+ )
+ if (snackbarResult == SnackbarResult.ActionPerformed) {
+ openDownloadFolder(context)
+ }
+ }
+
+ is DownloadState.Downloading -> {
+ snackbarHostState.showSnackbar(
+ message = downloadingLabel,
+ duration = SnackbarDuration.Long,
+ )
+ }
+
+ else -> Unit
+ }
+ }
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -105,18 +145,19 @@ fun VersionHistoryScreen(
private fun VersionHistoryScreenContent(
versionsGroupedByTime: List,
versionHistoryState: State,
- optionsBottomSheetState: WireModalSheetState,
- restoreDialogState: RestoreDialogState,
onRefresh: () -> Unit,
+ optionsBottomSheetState: WireModalSheetState>,
+ restoreDialogState: RestoreDialogState,
modifier: Modifier = Modifier,
fileName: String? = null,
restoreVersion: () -> Unit = {},
- downloadVersion: (String) -> Unit = {},
+ downloadVersion: (String, String) -> Unit = { _, _ -> },
showRestoreConfirmationDialog: (String) -> Unit = {},
onDismissRestoreConfirmationDialog: () -> Unit = {},
onGoToFileClicked: () -> Unit = {},
navigateBack: () -> Unit = {}
) {
+
WireScaffold(
modifier = modifier,
topBar = {
@@ -180,10 +221,11 @@ private fun VersionHistoryScreenContent(
}
group.versions.forEach {
item(it.versionId) {
+ val versionDate = group.dateLabel.asString()
VersionItem(
cellVersion = it,
onActionClick = { cellVersion ->
- optionsBottomSheetState.show(cellVersion)
+ optionsBottomSheetState.show(versionDate to cellVersion)
}
)
WireDivider(
@@ -216,6 +258,25 @@ private fun VersionHistoryScreenContent(
}
}
}
+
+ VersionActionBottomSheet(
+ sheetState = optionsBottomSheetState,
+ onDismiss = { optionsBottomSheetState.hide() },
+ onRestoreVersionClicked = showRestoreConfirmationDialog,
+ onDownloadVersionClicked = downloadVersion
+ )
+
+ with(restoreDialogState) {
+ if (visible) {
+ RestoreNodeVersionConfirmationDialog(
+ restoreVersionState = restoreVersionState,
+ restoreProgress = restoreProgress,
+ onConfirm = restoreVersion,
+ onDismiss = onDismissRestoreConfirmationDialog,
+ onGoToFileClicked = onGoToFileClicked,
+ )
+ }
+ }
}
@MultipleThemePreviews
@@ -243,7 +304,7 @@ fun PreviewVersionHistoryScreenContent() {
)
)
),
- optionsBottomSheetState = rememberWireModalSheetState(),
+ optionsBottomSheetState = rememberWireModalSheetState>(),
restoreDialogState = RestoreDialogState(),
onRefresh = {}
)
diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModel.kt
index 7e17a7ce667..e958024f333 100644
--- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModel.kt
+++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModel.kt
@@ -23,12 +23,18 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.feature.cells.R
+import com.wire.android.feature.cells.ui.edit.OnlineEditor
import com.wire.android.feature.cells.ui.navArgs
+import com.wire.android.feature.cells.ui.versioning.download.DownloadState
import com.wire.android.feature.cells.ui.versioning.restore.RestoreDialogState
import com.wire.android.feature.cells.ui.versioning.restore.RestoreVersionState
+import com.wire.android.feature.cells.util.FileHelper
import com.wire.android.util.FileSizeFormatter
+import com.wire.android.util.addBeforeExtension
+import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.android.util.ui.UIText
import com.wire.kalium.cells.domain.model.NodeVersion
+import com.wire.kalium.cells.domain.usecase.DownloadCellVersionUseCase
import com.wire.kalium.cells.domain.usecase.versioning.GetNodeVersionsUseCase
import com.wire.kalium.cells.domain.usecase.versioning.RestoreNodeVersionUseCase
import com.wire.kalium.common.functional.onFailure
@@ -36,6 +42,8 @@ import com.wire.kalium.common.functional.onSuccess
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okio.sink
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
@@ -49,6 +57,10 @@ class VersionHistoryViewModel @Inject constructor(
private val getNodeVersionsUseCase: GetNodeVersionsUseCase,
private val fileSizeFormatter: FileSizeFormatter,
private val restoreNodeVersionUseCase: RestoreNodeVersionUseCase,
+ private val downloadCellVersionUseCase: DownloadCellVersionUseCase,
+ private val fileHelper: FileHelper,
+ private val onlineEditor: OnlineEditor,
+ private val dispatchers: DispatcherProvider,
) : ViewModel() {
private val navArgs: VersionHistoryNavArgs = savedStateHandle.navArgs()
@@ -61,10 +73,17 @@ class VersionHistoryViewModel @Inject constructor(
var versionsGroupedByTime: MutableState> = mutableStateOf(listOf())
private set
- var restoreDialogState: MutableState =
- mutableStateOf(RestoreDialogState())
+ var restoreDialogState: MutableState = mutableStateOf(RestoreDialogState())
+ private set
+
+ var downloadState: MutableState = mutableStateOf(DownloadState.Idle)
+ private set
init {
+ initVersions()
+ }
+
+ fun initVersions() {
viewModelScope.launch {
versionHistoryState.value = VersionHistoryState.Loading
fetchNodeVersionsGroupedByDate()
@@ -77,17 +96,15 @@ class VersionHistoryViewModel @Inject constructor(
}
private suspend fun fetchNodeVersionsGroupedByDate() =
- navArgs.uuid?.let {
- getNodeVersionsUseCase(navArgs.uuid)
- .onSuccess {
- versionHistoryState.value = VersionHistoryState.Success
- versionsGroupedByTime.value = it.groupByDay()
- }
- // TODO: Handle error on UI
- .onFailure {
- versionHistoryState.value = VersionHistoryState.Failed
- }
- }
+ getNodeVersionsUseCase(navArgs.uuid)
+ .onSuccess {
+ versionHistoryState.value = VersionHistoryState.Success
+ versionsGroupedByTime.value = it.groupByDay()
+ }
+ // TODO: Handle error on UI
+ .onFailure {
+ versionHistoryState.value = VersionHistoryState.Failed
+ }
private fun List.groupByDay(): List {
val today = LocalDate.now()
@@ -131,7 +148,8 @@ class VersionHistoryViewModel @Inject constructor(
modifiedBy = apiItem.ownerName ?: "",
fileSize = fileSizeFormatter.formatSize(apiItem.size?.toLong() ?: 0),
modifiedAt = formattedTime,
- isCurrentVersion = groupIndex == 0 && itemIndex == 0
+ isCurrentVersion = groupIndex == 0 && itemIndex == 0,
+ presignedUrl = apiItem.getUrl?.url
)
}
VersionGroup(dateLabel, uiItems)
@@ -170,7 +188,7 @@ class VersionHistoryViewModel @Inject constructor(
restoreNodeVersionUseCase(navArgs.uuid ?: "", value.versionId)
.onSuccess {
delay(DELAY_500_MS) // delay since server takes some time to restore the version
- refreshVersions()
+ initVersions()
progressJob.cancel()
restoreDialogState.value = value.copy(
restoreVersionState = RestoreVersionState.Completed,
@@ -187,6 +205,57 @@ class VersionHistoryViewModel @Inject constructor(
}
}
+ // TODO: Unit test coming in another PR
+ fun downloadVersion(versionId: String, versionDate: String) {
+ viewModelScope.launch {
+ downloadState.value = DownloadState.Downloading(0, 0)
+
+ val cellVersion = findVersionById(versionId)
+ ?: return@launch run { downloadState.value = DownloadState.Failed }
+
+ val newFileName = fileName.addBeforeExtension("${versionDate}_${cellVersion.modifiedAt}")
+
+ val outputStream = withContext(dispatchers.io()) {
+ fileHelper.createDownloadFileStream(newFileName)
+ } ?: run {
+ downloadState.value = DownloadState.Failed
+ return@launch
+ }
+
+ val presignedUrl = cellVersion.presignedUrl
+ ?: return@launch run { downloadState.value = DownloadState.Failed }
+
+ outputStream.sink().use { sink ->
+ downloadCellVersionUseCase(
+ bufferedSink = sink,
+ preSignedUrl = presignedUrl,
+ onProgressUpdate = { progress, total ->
+ downloadState.value = DownloadState.Downloading(progress.toInt(), total)
+ }
+ )
+ .onSuccess {
+ downloadState.value = DownloadState.Downloaded(newFileName)
+ }
+ .onFailure {
+ downloadState.value = DownloadState.Failed
+ }
+ }
+ }
+ }
+
+ fun openOnlineEditor() {
+ val cellVersion = findVersionById(restoreDialogState.value.versionId)
+ cellVersion?.presignedUrl?.let {
+ onlineEditor.open(it)
+ }
+ }
+
+ private fun findVersionById(versionId: String): CellVersion? {
+ return versionsGroupedByTime.value
+ .flatMap { it.versions }
+ .find { it.versionId == versionId }
+ }
+
@Suppress("MagicNumber")
private fun simulateRestoreProgress() = viewModelScope.launch {
with(restoreDialogState) {
diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/download/DownloadState.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/download/DownloadState.kt
new file mode 100644
index 00000000000..90f34a513d3
--- /dev/null
+++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/download/DownloadState.kt
@@ -0,0 +1,25 @@
+/*
+ * Wire
+ * Copyright (C) 2025 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.feature.cells.ui.versioning.download
+
+sealed class DownloadState {
+ data object Idle : DownloadState()
+ data class Downloading(val progress: Int, val total: Long) : DownloadState()
+ data class Downloaded(val fileName: String) : DownloadState()
+ data object Failed : DownloadState()
+}
diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/restore/RestoreNodeVersionConfirmationDialog.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/restore/RestoreNodeVersionConfirmationDialog.kt
index 93d2ab74de2..90515c62059 100644
--- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/restore/RestoreNodeVersionConfirmationDialog.kt
+++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/restore/RestoreNodeVersionConfirmationDialog.kt
@@ -91,9 +91,9 @@ fun RestoreNodeVersionConfirmationDialog(
) {
when (restoreVersionState) {
RestoreVersionState.Completed, RestoreVersionState.Restoring -> GoToFileContent(
- restoreVersionState,
- restoreProgress,
- onGoToFileClicked
+ restoreVersionState = restoreVersionState,
+ restoreProgress = restoreProgress,
+ onGoToFileClicked = onGoToFileClicked
)
RestoreVersionState.Failed -> RestoreFailedContent(
diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/restore/RestoreVersionState.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/restore/RestoreVersionState.kt
index 09afce66b29..27d998be6ed 100644
--- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/restore/RestoreVersionState.kt
+++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/restore/RestoreVersionState.kt
@@ -15,6 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
+
package com.wire.android.feature.cells.ui.versioning.restore
enum class RestoreVersionState {
diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/util/FileHelper.kt b/features/cells/src/main/java/com/wire/android/feature/cells/util/FileHelper.kt
index 1ce3ed7b1c8..466961796ef 100644
--- a/features/cells/src/main/java/com/wire/android/feature/cells/util/FileHelper.kt
+++ b/features/cells/src/main/java/com/wire/android/feature/cells/util/FileHelper.kt
@@ -18,18 +18,50 @@
package com.wire.android.feature.cells.util
import android.content.ActivityNotFoundException
+import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
import androidx.core.content.FileProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import okio.Path
+import java.io.File
+import java.io.FileOutputStream
+import java.io.OutputStream
import javax.inject.Inject
class FileHelper @Inject constructor(
@ApplicationContext private val context: Context
) {
+ fun createDownloadFileStream(nodeName: String): OutputStream? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // Android 10+
+ val values = ContentValues().apply {
+ put(MediaStore.Downloads.DISPLAY_NAME, nodeName)
+ put(MediaStore.Downloads.MIME_TYPE, "application/octet-stream")
+ put(
+ MediaStore.Downloads.RELATIVE_PATH,
+ Environment.DIRECTORY_DOWNLOADS
+ )
+ }
+
+ val uri = context.contentResolver.insert(
+ MediaStore.Downloads.EXTERNAL_CONTENT_URI,
+ values
+ )
+ uri?.let { context.contentResolver.openOutputStream(it) }
+ } else {
+ // Android 8–9 (API 26–28)
+ val downloadsDir =
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+
+ val file = File(downloadsDir, nodeName)
+ FileOutputStream(file)
+ }
+
fun openAssetFileWithExternalApp(
localPath: Path,
assetName: String?,
diff --git a/features/cells/src/main/res/values/strings.xml b/features/cells/src/main/res/values/strings.xml
index b24e598c5f9..11ac27a2579 100644
--- a/features/cells/src/main/res/values/strings.xml
+++ b/features/cells/src/main/res/values/strings.xml
@@ -191,4 +191,7 @@
Yesterday, %1$s
Failed to load versions
Something went wrong while loading version list. Please retry.
+ saved to Downloads
+ Show
+ Downloading…
diff --git a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModelTest.kt b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModelTest.kt
index d2dc9c39998..327878a19c3 100644
--- a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModelTest.kt
+++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModelTest.kt
@@ -19,10 +19,14 @@ package com.wire.android.feature.cells.ui.versioning
import androidx.lifecycle.SavedStateHandle
import com.wire.android.feature.cells.R
+import com.wire.android.feature.cells.ui.edit.OnlineEditor
+import com.wire.android.feature.cells.util.FileHelper
import com.wire.android.util.FileSizeFormatter
+import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.android.util.ui.resolveForTest
import com.wire.android.util.ui.toUIText
import com.wire.kalium.cells.domain.model.NodeVersion
+import com.wire.kalium.cells.domain.usecase.DownloadCellVersionUseCase
import com.wire.kalium.cells.domain.usecase.versioning.GetNodeVersionsUseCase
import com.wire.kalium.cells.domain.usecase.versioning.RestoreNodeVersionUseCase
import com.wire.kalium.common.error.CoreFailure
@@ -57,8 +61,11 @@ class VersionHistoryViewModelTest {
private val savedStateHandle: SavedStateHandle = mockk(relaxed = true)
private val getNodeVersionsUseCase: GetNodeVersionsUseCase = mockk()
private val restoreNodeVersionUseCase: RestoreNodeVersionUseCase = mockk()
+ private val downloadCellVersionUseCase: DownloadCellVersionUseCase = mockk()
private val fileSizeFormatter: FileSizeFormatter = mockk()
-
+ val fileHelper: FileHelper = mockk()
+ val onlineEditor: OnlineEditor = mockk()
+ private val testDispatcherProvider: DispatcherProvider = mockk()
private val testNodeUuid = "test-node-uuid"
@BeforeEach
@@ -78,7 +85,16 @@ class VersionHistoryViewModelTest {
fun givenViewModel_whenItInits_thenIsFetchingStateIsManagedCorrectly() = runTest {
coEvery { getNodeVersionsUseCase(testNodeUuid) } returns Either.Right(emptyList())
- val viewModel = VersionHistoryViewModel(savedStateHandle, getNodeVersionsUseCase, fileSizeFormatter, restoreNodeVersionUseCase)
+ val viewModel = VersionHistoryViewModel(
+ savedStateHandle = savedStateHandle,
+ getNodeVersionsUseCase = getNodeVersionsUseCase,
+ fileSizeFormatter = fileSizeFormatter,
+ restoreNodeVersionUseCase = restoreNodeVersionUseCase,
+ downloadCellVersionUseCase = downloadCellVersionUseCase,
+ fileHelper = fileHelper,
+ onlineEditor = onlineEditor,
+ dispatchers = testDispatcherProvider,
+ )
assertEquals(VersionHistoryState.Idle, viewModel.versionHistoryState.value)
advanceUntilIdle()
@@ -131,7 +147,17 @@ class VersionHistoryViewModelTest {
coEvery { getNodeVersionsUseCase(testNodeUuid) } returns Either.Right(versionsFromApi)
every { fileSizeFormatter.formatSize(any()) } returns "30 MB"
- val viewModel = VersionHistoryViewModel(savedStateHandle, getNodeVersionsUseCase, fileSizeFormatter, restoreNodeVersionUseCase)
+ val viewModel = VersionHistoryViewModel(
+ savedStateHandle = savedStateHandle,
+ getNodeVersionsUseCase = getNodeVersionsUseCase,
+ fileSizeFormatter = fileSizeFormatter,
+ restoreNodeVersionUseCase = restoreNodeVersionUseCase,
+ downloadCellVersionUseCase = downloadCellVersionUseCase,
+ fileHelper = fileHelper,
+ onlineEditor = onlineEditor,
+ dispatchers = testDispatcherProvider,
+ )
+
testDispatcher.scheduler.advanceUntilIdle()
// Versions should be grouped into three sections (Today, Yesterday, and an older date)
@@ -176,21 +202,19 @@ class VersionHistoryViewModelTest {
fun givenApiFailure_whenViewModelInits_thenVersionListIsEmpty() = runTest {
coEvery { getNodeVersionsUseCase(testNodeUuid) } returns Either.Left(CoreFailure.MissingClientRegistration)
- val viewModel = VersionHistoryViewModel(savedStateHandle, getNodeVersionsUseCase, fileSizeFormatter, restoreNodeVersionUseCase)
+ val viewModel = VersionHistoryViewModel(
+ savedStateHandle = savedStateHandle,
+ getNodeVersionsUseCase = getNodeVersionsUseCase,
+ fileSizeFormatter = fileSizeFormatter,
+ restoreNodeVersionUseCase = restoreNodeVersionUseCase,
+ downloadCellVersionUseCase = downloadCellVersionUseCase,
+ fileHelper = fileHelper,
+ onlineEditor = onlineEditor,
+ dispatchers = testDispatcherProvider,
+ )
advanceUntilIdle()
assertTrue(viewModel.versionsGroupedByTime.value.isEmpty())
assertEquals(VersionHistoryState.Failed, viewModel.versionHistoryState.value)
}
-
- @Test
- fun givenMissingUuid_whenViewModelInits_thenNoFetchIsAttempted() = runTest {
- every { savedStateHandle.get("uuid") } returns null
-
- val viewModel = VersionHistoryViewModel(savedStateHandle, getNodeVersionsUseCase, fileSizeFormatter, restoreNodeVersionUseCase)
- advanceUntilIdle()
-
- assertTrue(viewModel.versionsGroupedByTime.value.isEmpty())
- assertEquals(VersionHistoryState.Loading, viewModel.versionHistoryState.value)
- }
}
diff --git a/kalium b/kalium
index 35685adde44..c15237a855f 160000
--- a/kalium
+++ b/kalium
@@ -1 +1 @@
-Subproject commit 35685adde4487d788ad049ebe6fadc456e091605
+Subproject commit c15237a855f52138ad00746b34285efd6cf6725f