Skip to content

Commit ed4336d

Browse files
authored
feat(drive): Make files available offline (WPB-23968) (#4811)
1 parent 13c6808 commit ed4336d

35 files changed

Lines changed: 1682 additions & 327 deletions

app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ import com.wire.kalium.cells.domain.usecase.publiclink.SetPublicLinkExpirationUs
6262
import com.wire.kalium.cells.domain.usecase.publiclink.UpdatePublicLinkPasswordUseCase
6363
import com.wire.kalium.cells.domain.usecase.versioning.GetNodeVersionsUseCase
6464
import com.wire.kalium.cells.domain.usecase.versioning.RestoreNodeVersionUseCase
65+
import com.wire.kalium.cells.domain.usecase.GetConversationNameUseCase
66+
import com.wire.kalium.cells.domain.usecase.GetUserNameUseCase
67+
import com.wire.kalium.cells.domain.usecase.offline.DeleteOfflineFileUseCase
68+
import com.wire.kalium.cells.domain.usecase.offline.GetOfflineFileUseCase
69+
import com.wire.kalium.cells.domain.usecase.offline.ObserveOfflineFilesUseCase
70+
import com.wire.kalium.cells.domain.usecase.offline.SaveOfflineFileUseCase
6571
import com.wire.kalium.cells.paginatedConversationsFlowUseCase
6672
import com.wire.kalium.cells.paginatedFilesFlowUseCase
6773
import com.wire.kalium.logic.CoreLogic
@@ -259,4 +265,28 @@ class CellsModule {
259265
@Provides
260266
fun provideGetPaginatedConversationsFlowUseCase(cellsScope: CellsScope): GetPaginatedCellConversationsFlowUseCase =
261267
cellsScope.paginatedConversationsFlowUseCase
268+
269+
@ViewModelScoped
270+
@Provides
271+
fun provideSaveOfflineFileUseCase(cellsScope: CellsScope): SaveOfflineFileUseCase = cellsScope.saveOfflineFile
272+
273+
@ViewModelScoped
274+
@Provides
275+
fun provideDeleteOfflineFileUseCase(cellsScope: CellsScope): DeleteOfflineFileUseCase = cellsScope.deleteOfflineFile
276+
277+
@ViewModelScoped
278+
@Provides
279+
fun provideObserveOfflineFilesUseCase(cellsScope: CellsScope): ObserveOfflineFilesUseCase = cellsScope.observeOfflineFiles
280+
281+
@ViewModelScoped
282+
@Provides
283+
fun provideGetOfflineFileUseCase(cellsScope: CellsScope): GetOfflineFileUseCase = cellsScope.getOfflineFile
284+
285+
@ViewModelScoped
286+
@Provides
287+
fun provideGetConversationNamesUseCase(cellsScope: CellsScope): GetConversationNameUseCase = cellsScope.getConversationName
288+
289+
@ViewModelScoped
290+
@Provides
291+
fun provideGetUserNamesUseCase(cellsScope: CellsScope): GetUserNameUseCase = cellsScope.getUserName
262292
}

app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ import com.wire.android.feature.cells.ui.edit.OnlineEditor
2929
import com.wire.android.ui.common.multipart.MultipartAttachmentUi
3030
import com.wire.android.ui.common.multipart.toUiModel
3131
import com.wire.android.util.FileManager
32-
import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase
3332
import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase
3433
import com.wire.kalium.cells.domain.usecase.GetWireCellConfigurationUseCase
34+
import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase
3535
import com.wire.kalium.common.functional.onSuccess
3636
import com.wire.kalium.logic.data.asset.AssetTransferStatus
3737
import com.wire.kalium.logic.data.asset.KaliumFileSystem
@@ -122,7 +122,11 @@ class MultipartAttachmentsViewModelImpl(
122122
attachment.isImage() && !attachment.fileNotFound() -> openInImageViewer(attachment.uuid)
123123
attachment.isEditSupported && isCollaboraEnabled && featureFlags.collaboraIntegration ->
124124
openOnlineEditor(attachment.uuid)
125-
attachment.fileNotFound() -> { refreshHelper.refresh(attachment.uuid) }
125+
126+
attachment.fileNotFound() -> {
127+
refreshHelper.refresh(attachment.uuid)
128+
}
129+
126130
attachment.localFileAvailable() -> openLocalFile(attachment)
127131
attachment.canOpenWithUrl() -> openUrl(attachment)
128132
else -> downloadAsset(attachment)
@@ -167,6 +171,7 @@ class MultipartAttachmentsViewModelImpl(
167171

168172
download(
169173
assetId = attachment.uuid,
174+
conversationId = null, // TODO to replace with real conversation id in next PR
170175
outFilePath = path,
171176
assetSize = attachment.assetSize ?: 0,
172177
) { progress ->

app/src/test/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModelTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ class MultipartAttachmentsViewModelTest {
258258
coEvery { refreshHelper.refresh(any()) } returns Unit
259259
coEvery { fileManager.openWithExternalApp(any(), any(), any(), any()) } returns Unit
260260
coEvery { fileManager.openUrlWithExternalApp(any(), any(), any()) } returns Unit
261-
coEvery { download(any(), any(), any(), any(), any()) } returns Unit.right()
261+
coEvery { download(any(), any(), any(), any(), any(), any(), any(), any()) } returns Unit.right()
262262
coEvery { getWireCellsConfig() } returns null
263263

264264
return this to MultipartAttachmentsViewModelImpl(

features/cells/src/main/java/com/wire/android/feature/cells/ui/AllFilesScreen.kt

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,21 @@
1717
*/
1818
package com.wire.android.feature.cells.ui
1919

20+
import androidx.compose.animation.AnimatedContent
2021
import androidx.compose.foundation.layout.Column
2122
import androidx.compose.foundation.layout.padding
2223
import androidx.compose.foundation.text.input.rememberTextFieldState
2324
import androidx.compose.runtime.Composable
2425
import androidx.compose.runtime.collectAsState
26+
import androidx.compose.runtime.getValue
2527
import androidx.compose.ui.Modifier
2628
import androidx.compose.ui.res.stringResource
2729
import androidx.paging.compose.collectAsLazyPagingItems
2830
import com.ramcosta.composedestinations.generated.cells.destinations.AddRemoveTagsScreenDestination
2931
import com.ramcosta.composedestinations.generated.cells.destinations.PublicLinkScreenDestination
3032
import com.ramcosta.composedestinations.generated.cells.destinations.SearchScreenDestination
3133
import com.wire.android.feature.cells.R
34+
import com.wire.android.feature.cells.ui.common.OfflineBanner
3235
import com.wire.android.feature.cells.ui.search.DriveSearchScreenType
3336
import com.wire.android.navigation.NavigationCommand
3437
import com.wire.android.navigation.WireNavigator
@@ -47,26 +50,33 @@ fun AllFilesScreen(
4750
) {
4851

4952
val pagingListItems = viewModel.nodesFlow.collectAsLazyPagingItems()
53+
val isOnline by viewModel.isOnline.collectAsState()
5054

5155
WireScaffold(
5256
modifier = modifier,
5357
topBar = {
5458
Column {
55-
SearchTopBar(
56-
modifier = Modifier,
57-
isSearchActive = false,
58-
searchBarHint = stringResource(R.string.search_label),
59-
searchQueryTextState = rememberTextFieldState(),
60-
onTap = {
61-
navigator.navigate(
62-
NavigationCommand(
63-
SearchScreenDestination(
64-
screenType = DriveSearchScreenType.DRIVE,
59+
AnimatedContent(isOnline) {
60+
if (it) {
61+
SearchTopBar(
62+
modifier = Modifier,
63+
isSearchActive = false,
64+
searchBarHint = stringResource(R.string.search_label),
65+
searchQueryTextState = rememberTextFieldState(),
66+
onTap = {
67+
navigator.navigate(
68+
NavigationCommand(
69+
SearchScreenDestination(
70+
screenType = DriveSearchScreenType.DRIVE,
71+
)
72+
)
6573
)
66-
)
74+
},
6775
)
68-
},
69-
)
76+
} else {
77+
OfflineBanner()
78+
}
79+
}
7080
}
7181
},
7282
) { innerPadding ->
@@ -78,6 +88,7 @@ fun AllFilesScreen(
7888
openFolder = { _, _, _ -> },
7989
menuState = viewModel.menu,
8090
isAllFiles = true,
91+
isOffline = !isOnline,
8192
isRestoreInProgress = viewModel.isRestoreInProgress.collectAsState().value,
8293
isDeleteInProgress = viewModel.isDeleteInProgress.collectAsState().value,
8394
isRecycleBin = viewModel.isRecycleBin(),

features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileActionsMenu.kt

Lines changed: 124 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package com.wire.android.feature.cells.ui
1919

2020
import com.wire.android.feature.cells.ui.model.CellNodeUi
2121
import com.wire.android.feature.cells.ui.model.NodeBottomSheetAction
22-
import com.wire.android.feature.cells.ui.model.OpenLoadState
2322
import com.wire.android.feature.cells.ui.model.isEditSupported
2423
import com.wire.android.feature.cells.ui.model.localFileAvailable
2524
import com.wire.kalium.logic.featureFlags.KaliumConfigs
@@ -36,64 +35,131 @@ class CellFileActionsMenu @Inject constructor(
3635
isAllFiles: Boolean,
3736
isSearching: Boolean,
3837
isCollaboraEnabled: Boolean,
39-
): List<NodeBottomSheetAction> =
40-
when {
41-
isRecycleBin -> {
42-
buildList {
43-
add(NodeBottomSheetAction.RESTORE)
44-
add(NodeBottomSheetAction.DELETE_PERMANENTLY)
38+
isOnline: Boolean = true,
39+
): List<NodeBottomSheetAction> {
40+
if (!isOnline) {
41+
return buildList {
42+
val canOpenOffline = cellNode is CellNodeUi.Folder ||
43+
(cellNode is CellNodeUi.File && cellNode.localFileAvailable())
44+
if (canOpenOffline) {
45+
add(NodeBottomSheetAction.OPEN)
46+
}
47+
if (cellNode is CellNodeUi.File && cellNode.isAvailableOffline) {
48+
add(NodeBottomSheetAction.REMOVE_OFFLINE_ACCESS)
4549
}
4650
}
51+
}
52+
return when {
53+
isRecycleBin -> recycleBinActions()
4754

4855
isAllFiles || isSearching -> {
49-
buildList {
50-
if (cellNode is CellNodeUi.File && cellNode.openLoadState is OpenLoadState.Loading) {
51-
add(NodeBottomSheetAction.CANCEL_LOADING)
52-
} else {
53-
if (cellNode is CellNodeUi.File && cellNode.localFileAvailable()) {
54-
add(NodeBottomSheetAction.SHARE)
55-
}
56-
add(NodeBottomSheetAction.PUBLIC_LINK)
57-
}
58-
}
56+
commonActions(cellNode)
5957
}
6058

6159
isConversationFiles -> {
62-
buildList {
63-
if (cellNode is CellNodeUi.File && cellNode.openLoadState is OpenLoadState.Loading) {
64-
add(NodeBottomSheetAction.CANCEL_LOADING)
65-
} else {
66-
if (cellNode is CellNodeUi.File && cellNode.localFileAvailable()) {
67-
add(NodeBottomSheetAction.SHARE)
68-
}
69-
add(NodeBottomSheetAction.PUBLIC_LINK)
70-
71-
if (isCollaboraEnabled && featureFlags.collaboraIntegration && cellNode.isEditSupported()) {
72-
add(NodeBottomSheetAction.EDIT)
73-
}
74-
75-
if (featureFlags.collaboraIntegration && cellNode.isEditSupported()) {
76-
add(NodeBottomSheetAction.VERSION_HISTORY)
77-
}
78-
79-
add(NodeBottomSheetAction.ADD_REMOVE_TAGS)
80-
add(NodeBottomSheetAction.MOVE)
81-
add(NodeBottomSheetAction.RENAME)
82-
add(NodeBottomSheetAction.DELETE)
83-
}
60+
val common = commonActions(cellNode)
61+
val isTerminal = cellNode is CellNodeUi.File &&
62+
(cellNode.isOpenLoading || cellNode.downloadProgress != null)
63+
if (isTerminal) {
64+
common
65+
} else {
66+
common + conversationActions(
67+
cellNode = cellNode,
68+
isCollaboraEnabled = isCollaboraEnabled,
69+
)
8470
}
8571
}
8672

87-
else -> {
88-
emptyList()
73+
else -> emptyList()
74+
}
75+
}
76+
77+
private fun recycleBinActions(): List<NodeBottomSheetAction> = listOf(
78+
NodeBottomSheetAction.RESTORE,
79+
NodeBottomSheetAction.DELETE_PERMANENTLY,
80+
)
81+
82+
private fun commonActions(
83+
cellNode: CellNodeUi,
84+
): List<NodeBottomSheetAction> = buildList {
85+
86+
if (cellNode is CellNodeUi.File) {
87+
88+
when {
89+
cellNode.isOpenLoading -> {
90+
add(NodeBottomSheetAction.CANCEL_LOADING)
91+
return@buildList
92+
}
93+
94+
cellNode.downloadProgress != null -> {
95+
add(NodeBottomSheetAction.CANCEL_DOWNLOAD)
96+
return@buildList
97+
}
98+
99+
else -> {
100+
101+
add(NodeBottomSheetAction.OPEN)
102+
103+
if (cellNode.localFileAvailable()) {
104+
add(NodeBottomSheetAction.SHARE)
105+
}
106+
107+
add(
108+
if (cellNode.isAvailableOffline) {
109+
NodeBottomSheetAction.REMOVE_OFFLINE_ACCESS
110+
} else {
111+
NodeBottomSheetAction.MAKE_AVAILABLE_OFFLINE
112+
},
113+
)
114+
}
89115
}
116+
} else {
117+
add(NodeBottomSheetAction.OPEN)
118+
}
119+
}
120+
121+
private fun conversationActions(
122+
cellNode: CellNodeUi,
123+
isCollaboraEnabled: Boolean,
124+
): List<NodeBottomSheetAction> = buildList {
125+
126+
val canEdit = cellNode is CellNodeUi.File &&
127+
isCollaboraEnabled &&
128+
featureFlags.collaboraIntegration &&
129+
cellNode.isEditSupported()
130+
131+
if (canEdit) {
132+
add(NodeBottomSheetAction.EDIT)
133+
}
134+
135+
if (
136+
cellNode is CellNodeUi.File &&
137+
featureFlags.collaboraIntegration &&
138+
cellNode.isEditSupported()
139+
) {
140+
add(NodeBottomSheetAction.VERSION_HISTORY)
90141
}
91142

143+
addAll(
144+
listOf(
145+
NodeBottomSheetAction.ADD_REMOVE_TAGS,
146+
NodeBottomSheetAction.PUBLIC_LINK,
147+
NodeBottomSheetAction.MOVE,
148+
NodeBottomSheetAction.RENAME,
149+
NodeBottomSheetAction.DELETE,
150+
),
151+
)
152+
}
153+
92154
internal sealed interface MenuActionResult
93155
internal data class Action(val action: CellViewAction) : MenuActionResult
156+
internal data class Open(val node: CellNodeUi) : MenuActionResult
94157
internal data class Share(val node: CellNodeUi.File) : MenuActionResult
95158
internal data class Edit(val node: CellNodeUi) : MenuActionResult
96159
internal data class CancelLoading(val node: CellNodeUi) : MenuActionResult
160+
internal data class CancelDownload(val node: CellNodeUi) : MenuActionResult
161+
internal data class MakeAvailableOffline(val node: CellNodeUi.File) : MenuActionResult
162+
internal data class RemoveOfflineAccess(val node: CellNodeUi.File) : MenuActionResult
97163

98164
internal fun onMenuItemAction(
99165
conversationId: String?,
@@ -103,6 +169,7 @@ class CellFileActionsMenu @Inject constructor(
103169
onResult: (MenuActionResult) -> Unit,
104170
) {
105171
val result = when (action) {
172+
NodeBottomSheetAction.OPEN -> Open(node)
106173
NodeBottomSheetAction.SHARE -> {
107174
if (node is CellNodeUi.File) {
108175
Share(node)
@@ -137,6 +204,22 @@ class CellFileActionsMenu @Inject constructor(
137204
NodeBottomSheetAction.EDIT -> Edit(node)
138205
NodeBottomSheetAction.VERSION_HISTORY -> Action(ShowVersionHistoryScreen(node.uuid, node.name ?: ""))
139206
NodeBottomSheetAction.CANCEL_LOADING -> CancelLoading(node)
207+
NodeBottomSheetAction.CANCEL_DOWNLOAD -> CancelDownload(node)
208+
NodeBottomSheetAction.MAKE_AVAILABLE_OFFLINE -> {
209+
if (node is CellNodeUi.File) {
210+
MakeAvailableOffline(node)
211+
} else {
212+
Action(ShowPublicLinkScreen(node))
213+
}
214+
}
215+
216+
NodeBottomSheetAction.REMOVE_OFFLINE_ACCESS -> {
217+
if (node is CellNodeUi.File) {
218+
RemoveOfflineAccess(node)
219+
} else {
220+
Action(ShowPublicLinkScreen(node))
221+
}
222+
}
140223
}
141224

142225
onResult(result)

0 commit comments

Comments
 (0)