From 15042f200e0a5f707063411aff113d9a0fe2bb56 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 12 May 2026 17:15:29 +0200 Subject: [PATCH 01/13] feat: display offline files --- .../android/di/accountScoped/CellsModule.kt | 10 ++ .../feature/cells/ui/AllFilesScreen.kt | 55 +++++++--- .../feature/cells/ui/CellFileActionsMenu.kt | 19 ++++ .../android/feature/cells/ui/CellViewModel.kt | 100 ++++++++++++++++-- .../cells/ui/ConversationFilesScreen.kt | 44 +++++--- .../cells/ui/OfflineFileDownloadController.kt | 4 + .../feature/cells/ui/common/OfflineBanner.kt | 79 ++++++++++++++ .../feature/cells/ui/model/CellNodeUi.kt | 2 + .../cells/ui/model/NodeBottomSheetAction.kt | 3 +- .../main/res/drawable/ic_cross_in_circle.xml | 30 ++++++ .../cells/src/main/res/drawable/ic_open.xml | 24 +++++ .../src/main/res/drawable/ic_wifi_signal.xml | 28 +++++ .../cells/src/main/res/values/strings.xml | 2 + kalium | 2 +- 14 files changed, 364 insertions(+), 38 deletions(-) create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/common/OfflineBanner.kt create mode 100644 features/cells/src/main/res/drawable/ic_cross_in_circle.xml create mode 100644 features/cells/src/main/res/drawable/ic_open.xml create mode 100644 features/cells/src/main/res/drawable/ic_wifi_signal.xml 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 f710aae5bf9..fa4370b406b 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 @@ -62,6 +62,8 @@ import com.wire.kalium.cells.domain.usecase.publiclink.SetPublicLinkExpirationUs import com.wire.kalium.cells.domain.usecase.publiclink.UpdatePublicLinkPasswordUseCase import com.wire.kalium.cells.domain.usecase.versioning.GetNodeVersionsUseCase import com.wire.kalium.cells.domain.usecase.versioning.RestoreNodeVersionUseCase +import com.wire.kalium.cells.domain.usecase.GetConversationNamesUseCase +import com.wire.kalium.cells.domain.usecase.GetUserNamesUseCase import com.wire.kalium.cells.domain.usecase.offline.DeleteOfflineFileUseCase import com.wire.kalium.cells.domain.usecase.offline.GetOfflineFileUseCase import com.wire.kalium.cells.domain.usecase.offline.ObserveOfflineFilesUseCase @@ -279,4 +281,12 @@ class CellsModule { @ViewModelScoped @Provides fun provideGetOfflineFileUseCase(cellsScope: CellsScope): GetOfflineFileUseCase = cellsScope.getOfflineFile + + @ViewModelScoped + @Provides + fun provideGetConversationNamesUseCase(cellsScope: CellsScope): GetConversationNamesUseCase = cellsScope.getConversationNames + + @ViewModelScoped + @Provides + fun provideGetUserNamesUseCase(cellsScope: CellsScope): GetUserNamesUseCase = cellsScope.getUserNames } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/AllFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/AllFilesScreen.kt index 3dc9c3ec67f..18474f9e9a3 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/AllFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/AllFilesScreen.kt @@ -17,11 +17,13 @@ */ package com.wire.android.feature.cells.ui +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel @@ -30,6 +32,7 @@ import com.ramcosta.composedestinations.generated.cells.destinations.AddRemoveTa import com.ramcosta.composedestinations.generated.cells.destinations.PublicLinkScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.SearchScreenDestination import com.wire.android.feature.cells.R +import com.wire.android.feature.cells.ui.common.OfflineBanner import com.wire.android.feature.cells.ui.search.DriveSearchScreenType import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.WireNavigator @@ -48,26 +51,52 @@ fun AllFilesScreen( ) { val pagingListItems = viewModel.nodesFlow.collectAsLazyPagingItems() + val isOnline by viewModel.isOnline.collectAsState() WireScaffold( modifier = modifier, topBar = { Column { - SearchTopBar( - modifier = Modifier, - isSearchActive = false, - searchBarHint = stringResource(R.string.search_label), - searchQueryTextState = rememberTextFieldState(), - onTap = { - navigator.navigate( - NavigationCommand( - SearchScreenDestination( - screenType = DriveSearchScreenType.DRIVE, + AnimatedContent(isOnline) { + if (it) { + SearchTopBar( + modifier = Modifier, + isSearchActive = false, + searchBarHint = stringResource(R.string.search_label), + searchQueryTextState = rememberTextFieldState(), + onTap = { + navigator.navigate( + NavigationCommand( + SearchScreenDestination( + screenType = DriveSearchScreenType.DRIVE, + ) + ) ) - ) + }, ) - }, - ) + } else { + OfflineBanner() + } + } +// if (isOnline) { +// SearchTopBar( +// modifier = Modifier, +// isSearchActive = false, +// searchBarHint = stringResource(R.string.search_label), +// searchQueryTextState = rememberTextFieldState(), +// onTap = { +// navigator.navigate( +// NavigationCommand( +// SearchScreenDestination( +// screenType = DriveSearchScreenType.DRIVE, +// ) +// ) +// ) +// }, +// ) +// } else { +// OfflineBanner() +// } } }, ) { innerPadding -> diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileActionsMenu.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileActionsMenu.kt index 8e604ec5323..a698296aa02 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileActionsMenu.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileActionsMenu.kt @@ -35,7 +35,20 @@ class CellFileActionsMenu @Inject constructor( isAllFiles: Boolean, isSearching: Boolean, isCollaboraEnabled: Boolean, + isOnline: Boolean = true, ): List { + if (!isOnline) { + return buildList { + val canOpenOffline = cellNode is CellNodeUi.Folder || + (cellNode is CellNodeUi.File && cellNode.localFileAvailable()) + if (canOpenOffline) { + add(NodeBottomSheetAction.OPEN) + } + if (cellNode is CellNodeUi.File && cellNode.isAvailableOffline) { + add(NodeBottomSheetAction.REMOVE_OFFLINE_ACCESS) + } + } + } return when { isRecycleBin -> recycleBinActions() @@ -79,6 +92,8 @@ class CellFileActionsMenu @Inject constructor( else -> { + add(NodeBottomSheetAction.OPEN) + if (cellNode.localFileAvailable()) { add(NodeBottomSheetAction.SHARE) } @@ -92,6 +107,8 @@ class CellFileActionsMenu @Inject constructor( ) } } + } else { + add(NodeBottomSheetAction.OPEN) } add(NodeBottomSheetAction.PUBLIC_LINK) @@ -131,6 +148,7 @@ class CellFileActionsMenu @Inject constructor( internal sealed interface MenuActionResult internal data class Action(val action: CellViewAction) : MenuActionResult + internal data class Open(val node: CellNodeUi) : MenuActionResult internal data class Share(val node: CellNodeUi.File) : MenuActionResult internal data class Edit(val node: CellNodeUi) : MenuActionResult internal data class CancelLoading(val node: CellNodeUi) : MenuActionResult @@ -146,6 +164,7 @@ class CellFileActionsMenu @Inject constructor( onResult: (MenuActionResult) -> Unit, ) { val result = when (action) { + NodeBottomSheetAction.OPEN -> Open(node) NodeBottomSheetAction.SHARE -> { if (node is CellNodeUi.File) { Share(node) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt index c2267803fb1..9b6cbfa02e9 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt @@ -34,6 +34,7 @@ import com.wire.android.feature.cells.ui.model.NodeBottomSheetAction import com.wire.android.feature.cells.ui.model.OpenLoadState import com.wire.android.feature.cells.ui.model.canOpenWithUrl import com.wire.android.feature.cells.ui.model.localFileAvailable +import com.wire.android.feature.cells.domain.model.AttachmentFileType import com.wire.android.feature.cells.ui.model.toUiModel import com.wire.android.feature.cells.ui.search.DriveSearchScreenType import com.wire.android.feature.cells.ui.search.SearchNavArgs @@ -45,18 +46,23 @@ import com.wire.kalium.cells.data.FileFilters import com.wire.kalium.cells.data.SortingSpec import com.wire.kalium.cells.domain.model.Node import com.wire.kalium.cells.domain.usecase.DeleteCellAssetUseCase +import com.wire.kalium.cells.domain.usecase.GetConversationNamesUseCase import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase +import com.wire.kalium.cells.domain.usecase.GetUserNamesUseCase import com.wire.kalium.cells.domain.usecase.GetWireCellConfigurationUseCase import com.wire.kalium.cells.domain.usecase.IsAtLeastOneCellAvailableUseCase import com.wire.kalium.cells.domain.usecase.RestoreNodeFromRecycleBinUseCase import com.wire.kalium.cells.domain.usecase.offline.DeleteOfflineFileUseCase import com.wire.kalium.cells.domain.usecase.offline.GetOfflineFileUseCase import com.wire.kalium.cells.domain.usecase.offline.ObserveOfflineFilesUseCase +import com.wire.kalium.cells.domain.usecase.offline.OfflineFileInfo import com.wire.kalium.common.functional.fold import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess import com.wire.kalium.logic.data.featureConfig.CollaboraEdition +import com.wire.kalium.network.NetworkState +import com.wire.kalium.network.NetworkStateObserver import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -72,7 +78,10 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -99,6 +108,9 @@ class CellViewModel @Inject constructor( private val observeOfflineFiles: ObserveOfflineFilesUseCase, private val deleteOfflineFile: DeleteOfflineFileUseCase, private val getOfflineFile: GetOfflineFileUseCase, + private val networkStateObserver: NetworkStateObserver, + private val getConversationNames: GetConversationNamesUseCase, + private val getUserNames: GetUserNamesUseCase, ) : ActionsViewModel() { private val navArgs: CellFilesNavArgs = ConversationFilesScreenDestination.argsFrom(savedStateHandle) @@ -147,6 +159,18 @@ class CellViewModel @Inject constructor( } ) + val isOnline: StateFlow = networkStateObserver.observeNetworkState() + .map { it is NetworkState.ConnectedWithInternet } + .transformLatest { online -> + if (online) { + emit(true) + } else { + delay(OFFLINE_TRANSITION_DELAY_MS) + emit(false) + } + } + .stateIn(viewModelScope, SharingStarted.Eagerly, true) + private var isCollaboraEnabled: Boolean = false init { @@ -219,15 +243,49 @@ class CellViewModel @Inject constructor( } }.shareIn(viewModelScope, started = SharingStarted.Eagerly, replay = 1) - internal val nodesFlow = cellAvailableFlow.flatMapLatest { cellAvailable -> - if (!cellAvailable || searchNavArgs != null) { - flowOf(emptyData) - } else { - sharedNodesFlow + private val offlineNodesFlow: Flow> = + combine( + observeOfflineFiles(), + sharedPathCache.openLoadStates, + offlineFileDownloadController.downloadProgresses, + ) { offlineFiles, openLoadStates, downloadProgresses -> + val conversationNames = getConversationNames() + val userNames = getUserNames() + val rootConversationId = navArgs.conversationId?.substringBefore("/") + val filtered = if (rootConversationId != null) { + offlineFiles.filter { it.conversationId == rootConversationId } + } else { + offlineFiles + } + PagingData.from( + data = filtered.map { info -> + info.toCellNodeUi( + conversationName = info.conversationId?.let { conversationNames[it] }, + userName = info.owner.ifEmpty { null }?.let { userNames[it] }, + openLoadState = openLoadStates[info.id], + downloadProgress = downloadProgresses[info.id], + ) + }, + sourceLoadStates = LoadStates( + refresh = LoadState.NotLoading(true), + prepend = LoadState.NotLoading(true), + append = LoadState.NotLoading(true), + ) + ) + } + + internal val nodesFlow = combine(cellAvailableFlow, isOnline) { cellAvailable, online -> + cellAvailable to online + }.flatMapLatest { (cellAvailable, online) -> + when { + !cellAvailable || searchNavArgs != null -> flowOf(emptyData) + !online -> offlineNodesFlow + else -> sharedNodesFlow } } fun onPullToRefresh() { + if (!isOnline.value) return _isPullToRefresh.value = true refreshNodes() } @@ -357,6 +415,7 @@ class CellViewModel @Inject constructor( isSearching = searchNavArgs?.screenType == DriveSearchScreenType.SHARED_DRIVE || searchNavArgs?.screenType == DriveSearchScreenType.DRIVE, isCollaboraEnabled = isCollaboraEnabled, + isOnline = isOnline.value, ) _menu.emit(MenuOptions(cellNode, menuItems)) @@ -371,6 +430,7 @@ class CellViewModel @Inject constructor( ) { result -> when (result) { is CellFileActionsMenu.Action -> sendAction(result.action) + is CellFileActionsMenu.Open -> sendIntent(CellViewIntent.OnItemClick(result.node)) is CellFileActionsMenu.Edit -> editNode(result.node.uuid) is CellFileActionsMenu.Share -> shareFile(result.node) is CellFileActionsMenu.CancelLoading -> cancelDownload(result.node.uuid) @@ -384,7 +444,9 @@ class CellViewModel @Inject constructor( private fun makeAvailableOffline(node: CellNodeUi.File) { offlineFileDownloadController.start( scope = viewModelScope, - cellNode = node, + cellNode = node.copy( + conversationId = navArgs.conversationId + ), onSuccess = { _ -> sendAction(ShowOfflineFileSaved) }, onError = { sendAction(ShowError(it)) }, ) @@ -506,6 +568,31 @@ class CellViewModel @Inject constructor( isCollaboraEnabled = config?.collabora != CollaboraEdition.NO } + private fun OfflineFileInfo.toCellNodeUi( + conversationName: String? = null, + userName: String? = null, + openLoadState: OpenLoadState? = null, + downloadProgress: Float? = null, + ): CellNodeUi.File { + val extension = name.substringAfterLast('.', "") + return CellNodeUi.File( + uuid = id, + name = name, + mimeType = "", + assetType = AttachmentFileType.fromExtension(extension), + size = size, + localPath = localPath, + ownerUserId = owner.ifEmpty { null }, + userName = userName, + userHandle = null, + conversationName = conversationName, + modifiedTime = modifiedTime, + isAvailableOffline = true, + openLoadState = openLoadState, + downloadProgress = downloadProgress, + ) + } + companion object { private val emptyData: PagingData = PagingData.empty( LoadStates( @@ -560,3 +647,4 @@ data class MenuOptions( ) private const val RESTORE_DELAY_MS = 300L +private const val OFFLINE_TRANSITION_DELAY_MS = 2_000L diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt index 3cba2524e53..2d9f6a0442b 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -56,6 +57,7 @@ import com.ramcosta.composedestinations.generated.cells.destinations.SearchScree import com.ramcosta.composedestinations.generated.cells.destinations.VersionHistoryScreenDestination import com.wire.android.feature.cells.R import com.wire.android.feature.cells.domain.model.AttachmentFileType +import com.wire.android.feature.cells.ui.common.OfflineBanner import com.wire.android.feature.cells.ui.create.FileTypeBottomSheetDialog import com.wire.android.feature.cells.ui.create.file.CreateFileScreenNavArgs import com.wire.android.feature.cells.ui.dialog.CellsNewActionBottomSheet @@ -103,6 +105,8 @@ fun ConversationFilesScreen( animatedVisibilityScope: AnimatedVisibilityScope, viewModel: CellViewModel = hiltViewModel(), ) { + val isOnline by viewModel.isOnline.collectAsState() + ConversationFilesScreenContent( animatedVisibilityScope = animatedVisibilityScope, navigator = navigator, @@ -112,6 +116,7 @@ fun ConversationFilesScreen( pagingListItems = viewModel.nodesFlow.collectAsLazyPagingItems(), menu = viewModel.menu, isSearchResult = false, + isOnline = isOnline, isRestoreInProgress = viewModel.isRestoreInProgress.collectAsState().value, isDeleteInProgress = viewModel.isDeleteInProgress.collectAsState().value, isRefreshing = viewModel.isPullToRefresh.collectAsState(), @@ -146,6 +151,7 @@ internal fun ConversationFilesScreenContent( screenTitle: String? = null, isRecycleBin: Boolean = false, isRestoreInProgress: Boolean = false, + isOnline: Boolean = true, breadcrumbs: Array? = emptyArray(), fileReadyFlow: Flow = emptyFlow(), ) { @@ -232,23 +238,27 @@ internal fun ConversationFilesScreenContent( } ) - SearchTopBar( - modifier = Modifier - .sharedElement( - sharedContentState = rememberSharedContentState(key = SHARED_ELEMENT_SEARCH_INPUT_KEY), - animatedVisibilityScope = animatedVisibilityScope - ), - isSearchActive = false, - searchBarHint = stringResource(R.string.search_label), - searchQueryTextState = TextFieldState(), - onTap = { - currentNodeUuid?.let { - navigator.navigate( - NavigationCommand(SearchScreenDestination(conversationId = it)) - ) - } - }, - ) + if (isOnline) { + SearchTopBar( + modifier = Modifier + .sharedElement( + sharedContentState = rememberSharedContentState(key = SHARED_ELEMENT_SEARCH_INPUT_KEY), + animatedVisibilityScope = animatedVisibilityScope + ), + isSearchActive = false, + searchBarHint = stringResource(R.string.search_label), + searchQueryTextState = TextFieldState(), + onTap = { + currentNodeUuid?.let { + navigator.navigate( + NavigationCommand(SearchScreenDestination(conversationId = it)) + ) + } + }, + ) + } else { + OfflineBanner() + } } }, floatingActionButton = { diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/OfflineFileDownloadController.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/OfflineFileDownloadController.kt index 41a20e507f5..32a156fd0ab 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/OfflineFileDownloadController.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/OfflineFileDownloadController.kt @@ -111,6 +111,8 @@ class OfflineFileDownloadController @Inject constructor( localPath = existingPath, size = cellNode.size, downloadedAt = System.currentTimeMillis(), + conversationId = cellNode.conversationId, + modifiedTime = cellNode.modifiedTime, ) ) onSuccess(existingPath) @@ -155,6 +157,8 @@ class OfflineFileDownloadController @Inject constructor( localPath = filePath.toString(), size = cellNode.size, downloadedAt = System.currentTimeMillis(), + conversationId = cellNode.conversationId, + modifiedTime = cellNode.modifiedTime, ) ) onSuccess(filePath.toString()) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/common/OfflineBanner.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/common/OfflineBanner.kt new file mode 100644 index 00000000000..589482bf5fb --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/common/OfflineBanner.kt @@ -0,0 +1,79 @@ +/* + * Wire + * Copyright (C) 2026 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.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import com.wire.android.feature.cells.R +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.preview.MultipleThemePreviews +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireTypography + +@Composable +internal fun OfflineBanner(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(dimensions().spacing12x), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_wifi_signal), + modifier = Modifier + .width(dimensions().spacing14x) + .height(dimensions().spacing14x) + .align(Alignment.CenterVertically), + contentDescription = null, + tint = colorsScheme().onBackground + ) + Text( + modifier = Modifier.padding(start = dimensions().spacing6x), + text = stringResource(R.string.offline_banner_message), + style = MaterialTheme.wireTypography.body01, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} + +@MultipleThemePreviews +@Composable +private fun PreviewOfflineBanner() { + WireTheme { + OfflineBanner() + } +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/model/CellNodeUi.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/model/CellNodeUi.kt index d7051b7f8b1..5fc8b0e2395 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/model/CellNodeUi.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/model/CellNodeUi.kt @@ -85,6 +85,7 @@ sealed class CellNodeUi { internal override val openLoadState: OpenLoadState? = null, override val downloadProgress: Float? = null, override val isAvailableOffline: Boolean = false, + val conversationId: String? = null, ) : CellNodeUi() } @@ -107,6 +108,7 @@ internal fun Node.File.toUiModel( userHandle = userHandle, ownerUserId = ownerUserId, conversationName = conversationName, + conversationId = conversationId, publicLinkId = publicLinkId, modifiedTime = formattedModifiedTime(), tags = tags, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/model/NodeBottomSheetAction.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/model/NodeBottomSheetAction.kt index 10f63ff52c4..39771462c78 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/model/NodeBottomSheetAction.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/model/NodeBottomSheetAction.kt @@ -24,6 +24,7 @@ enum class NodeBottomSheetAction( val icon: Int, val isHighlighted: Boolean = false ) { + OPEN(R.string.open_label, R.drawable.ic_open), SHARE(R.string.share_label, R.drawable.ic_share), PUBLIC_LINK(R.string.public_link, R.drawable.ic_link), ADD_REMOVE_TAGS(R.string.add_remove_tags_label, R.drawable.ic_tags), @@ -37,5 +38,5 @@ enum class NodeBottomSheetAction( CANCEL_LOADING(R.string.cancel_loading_label, com.wire.android.ui.common.R.drawable.ic_close, true), CANCEL_DOWNLOAD(R.string.cancel_download_label, com.wire.android.ui.common.R.drawable.ic_close, true), MAKE_AVAILABLE_OFFLINE(R.string.make_available_offline_label, R.drawable.ic_save), - REMOVE_OFFLINE_ACCESS(R.string.remove_offline_access_label, R.drawable.ic_save, true), + REMOVE_OFFLINE_ACCESS(R.string.remove_offline_access_label, R.drawable.ic_cross_in_circle, true), } diff --git a/features/cells/src/main/res/drawable/ic_cross_in_circle.xml b/features/cells/src/main/res/drawable/ic_cross_in_circle.xml new file mode 100644 index 00000000000..dd2786d0845 --- /dev/null +++ b/features/cells/src/main/res/drawable/ic_cross_in_circle.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/features/cells/src/main/res/drawable/ic_open.xml b/features/cells/src/main/res/drawable/ic_open.xml new file mode 100644 index 00000000000..c0bd6a871a3 --- /dev/null +++ b/features/cells/src/main/res/drawable/ic_open.xml @@ -0,0 +1,24 @@ + + + + diff --git a/features/cells/src/main/res/drawable/ic_wifi_signal.xml b/features/cells/src/main/res/drawable/ic_wifi_signal.xml new file mode 100644 index 00000000000..26356772174 --- /dev/null +++ b/features/cells/src/main/res/drawable/ic_wifi_signal.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/features/cells/src/main/res/values/strings.xml b/features/cells/src/main/res/values/strings.xml index 5cc6a00a466..a0cee253cd3 100644 --- a/features/cells/src/main/res/values/strings.xml +++ b/features/cells/src/main/res/values/strings.xml @@ -85,8 +85,10 @@ Make available offline Remove offline access File saved for offline use + You\'re offline and can see only saved files \"%1$s\" ready to open Open + Open Unable to create folder. Please try again Move to folder Move Here diff --git a/kalium b/kalium index e854de386df..d0e86c776ff 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit e854de386df9e1d3b3d26e5a01132fdd86d3842d +Subproject commit d0e86c776ffaf3e4f93c84f9aea432096faa4470 From 0d713bcfb7248c907e2293df336df72bf4388214 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 13 May 2026 08:59:09 +0200 Subject: [PATCH 02/13] feat: display offline files --- .../android/di/accountScoped/CellsModule.kt | 8 ++++---- .../android/feature/cells/ui/CellViewModel.kt | 14 ++++++------- .../feature/cells/ui/CellViewModelTest.kt | 20 +++++++++++++++++++ 3 files changed, 30 insertions(+), 12 deletions(-) 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 fa4370b406b..7360c6b1393 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 @@ -62,8 +62,8 @@ import com.wire.kalium.cells.domain.usecase.publiclink.SetPublicLinkExpirationUs import com.wire.kalium.cells.domain.usecase.publiclink.UpdatePublicLinkPasswordUseCase import com.wire.kalium.cells.domain.usecase.versioning.GetNodeVersionsUseCase import com.wire.kalium.cells.domain.usecase.versioning.RestoreNodeVersionUseCase -import com.wire.kalium.cells.domain.usecase.GetConversationNamesUseCase -import com.wire.kalium.cells.domain.usecase.GetUserNamesUseCase +import com.wire.kalium.cells.domain.usecase.GetConversationNameUseCase +import com.wire.kalium.cells.domain.usecase.GetUserNameUseCase import com.wire.kalium.cells.domain.usecase.offline.DeleteOfflineFileUseCase import com.wire.kalium.cells.domain.usecase.offline.GetOfflineFileUseCase import com.wire.kalium.cells.domain.usecase.offline.ObserveOfflineFilesUseCase @@ -284,9 +284,9 @@ class CellsModule { @ViewModelScoped @Provides - fun provideGetConversationNamesUseCase(cellsScope: CellsScope): GetConversationNamesUseCase = cellsScope.getConversationNames + fun provideGetConversationNamesUseCase(cellsScope: CellsScope): GetConversationNameUseCase = cellsScope.getConversationNames @ViewModelScoped @Provides - fun provideGetUserNamesUseCase(cellsScope: CellsScope): GetUserNamesUseCase = cellsScope.getUserNames + fun provideGetUserNamesUseCase(cellsScope: CellsScope): GetUserNameUseCase = cellsScope.getUserNames } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt index 9b6cbfa02e9..fb195d484bf 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt @@ -46,10 +46,10 @@ import com.wire.kalium.cells.data.FileFilters import com.wire.kalium.cells.data.SortingSpec import com.wire.kalium.cells.domain.model.Node import com.wire.kalium.cells.domain.usecase.DeleteCellAssetUseCase -import com.wire.kalium.cells.domain.usecase.GetConversationNamesUseCase +import com.wire.kalium.cells.domain.usecase.GetConversationNameUseCase import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase -import com.wire.kalium.cells.domain.usecase.GetUserNamesUseCase +import com.wire.kalium.cells.domain.usecase.GetUserNameUseCase import com.wire.kalium.cells.domain.usecase.GetWireCellConfigurationUseCase import com.wire.kalium.cells.domain.usecase.IsAtLeastOneCellAvailableUseCase import com.wire.kalium.cells.domain.usecase.RestoreNodeFromRecycleBinUseCase @@ -109,8 +109,8 @@ class CellViewModel @Inject constructor( private val deleteOfflineFile: DeleteOfflineFileUseCase, private val getOfflineFile: GetOfflineFileUseCase, private val networkStateObserver: NetworkStateObserver, - private val getConversationNames: GetConversationNamesUseCase, - private val getUserNames: GetUserNamesUseCase, + private val getConversationName: GetConversationNameUseCase, + private val getUserName: GetUserNameUseCase, ) : ActionsViewModel() { private val navArgs: CellFilesNavArgs = ConversationFilesScreenDestination.argsFrom(savedStateHandle) @@ -249,8 +249,6 @@ class CellViewModel @Inject constructor( sharedPathCache.openLoadStates, offlineFileDownloadController.downloadProgresses, ) { offlineFiles, openLoadStates, downloadProgresses -> - val conversationNames = getConversationNames() - val userNames = getUserNames() val rootConversationId = navArgs.conversationId?.substringBefore("/") val filtered = if (rootConversationId != null) { offlineFiles.filter { it.conversationId == rootConversationId } @@ -260,8 +258,8 @@ class CellViewModel @Inject constructor( PagingData.from( data = filtered.map { info -> info.toCellNodeUi( - conversationName = info.conversationId?.let { conversationNames[it] }, - userName = info.owner.ifEmpty { null }?.let { userNames[it] }, + conversationName = info.conversationId?.let { getConversationName(it) }, + userName = info.owner.ifEmpty { null }?.let { getUserName(it) }, openLoadState = openLoadStates[info.id], downloadProgress = downloadProgresses[info.id], ) diff --git a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/CellViewModelTest.kt b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/CellViewModelTest.kt index 7bda85948ef..aa615bb8728 100644 --- a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/CellViewModelTest.kt +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/CellViewModelTest.kt @@ -35,6 +35,8 @@ import com.wire.kalium.cells.domain.usecase.DeleteCellAssetUseCase import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase import com.wire.kalium.cells.domain.usecase.GetWireCellConfigurationUseCase +import com.wire.kalium.cells.domain.usecase.GetConversationNameUseCase +import com.wire.kalium.cells.domain.usecase.GetUserNameUseCase import com.wire.kalium.cells.domain.usecase.IsAtLeastOneCellAvailableUseCase import com.wire.kalium.cells.domain.usecase.RestoreNodeFromRecycleBinUseCase import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase @@ -42,6 +44,9 @@ import com.wire.kalium.cells.domain.usecase.offline.DeleteOfflineFileUseCase import com.wire.kalium.cells.domain.usecase.offline.GetOfflineFileUseCase import com.wire.kalium.cells.domain.usecase.offline.ObserveOfflineFilesUseCase import com.wire.kalium.common.functional.right +import com.wire.kalium.network.NetworkState +import com.wire.kalium.network.NetworkStateObserver +import kotlinx.coroutines.flow.MutableStateFlow import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -306,6 +311,15 @@ class CellViewModelTest { @MockK lateinit var getOfflineFile: GetOfflineFileUseCase + @MockK + lateinit var networkStateObserver: NetworkStateObserver + + @MockK + lateinit var getConversationNames: GetConversationNameUseCase + + @MockK + lateinit var getUserNames: GetUserNameUseCase + init { MockKAnnotations.init(this, relaxUnitFun = true) @@ -322,6 +336,9 @@ class CellViewModelTest { every { observeOfflineFiles() } returns emptyFlow() coEvery { getOfflineFile(any()) } returns null + every { networkStateObserver.observeNetworkState() } returns MutableStateFlow(NetworkState.ConnectedWithInternet) + coEvery { getConversationNames(any()) } returns null + coEvery { getUserNames(any()) } returns null coEvery { getCellFilesPagedUseCase.invoke(any(), any(), any(), any()) } returns flowOf( PagingData.from( @@ -408,6 +425,9 @@ class CellViewModelTest { observeOfflineFiles = observeOfflineFiles, deleteOfflineFile = deleteOfflineFile, getOfflineFile = getOfflineFile, + networkStateObserver = networkStateObserver, + getConversationName = getConversationNames, + getUserName = getUserNames, ) } } From a334c17841df545b71ee2621549009904f469fdd Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 13 May 2026 10:19:37 +0200 Subject: [PATCH 03/13] feat: display offline files --- .../multipart/MultipartAttachmentsViewModel.kt | 7 ++++++- .../com/wire/android/feature/cells/ui/CellScreenContent.kt | 6 +++--- .../com/wire/android/feature/cells/ui/CellViewModel.kt | 2 +- .../feature/cells/ui/OfflineFileDownloadController.kt | 5 +++-- .../android/feature/cells/ui/OpenFileDownloadController.kt | 1 + 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt index abff1e8ad23..c4a043c2c87 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt @@ -125,7 +125,11 @@ class MultipartAttachmentsViewModelImpl @Inject constructor( attachment.isImage() && !attachment.fileNotFound() -> openInImageViewer(attachment.uuid) attachment.isEditSupported && isCollaboraEnabled && featureFlags.collaboraIntegration -> openOnlineEditor(attachment.uuid) - attachment.fileNotFound() -> { refreshHelper.refresh(attachment.uuid) } + + attachment.fileNotFound() -> { + refreshHelper.refresh(attachment.uuid) + } + attachment.localFileAvailable() -> openLocalFile(attachment) attachment.canOpenWithUrl() -> openUrl(attachment) else -> downloadAsset(attachment) @@ -170,6 +174,7 @@ class MultipartAttachmentsViewModelImpl @Inject constructor( download( assetId = attachment.uuid, + conversationId = null, // TODO to replace with real conversation id in next PR outFilePath = path, assetSize = attachment.assetSize ?: 0, ) { progress -> 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 4a71aab73ed..dbf50a083ee 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 @@ -217,6 +217,7 @@ internal fun CellScreenContent( } ) } + val offlineFileSavedToastDescription = stringResource(R.string.offline_file_saved_message) HandleActions(actionsFlow) { action -> when (action) { @@ -245,11 +246,10 @@ internal fun CellScreenContent( is ShowFileDeletedMessage -> showDeleteConfirmation(context, action.isFile, action.permanently) is OpenFolder -> openFolder(action.path, action.title, action.parentFolderUuid) is ShowEditErrorDialog -> editNodeError = action.nodeUuid - is ShowOfflineFileSaved -> { - val description = stringResource(R.string.offline_file_saved_message) + is ShowOfflineFileSaved -> { Toast.makeText( context, - description, + offlineFileSavedToastDescription, Toast.LENGTH_SHORT ).show() } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt index fb195d484bf..3689bf80a15 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt @@ -584,7 +584,7 @@ class CellViewModel @Inject constructor( userName = userName, userHandle = null, conversationName = conversationName, - modifiedTime = modifiedTime, + modifiedTime = modifiedAt, isAvailableOffline = true, openLoadState = openLoadState, downloadProgress = downloadProgress, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/OfflineFileDownloadController.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/OfflineFileDownloadController.kt index 6c9da9d9b80..4542b2d8478 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/OfflineFileDownloadController.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/OfflineFileDownloadController.kt @@ -108,7 +108,7 @@ class OfflineFileDownloadController @Inject constructor( size = cellNode.size, downloadedAt = System.currentTimeMillis(), conversationId = cellNode.conversationId, - modifiedTime = cellNode.modifiedTime, + modifiedAt = cellNode.modifiedTime, ) ) onSuccess(existingPath) @@ -127,6 +127,7 @@ class OfflineFileDownloadController @Inject constructor( val result = download( assetId = cellNode.uuid, + conversationId = cellNode.conversationId, outFilePath = filePath, remoteFilePath = cellNode.remotePath, assetSize = cellNode.size ?: 0, @@ -154,7 +155,7 @@ class OfflineFileDownloadController @Inject constructor( size = cellNode.size, downloadedAt = System.currentTimeMillis(), conversationId = cellNode.conversationId, - modifiedTime = cellNode.modifiedTime, + modifiedAt = cellNode.modifiedTime, ) ) onSuccess(filePath.toString()) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/OpenFileDownloadController.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/OpenFileDownloadController.kt index bfbad9982bc..9982e665fb9 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/OpenFileDownloadController.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/OpenFileDownloadController.kt @@ -108,6 +108,7 @@ class OpenFileDownloadController @Inject constructor( val result = download( assetId = cellNode.uuid, + conversationId = cellNode.conversationId, outFilePath = filePath, remoteFilePath = cellNode.remotePath, assetSize = cellNode.size ?: 0, From af93b1dabd2cce6afa34fd61a50cfb167bd12dc4 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 13 May 2026 11:19:43 +0200 Subject: [PATCH 04/13] feat: cleanup --- .../feature/cells/ui/AllFilesScreen.kt | 20 +------------------ .../feature/cells/ui/CellFilesScreen.kt | 5 +++++ .../android/feature/cells/ui/CellListItem.kt | 13 ++++++------ .../feature/cells/ui/CellScreenContent.kt | 2 ++ .../cells/ui/ConversationFilesScreen.kt | 1 + 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/AllFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/AllFilesScreen.kt index 18474f9e9a3..52d22d6ef02 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/AllFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/AllFilesScreen.kt @@ -78,25 +78,6 @@ fun AllFilesScreen( OfflineBanner() } } -// if (isOnline) { -// SearchTopBar( -// modifier = Modifier, -// isSearchActive = false, -// searchBarHint = stringResource(R.string.search_label), -// searchQueryTextState = rememberTextFieldState(), -// onTap = { -// navigator.navigate( -// NavigationCommand( -// SearchScreenDestination( -// screenType = DriveSearchScreenType.DRIVE, -// ) -// ) -// ) -// }, -// ) -// } else { -// OfflineBanner() -// } } }, ) { innerPadding -> @@ -108,6 +89,7 @@ fun AllFilesScreen( openFolder = { _, _, _ -> }, menuState = viewModel.menu, isAllFiles = true, + isOffline = !isOnline, isRestoreInProgress = viewModel.isRestoreInProgress.collectAsState().value, isDeleteInProgress = viewModel.isDeleteInProgress.collectAsState().value, isRecycleBin = viewModel.isRecycleBin(), diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesScreen.kt index 658e515dbf2..0bc6436b4c6 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesScreen.kt @@ -66,6 +66,7 @@ internal fun CellFilesScreen( modifier: Modifier = Modifier, isPullToRefreshEnabled: Boolean = true, lazyListState: LazyListState = rememberLazyListState(), + showConversationName: Boolean = true, onItemMenuClick: (CellNodeUi) -> Unit ) { if (isPullToRefreshEnabled) { @@ -79,6 +80,7 @@ internal fun CellFilesScreen( lazyListState = lazyListState, onItemClick = onItemClick, onItemMenuClick = onItemMenuClick, + showConversationName = showConversationName, ) } } else { @@ -88,6 +90,7 @@ internal fun CellFilesScreen( lazyListState = lazyListState, onItemClick = onItemClick, onItemMenuClick = onItemMenuClick, + showConversationName = showConversationName, ) } } @@ -99,6 +102,7 @@ private fun ContentList( onItemClick: (CellNodeUi) -> Unit, onItemMenuClick: (CellNodeUi) -> Unit, modifier: Modifier = Modifier, + showConversationName: Boolean = true, ) { LazyColumn( modifier = modifier.fillMaxWidth(), @@ -118,6 +122,7 @@ private fun ContentList( .background(color = colorsScheme().surface) .clickable { onItemClick(item) }, cell = item, + showConversationName = showConversationName, onMenuClick = { onItemMenuClick(item) } ) WireDivider(modifier = Modifier.fillMaxWidth()) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellListItem.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellListItem.kt index c2f82dc2d3f..73385758ddf 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellListItem.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellListItem.kt @@ -95,6 +95,7 @@ internal fun CellListItem( cell: CellNodeUi, onMenuClick: () -> Unit, modifier: Modifier = Modifier, + showConversationName: Boolean = true, ) { val interactionSource = remember { MutableInteractionSource() } var showReadyState by remember { mutableStateOf(false) } @@ -132,7 +133,7 @@ internal fun CellListItem( ) Row(verticalAlignment = Alignment.CenterVertically) { - CellItemSubtitle(cell = cell, showReadyState = showReadyState) + CellItemSubtitle(cell = cell, showReadyState = showReadyState, showConversationName = showConversationName) } } @@ -184,7 +185,7 @@ private fun CellItemIcon(cell: CellNodeUi, showReadyState: Boolean) { } @Composable -private fun CellItemSubtitle(cell: CellNodeUi, showReadyState: Boolean) { +private fun CellItemSubtitle(cell: CellNodeUi, showReadyState: Boolean, showConversationName: Boolean) { when { cell.openLoadState is OpenLoadState.Loading -> Text( text = stringResource(R.string.tap_to_cancel_loading), @@ -241,7 +242,7 @@ private fun CellItemSubtitle(cell: CellNodeUi, showReadyState: Boolean) { modifier = Modifier.padding(end = dimensions().spacing4x) ) } - cell.subtitle()?.let { + cell.subtitle(showConversationName)?.let { Text( text = it, textAlign = TextAlign.Left, @@ -446,19 +447,19 @@ private fun PublicLinkIcon( } @Composable -private fun CellNodeUi.subtitle(): String? { +private fun CellNodeUi.subtitle(showConversationName: Boolean): String? { val formattedTime = modifiedTime?.let { remember(it) { Instant.fromEpochMilliseconds(it).cellFileDateTime() } } return when { - userName != null && conversationName != null -> + showConversationName && userName != null && conversationName != null -> stringResource(R.string.file_subtitle, userName!!, conversationName!!) userName != null && formattedTime != null -> stringResource(R.string.file_subtitle_modified, formattedTime, userName!!) userName != null -> userName - conversationName != null -> conversationName + showConversationName && conversationName != null -> conversationName formattedTime != null -> formattedTime else -> null } 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 dbf50a083ee..fde360bda53 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 @@ -94,6 +94,7 @@ internal fun CellScreenContent( isRecycleBin: Boolean = false, isAllFiles: Boolean = false, isSearchResult: Boolean = false, + isOffline: Boolean = false, isPullToRefreshEnabled: Boolean = true, lazyListState: LazyListState = rememberLazyListState(), retryEditNodeError: (String) -> Unit = {}, @@ -142,6 +143,7 @@ internal fun CellScreenContent( onItemMenuClick = { sendIntent(CellViewIntent.OnItemMenuClick(it)) }, isRefreshing = isRefreshing, onRefresh = onRefresh, + showConversationName = !isOffline || isAllFiles || isRecycleBin, ) } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt index 92769c56a78..ff7bfe42360 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt @@ -300,6 +300,7 @@ internal fun ConversationFilesScreenContent( isRestoreInProgress = isRestoreInProgress, isDeleteInProgress = isDeleteInProgress, isRecycleBin = isRecycleBin, + isOffline = !isOnline, openFolder = { path, title, parentFolderUuid -> navigator.navigate( NavigationCommand( From b59a08f69dd631888121d74886d0e21cf8b5afb0 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 13 May 2026 11:35:41 +0200 Subject: [PATCH 05/13] feat: cleanup --- .../com/wire/android/feature/cells/ui/CellFileActionsMenu.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileActionsMenu.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileActionsMenu.kt index a016835cf1e..a2580ce5317 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileActionsMenu.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileActionsMenu.kt @@ -104,8 +104,6 @@ class CellFileActionsMenu @Inject constructor( add(NodeBottomSheetAction.SHARE) } - add(NodeBottomSheetAction.PUBLIC_LINK) - add( if (cellNode.isAvailableOffline) { NodeBottomSheetAction.REMOVE_OFFLINE_ACCESS @@ -118,8 +116,6 @@ class CellFileActionsMenu @Inject constructor( } else { add(NodeBottomSheetAction.OPEN) } - - add(NodeBottomSheetAction.PUBLIC_LINK) } private fun conversationActions( @@ -147,6 +143,7 @@ class CellFileActionsMenu @Inject constructor( addAll( listOf( NodeBottomSheetAction.ADD_REMOVE_TAGS, + NodeBottomSheetAction.PUBLIC_LINK, NodeBottomSheetAction.MOVE, NodeBottomSheetAction.RENAME, NodeBottomSheetAction.DELETE, From c51175a306b86b9f00c772a715556f50d1b70616 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 13 May 2026 12:22:54 +0200 Subject: [PATCH 06/13] feat: support offline mode in search screen --- .../feature/cells/ui/search/SearchScreen.kt | 134 ++++++++++-------- 1 file changed, 76 insertions(+), 58 deletions(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt index fcdbd4af8a4..3120007e4bd 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt @@ -17,6 +17,7 @@ */ package com.wire.android.feature.cells.ui.search +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.foundation.layout.Column @@ -47,6 +48,7 @@ import com.ramcosta.composedestinations.generated.cells.destinations.VersionHist import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.CellScreenContent import com.wire.android.feature.cells.ui.CellViewModel +import com.wire.android.feature.cells.ui.common.OfflineBanner import com.wire.android.feature.cells.ui.model.CellNodeUi import com.wire.android.feature.cells.ui.search.filter.FilterChipsRow import com.wire.android.feature.cells.ui.search.filter.bottomsheet.FilterByTypeBottomSheet @@ -63,6 +65,8 @@ import com.wire.android.navigation.transition.SHARED_ELEMENT_SEARCH_INPUT_KEY import com.wire.android.ui.common.bottomsheet.WireSheetValue import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.common.topappbar.NavigationIconType +import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.topappbar.search.SearchTopBar @OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @@ -79,6 +83,7 @@ fun SearchScreen( searchScreenViewModel: SearchScreenViewModel = hiltViewModel(), ) { val uiState by searchScreenViewModel.uiState.collectAsStateWithLifecycle() + val isOnline by cellViewModel.isOnline.collectAsState() val filterTypeSheetState = rememberWireModalSheetState(WireSheetValue.Hidden) val filterTagsSheetState = rememberWireModalSheetState(WireSheetValue.Hidden) @@ -101,67 +106,80 @@ fun SearchScreen( WireScaffold( modifier = modifier, topBar = { - Column { - SearchTopBar( - modifier = Modifier.sharedElement( - sharedContentState = rememberSharedContentState(key = SHARED_ELEMENT_SEARCH_INPUT_KEY), - animatedVisibilityScope = animatedVisibilityScope - ), - isSearchActive = uiState.isSearchActive, - shouldClearTextOnClearFocus = false, - keepBackButtonVisible = true, - searchBarHint = when (searchScreenViewModel.screenType) { - DriveSearchScreenType.SHARED_DRIVE -> stringResource(R.string.search_shared_drive_text_input_hint) - DriveSearchScreenType.DRIVE -> stringResource(R.string.search_drive_text_input_hint) - }, - searchQueryTextState = searchState, - onCloseSearchClicked = { navigator.navigateBack() }, - onActiveChanged = { - searchScreenViewModel.onSetSearchActive(it) - }, - ) - FilterChipsRow( - state = uiState.chipsState, - screenType = searchScreenViewModel.screenType, - onFilterByTagsClicked = { - searchScreenViewModel.onSetSearchActive(false) - filterTagsSheetState.show(Unit, isImeVisible) - }, - onFilterByTypeClicked = { - searchScreenViewModel.onSetSearchActive(false) - filterTypeSheetState.show(Unit, isImeVisible) - }, - onFilterByOwnerClicked = { - searchScreenViewModel.onSetSearchActive(false) - filterOwnerSheetState.show(Unit, isImeVisible) - }, - onFilterBySharedByLinkClicked = { - searchScreenViewModel.onSharedByMeClicked() - }, - onFilterByConversationClicked = { - searchScreenViewModel.onSetSearchActive(false) - filterConversationSheetState.show(Unit, isImeVisible) - }, - onRemoveAllFiltersClicked = { - searchScreenViewModel.onRemoveAllFilters() - } - ) + AnimatedContent(isOnline) { online -> + if (online) { + Column { + SearchTopBar( + modifier = Modifier.sharedElement( + sharedContentState = rememberSharedContentState(key = SHARED_ELEMENT_SEARCH_INPUT_KEY), + animatedVisibilityScope = animatedVisibilityScope + ), + isSearchActive = uiState.isSearchActive, + shouldClearTextOnClearFocus = false, + keepBackButtonVisible = true, + searchBarHint = when (searchScreenViewModel.screenType) { + DriveSearchScreenType.SHARED_DRIVE -> stringResource(R.string.search_shared_drive_text_input_hint) + DriveSearchScreenType.DRIVE -> stringResource(R.string.search_drive_text_input_hint) + }, + searchQueryTextState = searchState, + onCloseSearchClicked = { navigator.navigateBack() }, + onActiveChanged = { + searchScreenViewModel.onSetSearchActive(it) + }, + ) + FilterChipsRow( + state = uiState.chipsState, + screenType = searchScreenViewModel.screenType, + onFilterByTagsClicked = { + searchScreenViewModel.onSetSearchActive(false) + filterTagsSheetState.show(Unit, isImeVisible) + }, + onFilterByTypeClicked = { + searchScreenViewModel.onSetSearchActive(false) + filterTypeSheetState.show(Unit, isImeVisible) + }, + onFilterByOwnerClicked = { + searchScreenViewModel.onSetSearchActive(false) + filterOwnerSheetState.show(Unit, isImeVisible) + }, + onFilterBySharedByLinkClicked = { + searchScreenViewModel.onSharedByMeClicked() + }, + onFilterByConversationClicked = { + searchScreenViewModel.onSetSearchActive(false) + filterConversationSheetState.show(Unit, isImeVisible) + }, + onRemoveAllFiltersClicked = { + searchScreenViewModel.onRemoveAllFilters() + } + ) - with(uiState) { - SortRowWithMenu( - screenType = searchScreenViewModel.screenType, - sortingCriteria = sortingCriteria, - isSearchResult = searchState.text.isNotEmpty() || hasAnyFilter, - onSortByClicked = { - searchScreenViewModel.setSortBy(it) - }, - onOrderClicked = { - searchScreenViewModel.setSorting(it) + with(uiState) { + SortRowWithMenu( + screenType = searchScreenViewModel.screenType, + sortingCriteria = sortingCriteria, + isSearchResult = searchState.text.isNotEmpty() || hasAnyFilter, + onSortByClicked = { + searchScreenViewModel.setSortBy(it) + }, + onOrderClicked = { + searchScreenViewModel.setSorting(it) + } + ) } - ) + } + } else { + Column { + WireCenterAlignedTopAppBar( + title = "", + navigationIconType = NavigationIconType.Close(), + onNavigationPressed = { navigator.navigateBack() }, + ) + OfflineBanner() + } } } - } + }, ) { innerPadding -> val lazyListState = rememberLazyListState() @@ -173,7 +191,7 @@ fun SearchScreen( val lazyItems = if (isShowingFilteredResults) filteredItems else initialItems LaunchedEffect(uiState.sortingCriteria) { - lazyListState.animateScrollToItem(0) + lazyListState.animateScrollToItem(0) } CellScreenContent( From 389525d29c650fefd081028a4f9b3108c64f1513 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 13 May 2026 12:45:17 +0200 Subject: [PATCH 07/13] chore: cleanup --- .../com/wire/android/feature/cells/ui/CellViewModel.kt | 10 ---------- .../feature/cells/ui/ConversationFilesScreen.kt | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt index 3689bf80a15..fe58bc92ff4 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt @@ -81,7 +81,6 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -161,14 +160,6 @@ class CellViewModel @Inject constructor( val isOnline: StateFlow = networkStateObserver.observeNetworkState() .map { it is NetworkState.ConnectedWithInternet } - .transformLatest { online -> - if (online) { - emit(true) - } else { - delay(OFFLINE_TRANSITION_DELAY_MS) - emit(false) - } - } .stateIn(viewModelScope, SharingStarted.Eagerly, true) private var isCollaboraEnabled: Boolean = false @@ -645,4 +636,3 @@ data class MenuOptions( ) private const val RESTORE_DELAY_MS = 300L -private const val OFFLINE_TRANSITION_DELAY_MS = 2_000L diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt index ff7bfe42360..33dc2dc0202 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt @@ -229,7 +229,7 @@ internal fun ConversationFilesScreenContent( navigationIconType = NavigationIconType.Back(), elevation = dimensions().spacing0x, actions = { - if (!isRecycleBin) { + if (!isRecycleBin && isOnline) { MoreOptionIcon( contentDescription = R.string.content_description_conversation_files_more_button, onButtonClicked = { optionsBottomSheetState.show() } From 6489c308cc9fa53ee16601b77ade893786af290e Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 13 May 2026 12:45:28 +0200 Subject: [PATCH 08/13] chore: kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 8cb94930725..7fc7a5b5f05 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 8cb949307259386c05a9ef49a7e99dee37fd52cc +Subproject commit 7fc7a5b5f05381c30189f3c002520516969a4f57 From 785f817c73702e4219c2f11a5ce3161ae38954a4 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 13 May 2026 13:14:52 +0200 Subject: [PATCH 09/13] chore: cleanup --- .../java/com/wire/android/feature/cells/ui/CellViewModel.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt index fe58bc92ff4..c947c64809a 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt @@ -160,7 +160,11 @@ class CellViewModel @Inject constructor( val isOnline: StateFlow = networkStateObserver.observeNetworkState() .map { it is NetworkState.ConnectedWithInternet } - .stateIn(viewModelScope, SharingStarted.Eagerly, true) + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = networkStateObserver.observeNetworkState().value is NetworkState.ConnectedWithInternet, + ) private var isCollaboraEnabled: Boolean = false From 3965331ae72193a8c15b9d7c1983afc00d609ad4 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 13 May 2026 15:08:29 +0200 Subject: [PATCH 10/13] chore: detekt --- .../main/java/com/wire/android/feature/cells/ui/CellViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt index c947c64809a..1dc30c33ede 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt @@ -570,6 +570,7 @@ class CellViewModel @Inject constructor( val extension = name.substringAfterLast('.', "") return CellNodeUi.File( uuid = id, + conversationId = conversationId, name = name, mimeType = "", assetType = AttachmentFileType.fromExtension(extension), From 6125be15ddcf066e9d43ee7537fe50655465d1e3 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 13 May 2026 15:10:44 +0200 Subject: [PATCH 11/13] chore: kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 7fc7a5b5f05..3cb3dc620e2 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 7fc7a5b5f05381c30189f3c002520516969a4f57 +Subproject commit 3cb3dc620e2fd725bb793ae6bdc57058140d75e2 From 92c9caa5ef5e221545c1de986967997ce6b58404 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 13 May 2026 16:13:24 +0200 Subject: [PATCH 12/13] chore: cleanup --- .../messagetypes/multipart/MultipartAttachmentsViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt index 1fbcfe44ae9..ac1aafa85c8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt @@ -177,7 +177,6 @@ class MultipartAttachmentsViewModelImpl @Inject constructor( conversationId = null, // TODO to replace with real conversation id in next PR outFilePath = path, assetSize = attachment.assetSize ?: 0, - conversationId = null, // TODO to replace with real conversation id in next PR ) { progress -> attachment.assetSize?.let { val value = progress.toFloat() / it From 7bedbada631f4aaf2d24baa3f8ba316dcd286191 Mon Sep 17 00:00:00 2001 From: ohassine Date: Mon, 18 May 2026 12:38:41 +0200 Subject: [PATCH 13/13] refactor: add mimeType --- .../com/wire/android/di/accountScoped/CellsModule.kt | 4 ++-- .../com/wire/android/feature/cells/ui/CellViewModel.kt | 9 +++++++-- .../feature/cells/ui/OfflineFileDownloadController.kt | 2 ++ kalium | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) 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 7360c6b1393..ce080517f9f 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 @@ -284,9 +284,9 @@ class CellsModule { @ViewModelScoped @Provides - fun provideGetConversationNamesUseCase(cellsScope: CellsScope): GetConversationNameUseCase = cellsScope.getConversationNames + fun provideGetConversationNamesUseCase(cellsScope: CellsScope): GetConversationNameUseCase = cellsScope.getConversationName @ViewModelScoped @Provides - fun provideGetUserNamesUseCase(cellsScope: CellsScope): GetUserNameUseCase = cellsScope.getUserNames + fun provideGetUserNamesUseCase(cellsScope: CellsScope): GetUserNameUseCase = cellsScope.getUserName } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt index 1dc30c33ede..8e917a9929f 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt @@ -567,13 +567,18 @@ class CellViewModel @Inject constructor( openLoadState: OpenLoadState? = null, downloadProgress: Float? = null, ): CellNodeUi.File { + val resolvedMimeType = mimeType.orEmpty() val extension = name.substringAfterLast('.', "") return CellNodeUi.File( uuid = id, conversationId = conversationId, name = name, - mimeType = "", - assetType = AttachmentFileType.fromExtension(extension), + mimeType = resolvedMimeType, + assetType = if (resolvedMimeType.isNotBlank()) { + AttachmentFileType.fromMimeType(resolvedMimeType) + } else { + AttachmentFileType.fromExtension(extension) + }, size = size, localPath = localPath, ownerUserId = owner.ifEmpty { null }, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/OfflineFileDownloadController.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/OfflineFileDownloadController.kt index 4542b2d8478..31263a60eb9 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/OfflineFileDownloadController.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/OfflineFileDownloadController.kt @@ -103,6 +103,7 @@ class OfflineFileDownloadController @Inject constructor( OfflineFileInfo( id = cellNode.uuid, name = nodeName, + mimeType = cellNode.mimeType, owner = cellNode.ownerUserId ?: "", localPath = existingPath, size = cellNode.size, @@ -150,6 +151,7 @@ class OfflineFileDownloadController @Inject constructor( OfflineFileInfo( id = cellNode.uuid, name = nodeName, + mimeType = cellNode.mimeType, owner = cellNode.ownerUserId ?: "", localPath = filePath.toString(), size = cellNode.size, diff --git a/kalium b/kalium index 3cb3dc620e2..012b5dee559 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 3cb3dc620e2fd725bb793ae6bdc57058140d75e2 +Subproject commit 012b5dee559d8f3111e948d9b83e8d84f0e1e9de