Skip to content

Commit 63a3bf4

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

File tree

3 files changed

+296
-12
lines changed

3 files changed

+296
-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,278 @@
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+
@Test
99+
fun `with mixed media attachments when mapped the attachments are grouped correctly`() = runTest {
100+
val (_, viewModel) = Arrangement()
101+
.arrange()
102+
103+
val result = viewModel.mapAttachments(
104+
listOf(
105+
testAssetContent.copy(id = "asset_1"),
106+
testAssetContent.copy(id = "asset_2"),
107+
testAssetContent.copy(id = "asset_3"),
108+
testAssetContent.copy(id = "asset_4", mimeType = "application/pdf"),
109+
testAssetContent.copy(id = "asset_5"),
110+
)
111+
)
112+
113+
assertEquals(
114+
listOf(
115+
MultipartAttachmentsViewModel.MultipartAttachmentGroup.Media(
116+
attachments = listOf(
117+
testAttachmentUi.copy(uuid = "asset_1"),
118+
testAttachmentUi.copy(uuid = "asset_2"),
119+
testAttachmentUi.copy(uuid = "asset_3"),
120+
)
121+
),
122+
MultipartAttachmentsViewModel.MultipartAttachmentGroup.Files(
123+
attachments = listOf(
124+
testAttachmentUi.copy(uuid = "asset_4", mimeType = "application/pdf", assetType = AttachmentFileType.PDF),
125+
)
126+
),
127+
MultipartAttachmentsViewModel.MultipartAttachmentGroup.Media(
128+
attachments = listOf(
129+
testAttachmentUi.copy(uuid = "asset_5"),
130+
)
131+
),
132+
),
133+
result
134+
)
135+
}
136+
137+
@Test
138+
fun `with image attachment when clicked then image opened in internal viewer`() = runTest {
139+
val (_, viewModel) = Arrangement()
140+
.arrange()
141+
142+
val callback = mockk<OpenImageCallback>(relaxed = true)
143+
144+
viewModel.onClick(testAttachmentUi, callback)
145+
146+
coVerify(exactly = 1) { callback.invoke(testAttachmentUi.uuid) }
147+
}
148+
149+
@Test
150+
fun `with image attachment with not found status when clicked then image is not opened`() = runTest {
151+
val (arrangement, viewModel) = Arrangement()
152+
.arrange()
153+
154+
val callback = mockk<OpenImageCallback>(relaxed = true)
155+
156+
viewModel.onClick(
157+
attachment = testAttachmentUi.copy(
158+
transferStatus = AssetTransferStatus.NOT_FOUND,
159+
),
160+
openInImageViewer = callback
161+
)
162+
163+
coVerify(exactly = 0) { callback.invoke(testAttachmentUi.uuid) }
164+
coVerify(exactly = 1) { arrangement.refreshAsset(testAttachmentUi.uuid) }
165+
}
166+
167+
@Test
168+
fun `with file attachment with not found status when clicked then refresh is called`() = runTest {
169+
val (arrangement, viewModel) = Arrangement()
170+
.arrange()
171+
172+
val callback = mockk<OpenImageCallback>(relaxed = true)
173+
174+
viewModel.onClick(
175+
attachment = testAttachmentUi.copy(
176+
mimeType = "application/pdf",
177+
transferStatus = AssetTransferStatus.NOT_FOUND,
178+
),
179+
openInImageViewer = callback
180+
)
181+
182+
coVerify(exactly = 0) { callback.invoke(testAttachmentUi.uuid) }
183+
coVerify(exactly = 1) { arrangement.refreshAsset(testAttachmentUi.uuid) }
184+
}
185+
186+
@Test
187+
fun `with file attachment with local file available when clicked then file is opened locally`() = runTest {
188+
val (arrangement, viewModel) = Arrangement()
189+
.arrange()
190+
191+
val callback = mockk<OpenImageCallback>(relaxed = true)
192+
193+
viewModel.onClick(
194+
attachment = testAttachmentUi.copy(
195+
mimeType = "application/pdf",
196+
localPath = "local/path",
197+
),
198+
openInImageViewer = callback
199+
)
200+
201+
coVerify(exactly = 1) { arrangement.fileManager.openWithExternalApp(any(), any(), any(), any()) }
202+
}
203+
204+
@Test
205+
fun `with file attachment openable via url when clicked then file is opened via url`() = runTest {
206+
val (arrangement, viewModel) = Arrangement()
207+
.arrange()
208+
209+
val callback = mockk<OpenImageCallback>(relaxed = true)
210+
211+
viewModel.onClick(
212+
attachment = testAttachmentUi.copy(
213+
mimeType = "application/pdf",
214+
contentUrl = "content/url",
215+
),
216+
openInImageViewer = callback
217+
)
218+
219+
coVerify(exactly = 1) { arrangement.fileManager.openUrlWithExternalApp(any(), any(), any()) }
220+
}
221+
222+
// TODO: Refresh asset tests (part of refresh update PR)
223+
224+
private class Arrangement {
225+
226+
init {
227+
MockKAnnotations.init(this)
228+
}
229+
230+
@MockK
231+
lateinit var refreshAsset: RefreshCellAssetStateUseCase
232+
233+
@MockK
234+
lateinit var download: DownloadCellFileUseCase
235+
236+
@MockK
237+
lateinit var fileManager: FileManager
238+
239+
val kaliumFileSystem: KaliumFileSystem = FakeKaliumFileSystem()
240+
241+
suspend fun arrange(): Pair<Arrangement, MultipartAttachmentsViewModel> {
242+
243+
coEvery { refreshAsset(any()) } returns Unit.right()
244+
coEvery { fileManager.openWithExternalApp(any(), any(), any(), any()) } returns Unit
245+
coEvery { fileManager.openUrlWithExternalApp(any(), any(), any()) } returns Unit
246+
coEvery { download(any(), any(), any(), any(), any()) } returns Unit.right()
247+
248+
return this to MultipartAttachmentsViewModel(
249+
refreshAsset = refreshAsset,
250+
download = download,
251+
fileManager = fileManager,
252+
kaliumFileSystem = kaliumFileSystem,
253+
)
254+
}
255+
}
256+
257+
private companion object {
258+
val testAssetContent = CellAssetContent(
259+
id = "assetId1",
260+
versionId = "1",
261+
mimeType = "image/png",
262+
assetPath = "/filename",
263+
assetSize = 0,
264+
metadata = null,
265+
transferStatus = AssetTransferStatus.NOT_DOWNLOADED,
266+
)
267+
val testAttachmentUi = MultipartAttachmentUi(
268+
uuid = "asset_1",
269+
source = AssetSource.CELL,
270+
fileName = "filename",
271+
localPath = null,
272+
mimeType = "image/png",
273+
assetType = AttachmentFileType.IMAGE,
274+
assetSize = 0,
275+
transferStatus = AssetTransferStatus.NOT_DOWNLOADED,
276+
)
277+
}
278+
}

0 commit comments

Comments
 (0)