Merge branch 'vitorpamplona:main' into profiles-list-management
This commit is contained in:
+16
-6
@@ -44,14 +44,14 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
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.upload.ChatFileUploadState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findNostrEventUris
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent
|
||||
@@ -301,6 +301,8 @@ open class ChatNewMessageViewModel : ViewModel() {
|
||||
TextFieldValue(draftEvent.content)
|
||||
}
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
requiresNIP17 = draftEvent is NIP17Group
|
||||
nip17 = draftEvent is NIP17Group
|
||||
|
||||
@@ -348,9 +350,15 @@ open class ChatNewMessageViewModel : ViewModel() {
|
||||
val uploadState = uploadState ?: return
|
||||
|
||||
if (nip17) {
|
||||
ChatFileUploader(room, account).uploadNIP17(uploadState, viewModelScope, onError, context, onceUploaded)
|
||||
ChatFileUploader(room, account).uploadNIP17(uploadState, viewModelScope, onError, context) {
|
||||
saveDraft()
|
||||
onceUploaded()
|
||||
}
|
||||
} else {
|
||||
ChatFileUploader(room, account).uploadNIP04(uploadState, viewModelScope, onError, context, onceUploaded)
|
||||
ChatFileUploader(room, account).uploadNIP04(uploadState, viewModelScope, onError, context) {
|
||||
saveDraft()
|
||||
onceUploaded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +380,7 @@ open class ChatNewMessageViewModel : ViewModel() {
|
||||
ChatMessageEvent.build(message, room.users.map { LocalCache.getOrCreateUser(it).toPTag() }) {
|
||||
hashtags(findHashtags(message))
|
||||
references(findURLs(message))
|
||||
quotes(findNostrUris(message))
|
||||
quotes(findNostrEventUris(message))
|
||||
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
@@ -381,7 +389,7 @@ open class ChatNewMessageViewModel : ViewModel() {
|
||||
ChatMessageEvent.reply(message, replyHint) {
|
||||
hashtags(findHashtags(message))
|
||||
references(findURLs(message))
|
||||
quotes(findNostrUris(message))
|
||||
quotes(findNostrEventUris(message))
|
||||
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
@@ -433,6 +441,8 @@ open class ChatNewMessageViewModel : ViewModel() {
|
||||
userSuggestions.reset()
|
||||
userSuggestionsMainMessage = null
|
||||
|
||||
iMetaAttachments.reset()
|
||||
|
||||
if (emojiSearch.value.isNotEmpty()) {
|
||||
emojiSearch.tryEmit("")
|
||||
}
|
||||
|
||||
+11
-1
@@ -67,6 +67,12 @@ class IMetaAttachments {
|
||||
}
|
||||
}
|
||||
|
||||
fun addAll(imetas: List<IMetaTag>) {
|
||||
imetas.forEach {
|
||||
replace(it.url, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(url: String) {
|
||||
iMetaAttachments = iMetaAttachments.filter { it.url != url }
|
||||
}
|
||||
@@ -100,5 +106,9 @@ class IMetaAttachments {
|
||||
replace(iMeta.url, iMeta)
|
||||
}
|
||||
|
||||
fun filterIsIn(urls: Set<String>) = iMetaAttachments.filter { it.url !in urls }
|
||||
fun filterIsIn(urls: Set<String>) = iMetaAttachments.filter { it.url in urls }
|
||||
|
||||
fun reset() {
|
||||
iMetaAttachments = emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
+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,
|
||||
)
|
||||
}
|
||||
+8
-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
|
||||
@@ -262,6 +262,8 @@ open class ChannelNewMessageViewModel : ViewModel() {
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
}
|
||||
|
||||
@@ -307,6 +309,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 +352,8 @@ open class ChannelNewMessageViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
uploadState.reset()
|
||||
onceUploaded()
|
||||
saveDraft()
|
||||
} else {
|
||||
val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct()
|
||||
|
||||
@@ -494,6 +499,8 @@ open class ChannelNewMessageViewModel : ViewModel() {
|
||||
userSuggestions.reset()
|
||||
userSuggestionsMainMessage = null
|
||||
|
||||
iMetaAttachments.reset()
|
||||
|
||||
if (emojiSearch.value.isNotEmpty()) {
|
||||
emojiSearch.tryEmit("")
|
||||
}
|
||||
|
||||
+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
|
||||
@@ -81,6 +81,13 @@
|
||||
<string name="thank_you_so_much">Děkuji moc!</string>
|
||||
<string name="amount_in_sats">Částka v sats</string>
|
||||
<string name="send_sats">Poslat sats</string>
|
||||
<string name="secret_emoji_maker">Tajný tvůrce emoji</string>
|
||||
<string name="secret_emoji_maker_explainer">Přidejte emoji se skrytou zprávou</string>
|
||||
<string name="secret_note_to_receiver">Tajná poznámka pro příjemce</string>
|
||||
<string name="secret_note_to_receiver_placeholder">Moje skrytá zpráva</string>
|
||||
<string name="secret_visible_text">Viditelná předpona</string>
|
||||
<string name="secret_visible_text_placeholder">😎</string>
|
||||
<string name="secret_add_to_text">Přidat k příspěvku</string>
|
||||
<string name="error_parsing_preview_for">"Chyba při zpracování náhledu pro %1$s : %2$s"</string>
|
||||
<string name="preview_card_image_for">"Náhled obrázku karty pro %1$s"</string>
|
||||
<string name="new_channel">Nový kanál</string>
|
||||
|
||||
@@ -81,6 +81,13 @@
|
||||
<string name="thank_you_so_much">Vielen Dank!</string>
|
||||
<string name="amount_in_sats">Betrag in Sats</string>
|
||||
<string name="send_sats">Sats senden</string>
|
||||
<string name="secret_emoji_maker">Geheimer Emoji-Maker</string>
|
||||
<string name="secret_emoji_maker_explainer">Füge einen Emoji mit versteckter Nachricht hinzu</string>
|
||||
<string name="secret_note_to_receiver">Geheime Notiz an Empfänger</string>
|
||||
<string name="secret_note_to_receiver_placeholder">Meine versteckte Nachricht</string>
|
||||
<string name="secret_visible_text">Sichtbares Präfix</string>
|
||||
<string name="secret_visible_text_placeholder">😎</string>
|
||||
<string name="secret_add_to_text">Zum Beitrag hinzufügen</string>
|
||||
<string name="error_parsing_preview_for">"Fehler beim Parsen der Vorschau für %1$s: %2$s"</string>
|
||||
<string name="preview_card_image_for">"Vorschaukartenbild für %1$s"</string>
|
||||
<string name="new_channel">Neuer Kanal</string>
|
||||
|
||||
@@ -81,6 +81,13 @@
|
||||
<string name="thank_you_so_much">Obrigado!</string>
|
||||
<string name="amount_in_sats">Quantidade em Sats</string>
|
||||
<string name="send_sats">Enviar Sats</string>
|
||||
<string name="secret_emoji_maker">Criador de Emoji Secreto</string>
|
||||
<string name="secret_emoji_maker_explainer">Adicionar um emoji com mensagem oculta na postagem</string>
|
||||
<string name="secret_note_to_receiver">Nota Secreta para o Receptor</string>
|
||||
<string name="secret_note_to_receiver_placeholder">Minha mensagem oculta</string>
|
||||
<string name="secret_visible_text">Prefixo Visível</string>
|
||||
<string name="secret_visible_text_placeholder">😎</string>
|
||||
<string name="secret_add_to_text">Adicionar à postagem</string>
|
||||
<string name="error_parsing_preview_for">"Erro ao analisar a visualização para %1$s: %2$s"</string>
|
||||
<string name="preview_card_image_for">"Visualize a imagem do cartão para %1$s"</string>
|
||||
<string name="new_channel">Novo Canal</string>
|
||||
|
||||
@@ -81,6 +81,13 @@
|
||||
<string name="thank_you_so_much">Tack så mycket!</string>
|
||||
<string name="amount_in_sats">Summa i Sats</string>
|
||||
<string name="send_sats">Skicka Sats</string>
|
||||
<string name="secret_emoji_maker">Hemlig emoji-skapare</string>
|
||||
<string name="secret_emoji_maker_explainer">Lägg till en emoji med dolt meddelande</string>
|
||||
<string name="secret_note_to_receiver">Hemlig anteckning till mottagaren</string>
|
||||
<string name="secret_note_to_receiver_placeholder">Mitt dolda inlägg</string>
|
||||
<string name="secret_visible_text">Synligt Prefix</string>
|
||||
<string name="secret_visible_text_placeholder">😎</string>
|
||||
<string name="secret_add_to_text">Lägg till inlägg</string>
|
||||
<string name="error_parsing_preview_for">"Fel vid analys av förhandsvisning för %1$s : %2$s"</string>
|
||||
<string name="preview_card_image_for">"Förhandsgranska kortbild för %1$s"</string>
|
||||
<string name="new_channel">Ny kanal</string>
|
||||
|
||||
@@ -23,3 +23,5 @@ package com.vitorpamplona.quartz.nip10Notes.content
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
|
||||
fun findNostrUris(content: String) = Nip19Parser.parseAll(content)
|
||||
|
||||
fun findNostrEventUris(content: String) = Nip19Parser.parseAllEvents(content)
|
||||
|
||||
@@ -53,6 +53,12 @@ object Nip19Parser {
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
)
|
||||
|
||||
val nip19regexEvents: Pattern =
|
||||
Pattern.compile(
|
||||
"(nostr:)?@?(nevent1|naddr1|note1|nrelay1|nembed1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)([\\S]*)",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class ParseReturn(
|
||||
val entity: Entity,
|
||||
@@ -130,8 +136,11 @@ object Nip19Parser {
|
||||
null
|
||||
}
|
||||
|
||||
fun parseAll(content: String): List<Entity> {
|
||||
val matcher2 = nip19regex.matcher(content)
|
||||
fun parseAll(
|
||||
content: String,
|
||||
regex: Pattern,
|
||||
): List<Entity> {
|
||||
val matcher2 = regex.matcher(content)
|
||||
val returningList = mutableListOf<Entity>()
|
||||
while (matcher2.find()) {
|
||||
val type = matcher2.group(2) // npub1
|
||||
@@ -139,7 +148,7 @@ object Nip19Parser {
|
||||
val additionalChars = matcher2.group(4) // additional chars
|
||||
|
||||
if (type != null) {
|
||||
val parsed = Nip19Parser.parseComponents(type, key, additionalChars)?.entity
|
||||
val parsed = parseComponents(type, key, additionalChars)?.entity
|
||||
|
||||
if (parsed != null) {
|
||||
returningList.add(parsed)
|
||||
@@ -148,6 +157,10 @@ object Nip19Parser {
|
||||
}
|
||||
return returningList
|
||||
}
|
||||
|
||||
fun parseAll(content: String): List<Entity> = parseAll(content, nip19regex)
|
||||
|
||||
fun parseAllEvents(content: String): List<Entity> = parseAll(content, nip19regexEvents)
|
||||
}
|
||||
|
||||
fun decodePublicKey(key: String): ByteArray =
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ class BloomFilterMurMur3Test {
|
||||
failureCounter++
|
||||
}
|
||||
}
|
||||
assertEquals(0, failureCounter)
|
||||
assertTrue(failureCounter <= 2)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user