Skip to content

Commit fbf58c6

Browse files
committed
fix: opening deleted file (WPB-21762)
1 parent 7bfde08 commit fbf58c6

File tree

3 files changed

+287
-12
lines changed

3 files changed

+287
-12
lines changed

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,10 @@ fun MultipartAttachmentsView(
6868
messageStyle = messageStyle,
6969
accent = accent,
7070
onClick = {
71-
if (it.mimeType.startsWith("image/")) {
72-
onImageAttachmentClick(it.uuid)
73-
} else {
74-
viewModel.onClick(it)
75-
}
71+
viewModel.onClick(
72+
attachment = it,
73+
openInImageViewer = onImageAttachmentClick,
74+
)
7675
},
7776
)
7877
}
@@ -90,19 +89,23 @@ fun MultipartAttachmentsView(
9089
attachments = group.attachments,
9190
messageStyle = messageStyle,
9291
onClick = {
93-
if (it.mimeType.startsWith("image/")) {
94-
onImageAttachmentClick(it.uuid)
95-
} else {
96-
viewModel.onClick(it)
97-
}
92+
viewModel.onClick(
93+
attachment = it,
94+
openInImageViewer = onImageAttachmentClick,
95+
)
9896
},
9997
)
10098

10199
is MultipartAttachmentsViewModel.MultipartAttachmentGroup.Files ->
102100
AttachmentsList(
103101
attachments = group.attachments,
104102
messageStyle = messageStyle,
105-
onClick = { viewModel.onClick(it) },
103+
onClick = {
104+
viewModel.onClick(
105+
attachment = it,
106+
openInImageViewer = onImageAttachmentClick,
107+
)
108+
},
106109
accent = accent
107110
)
108111
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,9 @@ class MultipartAttachmentsViewModel @Inject constructor(
9595
data class Files(val attachments: List<MultipartAttachmentUi>) : MultipartAttachmentGroup
9696
}
9797

98-
fun onClick(attachment: MultipartAttachmentUi) {
98+
fun onClick(attachment: MultipartAttachmentUi, openInImageViewer: (String) -> Unit) {
9999
when {
100+
attachment.isImage() && !attachment.fileNotFound() -> openInImageViewer(attachment.uuid)
100101
attachment.fileNotFound() -> { refreshAssetState(attachment) }
101102
attachment.localFileAvailable() -> openLocalFile(attachment)
102103
attachment.canOpenWithUrl() -> openUrl(attachment)
@@ -173,6 +174,8 @@ private fun MessageAttachment.mimeType() =
173174
is CellAssetContent -> mimeType
174175
}
175176

177+
private fun MultipartAttachmentUi.isImage() = AttachmentFileType.fromMimeType(mimeType) == IMAGE
178+
176179
private fun MessageAttachment.isMediaAttachment() =
177180
when (AttachmentFileType.fromMimeType(mimeType())) {
178181
IMAGE, VIDEO -> true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2025 Wire Swiss GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see http://www.gnu.org/licenses/.
17+
*/
18+
package com.wire.android.ui.home.conversations.model.messagetypes.multipart
19+
20+
import com.wire.android.feature.cells.domain.model.AttachmentFileType
21+
import com.wire.android.framework.FakeKaliumFileSystem
22+
import com.wire.android.ui.common.multipart.AssetSource
23+
import com.wire.android.ui.common.multipart.MultipartAttachmentUi
24+
import com.wire.android.util.FileManager
25+
import com.wire.kalium.cells.domain.usecase.DownloadCellFileUseCase
26+
import com.wire.kalium.cells.domain.usecase.RefreshCellAssetStateUseCase
27+
import com.wire.kalium.common.functional.right
28+
import com.wire.kalium.logic.data.asset.AssetTransferStatus
29+
import com.wire.kalium.logic.data.asset.KaliumFileSystem
30+
import com.wire.kalium.logic.data.message.CellAssetContent
31+
import io.mockk.MockKAnnotations
32+
import io.mockk.coEvery
33+
import io.mockk.coVerify
34+
import io.mockk.impl.annotations.MockK
35+
import io.mockk.mockk
36+
import kotlinx.coroutines.test.runTest
37+
import org.junit.jupiter.api.Assertions.assertEquals
38+
import org.junit.jupiter.api.Test
39+
40+
typealias OpenImageCallback = (s: String) -> Unit
41+
42+
class MultipartAttachmentsViewModelTest {
43+
44+
@Test
45+
fun `with multiple media attachments when mapped the attachments are grouped correctly`() = runTest {
46+
val (_, viewModel) = Arrangement()
47+
.arrange()
48+
49+
val result = viewModel.mapAttachments(
50+
listOf(
51+
testAssetContent.copy(id = "asset_1"),
52+
testAssetContent.copy(id = "asset_2"),
53+
testAssetContent.copy(id = "asset_3"),
54+
)
55+
)
56+
57+
assertEquals(
58+
listOf(
59+
MultipartAttachmentsViewModel.MultipartAttachmentGroup.Media(
60+
attachments = listOf(
61+
testAttachmentUi.copy(uuid = "asset_1"),
62+
testAttachmentUi.copy(uuid = "asset_2"),
63+
testAttachmentUi.copy(uuid = "asset_3"),
64+
)
65+
)
66+
),
67+
result
68+
)
69+
}
70+
71+
@Test
72+
fun `with multiple file attachments when mapped the attachments are grouped correctly`() = runTest {
73+
val (_, viewModel) = Arrangement()
74+
.arrange()
75+
76+
val result = viewModel.mapAttachments(
77+
listOf(
78+
testAssetContent.copy(id = "asset_1", mimeType = "application/pdf"),
79+
testAssetContent.copy(id = "asset_2", mimeType = "application/pdf"),
80+
testAssetContent.copy(id = "asset_3", mimeType = "application/pdf"),
81+
)
82+
)
83+
84+
assertEquals(
85+
listOf(
86+
MultipartAttachmentsViewModel.MultipartAttachmentGroup.Files(
87+
attachments = listOf(
88+
testAttachmentUi.copy(uuid = "asset_1", mimeType = "application/pdf", assetType = AttachmentFileType.PDF),
89+
testAttachmentUi.copy(uuid = "asset_2", mimeType = "application/pdf", assetType = AttachmentFileType.PDF),
90+
testAttachmentUi.copy(uuid = "asset_3", mimeType = "application/pdf", assetType = AttachmentFileType.PDF),
91+
)
92+
)
93+
),
94+
result
95+
)
96+
}
97+
98+
// mixed media and non media
99+
@Test
100+
fun `with mixed media attachments when mapped the attachments are grouped correctly`() = runTest {
101+
val (_, viewModel) = Arrangement()
102+
.arrange()
103+
104+
val result = viewModel.mapAttachments(
105+
listOf(
106+
testAssetContent.copy(id = "asset_1"),
107+
testAssetContent.copy(id = "asset_2"),
108+
testAssetContent.copy(id = "asset_3"),
109+
testAssetContent.copy(id = "asset_4", mimeType = "application/pdf"),
110+
testAssetContent.copy(id = "asset_5"),
111+
)
112+
)
113+
114+
assertEquals(
115+
listOf(
116+
MultipartAttachmentsViewModel.MultipartAttachmentGroup.Media(
117+
attachments = listOf(
118+
testAttachmentUi.copy(uuid = "asset_1"),
119+
testAttachmentUi.copy(uuid = "asset_2"),
120+
testAttachmentUi.copy(uuid = "asset_3"),
121+
)
122+
),
123+
MultipartAttachmentsViewModel.MultipartAttachmentGroup.Files(
124+
attachments = listOf(
125+
testAttachmentUi.copy(uuid = "asset_4", mimeType = "application/pdf", assetType = AttachmentFileType.PDF),
126+
)
127+
),
128+
MultipartAttachmentsViewModel.MultipartAttachmentGroup.Media(
129+
attachments = listOf(
130+
testAttachmentUi.copy(uuid = "asset_5"),
131+
)
132+
),
133+
),
134+
result
135+
)
136+
}
137+
138+
139+
// on click tests
140+
141+
@Test
142+
fun `with image attachment when clicked then image opened in internal viewer`() = runTest {
143+
val (_, viewModel) = Arrangement()
144+
.arrange()
145+
146+
val callback = mockk<OpenImageCallback>(relaxed = true)
147+
148+
viewModel.onClick(testAttachmentUi, callback)
149+
150+
coVerify(exactly = 1) { callback.invoke(testAttachmentUi.uuid) }
151+
}
152+
153+
// image click not found
154+
@Test
155+
fun `with image attachment with not found status when clicked then image is not opened`() = runTest {
156+
val (arrangement, viewModel) = Arrangement()
157+
.arrange()
158+
159+
val callback = mockk<OpenImageCallback>(relaxed = true)
160+
161+
viewModel.onClick(testAttachmentUi.copy(
162+
transferStatus = AssetTransferStatus.NOT_FOUND,
163+
), callback)
164+
165+
coVerify(exactly = 0) { callback.invoke(testAttachmentUi.uuid) }
166+
coVerify(exactly = 1) { arrangement.refreshAsset(testAttachmentUi.uuid) }
167+
}
168+
169+
@Test
170+
fun `with file attachment with not found status when clicked then refresh is called`() = runTest {
171+
val (arrangement, viewModel) = Arrangement()
172+
.arrange()
173+
174+
val callback = mockk<OpenImageCallback>(relaxed = true)
175+
176+
viewModel.onClick(testAttachmentUi.copy(
177+
mimeType = "application/pdf",
178+
transferStatus = AssetTransferStatus.NOT_FOUND,
179+
), callback)
180+
181+
coVerify(exactly = 0) { callback.invoke(testAttachmentUi.uuid) }
182+
coVerify(exactly = 1) { arrangement.refreshAsset(testAttachmentUi.uuid) }
183+
}
184+
185+
@Test
186+
fun `with file attachment with local file available when clicked then file is opened locally`() = runTest {
187+
val (arrangement, viewModel) = Arrangement()
188+
.arrange()
189+
190+
val callback = mockk<OpenImageCallback>(relaxed = true)
191+
192+
viewModel.onClick(testAttachmentUi.copy(
193+
mimeType = "application/pdf",
194+
localPath = "local/path",
195+
), callback)
196+
197+
coVerify(exactly = 1) { arrangement.fileManager.openWithExternalApp(any(), any(), any(), any()) }
198+
}
199+
200+
@Test
201+
fun `with file attachment openable via url when clicked then file is opened via url`() = runTest {
202+
val (arrangement, viewModel) = Arrangement()
203+
.arrange()
204+
205+
val callback = mockk<OpenImageCallback>(relaxed = true)
206+
207+
viewModel.onClick(testAttachmentUi.copy(
208+
mimeType = "application/pdf",
209+
contentUrl = "content/url",
210+
), callback)
211+
212+
coVerify(exactly = 1) { arrangement.fileManager.openUrlWithExternalApp(any(), any(), any()) }
213+
}
214+
215+
// TODO: Refresh asset tests (part of refresh update PR)
216+
217+
private class Arrangement {
218+
219+
init {
220+
MockKAnnotations.init(this)
221+
}
222+
223+
@MockK
224+
lateinit var refreshAsset: RefreshCellAssetStateUseCase
225+
@MockK
226+
lateinit var download: DownloadCellFileUseCase
227+
@MockK
228+
lateinit var fileManager: FileManager
229+
230+
val kaliumFileSystem: KaliumFileSystem = FakeKaliumFileSystem()
231+
232+
suspend fun arrange(): Pair<Arrangement, MultipartAttachmentsViewModel> {
233+
234+
coEvery { refreshAsset(any()) } returns Unit.right()
235+
coEvery { fileManager.openWithExternalApp(any(), any(), any(), any()) } returns Unit
236+
coEvery { fileManager.openUrlWithExternalApp(any(), any(), any()) } returns Unit
237+
coEvery { download(any(), any(), any(), any(), any()) } returns Unit.right()
238+
239+
return this to MultipartAttachmentsViewModel(
240+
refreshAsset = refreshAsset,
241+
download = download,
242+
fileManager = fileManager,
243+
kaliumFileSystem = kaliumFileSystem,
244+
)
245+
}
246+
}
247+
248+
private companion object {
249+
val testAssetContent = CellAssetContent(
250+
id = "assetId1",
251+
versionId = "1",
252+
mimeType = "image/png",
253+
assetPath = "/filename",
254+
assetSize = 0,
255+
metadata = null,
256+
transferStatus = AssetTransferStatus.NOT_DOWNLOADED,
257+
)
258+
val testAttachmentUi = MultipartAttachmentUi(
259+
uuid = "asset_1",
260+
source = AssetSource.CELL,
261+
fileName = "filename",
262+
localPath = null,
263+
mimeType = "image/png",
264+
assetType = AttachmentFileType.IMAGE,
265+
assetSize = 0,
266+
transferStatus = AssetTransferStatus.NOT_DOWNLOADED,
267+
)
268+
}
269+
}

0 commit comments

Comments
 (0)