Skip to content

Commit d088785

Browse files
Merge pull request #6327 from nextcloud/upload-placeholder
Show file upload progress and placeholder media message in chat.
2 parents bb7ce51 + 347f82d commit d088785

25 files changed

Lines changed: 1424 additions & 385 deletions

app/src/main/java/com/nextcloud/talk/api/NcApi.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,8 @@ Observable<GenericOverall> createRemoteShare(@Nullable @Header("Authorization")
410410
@Field("path") String remotePath,
411411
@Field("shareWith") String roomToken,
412412
@Field("shareType") String shareType,
413-
@Field("talkMetaData") String talkMetaData);
413+
@Field("talkMetaData") String talkMetaData,
414+
@Field("referenceId") String referenceId);
414415

415416
@FormUrlEncoded
416417
@PUT

app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewScreen.kt

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.Column
1616
import androidx.compose.foundation.layout.Row
1717
import androidx.compose.foundation.layout.fillMaxSize
1818
import androidx.compose.foundation.layout.fillMaxWidth
19+
import androidx.compose.foundation.layout.height
1920
import androidx.compose.foundation.layout.imePadding
2021
import androidx.compose.foundation.layout.navigationBarsPadding
2122
import androidx.compose.foundation.layout.padding
@@ -58,6 +59,9 @@ import kotlinx.coroutines.launch
5859

5960
private const val MAX_ADD_MORE_FILES = 10
6061

62+
private const val APP_BAR_HEIGHT_DP = 64
63+
private const val APP_BAR_HORIZONTAL_PADDING_DP = 4
64+
6165
/**
6266
* Full-screen dialog content for reviewing, reordering and captioning files picked for upload,
6367
* hosted by [FileAttachmentPreviewFragment]. [viewModel] owns the file list and its (IO-derived)
@@ -161,7 +165,8 @@ private fun PreviewTopBar(conversationName: String, onDismiss: () -> Unit) {
161165
verticalAlignment = Alignment.CenterVertically,
162166
modifier = Modifier
163167
.fillMaxWidth()
164-
.padding(16.dp)
168+
.height(APP_BAR_HEIGHT_DP.dp)
169+
.padding(horizontal = APP_BAR_HORIZONTAL_PADDING_DP.dp)
165170
) {
166171
IconButton(onClick = onDismiss) {
167172
Icon(
@@ -170,16 +175,21 @@ private fun PreviewTopBar(conversationName: String, onDismiss: () -> Unit) {
170175
)
171176
}
172177

173-
Column(modifier = Modifier.weight(1f)) {
178+
Column(
179+
modifier = Modifier
180+
.weight(1f)
181+
.padding(horizontal = APP_BAR_HORIZONTAL_PADDING_DP.dp)
182+
) {
174183
Text(
175184
text = conversationName,
176-
style = MaterialTheme.typography.titleMedium,
185+
style = MaterialTheme.typography.titleLarge,
186+
color = MaterialTheme.colorScheme.onSurface,
177187
maxLines = 1,
178188
overflow = TextOverflow.Ellipsis
179189
)
180190
Text(
181191
text = stringResource(R.string.nc_add_file),
182-
style = MaterialTheme.typography.bodyMedium,
192+
style = MaterialTheme.typography.labelMedium,
183193
color = MaterialTheme.colorScheme.onSurfaceVariant,
184194
maxLines = 1,
185195
overflow = TextOverflow.Ellipsis
@@ -236,6 +246,7 @@ private fun rememberPreviewViewModel(files: List<String>): FileAttachmentPreview
236246
showBackground = true,
237247
uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL
238248
)
249+
@Preview(name = "RTL Arabic", showBackground = true, locale = "ar")
239250
@Composable
240251
private fun FileAttachmentPreviewContentPreview() {
241252
val colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()

app/src/main/java/com/nextcloud/talk/attachmentpreview/FileThumbnailImage.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.ui.unit.Dp
2525
import androidx.compose.ui.unit.dp
2626
import coil.compose.AsyncImage
2727
import com.nextcloud.talk.R
28+
import com.nextcloud.talk.ui.theme.mimetypeIconTint
2829
import com.nextcloud.talk.utils.DrawableUtils
2930

3031
internal const val THUMBNAIL_CORNER_RADIUS_DP = 16
@@ -74,17 +75,19 @@ internal fun FileThumbnailImage(
7475
}
7576
}
7677

77-
MediaKind.OTHER ->
78+
MediaKind.OTHER -> {
79+
val mimetypeIcon = DrawableUtils.getDrawableResourceIdForMimeType(description.mimeType)
7880
Box(
7981
modifier = modifier.clip(shape).background(backgroundColor),
8082
contentAlignment = Alignment.Center
8183
) {
8284
Icon(
83-
painter = painterResource(DrawableUtils.getDrawableResourceIdForMimeType(description.mimeType)),
85+
painter = painterResource(mimetypeIcon),
8486
contentDescription = description.name,
85-
tint = Color.Unspecified,
87+
tint = mimetypeIconTint(mimetypeIcon),
8688
modifier = Modifier.size(iconSize)
8789
)
8890
}
91+
}
8992
}
9093
}

app/src/main/java/com/nextcloud/talk/attachmentpreview/ThumbnailStrip.kt

Lines changed: 33 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
package com.nextcloud.talk.attachmentpreview
88

9+
import androidx.annotation.StringRes
910
import androidx.compose.foundation.background
1011
import androidx.compose.foundation.border
1112
import androidx.compose.foundation.clickable
@@ -25,9 +26,10 @@ import androidx.compose.foundation.lazy.itemsIndexed
2526
import androidx.compose.foundation.shape.CircleShape
2627
import androidx.compose.foundation.shape.RoundedCornerShape
2728
import androidx.compose.material.icons.Icons
28-
import androidx.compose.material.icons.filled.Delete
29-
import androidx.compose.material.icons.filled.PhotoCamera
30-
import androidx.compose.material.icons.filled.Videocam
29+
import androidx.compose.material.icons.outlined.Delete
30+
import androidx.compose.material.icons.outlined.PhotoCamera
31+
import androidx.compose.material.icons.outlined.PhotoLibrary
32+
import androidx.compose.material.icons.outlined.Videocam
3133
import androidx.compose.material3.Icon
3234
import androidx.compose.material3.MaterialTheme
3335
import androidx.compose.runtime.Composable
@@ -40,6 +42,7 @@ import androidx.compose.ui.Modifier
4042
import androidx.compose.ui.draw.clip
4143
import androidx.compose.ui.graphics.Color
4244
import androidx.compose.ui.graphics.graphicsLayer
45+
import androidx.compose.ui.graphics.vector.ImageVector
4346
import androidx.compose.ui.input.pointer.pointerInput
4447
import androidx.compose.ui.platform.LocalDensity
4548
import androidx.compose.ui.res.painterResource
@@ -107,9 +110,27 @@ internal fun ThumbnailStrip(
107110
)
108111
}
109112
}
110-
item { AddMoreTile(onClick = onAddMore) }
111-
item { TakePhotoTile(onClick = onTakePhoto) }
112-
item { TakeVideoTile(onClick = onTakeVideo) }
113+
item {
114+
ActionTile(
115+
icon = Icons.Outlined.PhotoLibrary,
116+
contentDescription = R.string.nc_add_more_files,
117+
onClick = onAddMore
118+
)
119+
}
120+
item {
121+
ActionTile(
122+
icon = Icons.Outlined.PhotoCamera,
123+
contentDescription = R.string.take_photo,
124+
onClick = onTakePhoto
125+
)
126+
}
127+
item {
128+
ActionTile(
129+
icon = Icons.Outlined.Videocam,
130+
contentDescription = R.string.nc_take_video,
131+
onClick = onTakeVideo
132+
)
133+
}
113134
}
114135
}
115136

@@ -270,7 +291,7 @@ private fun StripThumbnail(
270291
contentAlignment = Alignment.Center
271292
) {
272293
Icon(
273-
imageVector = Icons.Filled.Delete,
294+
imageVector = Icons.Outlined.Delete,
274295
contentDescription = stringResource(R.string.nc_remove_file),
275296
tint = Color.White,
276297
modifier = Modifier.size(STRIP_ICON_SIZE_DP.dp)
@@ -281,54 +302,19 @@ private fun StripThumbnail(
281302
}
282303

283304
@Composable
284-
private fun AddMoreTile(onClick: () -> Unit) {
285-
Box(
286-
modifier = Modifier
287-
.size(STRIP_THUMBNAIL_SIZE_DP.dp)
288-
.clip(RoundedCornerShape(THUMBNAIL_CORNER_RADIUS_DP.dp))
289-
.background(MaterialTheme.colorScheme.surfaceVariant)
290-
.clickable(onClick = onClick),
291-
contentAlignment = Alignment.Center
292-
) {
293-
Icon(
294-
painter = painterResource(R.drawable.baseline_photo_library_24),
295-
contentDescription = stringResource(R.string.nc_add_more_files),
296-
modifier = Modifier.size(STRIP_ICON_SIZE_DP.dp)
297-
)
298-
}
299-
}
300-
301-
@Composable
302-
private fun TakePhotoTile(onClick: () -> Unit) {
303-
Box(
304-
modifier = Modifier
305-
.size(STRIP_THUMBNAIL_SIZE_DP.dp)
306-
.clip(RoundedCornerShape(THUMBNAIL_CORNER_RADIUS_DP.dp))
307-
.background(MaterialTheme.colorScheme.surfaceVariant)
308-
.clickable(onClick = onClick),
309-
contentAlignment = Alignment.Center
310-
) {
311-
Icon(
312-
imageVector = Icons.Filled.PhotoCamera,
313-
contentDescription = stringResource(R.string.take_photo),
314-
modifier = Modifier.size(STRIP_ICON_SIZE_DP.dp)
315-
)
316-
}
317-
}
318-
319-
@Composable
320-
private fun TakeVideoTile(onClick: () -> Unit) {
305+
private fun ActionTile(icon: ImageVector, @StringRes contentDescription: Int, onClick: () -> Unit) {
321306
Box(
322307
modifier = Modifier
323308
.size(STRIP_THUMBNAIL_SIZE_DP.dp)
324309
.clip(RoundedCornerShape(THUMBNAIL_CORNER_RADIUS_DP.dp))
325-
.background(MaterialTheme.colorScheme.surfaceVariant)
310+
.background(MaterialTheme.colorScheme.secondaryContainer)
326311
.clickable(onClick = onClick),
327312
contentAlignment = Alignment.Center
328313
) {
329314
Icon(
330-
imageVector = Icons.Filled.Videocam,
331-
contentDescription = stringResource(R.string.nc_take_video),
315+
imageVector = icon,
316+
contentDescription = stringResource(contentDescription),
317+
tint = MaterialTheme.colorScheme.onSecondaryContainer,
332318
modifier = Modifier.size(STRIP_ICON_SIZE_DP.dp)
333319
)
334320
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
package com.nextcloud.talk.attachmentpreview
8+
9+
import android.content.res.Configuration
10+
import android.graphics.Bitmap
11+
import androidx.compose.foundation.isSystemInDarkTheme
12+
import androidx.compose.material3.MaterialTheme
13+
import androidx.compose.material3.Surface
14+
import androidx.compose.material3.darkColorScheme
15+
import androidx.compose.material3.lightColorScheme
16+
import androidx.compose.runtime.Composable
17+
import androidx.compose.ui.tooling.preview.Preview
18+
import androidx.core.graphics.createBitmap
19+
20+
private const val PREVIEW_THUMBNAIL_PX = 64
21+
22+
// Coil can't load the image tiles' content in a preview, so only the video tile gets a stand-in
23+
// bitmap - the flat color is enough to tell "has a thumbnail" apart from the icon fallback.
24+
private fun previewVideoThumbnail(): Bitmap =
25+
createBitmap(PREVIEW_THUMBNAIL_PX, PREVIEW_THUMBNAIL_PX)
26+
.apply { eraseColor(android.graphics.Color.DKGRAY) }
27+
28+
private fun previewDescription(
29+
uri: String,
30+
name: String,
31+
mimeType: String,
32+
detail: String,
33+
videoThumbnail: Bitmap? = null
34+
) = FileDescription(
35+
uri = uri,
36+
name = name,
37+
kind = mediaKind(mimeType),
38+
mimeType = mimeType,
39+
detail = detail,
40+
videoThumbnail = videoThumbnail
41+
)
42+
43+
private fun previewDescriptions() =
44+
listOf(
45+
previewDescription("file:///sdcard/DCIM/photo.jpg", "photo.jpg", "image/jpeg", "2048×1152, 210 kB"),
46+
previewDescription(
47+
uri = "file:///sdcard/DCIM/clip.mp4",
48+
name = "clip.mp4",
49+
mimeType = "video/mp4",
50+
detail = "0:42, 8 MB",
51+
videoThumbnail = previewVideoThumbnail()
52+
),
53+
previewDescription("file:///sdcard/Download/archive.zip", "archive.zip", "application/zip", "4 MB"),
54+
previewDescription("file:///sdcard/Documents/report.pdf", "report.pdf", "application/pdf", "820 kB")
55+
)
56+
57+
@Composable
58+
private fun ThumbnailStripPreviewContainer(descriptions: List<FileDescription>, selectedIndex: Int) {
59+
val colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
60+
MaterialTheme(colorScheme = colorScheme) {
61+
Surface {
62+
ThumbnailStrip(
63+
descriptions = descriptions,
64+
selectedIndex = selectedIndex,
65+
onSelect = {},
66+
onRemove = {},
67+
onReorder = { _, _ -> },
68+
onAddMore = {},
69+
onTakePhoto = {},
70+
onTakeVideo = {}
71+
)
72+
}
73+
}
74+
}
75+
76+
@Preview(name = "Light Mode", showBackground = true)
77+
@Preview(
78+
name = "Dark Mode",
79+
showBackground = true,
80+
uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL
81+
)
82+
@Composable
83+
private fun ThumbnailStripPreview() {
84+
ThumbnailStripPreviewContainer(descriptions = previewDescriptions(), selectedIndex = 0)
85+
}
86+
87+
/** The mimetype-icon tiles, where [mimetypeIconTint] decides whether an icon is themed or keeps its own colors. */
88+
@Preview(name = "Documents Light", showBackground = true)
89+
@Preview(
90+
name = "Documents Dark",
91+
showBackground = true,
92+
uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL
93+
)
94+
@Composable
95+
private fun ThumbnailStripDocumentsPreview() {
96+
ThumbnailStripPreviewContainer(
97+
descriptions = listOf(
98+
previewDescription("file:///sdcard/Download/archive.zip", "archive.zip", "application/zip", "4 MB"),
99+
previewDescription("file:///sdcard/Documents/report.pdf", "report.pdf", "application/pdf", "820 kB"),
100+
previewDescription("file:///sdcard/Documents/notes.txt", "notes.txt", "text/plain", "2 kB"),
101+
previewDescription("file:///sdcard/Documents/letter.doc", "letter.doc", "application/msword", "34 kB")
102+
),
103+
selectedIndex = 1
104+
)
105+
}
106+
107+
/** A single file shows no thumbnails at all - just the centered action tiles. */
108+
@Preview(name = "Single File", showBackground = true)
109+
@Composable
110+
private fun ThumbnailStripSingleFilePreview() {
111+
ThumbnailStripPreviewContainer(
112+
descriptions = previewDescriptions().take(1),
113+
selectedIndex = 0
114+
)
115+
}

app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ import com.nextcloud.talk.ui.chat.ChatMessageCallbacks
169169
import com.nextcloud.talk.ui.chat.ChatView
170170
import com.nextcloud.talk.ui.chat.ChatViewCallbacks
171171
import com.nextcloud.talk.ui.chat.ChatViewState
172+
import com.nextcloud.talk.ui.chat.LocalUploadProgressProvider
173+
import com.nextcloud.talk.ui.chat.LocalUploadedLocalPreviewProvider
172174
import com.nextcloud.talk.ui.dialog.DateTimeCompose
173175
import com.nextcloud.talk.ui.dialog.GetPinnedOptionsDialog
174176
import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
@@ -876,10 +878,15 @@ class ChatActivity :
876878

877879
SideEffect { chatListState = listState }
878880

881+
val uploadProgressMap by chatViewModel.uploadProgressMap.collectAsStateWithLifecycle()
882+
val uploadedLocalPreviewMap by chatViewModel.uploadedLocalPreviewMap.collectAsStateWithLifecycle()
883+
879884
CompositionLocalProvider(
880885
LocalViewThemeUtils provides viewThemeUtils,
881886
LocalMessageUtils provides messageUtils,
882-
LocalOpenGraphFetcher provides { url -> chatViewModel.fetchOpenGraph(url) }
887+
LocalOpenGraphFetcher provides { url -> chatViewModel.fetchOpenGraph(url) },
888+
LocalUploadProgressProvider provides { refId -> uploadProgressMap[refId] },
889+
LocalUploadedLocalPreviewProvider provides { refId -> uploadedLocalPreviewMap[refId] }
883890
) {
884891
val isOneToOneConversation by remember { mutableStateOf(uiState.isOneToOneConversation) }
885892
Log.d(TAG, "isOneToOneConversation=" + isOneToOneConversation)
@@ -938,7 +945,8 @@ class ChatActivity :
938945
onSystemMessageExpandClick = { messageId ->
939946
chatViewModel.toggleSystemMessageCollapse(messageId)
940947
},
941-
onAvatarClick = { messageId -> chatViewModel.showProfileSheet(messageId.toLong()) }
948+
onAvatarClick = { messageId -> chatViewModel.showProfileSheet(messageId.toLong()) },
949+
onCancelUpload = { referenceId -> chatViewModel.cancelUpload(referenceId) }
942950
)
943951
),
944952
listState = listState

0 commit comments

Comments
 (0)