mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
Generalizes the chat upload UI and applies it to channels
This commit is contained in:
+4
-18
@@ -40,14 +40,12 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||
@@ -58,7 +56,7 @@ import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.ShowEmojiSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploadDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
@@ -119,22 +117,10 @@ fun PrivateMessageEditFieldRow(
|
||||
|
||||
channelScreenModel.uploadState?.let { uploading ->
|
||||
uploading.multiOrchestrator?.let { selectedFiles ->
|
||||
val context = LocalContext.current
|
||||
|
||||
ChatFileUploadDialog(
|
||||
room = channelScreenModel.room!!,
|
||||
RoomChatFileUploadDialog(
|
||||
channelScreenModel = channelScreenModel,
|
||||
state = uploading,
|
||||
upload = {
|
||||
channelScreenModel.upload(
|
||||
onError = accountViewModel::toast,
|
||||
context = context,
|
||||
onceUploaded = onSendNewMessage,
|
||||
)
|
||||
|
||||
if (uploading.selectedServer.type != ServerType.NIP95) {
|
||||
accountViewModel.account.settings.changeDefaultFileServer(uploading.selectedServer)
|
||||
}
|
||||
},
|
||||
onUpload = onSendNewMessage,
|
||||
onCancel = uploading::reset,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
|
||||
+32
-234
@@ -20,264 +20,62 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.TitleIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.navigation.rememberHeightDecreaser
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SettingSwitchItem
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameOnlyDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.ChatNewMessageViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChatFileUploadDialog(
|
||||
room: ChatroomKey,
|
||||
fun RoomChatFileUploadDialog(
|
||||
channelScreenModel: ChatNewMessageViewModel,
|
||||
state: ChatFileUploadState,
|
||||
upload: () -> Unit,
|
||||
onUpload: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
val room = channelScreenModel.room ?: return
|
||||
val context = LocalContext.current
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = { onCancel() },
|
||||
properties =
|
||||
DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnClickOutside = false,
|
||||
decorFitsSystemWindows = false,
|
||||
),
|
||||
) {
|
||||
SetDialogToEdgeToEdge()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
scrollBehavior = rememberHeightDecreaser(),
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
NonClickableUserPictures(
|
||||
room = room,
|
||||
accountViewModel = accountViewModel,
|
||||
size = Size34dp,
|
||||
)
|
||||
|
||||
RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp), FontWeight.Normal, accountViewModel)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = TitleIconModifier,
|
||||
onClick = {
|
||||
state.reset()
|
||||
onCancel()
|
||||
},
|
||||
) {
|
||||
ArrowBackIcon()
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
SendButton(
|
||||
modifier = Modifier.padding(end = 5.dp),
|
||||
onPost = upload,
|
||||
isActive = state.canPost(),
|
||||
)
|
||||
},
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
ChatFileUploadDialog(
|
||||
state,
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
NonClickableUserPictures(
|
||||
room = room,
|
||||
accountViewModel = accountViewModel,
|
||||
size = Size34dp,
|
||||
)
|
||||
},
|
||||
) { pad ->
|
||||
Surface(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(pad)
|
||||
.consumeWindowInsets(pad)
|
||||
.imePadding(),
|
||||
) {
|
||||
Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, bottom = 10.dp)) {
|
||||
Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) {
|
||||
ImageVideoPostChat(state, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp), FontWeight.Normal, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImageVideoPostChat(
|
||||
fileUploadState: ChatFileUploadState,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val fileServers by accountViewModel.account.liveServerList.collectAsState()
|
||||
|
||||
val fileServerOptions =
|
||||
remember(fileServers) {
|
||||
fileServers
|
||||
.mapNotNull {
|
||||
if (it.type != ServerType.NIP95) {
|
||||
TitleExplainer(it.name, it.baseUrl)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
fileUploadState.multiOrchestrator?.let {
|
||||
ShowImageUploadGallery(
|
||||
it,
|
||||
fileUploadState::deleteMediaToUpload,
|
||||
accountViewModel,
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.content_description)) },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 3.dp).height(150.dp),
|
||||
maxLines = 10,
|
||||
value = fileUploadState.caption,
|
||||
onValueChange = { fileUploadState.caption = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.content_description_example),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
),
|
||||
)
|
||||
|
||||
SettingSwitchItem(
|
||||
title = R.string.add_sensitive_content_label,
|
||||
description = R.string.add_sensitive_content_description,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
checked = fileUploadState.contentWarning,
|
||||
onCheckedChange = fileUploadState::updateContentWarning,
|
||||
)
|
||||
|
||||
SettingsRow(R.string.file_server, R.string.file_server_description) {
|
||||
TextSpinner(
|
||||
label = "",
|
||||
placeholder =
|
||||
fileServers
|
||||
.firstOrNull { it == accountViewModel.account.settings.defaultFileServer }
|
||||
?.name
|
||||
?: fileServers[0].name,
|
||||
options = fileServerOptions,
|
||||
onSelect = { fileUploadState.selectedServer = fileServers[it] },
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(Size5dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.media_compression_quality_label),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.media_compression_quality_explainer),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
maxLines = 5,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text =
|
||||
when (fileUploadState.mediaQualitySlider) {
|
||||
0 -> stringRes(R.string.media_compression_quality_low)
|
||||
1 -> stringRes(R.string.media_compression_quality_medium)
|
||||
2 -> stringRes(R.string.media_compression_quality_high)
|
||||
3 -> stringRes(R.string.media_compression_quality_uncompressed)
|
||||
else -> stringRes(R.string.media_compression_quality_medium)
|
||||
},
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
upload = {
|
||||
channelScreenModel.upload(
|
||||
onError = accountViewModel::toast,
|
||||
context = context,
|
||||
onceUploaded = onUpload,
|
||||
)
|
||||
}
|
||||
|
||||
Slider(
|
||||
value = fileUploadState.mediaQualitySlider.toFloat(),
|
||||
onValueChange = { fileUploadState.mediaQualitySlider = it.toInt() },
|
||||
valueRange = 0f..3f,
|
||||
steps = 2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SendButton(
|
||||
onPost: () -> Unit = {},
|
||||
isActive: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
enabled = isActive,
|
||||
onClick = onPost,
|
||||
) {
|
||||
Text(text = stringRes(R.string.accessibility_send))
|
||||
}
|
||||
if (state.selectedServer.type != ServerType.NIP95) {
|
||||
accountViewModel.account.settings.changeDefaultFileServer(state.selectedServer)
|
||||
}
|
||||
},
|
||||
onCancel,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
+1
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
|
||||
@Composable
|
||||
fun ChannelFileUploadDialog(
|
||||
channelScreenModel: ChannelNewMessageViewModel,
|
||||
state: ChatFileUploadState,
|
||||
onUpload: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val channel = channelScreenModel.channel ?: return
|
||||
val context = LocalContext.current
|
||||
|
||||
ChatFileUploadDialog(
|
||||
state,
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
when (channel) {
|
||||
is PublicChatChannel -> {
|
||||
channel.profilePicture()?.let {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = channel.idHex,
|
||||
model = it,
|
||||
contentDescription = stringRes(R.string.profile_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = HeaderPictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.padding(start = 10.dp)
|
||||
.height(35.dp)
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = channel.toBestDisplayName(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is LiveActivitiesChannel -> {
|
||||
channel.creator?.let {
|
||||
UserPicture(
|
||||
user = it,
|
||||
size = Size34dp,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 10.dp)
|
||||
.height(35.dp)
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = channel.toBestDisplayName(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
upload = {
|
||||
channelScreenModel.upload(
|
||||
onError = accountViewModel::toast,
|
||||
context = context,
|
||||
onceUploaded = onUpload,
|
||||
)
|
||||
|
||||
if (state.selectedServer.type != ServerType.NIP95) {
|
||||
accountViewModel.account.settings.changeDefaultFileServer(state.selectedServer)
|
||||
}
|
||||
},
|
||||
onCancel,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
+3
-1
@@ -54,7 +54,7 @@ import com.vitorpamplona.amethyst.ui.components.Split
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.UserSuggestions
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploadState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
|
||||
@@ -307,6 +307,7 @@ open class ChannelNewMessageViewModel : ViewModel() {
|
||||
fun upload(
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
context: Context,
|
||||
onceUploaded: () -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val myAccount = account ?: return@launch
|
||||
@@ -349,6 +350,7 @@ open class ChannelNewMessageViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
uploadState.reset()
|
||||
onceUploaded()
|
||||
} else {
|
||||
val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct()
|
||||
|
||||
|
||||
+13
@@ -81,6 +81,19 @@ fun EditFieldRow(
|
||||
}
|
||||
}
|
||||
|
||||
channelScreenModel.uploadState?.let { uploading ->
|
||||
uploading.multiOrchestrator?.let { selectedFiles ->
|
||||
ChannelFileUploadDialog(
|
||||
channelScreenModel = channelScreenModel,
|
||||
state = uploading,
|
||||
onUpload = onSendNewMessage,
|
||||
onCancel = uploading::reset,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = EditFieldModifier,
|
||||
) {
|
||||
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.TitleIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.navigation.rememberHeightDecreaser
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SettingSwitchItem
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChatFileUploadDialog(
|
||||
state: ChatFileUploadState,
|
||||
title: @Composable () -> Unit,
|
||||
upload: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = { onCancel() },
|
||||
properties =
|
||||
DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnClickOutside = false,
|
||||
decorFitsSystemWindows = false,
|
||||
),
|
||||
) {
|
||||
SetDialogToEdgeToEdge()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
scrollBehavior = rememberHeightDecreaser(),
|
||||
modifier = Modifier,
|
||||
title = title,
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = TitleIconModifier,
|
||||
onClick = {
|
||||
state.reset()
|
||||
onCancel()
|
||||
},
|
||||
) {
|
||||
ArrowBackIcon()
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
SendButton(
|
||||
modifier = Modifier.padding(end = 5.dp),
|
||||
onPost = upload,
|
||||
isActive = state.canPost(),
|
||||
)
|
||||
},
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { pad ->
|
||||
Surface(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(pad)
|
||||
.consumeWindowInsets(pad)
|
||||
.imePadding(),
|
||||
) {
|
||||
Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, bottom = 10.dp)) {
|
||||
Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) {
|
||||
ImageVideoPostChat(state, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImageVideoPostChat(
|
||||
fileUploadState: ChatFileUploadState,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val fileServers by accountViewModel.account.liveServerList.collectAsState()
|
||||
|
||||
val fileServerOptions =
|
||||
remember(fileServers) {
|
||||
fileServers
|
||||
.mapNotNull {
|
||||
if (it.type != ServerType.NIP95) {
|
||||
TitleExplainer(it.name, it.baseUrl)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
fileUploadState.multiOrchestrator?.let {
|
||||
ShowImageUploadGallery(
|
||||
it,
|
||||
fileUploadState::deleteMediaToUpload,
|
||||
accountViewModel,
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.content_description)) },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 3.dp).height(150.dp),
|
||||
maxLines = 10,
|
||||
value = fileUploadState.caption,
|
||||
onValueChange = { fileUploadState.caption = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.content_description_example),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
),
|
||||
)
|
||||
|
||||
SettingSwitchItem(
|
||||
title = R.string.add_sensitive_content_label,
|
||||
description = R.string.add_sensitive_content_description,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
checked = fileUploadState.contentWarning,
|
||||
onCheckedChange = fileUploadState::updateContentWarning,
|
||||
)
|
||||
|
||||
SettingsRow(R.string.file_server, R.string.file_server_description) {
|
||||
TextSpinner(
|
||||
label = "",
|
||||
placeholder =
|
||||
fileServers
|
||||
.firstOrNull { it == accountViewModel.account.settings.defaultFileServer }
|
||||
?.name
|
||||
?: fileServers[0].name,
|
||||
options = fileServerOptions,
|
||||
onSelect = { fileUploadState.selectedServer = fileServers[it] },
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(Size5dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.media_compression_quality_label),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.media_compression_quality_explainer),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
maxLines = 5,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text =
|
||||
when (fileUploadState.mediaQualitySlider) {
|
||||
0 -> stringRes(R.string.media_compression_quality_low)
|
||||
1 -> stringRes(R.string.media_compression_quality_medium)
|
||||
2 -> stringRes(R.string.media_compression_quality_high)
|
||||
3 -> stringRes(R.string.media_compression_quality_uncompressed)
|
||||
else -> stringRes(R.string.media_compression_quality_medium)
|
||||
},
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
|
||||
Slider(
|
||||
value = fileUploadState.mediaQualitySlider.toFloat(),
|
||||
onValueChange = { fileUploadState.mediaQualitySlider = it.toInt() },
|
||||
valueRange = 0f..3f,
|
||||
steps = 2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SendButton(
|
||||
onPost: () -> Unit = {},
|
||||
isActive: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
enabled = isActive,
|
||||
onClick = onPost,
|
||||
) {
|
||||
Text(text = stringRes(R.string.accessibility_send))
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
Reference in New Issue
Block a user