Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
15042f2
feat: display offline files
ohassine May 12, 2026
0d713bc
feat: display offline files
ohassine May 13, 2026
c5225a5
Merge remote-tracking branch 'origin/Make-files-available-offline' in…
ohassine May 13, 2026
a334c17
feat: display offline files
ohassine May 13, 2026
af93b1d
feat: cleanup
ohassine May 13, 2026
b59a08f
feat: cleanup
ohassine May 13, 2026
c51175a
feat: support offline mode in search screen
ohassine May 13, 2026
389525d
chore: cleanup
ohassine May 13, 2026
6489c30
chore: kalium
ohassine May 13, 2026
11509a2
Merge remote-tracking branch 'origin/develop' into display-offline-files
ohassine May 13, 2026
bb0cda9
Merge branch 'Make-files-available-offline' into display-offline-files
ohassine May 13, 2026
785f817
chore: cleanup
ohassine May 13, 2026
155c2d1
Merge remote-tracking branch 'origin/Make-files-available-offline' in…
ohassine May 13, 2026
0d26300
Merge branch 'Make-files-available-offline' into display-offline-files
ohassine May 13, 2026
3965331
chore: detekt
ohassine May 13, 2026
6125be1
chore: kalium
ohassine May 13, 2026
e44bd6f
Merge branch 'Make-files-available-offline' into display-offline-files
ohassine May 13, 2026
c69e635
Merge remote-tracking branch 'origin/Make-files-available-offline' in…
ohassine May 13, 2026
92c9caa
chore: cleanup
ohassine May 13, 2026
7bedbad
refactor: add mimeType
ohassine May 18, 2026
dffb611
feat: show offline indicator for files in conversation
ohassine May 18, 2026
8f60e28
Merge remote-tracking branch 'origin/Make-files-available-offline' in…
ohassine May 20, 2026
aac5ec9
chore: cleanup
ohassine May 20, 2026
5bba7d3
chore: pass conversationId using savedStateHandle instead of Assisted…
ohassine May 20, 2026
5f92202
Merge remote-tracking branch 'origin/Make-files-available-offline' in…
ohassine May 21, 2026
1f60146
Merge remote-tracking branch 'origin/Make-files-available-offline' in…
ohassine May 21, 2026
3ef3638
refactor: move mapping to viewmodel
ohassine May 21, 2026
e63df09
refactor: address comments
ohassine May 21, 2026
63a0e33
feat: use case for observing offline files by conversationId
ohassine May 21, 2026
d1ba210
feat: cleanup
ohassine May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,19 @@ data class MultipartAttachmentUi(
val transferStatus: AssetTransferStatus,
val progress: Float? = null,
val isEditSupported: Boolean = false,
val isAvailableOffline: Boolean = false,
)

enum class AssetSource {
CELL, ASSET_STORAGE
}

fun MessageAttachment.toUiModel(progress: Float? = null) = when (this) {
is AssetContent -> this.toUiModel(progress)
is CellAssetContent -> this.toUiModel(progress)
fun MessageAttachment.toUiModel(progress: Float? = null, isAvailableOffline: Boolean = false) = when (this) {
is AssetContent -> this.toUiModel(progress, isAvailableOffline)
is CellAssetContent -> this.toUiModel(progress, isAvailableOffline)
}

fun CellAssetContent.toUiModel(progress: Float?) = MultipartAttachmentUi(
fun CellAssetContent.toUiModel(progress: Float?, isAvailableOffline: Boolean = false) = MultipartAttachmentUi(
uuid = this.id,
source = AssetSource.CELL,
fileName = this.assetPath?.substringAfterLast("/"),
Expand All @@ -67,9 +68,10 @@ fun CellAssetContent.toUiModel(progress: Float?) = MultipartAttachmentUi(
progress = progress,
contentHash = contentHash,
isEditSupported = isEditSupported,
isAvailableOffline = isAvailableOffline,
)

fun AssetContent.toUiModel(progress: Float?) = MultipartAttachmentUi(
fun AssetContent.toUiModel(progress: Float?, isAvailableOffline: Boolean = false) = MultipartAttachmentUi(
uuid = this.remoteData.assetId,
source = AssetSource.ASSET_STORAGE,
fileName = this.name,
Expand All @@ -84,4 +86,5 @@ fun AssetContent.toUiModel(progress: Float?) = MultipartAttachmentUi(
contentHash = null,
contentUrl = null,
isEditSupported = false,
isAvailableOffline = isAvailableOffline,
)
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,6 @@ private fun MessageContent(
Spacer(modifier = Modifier.height(dimensions().spacing8x))
}
MultipartAttachmentsView(
conversationId = message.conversationId,
attachments = messageContent.attachments,
messageStyle = messageStyle,
onImageAttachmentClick = onMultipartImageClick
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onVisibilityChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import coil3.decode.Decoder
import coil3.request.ImageRequest
import coil3.request.crossfade
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.wire.android.ui.common.colorsScheme
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.multipart.MultipartAttachmentUi
Expand All @@ -43,7 +45,6 @@ import com.wire.android.ui.home.conversations.model.messagetypes.multipart.grid.
import com.wire.android.ui.home.conversations.model.messagetypes.multipart.standalone.AssetPreview
import com.wire.kalium.logic.data.asset.AssetTransferStatus
import com.wire.kalium.logic.data.asset.isFailed
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.message.MessageAttachment

/**
Expand All @@ -52,20 +53,21 @@ import com.wire.kalium.logic.data.message.MessageAttachment
*/
@Composable
fun MultipartAttachmentsView(
conversationId: ConversationId,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the conversationId cannot be passed via view parameters anymore?

attachments: List<MessageAttachment>,
messageStyle: MessageStyle,
onImageAttachmentClick: (String) -> Unit,
modifier: Modifier = Modifier,
viewModel: MultipartAttachmentsViewModel = when {
LocalInspectionMode.current -> MultipartAttachmentsViewModelPreview
else -> hiltViewModel<MultipartAttachmentsViewModelImpl>(key = conversationId.value)
else -> hiltViewModel<MultipartAttachmentsViewModelImpl>()
Comment thread
ohassine marked this conversation as resolved.
Outdated
}
) {
val offlineAttachmentIds by viewModel.offlineAttachmentIds.collectAsStateWithLifecycle()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: do not expose offlineAttachmentIds outside of viewmodel just for mapping.
Add mapAttachment(attachment) method for mapping one attachment and then both mapAttachment/mapAttachments can do the mapping using private offlineAttachmentIds in viewmodel.


// TODO I found out that empty attachments list is not handled here and it shows empty message with no information
if (attachments.size == 1) {
attachments.first().toUiModel().let {
val attachment = attachments.first()
attachment.toUiModel(isAvailableOffline = attachment.assetId() in offlineAttachmentIds).let {
AssetPreview(
modifier = modifier
.onVisibilityChanged { visible ->
Expand All @@ -86,7 +88,10 @@ fun MultipartAttachmentsView(
)
}
} else {
val groups = viewModel.mapAttachments(attachments)
val groups = viewModel.mapAttachments(
attachments = attachments,
offlineAttachmentIds = offlineAttachmentIds,
)

Column(
modifier = modifier
Expand Down Expand Up @@ -130,6 +135,7 @@ fun MultipartAttachmentsView(
}
}


@Composable
private fun AttachmentsList(
attachments: List<MultipartAttachmentUi>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
package com.wire.android.ui.home.conversations.model.messagetypes.multipart

import androidx.compose.runtime.mutableStateMapOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.app.navArgs
import com.wire.android.appLogger
import com.wire.android.feature.cells.domain.model.AttachmentFileType
import com.wire.android.feature.cells.domain.model.AttachmentFileType.IMAGE
Expand All @@ -28,10 +30,12 @@ import com.wire.android.feature.cells.domain.model.AttachmentFileType.VIDEO
import com.wire.android.feature.cells.ui.edit.OnlineEditor
import com.wire.android.ui.common.multipart.MultipartAttachmentUi
import com.wire.android.ui.common.multipart.toUiModel
import com.wire.android.ui.home.conversations.ConversationNavArgs
import com.wire.android.util.FileManager
import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase
import com.wire.kalium.cells.domain.usecase.GetWireCellConfigurationUseCase
import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase
import com.wire.kalium.cells.domain.usecase.offline.ObserveOfflineFilesUseCase
import com.wire.kalium.common.functional.onSuccess
import com.wire.kalium.logic.data.asset.AssetTransferStatus
import com.wire.kalium.logic.data.asset.KaliumFileSystem
Expand All @@ -42,36 +46,50 @@ import com.wire.kalium.logic.data.message.MessageAttachment
import com.wire.kalium.logic.featureFlags.KaliumConfigs
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import okio.Path.Companion.toPath
import javax.inject.Inject

interface MultipartAttachmentsViewModel {
val offlineAttachmentIds: StateFlow<Set<String>>
fun onClick(attachment: MultipartAttachmentUi, openInImageViewer: (String) -> Unit)
fun mapAttachments(
attachments: List<MessageAttachment>
attachments: List<MessageAttachment>,
offlineAttachmentIds: Set<String> = emptySet(),
): List<MultipartAttachmentGroup> {

val result = mutableListOf<MultipartAttachmentGroup>()
var group: MultipartAttachmentGroup? = null

attachments.forEach {
val isAvailableOffline = it.assetId() in offlineAttachmentIds
if (it.isMediaAttachment()) {
group = when (group) {
null -> MultipartAttachmentGroup.Media(listOf(it.toUiModel()))
is MultipartAttachmentGroup.Media -> group.copy(group.attachments + it.toUiModel())
null -> MultipartAttachmentGroup.Media(listOf(it.toUiModel(isAvailableOffline = isAvailableOffline)))
is MultipartAttachmentGroup.Media -> {
val newAttachment = it.toUiModel(isAvailableOffline = isAvailableOffline)
group.copy(attachments = group.attachments + newAttachment)
}
else -> {
result.add(group)
MultipartAttachmentGroup.Media(listOf(it.toUiModel()))
MultipartAttachmentGroup.Media(listOf(it.toUiModel(isAvailableOffline = isAvailableOffline)))
}
}
} else {
group = when (group) {
null -> MultipartAttachmentGroup.Files(listOf(it.toUiModel()))
is MultipartAttachmentGroup.Files -> group.copy(group.attachments + it.toUiModel())
null -> MultipartAttachmentGroup.Files(listOf(it.toUiModel(isAvailableOffline = isAvailableOffline)))
is MultipartAttachmentGroup.Files -> {
val newAttachment = it.toUiModel(isAvailableOffline = isAvailableOffline)
group.copy(attachments = group.attachments + newAttachment)
}
else -> {
result.add(group)
MultipartAttachmentGroup.Files(listOf(it.toUiModel()))
MultipartAttachmentGroup.Files(listOf(it.toUiModel(isAvailableOffline = isAvailableOffline)))
}
}
}
Expand All @@ -95,13 +113,16 @@ interface MultipartAttachmentsViewModel {

@Suppress("EmptyFunctionBlock")
object MultipartAttachmentsViewModelPreview : MultipartAttachmentsViewModel {
override val offlineAttachmentIds: StateFlow<Set<String>> = MutableStateFlow(emptySet<String>())
override fun onClick(attachment: MultipartAttachmentUi, openInImageViewer: (String) -> Unit) {}
override fun onAttachmentsVisible(attachments: List<MessageAttachment>) {}
override fun onAttachmentsHidden(attachments: List<MessageAttachment>) {}
}

@Suppress("LongParameterList")
@HiltViewModel
class MultipartAttachmentsViewModelImpl @Inject constructor(
savedStateHandle: SavedStateHandle,
private val refreshHelper: CellAssetRefreshHelper,
private val download: DownloadCellFileUseCase,
private val getEditorUrl: GetEditorUrlUseCase,
Expand All @@ -110,17 +131,25 @@ class MultipartAttachmentsViewModelImpl @Inject constructor(
private val kaliumFileSystem: KaliumFileSystem,
private val featureFlags: KaliumConfigs,
private val getWireCellsConfig: GetWireCellConfigurationUseCase,
observeOfflineFiles: ObserveOfflineFilesUseCase,
) : ViewModel(), MultipartAttachmentsViewModel {
private val conversationId = savedStateHandle.navArgs<ConversationNavArgs>().conversationId.value

private val uploadProgress = mutableStateMapOf<String, Float>()
override val offlineAttachmentIds: StateFlow<Set<String>> = observeOfflineFiles()
Comment thread
ohassine marked this conversation as resolved.
Outdated
.map { offlineFiles -> offlineFiles.mapTo(mutableSetOf()) { it.id } }
.stateIn(viewModelScope, SharingStarted.Eagerly, emptySet())

private var isCollaboraEnabled: Boolean = false

init {
loadWireCellConfig()
}

override fun onClick(attachment: MultipartAttachmentUi, openInImageViewer: (String) -> Unit) {
override fun onClick(
attachment: MultipartAttachmentUi,
openInImageViewer: (String) -> Unit,
) {
when {
attachment.isImage() && !attachment.fileNotFound() -> openInImageViewer(attachment.uuid)
attachment.isEditSupported && isCollaboraEnabled && featureFlags.collaboraIntegration ->
Expand Down Expand Up @@ -174,7 +203,7 @@ class MultipartAttachmentsViewModelImpl @Inject constructor(

download(
assetId = attachment.uuid,
conversationId = null, // TODO to replace with real conversation id in next PR
conversationId = conversationId,
outFilePath = path,
assetSize = attachment.assetSize ?: 0,
) { progress ->
Expand Down Expand Up @@ -208,7 +237,7 @@ class MultipartAttachmentsViewModelImpl @Inject constructor(
}
}

private fun MessageAttachment.assetId() =
internal fun MessageAttachment.assetId() =
when (this) {
is AssetContent -> remoteData.assetId
is CellAssetContent -> id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import com.wire.android.feature.cells.R
import com.wire.android.feature.cells.domain.model.AttachmentFileType
import com.wire.android.ui.common.applyIf
import com.wire.android.ui.common.colorsScheme
Expand Down Expand Up @@ -92,6 +95,20 @@ internal fun AssetGridPreview(
}
}

if (item.isAvailableOffline) {
Icon(
modifier = Modifier
.padding(
end = dimensions().spacing6x,
top = dimensions().spacing6x
)
.align(Alignment.TopEnd),
painter = painterResource(R.drawable.ic_downloaded),
contentDescription = null,
tint = colorsScheme().secondaryText,
)
}

item.progress?.let {
CircularProgressIndicator(
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,17 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import com.wire.android.feature.cells.R
import com.wire.android.feature.cells.domain.model.AttachmentFileType
import com.wire.android.ui.common.applyIf
import com.wire.android.ui.common.colorsScheme
Expand Down Expand Up @@ -76,6 +81,19 @@ fun AssetPreview(
item.isEditSupported -> EditableAssetPreview(item, messageStyle)
else -> FileAssetPreview(item, messageStyle)
}
if (item.isAvailableOffline) {
Icon(
modifier = Modifier
.padding(
end = dimensions().spacing6x,
top = dimensions().spacing6x
)
.align(Alignment.TopEnd),
painter = painterResource(R.drawable.ic_downloaded),
contentDescription = null,
tint = colorsScheme().secondaryText,
)
}
} else {
AssetNotAvailablePreview(messageStyle = messageStyle)
}
Expand Down
Loading
Loading