Final touches to remove nip04 dms

This commit is contained in:
Vitor Pamplona
2026-03-19 15:23:48 -04:00
parent 695d3c7a23
commit ad53bff8e3
7 changed files with 222 additions and 125 deletions
@@ -26,14 +26,16 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMs import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMsAndDisplayIfNotFound import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMsAndDisplayIfNotFound
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView
@@ -44,6 +46,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.Privat
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@@ -94,8 +97,13 @@ fun ChatroomView(
// Reactively check if recipients have DM relays for NIP-17 delivery // Reactively check if recipients have DM relays for NIP-17 delivery
for (userHex in room.users) { for (userHex in room.users) {
ObserveRelayListForDMs(pubkey = userHex, accountViewModel = accountViewModel) { LoadAddressableNote(
newPostModel.updateRecipientRelayStatus() ChatMessageRelayListEvent.createAddress(userHex),
accountViewModel,
) { note ->
if (note != null) {
EventFinderFilterAssemblerSubscription(note, accountViewModel)
}
} }
} }
@@ -73,6 +73,7 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes
import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
@@ -93,10 +94,24 @@ import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Stable @Stable
@@ -125,10 +140,57 @@ class ChatNewMessageViewModel :
} }
} }
var room: ChatroomKey? by mutableStateOf(null) val room = MutableStateFlow<ChatroomKey?>(null)
/** Whether any recipients are missing DM relay lists */ val roomUsers: StateFlow<List<User>> =
var recipientsMissingDmRelays by mutableStateOf(false) room
.mapNotNull {
it?.users?.mapNotNull { userHex -> LocalCache.checkGetOrCreateUser(userHex) } ?: emptyList()
}.flowOn(Dispatchers.IO)
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
room.value?.users?.mapNotNull { userHex -> LocalCache.checkGetOrCreateUser(userHex) } ?: emptyList(),
)
@OptIn(ExperimentalCoroutinesApi::class)
val recipientsMissingDmRelays: StateFlow<ImmutableList<User>> =
roomUsers
.transformLatest {
val dmRelayListNoteFlows =
it.map { user ->
user.dmRelayListNote
.flow()
.metadata.stateFlow
}
if (dmRelayListNoteFlows.isEmpty()) {
emitAll(MutableStateFlow(persistentListOf()))
} else {
val flow =
combine(dmRelayListNoteFlows) { dmRelayListNotes ->
dmRelayListNotes
.mapNotNull { noteState ->
val noteEvent = noteState.note.event as? ChatMessageRelayListEvent
if (noteEvent == null || noteEvent.relays().isEmpty()) {
noteState.note.author
} else {
null
}
}.toImmutableList()
}
emitAll(flow)
}
}.onStart {
}.onCompletion {
}.stateIn(
viewModelScope,
SharingStarted.Eagerly,
roomUsers.value
.filter { user ->
user.dmInboxRelays().isNullOrEmpty()
}.toImmutableList(),
)
val replyTo = mutableStateOf<Note?>(null) val replyTo = mutableStateOf<Note?>(null)
@@ -180,9 +242,6 @@ class ChatNewMessageViewModel :
var wantsZapraiser by mutableStateOf(false) var wantsZapraiser by mutableStateOf(false)
override var zapRaiserAmount = mutableStateOf<Long?>(null) override var zapRaiserAmount = mutableStateOf<Long?>(null)
// NIP17 is always enabled - NIP-04 is deprecated for sending
val nip17: Boolean get() = true
fun lnAddress(): String? = account.userProfile().lnAddress() fun lnAddress(): String? = account.userProfile().lnAddress()
fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null
@@ -208,26 +267,11 @@ class ChatNewMessageViewModel :
} }
fun load(room: ChatroomKey) { fun load(room: ChatroomKey) {
this.room = room this.room.tryEmit(room)
this.toUsers = this.toUsers =
TextFieldValue( TextFieldValue(
room.users.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" }, room.users.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
) )
updateRecipientRelayStatus()
}
fun updateRecipientRelayStatus() {
val room = this.room
if (room != null) {
this.recipientsMissingDmRelays =
room.users.any { hexKey ->
val user = LocalCache.getOrCreateUser(hexKey)
user.dmInboxRelays().isNullOrEmpty()
}
} else {
this.recipientsMissingDmRelays = false
}
} }
fun reply(replyNote: Note) { fun reply(replyNote: Note) {
@@ -360,8 +404,6 @@ class ChatNewMessageViewModel :
urlPreviews.update(message) urlPreviews.update(message)
iMetaAttachments.addAll(draftEvent.imetas()) iMetaAttachments.addAll(draftEvent.imetas())
updateRecipientRelayStatus()
} }
suspend fun sendPostSync() { suspend fun sendPostSync() {
@@ -423,7 +465,7 @@ class ChatNewMessageViewModel :
context: Context, context: Context,
onceUploaded: () -> Unit, onceUploaded: () -> Unit,
) { ) {
val room = room ?: return val room = room.value ?: return
val uploadState = uploadState ?: return val uploadState = uploadState ?: return
accountViewModel.launchSigner { accountViewModel.launchSigner {
@@ -471,12 +513,12 @@ class ChatNewMessageViewModel :
val onError = pendingRetryOnError val onError = pendingRetryOnError
val context = pendingRetryContext val context = pendingRetryContext
val onceUploaded = pendingRetryOnceUploaded val onceUploaded = pendingRetryOnceUploaded
val room = room val room = room.value
val uploadState = uploadState val uploadState = uploadState
dismissEncryptedUploadError() dismissEncryptedUploadError()
if (uploadState == null || context == null) return if (room == null || uploadState == null || context == null) return
uploadState.encryptFiles = false uploadState.encryptFiles = false
@@ -495,7 +537,6 @@ class ChatNewMessageViewModel :
} }
RetryMode.SEND -> { RetryMode.SEND -> {
if (room == null) return@launchSigner
ChatFileUploader(account).justUploadNIP17Unencrypted( ChatFileUploader(account).justUploadNIP17Unencrypted(
uploadState, uploadState,
onError ?: accountViewModel.toastManager::toast, onError ?: accountViewModel.toastManager::toast,
@@ -511,7 +552,7 @@ class ChatNewMessageViewModel :
} }
private suspend fun innerSendPost(draftTag: String?) { private suspend fun innerSendPost(draftTag: String?) {
val room = room ?: return val room = room.value ?: return
val urls = findURLs(message.text) val urls = findURLs(message.text)
val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet()) val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet())
@@ -661,12 +702,10 @@ class ChatNewMessageViewModel :
val users = toUsersTagger.pTags?.mapTo(mutableSetOf()) { it.pubkeyHex } val users = toUsersTagger.pTags?.mapTo(mutableSetOf()) { it.pubkeyHex }
if (users.isNullOrEmpty()) { if (users.isNullOrEmpty()) {
room = null room.emit(null)
updateRecipientRelayStatus()
} else { } else {
if (users != room?.users) { if (users != room.value?.users) {
room = ChatroomKey(users) room.emit(ChatroomKey(users))
updateRecipientRelayStatus()
} }
} }
} }
@@ -743,7 +782,7 @@ class ChatNewMessageViewModel :
(!wantsZapraiser || zapRaiserAmount.value != null) && (!wantsZapraiser || zapRaiserAmount.value != null) &&
(toUsers.text.isNotBlank()) && (toUsers.text.isNotBlank()) &&
uploadState?.multiOrchestrator == null && uploadState?.multiOrchestrator == null &&
!recipientsMissingDmRelays recipientsMissingDmRelays.value.isEmpty()
fun insertAtCursor(newElement: String) { fun insertAtCursor(newElement: String) {
message = message.insertUrlAtCursor(newElement) message = message.insertUrlAtCursor(newElement)
@@ -197,7 +197,7 @@ fun NewGroupDMScreen(
// function when the postViewModel is released // function when the postViewModel is released
accountViewModel.launchSigner { accountViewModel.launchSigner {
postViewModel.sendPostSync() postViewModel.sendPostSync()
postViewModel.room?.let { postViewModel.room.value?.let {
nav.nav(routeToMessage(it, null, null, null, null, accountViewModel)) nav.nav(routeToMessage(it, null, null, null, null, accountViewModel))
} }
} }
@@ -332,8 +332,9 @@ fun GroupDMScreenContent(
) )
} }
if (postViewModel.recipientsMissingDmRelays) { val missingRelays by postViewModel.recipientsMissingDmRelays.collectAsStateWithLifecycle()
RecipientMissingRelaysWarning() if (missingRelays.isNotEmpty()) {
RecipientMissingRelaysWarning(missingRelays, accountViewModel, nav)
} }
BottomRowActions(postViewModel, accountViewModel) BottomRowActions(postViewModel, accountViewModel)
@@ -397,7 +398,9 @@ private fun BottomRowActions(
.height(50.dp), .height(50.dp),
verticalAlignment = CenterVertically, verticalAlignment = CenterVertically,
) { ) {
if (postViewModel.room != null) { val room by postViewModel.room.collectAsStateWithLifecycle()
if (room != null) {
SelectFromGallery( SelectFromGallery(
isUploading = postViewModel.isUploadingImage, isUploading = postViewModel.isUploadingImage,
enabled = !postViewModel.isUploadingFile, enabled = !postViewModel.isUploadingFile,
@@ -428,7 +431,7 @@ private fun BottomRowActions(
} }
} }
if (postViewModel.room != null) { if (room != null) {
TakePictureButton( TakePictureButton(
onPictureTaken = { postViewModel.pickedMedia(it) }, onPictureTaken = { postViewModel.pickedMedia(it) },
) )
@@ -447,7 +450,7 @@ private fun BottomRowActions(
} }
} }
if (postViewModel.room != null) { if (room != null) {
TakeVideoButton( TakeVideoButton(
onVideoTaken = { postViewModel.pickedMedia(it) }, onVideoTaken = { postViewModel.pickedMedia(it) },
) )
@@ -554,8 +557,6 @@ fun SendDirectMessageTo(
focusedBorderColor = Color.Transparent, focusedBorderColor = Color.Transparent,
), ),
) )
Nip17Indicator(postViewModel)
} }
HorizontalDivider(thickness = DividerThickness) HorizontalDivider(thickness = DividerThickness)
@@ -20,35 +20,50 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send
import android.R.attr.maxLines
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.input.InputTransformation.Companion.keyboardOptions
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
import com.vitorpamplona.amethyst.ui.note.showCount
import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog
@@ -61,9 +76,13 @@ import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.Font12SP
import com.vitorpamplona.amethyst.ui.theme.PostKeyboard import com.vitorpamplona.amethyst.ui.theme.PostKeyboard
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.SpacedBy10dp
import com.vitorpamplona.amethyst.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.ui.theme.SuggestionListDefaultHeightChat
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Preview @Preview
@Composable @Composable
@@ -164,8 +183,13 @@ fun PrivateMessageEditFieldRow(
} }
} }
val missingRelays by channelScreenModel.recipientsMissingDmRelays.collectAsStateWithLifecycle()
if (missingRelays.isNotEmpty()) {
RecipientMissingRelaysWarning(missingRelays, accountViewModel, nav)
} else {
EditField(channelScreenModel, onSendNewMessage, accountViewModel) EditField(channelScreenModel, onSendNewMessage, accountViewModel)
} }
}
} }
@Composable @Composable
@@ -174,9 +198,6 @@ fun EditField(
onSendNewMessage: () -> Unit, onSendNewMessage: () -> Unit,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
if (channelScreenModel.recipientsMissingDmRelays) {
RecipientMissingRelaysWarning()
} else {
ThinPaddingTextField( ThinPaddingTextField(
value = channelScreenModel.message, value = channelScreenModel.message,
onValueChange = { channelScreenModel.updateMessage(it) }, onValueChange = { channelScreenModel.updateMessage(it) },
@@ -210,19 +231,48 @@ fun EditField(
), ),
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary), visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
) )
}
@Preview
@Composable
fun RecipientMissingRelaysWarningPreview() {
val user1 = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
val user2 = LocalCache.getOrCreateUser("ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b")
ThemeComparisonColumn {
RecipientMissingRelaysWarning(
persistentListOf(user1, user2),
mockAccountViewModel(),
EmptyNav(),
)
} }
} }
@Composable @Composable
fun RecipientMissingRelaysWarning() { fun RecipientMissingRelaysWarning(
users: ImmutableList<User>,
accountViewModel: AccountViewModel,
nav: INav,
) {
Row( Row(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 10.dp), .padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = SpacedBy10dp,
) { ) {
UserGallery(users) { user ->
ClickableUserPicture(
user,
Size25dp,
accountViewModel,
onClick = {
nav.nav { routeFor(user) }
},
)
}
Text( Text(
text = stringRes(R.string.recipient_missing_dm_relays), text = stringRes(R.string.recipient_missing_dm_relays),
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
@@ -233,6 +283,40 @@ fun RecipientMissingRelaysWarning() {
} }
} }
@Composable
fun UserGallery(
users: ImmutableList<User>,
galleryUser: @Composable RowScope.(user: User) -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy((-10).dp),
) {
users.take(6).forEach {
key(it.pubkeyHex) {
galleryUser(it)
}
}
if (users.size > 6) {
Box(
contentAlignment = Alignment.Center,
modifier =
Modifier
.size(Size25dp)
.clip(shape = CircleShape)
.background(MaterialTheme.colorScheme.secondaryContainer),
) {
Text(
text = "+" + showCount(users.size - 6),
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
}
@Composable @Composable
fun KeyboardLeadingIcon( fun KeyboardLeadingIcon(
channelScreenModel: ChatNewMessageViewModel, channelScreenModel: ChatNewMessageViewModel,
@@ -240,7 +324,7 @@ fun KeyboardLeadingIcon(
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 4.dp, end = 10.dp), modifier = Modifier.padding(start = 4.dp, end = 4.dp),
) { ) {
SelectFromGallery( SelectFromGallery(
isUploading = channelScreenModel.isUploadingImage, isUploading = channelScreenModel.isUploadingImage,
@@ -248,8 +332,6 @@ fun KeyboardLeadingIcon(
modifier = Modifier, modifier = Modifier,
onImageChosen = channelScreenModel::pickedMedia, onImageChosen = channelScreenModel::pickedMedia,
) )
Nip17Indicator(channelScreenModel)
} }
} }
@@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator 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.privateDM.send.IMetaAttachments
import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip01Core.tags.references.references
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
@@ -100,40 +99,6 @@ class ChatFileSender(
account.sendNip17PrivateMessage(template) account.sendNip17PrivateMessage(template)
} }
// ------
// NIP 04
// ------
suspend fun sendNIP04(uploads: List<SuccessfulUploads>) {
uploads.forEach {
if (it.cipher == null) {
sendNIP04(it.result, it.caption, it.contentWarningReason)
}
}
}
suspend fun sendNIP04(
result: UploadOrchestrator.OrchestratorResult.ServerResult,
caption: String?,
contentWarningReason: String?,
) {
val iMetaAttachments = IMetaAttachments()
iMetaAttachments.add(result, caption, contentWarningReason)
val toUser = chatroom.users.first().let { LocalCache.getOrCreateUser(it).toPTag() }
val template =
PrivateDmEvent.build(
toUser = toUser,
message = result.url,
imetas = iMetaAttachments.iMetaAttachments,
replyingTo = null,
signer = account.signer,
)
account.sendNip04PrivateMessage(template)
}
suspend fun sendAll(uploads: List<SuccessfulUploads>) { suspend fun sendAll(uploads: List<SuccessfulUploads>) {
sendNIP17(uploads) sendNIP17(uploads)
} }
@@ -28,6 +28,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -46,7 +47,8 @@ fun RoomChatFileUploadDialog(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val room = channelScreenModel.room ?: return val roomState = channelScreenModel.room.collectAsStateWithLifecycle()
val room = roomState.value ?: return
val context = LocalContext.current val context = LocalContext.current
ChatFileUploadDialog( ChatFileUploadDialog(
@@ -74,6 +76,6 @@ fun RoomChatFileUploadDialog(
onCancel, onCancel,
accountViewModel, accountViewModel,
nav, nav,
isNip17 = channelScreenModel.nip17, isNip17 = true,
) )
} }
+1 -1
View File
@@ -1351,7 +1351,7 @@
<string name="dm_relays_not_found_examples2">Good options are:\n - auth.nostr1.com (free)\n - inbox.nostr.wine (paid)\n - relay.0xchat.com (free)</string> <string name="dm_relays_not_found_examples2">Good options are:\n - auth.nostr1.com (free)\n - inbox.nostr.wine (paid)\n - relay.0xchat.com (free)</string>
<string name="dm_relays_not_found_editing">Insert between 13 relays to serve as your private inbox. DM Inbox relays should accept any message from anyone, but only allow you to download them.</string> <string name="dm_relays_not_found_editing">Insert between 13 relays to serve as your private inbox. DM Inbox relays should accept any message from anyone, but only allow you to download them.</string>
<string name="dm_relays_not_found_create_now">Set up now</string> <string name="dm_relays_not_found_create_now">Set up now</string>
<string name="recipient_missing_dm_relays">This user hasn\'t set up DM inbox relays. Messages cannot be delivered until they configure their relay list.</string> <string name="recipient_missing_dm_relays">DM inbox relays not found. Messages cannot be delivered until they configure their relay list.</string>
<string name="search_relays_title">Search Relays</string> <string name="search_relays_title">Search Relays</string>
<string name="search_relays_not_found">Set up your Search relays</string> <string name="search_relays_not_found">Set up your Search relays</string>