Skip to content

Commit 79c8acb

Browse files
authored
Merge pull request #6604 from nextcloud/filesPermissionShareOptions
Files permission share options
2 parents 4d387c5 + ea233c4 commit 79c8acb

10 files changed

Lines changed: 235 additions & 98 deletions

File tree

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

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ import javax.inject.Inject
3232
class FileAttachmentPreviewFragment : DialogFragment() {
3333
private lateinit var filesList: ArrayList<String>
3434
private var conversationName: String = ""
35-
private var uploadFiles: (files: MutableList<String>, caption: String, compressImages: Boolean) -> Unit =
36-
{ _, _, _ -> }
35+
private var showFilePermissionsOption: Boolean = false
36+
private var uploadFiles:
37+
(files: MutableList<String>, caption: String, compressImages: Boolean, allowUpdate: Boolean) -> Unit =
38+
{ _, _, _, _ -> }
3739
private var composeView: ComposeView? = null
3840

3941
@Inject
@@ -49,14 +51,22 @@ class FileAttachmentPreviewFragment : DialogFragment() {
4951
ViewModelProvider(this, viewModelFactory)[FileAttachmentPreviewViewModel::class.java]
5052
}
5153

52-
fun setListener(uploadFiles: (files: MutableList<String>, caption: String, compressImages: Boolean) -> Unit) {
54+
fun setListener(
55+
uploadFiles: (
56+
files: MutableList<String>,
57+
caption: String,
58+
compressImages: Boolean,
59+
allowUpdate: Boolean
60+
) -> Unit
61+
) {
5362
this.uploadFiles = uploadFiles
5463
}
5564

5665
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
5766
arguments?.let {
5867
filesList = it.getStringArrayList(FILES_TO_UPLOAD_ARG)!!
5968
conversationName = it.getString(CONVERSATION_NAME_ARG, "")
69+
showFilePermissionsOption = it.getBoolean(FILE_PERMISSIONS_OPTION_ARG, false)
6070
}
6171

6272
composeView = ComposeView(requireContext())
@@ -102,9 +112,10 @@ class FileAttachmentPreviewFragment : DialogFragment() {
102112
viewModel = viewModel,
103113
conversationName = conversationName,
104114
initialCompressImages = appPreferences.compressUploadImages,
115+
showFilePermissionsOption = showFilePermissionsOption,
105116
onDismiss = { dismiss() },
106-
onSend = { files, caption, compressImages ->
107-
uploadFiles(files.toMutableList(), caption, compressImages)
117+
onSend = { files, caption, compressImages, allowUpdate ->
118+
uploadFiles(files.toMutableList(), caption, compressImages, allowUpdate)
108119
dismiss()
109120
}
110121
)
@@ -123,13 +134,19 @@ class FileAttachmentPreviewFragment : DialogFragment() {
123134
private const val LIGHT_LUMINANCE_THRESHOLD = 0.5f
124135
private const val FILES_TO_UPLOAD_ARG = "FILES_TO_UPLOAD_ARG"
125136
private const val CONVERSATION_NAME_ARG = "CONVERSATION_NAME_ARG"
137+
private const val FILE_PERMISSIONS_OPTION_ARG = "FILE_PERMISSIONS_OPTION_ARG"
126138

127139
@JvmStatic
128-
fun newInstance(filesToUpload: MutableList<String>, conversationName: String): FileAttachmentPreviewFragment {
140+
fun newInstance(
141+
filesToUpload: MutableList<String>,
142+
conversationName: String,
143+
showFilePermissionsOption: Boolean = false
144+
): FileAttachmentPreviewFragment {
129145
val fileAttachmentFragment = FileAttachmentPreviewFragment()
130146
val args = Bundle()
131147
args.putStringArrayList(FILES_TO_UPLOAD_ARG, ArrayList(filesToUpload))
132148
args.putString(CONVERSATION_NAME_ARG, conversationName)
149+
args.putBoolean(FILE_PERMISSIONS_OPTION_ARG, showFilePermissionsOption)
133150
fileAttachmentFragment.arguments = args
134151
return fileAttachmentFragment
135152
}

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

Lines changed: 163 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ import androidx.activity.compose.rememberLauncherForActivityResult
1111
import androidx.activity.result.PickVisualMediaRequest
1212
import androidx.activity.result.contract.ActivityResultContracts
1313
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
14+
import androidx.compose.foundation.border
1415
import androidx.compose.foundation.isSystemInDarkTheme
16+
import androidx.compose.foundation.layout.Arrangement
17+
import androidx.compose.foundation.layout.Box
1518
import androidx.compose.foundation.layout.Column
19+
import androidx.compose.foundation.layout.PaddingValues
1620
import androidx.compose.foundation.layout.Row
1721
import androidx.compose.foundation.layout.fillMaxSize
1822
import androidx.compose.foundation.layout.fillMaxWidth
@@ -23,15 +27,23 @@ import androidx.compose.foundation.layout.padding
2327
import androidx.compose.foundation.layout.size
2428
import androidx.compose.foundation.layout.statusBarsPadding
2529
import androidx.compose.foundation.pager.rememberPagerState
30+
import androidx.compose.foundation.shape.CircleShape
31+
import androidx.compose.foundation.shape.RoundedCornerShape
2632
import androidx.compose.material.icons.Icons
33+
import androidx.compose.material.icons.filled.ArrowDropDown
34+
import androidx.compose.material.icons.filled.Check
2735
import androidx.compose.material.icons.filled.Close
36+
import androidx.compose.material.icons.filled.Edit
37+
import androidx.compose.material.icons.filled.EditOff
38+
import androidx.compose.material3.Button
39+
import androidx.compose.material3.ButtonDefaults
40+
import androidx.compose.material3.DropdownMenu
41+
import androidx.compose.material3.DropdownMenuItem
2842
import androidx.compose.material3.HorizontalDivider
2943
import androidx.compose.material3.Icon
3044
import androidx.compose.material3.IconButton
45+
import androidx.compose.material3.LocalContentColor
3146
import androidx.compose.material3.MaterialTheme
32-
import androidx.compose.material3.SegmentedButton
33-
import androidx.compose.material3.SegmentedButtonDefaults
34-
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
3547
import androidx.compose.material3.Surface
3648
import androidx.compose.material3.Text
3749
import androidx.compose.material3.darkColorScheme
@@ -47,8 +59,8 @@ import androidx.compose.runtime.setValue
4759
import androidx.compose.ui.Alignment
4860
import androidx.compose.ui.Modifier
4961
import androidx.compose.ui.platform.LocalContext
50-
import androidx.compose.ui.res.painterResource
5162
import androidx.compose.ui.res.stringResource
63+
import androidx.compose.ui.text.font.FontWeight
5264
import androidx.compose.ui.text.style.TextOverflow
5365
import androidx.compose.ui.tooling.preview.Preview
5466
import androidx.compose.ui.unit.dp
@@ -67,20 +79,22 @@ private const val APP_BAR_HORIZONTAL_PADDING_DP = 4
6779
* hosted by [FileAttachmentPreviewFragment]. [viewModel] owns the file list and its (IO-derived)
6880
* descriptions so both survive configuration changes; everything else here is ephemeral UI state.
6981
*/
70-
@Suppress("LongMethod")
82+
@Suppress("LongMethod", "LongParameterList")
7183
@Composable
7284
internal fun FileAttachmentPreviewContent(
7385
viewModel: FileAttachmentPreviewViewModel,
7486
conversationName: String,
7587
initialCompressImages: Boolean,
88+
showFilePermissionsOption: Boolean = false,
7689
onDismiss: () -> Unit,
77-
onSend: (files: List<String>, caption: String, compressImages: Boolean) -> Unit
90+
onSend: (files: List<String>, caption: String, compressImages: Boolean, allowUpdate: Boolean) -> Unit
7891
) {
7992
val context = LocalContext.current
8093
val currentFiles = viewModel.files
8194
val hasCompressibleMedia = currentFiles.any { isCompressible(FileUtils.resolveMimeType(context, it.toUri())) }
8295
var caption by rememberSaveable { mutableStateOf("") }
8396
var compressImages by rememberSaveable { mutableStateOf(hasCompressibleMedia && initialCompressImages) }
97+
var allowUpdate by rememberSaveable { mutableStateOf(false) }
8498

8599
LaunchedEffect(currentFiles.toSet(), compressImages) {
86100
viewModel.describeFiles(compressImages)
@@ -139,21 +153,34 @@ internal fun FileAttachmentPreviewContent(
139153
}
140154
}
141155

142-
if (hasCompressibleMedia) {
143-
MediaQualitySegmentedButton(
144-
highQuality = !compressImages,
145-
onHighQualityChange = { highQuality -> compressImages = !highQuality },
156+
if (showFilePermissionsOption || hasCompressibleMedia) {
157+
Row(
146158
modifier = Modifier
147159
.fillMaxWidth()
148-
.padding(horizontal = 16.dp, vertical = 8.dp)
149-
)
160+
.padding(horizontal = 16.dp, vertical = 8.dp),
161+
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
162+
) {
163+
if (showFilePermissionsOption) {
164+
FilePermissionOptionButton(
165+
allowUpdate = allowUpdate,
166+
onAllowUpdateChange = { allowUpdate = it }
167+
)
168+
}
169+
170+
if (hasCompressibleMedia) {
171+
MediaQualityOptionButton(
172+
highQuality = !compressImages,
173+
onHighQualityChange = { highQuality -> compressImages = !highQuality }
174+
)
175+
}
176+
}
150177
}
151178

152179
CaptionInputBar(
153180
caption = caption,
154181
onCaptionChange = { caption = it },
155182
sendEnabled = currentFiles.isNotEmpty(),
156-
onSend = { onSend(currentFiles.toList(), caption, compressImages) }
183+
onSend = { onSend(currentFiles.toList(), caption, compressImages, allowUpdate) }
157184
)
158185
}
159186
}
@@ -198,39 +225,134 @@ private fun PreviewTopBar(conversationName: String, onDismiss: () -> Unit) {
198225
}
199226
}
200227

228+
private const val OPTION_BUTTON_ICON_SIZE_DP = 16
229+
private const val OPTION_BUTTON_HORIZONTAL_PADDING_DP = 12
230+
private const val OPTION_BUTTON_VERTICAL_PADDING_DP = 6
231+
private const val OPTION_BUTTON_LABEL_PADDING_DP = 4
232+
private const val TEXT_BADGE_BORDER_DP = 1
233+
private const val TEXT_BADGE_CORNER_RADIUS_DP = 4
234+
private const val TEXT_BADGE_HORIZONTAL_PADDING_DP = 3
235+
201236
@Composable
202-
private fun MediaQualitySegmentedButton(
203-
highQuality: Boolean,
204-
onHighQualityChange: (Boolean) -> Unit,
205-
modifier: Modifier = Modifier
206-
) {
207-
SingleChoiceSegmentedButtonRow(modifier = modifier) {
208-
SegmentedButton(
209-
selected = highQuality,
210-
onClick = { onHighQualityChange(true) },
211-
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),
212-
icon = {
213-
Icon(
214-
painter = painterResource(R.drawable.high_quality_24px),
215-
contentDescription = null,
216-
modifier = Modifier.size(SegmentedButtonDefaults.IconSize)
217-
)
218-
},
219-
label = { Text(stringResource(R.string.nc_media_quality_original)) }
237+
private fun OptionButton(label: String, onClick: () -> Unit, icon: @Composable () -> Unit) {
238+
Button(
239+
onClick = onClick,
240+
shape = CircleShape,
241+
colors = ButtonDefaults.buttonColors(
242+
containerColor = MaterialTheme.colorScheme.surfaceVariant,
243+
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
244+
),
245+
contentPadding = PaddingValues(
246+
horizontal = OPTION_BUTTON_HORIZONTAL_PADDING_DP.dp,
247+
vertical = OPTION_BUTTON_VERTICAL_PADDING_DP.dp
248+
)
249+
) {
250+
icon()
251+
Text(
252+
text = label,
253+
style = MaterialTheme.typography.labelMedium,
254+
maxLines = 1,
255+
overflow = TextOverflow.Ellipsis,
256+
modifier = Modifier.padding(horizontal = OPTION_BUTTON_LABEL_PADDING_DP.dp)
220257
)
221-
SegmentedButton(
222-
selected = !highQuality,
223-
onClick = { onHighQualityChange(false) },
224-
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),
258+
Icon(
259+
imageVector = Icons.Filled.ArrowDropDown,
260+
contentDescription = null,
261+
modifier = Modifier.size(OPTION_BUTTON_ICON_SIZE_DP.dp)
262+
)
263+
}
264+
}
265+
266+
@Composable
267+
private fun TextBadge(text: String) {
268+
Text(
269+
text = text,
270+
style = MaterialTheme.typography.labelSmall,
271+
fontWeight = FontWeight.Bold,
272+
color = LocalContentColor.current,
273+
modifier = Modifier
274+
.border(
275+
width = TEXT_BADGE_BORDER_DP.dp,
276+
color = LocalContentColor.current,
277+
shape = RoundedCornerShape(TEXT_BADGE_CORNER_RADIUS_DP.dp)
278+
)
279+
.padding(horizontal = TEXT_BADGE_HORIZONTAL_PADDING_DP.dp)
280+
)
281+
}
282+
283+
@Composable
284+
private fun MediaQualityOptionButton(highQuality: Boolean, onHighQualityChange: (Boolean) -> Unit) {
285+
var expanded by remember { mutableStateOf(false) }
286+
287+
Box {
288+
OptionButton(
289+
label = stringResource(
290+
if (highQuality) R.string.nc_media_quality_original else R.string.nc_media_quality_reduced
291+
),
292+
onClick = { expanded = true },
293+
icon = { TextBadge(if (highQuality) "HD" else "SD") }
294+
)
295+
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
296+
DropdownMenuItem(
297+
text = { Text(stringResource(R.string.nc_media_quality_original)) },
298+
leadingIcon = { TextBadge("HD") },
299+
trailingIcon = { if (highQuality) Icon(Icons.Filled.Check, contentDescription = null) },
300+
onClick = {
301+
onHighQualityChange(true)
302+
expanded = false
303+
}
304+
)
305+
DropdownMenuItem(
306+
text = { Text(stringResource(R.string.nc_media_quality_reduced)) },
307+
leadingIcon = { TextBadge("SD") },
308+
trailingIcon = { if (!highQuality) Icon(Icons.Filled.Check, contentDescription = null) },
309+
onClick = {
310+
onHighQualityChange(false)
311+
expanded = false
312+
}
313+
)
314+
}
315+
}
316+
}
317+
318+
@Composable
319+
private fun FilePermissionOptionButton(allowUpdate: Boolean, onAllowUpdateChange: (Boolean) -> Unit) {
320+
var expanded by remember { mutableStateOf(false) }
321+
322+
Box {
323+
OptionButton(
324+
label = stringResource(
325+
if (allowUpdate) R.string.nc_file_permission_editable else R.string.nc_file_permission_view_only
326+
),
327+
onClick = { expanded = true },
225328
icon = {
226329
Icon(
227-
painter = painterResource(R.drawable.high_quality_off_24px),
330+
imageVector = if (allowUpdate) Icons.Filled.Edit else Icons.Filled.EditOff,
228331
contentDescription = null,
229-
modifier = Modifier.size(SegmentedButtonDefaults.IconSize)
332+
modifier = Modifier.size(OPTION_BUTTON_ICON_SIZE_DP.dp)
230333
)
231-
},
232-
label = { Text(stringResource(R.string.nc_media_quality_reduced)) }
334+
}
233335
)
336+
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
337+
DropdownMenuItem(
338+
text = { Text(stringResource(R.string.nc_file_permission_view_only)) },
339+
leadingIcon = { Icon(Icons.Filled.EditOff, contentDescription = null) },
340+
trailingIcon = { if (!allowUpdate) Icon(Icons.Filled.Check, contentDescription = null) },
341+
onClick = {
342+
onAllowUpdateChange(false)
343+
expanded = false
344+
}
345+
)
346+
DropdownMenuItem(
347+
text = { Text(stringResource(R.string.nc_file_permission_editable)) },
348+
leadingIcon = { Icon(Icons.Filled.Edit, contentDescription = null) },
349+
trailingIcon = { if (allowUpdate) Icon(Icons.Filled.Check, contentDescription = null) },
350+
onClick = {
351+
onAllowUpdateChange(true)
352+
expanded = false
353+
}
354+
)
355+
}
234356
}
235357
}
236358

@@ -263,7 +385,7 @@ private fun FileAttachmentPreviewContentPreview() {
263385
conversationName = "Team Chat",
264386
initialCompressImages = true,
265387
onDismiss = {},
266-
onSend = { _, _, _ -> }
388+
onSend = { _, _, _, _ -> }
267389
)
268390
}
269391
}
@@ -277,7 +399,7 @@ private fun FileAttachmentPreviewContentSingleFilePreview() {
277399
conversationName = "Team Chat",
278400
initialCompressImages = true,
279401
onDismiss = {},
280-
onSend = { _, _, _ -> }
402+
onSend = { _, _, _, _ -> }
281403
)
282404
}
283405
}

0 commit comments

Comments
 (0)