Merge pull request #1878 from vitorpamplona/claude/event-sync-screen-sYGtN
Claude/event sync screen
This commit is contained in:
+13
-5
@@ -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
|
||||||
@@ -92,10 +95,15 @@ fun ChatroomView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.users.size == 1) {
|
// Reactively check if recipients have DM relays for NIP-17 delivery
|
||||||
// Activates NIP-17 if the user has DM relays
|
for (userHex in room.users) {
|
||||||
ObserveRelayListForDMs(pubkey = room.users.first(), accountViewModel = accountViewModel) {
|
LoadAddressableNote(
|
||||||
newPostModel.nip17 = !it?.relays().isNullOrEmpty()
|
ChatMessageRelayListEvent.createAddress(userHex),
|
||||||
|
accountViewModel,
|
||||||
|
) { note ->
|
||||||
|
if (note != null) {
|
||||||
|
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+139
-148
@@ -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
|
||||||
@@ -88,16 +89,29 @@ import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup
|
|||||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
||||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
||||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
|
||||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||||
import com.vitorpamplona.quartz.utils.Hex
|
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
|
||||||
@@ -126,9 +140,57 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var room: ChatroomKey? by mutableStateOf(null)
|
val room = MutableStateFlow<ChatroomKey?>(null)
|
||||||
|
|
||||||
var requiresNIP17: Boolean = false
|
val roomUsers: StateFlow<List<User>> =
|
||||||
|
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 Wrapped DMs / Group messages
|
|
||||||
var nip17 by mutableStateOf(false)
|
|
||||||
|
|
||||||
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" },
|
||||||
)
|
)
|
||||||
|
|
||||||
updateNIP17StatusFromRoom()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNIP17StatusFromRoom() {
|
|
||||||
val room = this.room
|
|
||||||
if (room != null) {
|
|
||||||
this.requiresNIP17 = room.users.size > 1
|
|
||||||
if (this.requiresNIP17) {
|
|
||||||
this.nip17 = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.requiresNIP17 = false
|
|
||||||
this.nip17 = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reply(replyNote: Note) {
|
fun reply(replyNote: Note) {
|
||||||
@@ -360,9 +404,6 @@ class ChatNewMessageViewModel :
|
|||||||
urlPreviews.update(message)
|
urlPreviews.update(message)
|
||||||
|
|
||||||
iMetaAttachments.addAll(draftEvent.imetas())
|
iMetaAttachments.addAll(draftEvent.imetas())
|
||||||
|
|
||||||
requiresNIP17 = draftEvent is NIP17Group
|
|
||||||
nip17 = draftEvent is NIP17Group
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendPostSync() {
|
suspend fun sendPostSync() {
|
||||||
@@ -402,27 +443,19 @@ class ChatNewMessageViewModel :
|
|||||||
val uploadState = uploadState ?: return
|
val uploadState = uploadState ?: return
|
||||||
|
|
||||||
accountViewModel.launchSigner {
|
accountViewModel.launchSigner {
|
||||||
if (nip17) {
|
ChatFileUploader(account).justUploadNIP17(
|
||||||
ChatFileUploader(account).justUploadNIP17(
|
uploadState,
|
||||||
uploadState,
|
onError,
|
||||||
onError,
|
onEncryptedUploadError = { title, message ->
|
||||||
onEncryptedUploadError = { title, message ->
|
encryptedUploadErrorTitle = title
|
||||||
encryptedUploadErrorTitle = title
|
encryptedUploadErrorMessage = message
|
||||||
encryptedUploadErrorMessage = message
|
pendingRetryMode = RetryMode.HOLD
|
||||||
pendingRetryMode = RetryMode.HOLD
|
},
|
||||||
},
|
context,
|
||||||
context,
|
) {
|
||||||
) {
|
uploadsWaitingToBeSent += it
|
||||||
uploadsWaitingToBeSent += it
|
draftTag.newVersion()
|
||||||
draftTag.newVersion()
|
onceUploaded()
|
||||||
onceUploaded()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ChatFileUploader(account).justUploadNIP04(uploadState, onError, context) {
|
|
||||||
uploadsWaitingToBeSent += it
|
|
||||||
draftTag.newVersion()
|
|
||||||
onceUploaded()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,34 +465,26 @@ 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 {
|
||||||
if (nip17) {
|
ChatFileUploader(account).justUploadNIP17(
|
||||||
ChatFileUploader(account).justUploadNIP17(
|
uploadState,
|
||||||
uploadState,
|
onError,
|
||||||
onError,
|
onEncryptedUploadError = { title, message ->
|
||||||
onEncryptedUploadError = { title, message ->
|
encryptedUploadErrorTitle = title
|
||||||
encryptedUploadErrorTitle = title
|
encryptedUploadErrorMessage = message
|
||||||
encryptedUploadErrorMessage = message
|
pendingRetryMode = RetryMode.SEND
|
||||||
pendingRetryMode = RetryMode.SEND
|
pendingRetryOnError = onError
|
||||||
pendingRetryOnError = onError
|
pendingRetryContext = context
|
||||||
pendingRetryContext = context
|
pendingRetryOnceUploaded = onceUploaded
|
||||||
pendingRetryOnceUploaded = onceUploaded
|
},
|
||||||
},
|
context,
|
||||||
context,
|
) {
|
||||||
) {
|
ChatFileSender(room, account).sendNIP17(it)
|
||||||
ChatFileSender(room, account).sendNIP17(it)
|
draftTag.newVersion()
|
||||||
draftTag.newVersion()
|
onceUploaded()
|
||||||
onceUploaded()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ChatFileUploader(account).justUploadNIP04(uploadState, onError, context) {
|
|
||||||
ChatFileSender(room, account).sendNIP04(it)
|
|
||||||
draftTag.newVersion()
|
|
||||||
onceUploaded()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -488,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
|
||||||
|
|
||||||
@@ -512,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,
|
||||||
@@ -528,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())
|
||||||
@@ -541,70 +565,49 @@ class ChatNewMessageViewModel :
|
|||||||
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null
|
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null
|
||||||
val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null
|
val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null
|
||||||
|
|
||||||
if (nip17 || room.users.size > 1 || replyTo.value?.event is NIP17Group) {
|
val replyHint = replyTo.value?.toEventHint<BaseDMGroupEvent>()
|
||||||
val replyHint = replyTo.value?.toEventHint<BaseDMGroupEvent>()
|
|
||||||
|
|
||||||
val template =
|
val template =
|
||||||
if (replyHint == null) {
|
if (replyHint == null) {
|
||||||
ChatMessageEvent.build(message, room.users.map { LocalCache.getOrCreateUser(it).toPTag() }) {
|
ChatMessageEvent.build(message, room.users.map { LocalCache.getOrCreateUser(it).toPTag() }) {
|
||||||
hashtags(findHashtags(message))
|
hashtags(findHashtags(message))
|
||||||
references(findURLs(message))
|
references(findURLs(message))
|
||||||
quotes(findNostrEventUris(message))
|
quotes(findNostrEventUris(message))
|
||||||
|
|
||||||
geoHash?.let { geohash(it) }
|
geoHash?.let { geohash(it) }
|
||||||
localZapRaiserAmount?.let { zapraiser(it) }
|
localZapRaiserAmount?.let { zapraiser(it) }
|
||||||
zapReceiver?.let { zapSplits(it) }
|
zapReceiver?.let { zapSplits(it) }
|
||||||
contentWarningReason?.let { contentWarning(it) }
|
contentWarningReason?.let { contentWarning(it) }
|
||||||
localExpirationDate?.let { expiration(it) }
|
|
||||||
|
|
||||||
emojis(emojis)
|
|
||||||
imetas(usedAttachments)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ChatMessageEvent.reply(message, replyHint) {
|
|
||||||
hashtags(findHashtags(message))
|
|
||||||
references(findURLs(message))
|
|
||||||
quotes(findNostrEventUris(message))
|
|
||||||
|
|
||||||
geoHash?.let { geohash(it) }
|
|
||||||
localZapRaiserAmount?.let { zapraiser(it) }
|
|
||||||
zapReceiver?.let { zapSplits(it) }
|
|
||||||
contentWarningReason?.let { contentWarning(it) }
|
|
||||||
localExpirationDate?.let { expiration(it) }
|
|
||||||
|
|
||||||
emojis(emojis)
|
|
||||||
imetas(usedAttachments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (draftTag != null) {
|
|
||||||
accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template)
|
|
||||||
} else {
|
|
||||||
accountViewModel.account.sendNip17PrivateMessage(template)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val toUser = room.users.first().let { LocalCache.getOrCreateUser(it).toPTag() }
|
|
||||||
|
|
||||||
val template =
|
|
||||||
PrivateDmEvent.build(
|
|
||||||
toUser = toUser,
|
|
||||||
message = message,
|
|
||||||
imetas = usedAttachments,
|
|
||||||
replyingTo = replyTo.value?.toEventHint<PrivateDmEvent>(),
|
|
||||||
signer = accountViewModel.account.signer,
|
|
||||||
) {
|
|
||||||
localExpirationDate?.let { expiration(it) }
|
localExpirationDate?.let { expiration(it) }
|
||||||
}
|
|
||||||
|
|
||||||
if (draftTag != null) {
|
emojis(emojis)
|
||||||
accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template)
|
imetas(usedAttachments)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
accountViewModel.account.sendNip04PrivateMessage(template)
|
ChatMessageEvent.reply(message, replyHint) {
|
||||||
|
hashtags(findHashtags(message))
|
||||||
|
references(findURLs(message))
|
||||||
|
quotes(findNostrEventUris(message))
|
||||||
|
|
||||||
|
geoHash?.let { geohash(it) }
|
||||||
|
localZapRaiserAmount?.let { zapraiser(it) }
|
||||||
|
zapReceiver?.let { zapSplits(it) }
|
||||||
|
contentWarningReason?.let { contentWarning(it) }
|
||||||
|
localExpirationDate?.let { expiration(it) }
|
||||||
|
|
||||||
|
emojis(emojis)
|
||||||
|
imetas(usedAttachments)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (draftTag != null) {
|
||||||
|
accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template)
|
||||||
|
} else {
|
||||||
|
accountViewModel.account.sendNip17PrivateMessage(template)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (draftTag == null) {
|
if (draftTag == null) {
|
||||||
ChatFileSender(room, accountViewModel.account).sendAll(uploadsWaitingToBeSent)
|
ChatFileSender(room, accountViewModel.account).sendNIP17(uploadsWaitingToBeSent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,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)
|
||||||
updateNIP17StatusFromRoom()
|
|
||||||
} else {
|
} else {
|
||||||
if (users != room?.users) {
|
if (users != room.value?.users) {
|
||||||
room = ChatroomKey(users)
|
room.emit(ChatroomKey(users))
|
||||||
updateNIP17StatusFromRoom()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -737,9 +738,6 @@ class ChatNewMessageViewModel :
|
|||||||
val lastWord = toUsers.currentWord()
|
val lastWord = toUsers.currentWord()
|
||||||
toUsers = userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
toUsers = userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
||||||
updateRoomFromUsersInput()
|
updateRoomFromUsersInput()
|
||||||
|
|
||||||
val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(item.pubkeyHex))?.event as? AdvertisedRelayListEvent)?.readRelaysNorm()
|
|
||||||
nip17 = relayList != null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userSuggestionsMainMessage = null
|
userSuggestionsMainMessage = null
|
||||||
@@ -783,7 +781,8 @@ class ChatNewMessageViewModel :
|
|||||||
!wantsInvoice &&
|
!wantsInvoice &&
|
||||||
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
||||||
(toUsers.text.isNotBlank()) &&
|
(toUsers.text.isNotBlank()) &&
|
||||||
uploadState?.multiOrchestrator == null
|
uploadState?.multiOrchestrator == null &&
|
||||||
|
recipientsMissingDmRelays.value.isEmpty()
|
||||||
|
|
||||||
fun insertAtCursor(newElement: String) {
|
fun insertAtCursor(newElement: String) {
|
||||||
message = message.insertUrlAtCursor(newElement)
|
message = message.insertUrlAtCursor(newElement)
|
||||||
@@ -795,15 +794,7 @@ class ChatNewMessageViewModel :
|
|||||||
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleNIP04And24() {
|
// NIP-04 sending is deprecated. NIP-17 is always used.
|
||||||
if (requiresNIP17) {
|
|
||||||
nip17 = true
|
|
||||||
} else {
|
|
||||||
nip17 = !nip17
|
|
||||||
}
|
|
||||||
|
|
||||||
draftTag.newVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateZapPercentage(
|
override fun updateZapPercentage(
|
||||||
index: Int,
|
index: Int,
|
||||||
|
|||||||
+11
-6
@@ -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,6 +332,11 @@ fun GroupDMScreenContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val missingRelays by postViewModel.recipientsMissingDmRelays.collectAsStateWithLifecycle()
|
||||||
|
if (missingRelays.isNotEmpty()) {
|
||||||
|
RecipientMissingRelaysWarning(missingRelays, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
BottomRowActions(postViewModel, accountViewModel)
|
BottomRowActions(postViewModel, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -393,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,
|
||||||
@@ -424,7 +431,7 @@ private fun BottomRowActions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postViewModel.room != null) {
|
if (room != null) {
|
||||||
TakePictureButton(
|
TakePictureButton(
|
||||||
onPictureTaken = { postViewModel.pickedMedia(it) },
|
onPictureTaken = { postViewModel.pickedMedia(it) },
|
||||||
)
|
)
|
||||||
@@ -443,7 +450,7 @@ private fun BottomRowActions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postViewModel.room != null) {
|
if (room != null) {
|
||||||
TakeVideoButton(
|
TakeVideoButton(
|
||||||
onVideoTaken = { postViewModel.pickedMedia(it) },
|
onVideoTaken = { postViewModel.pickedMedia(it) },
|
||||||
)
|
)
|
||||||
@@ -550,8 +557,6 @@ fun SendDirectMessageTo(
|
|||||||
focusedBorderColor = Color.Transparent,
|
focusedBorderColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
ToggleNip17Button(postViewModel, accountViewModel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
|||||||
+111
-5
@@ -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,7 +183,12 @@ fun PrivateMessageEditFieldRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EditField(channelScreenModel, onSendNewMessage, accountViewModel)
|
val missingRelays by channelScreenModel.recipientsMissingDmRelays.collectAsStateWithLifecycle()
|
||||||
|
if (missingRelays.isNotEmpty()) {
|
||||||
|
RecipientMissingRelaysWarning(missingRelays, accountViewModel, nav)
|
||||||
|
} else {
|
||||||
|
EditField(channelScreenModel, onSendNewMessage, accountViewModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +233,90 @@ fun EditField(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun RecipientMissingRelaysWarningPreview() {
|
||||||
|
val user1 = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
||||||
|
val user2 = LocalCache.getOrCreateUser("ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b")
|
||||||
|
|
||||||
|
ThemeComparisonColumn {
|
||||||
|
RecipientMissingRelaysWarning(
|
||||||
|
persistentListOf(user1, user2),
|
||||||
|
mockAccountViewModel(),
|
||||||
|
EmptyNav(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RecipientMissingRelaysWarning(
|
||||||
|
users: ImmutableList<User>,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = SpacedBy10dp,
|
||||||
|
) {
|
||||||
|
UserGallery(users) { user ->
|
||||||
|
ClickableUserPicture(
|
||||||
|
user,
|
||||||
|
Size25dp,
|
||||||
|
accountViewModel,
|
||||||
|
onClick = {
|
||||||
|
nav.nav { routeFor(user) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.recipient_missing_dm_relays),
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
fontSize = Font12SP,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,
|
||||||
@@ -216,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,
|
||||||
@@ -224,8 +332,6 @@ fun KeyboardLeadingIcon(
|
|||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
onImageChosen = channelScreenModel::pickedMedia,
|
onImageChosen = channelScreenModel::pickedMedia,
|
||||||
)
|
)
|
||||||
|
|
||||||
ToggleNip17Button(channelScreenModel, accountViewModel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-78
@@ -25,94 +25,25 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialog
|
|
||||||
import com.vitorpamplona.amethyst.ui.painterRes
|
import com.vitorpamplona.amethyst.ui.painterRes
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.IncognitoIconButtonModifier
|
import com.vitorpamplona.amethyst.ui.theme.IncognitoIconButtonModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ToggleNip17Button(
|
fun Nip17Indicator(channelScreenModel: ChatNewMessageViewModel) {
|
||||||
channelScreenModel: ChatNewMessageViewModel,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
) {
|
|
||||||
var wantsToActivateNIP17 by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (wantsToActivateNIP17) {
|
|
||||||
NewFeatureNIP17AlertDialog(
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
onConfirm = { channelScreenModel.toggleNIP04And24() },
|
|
||||||
onDismiss = { wantsToActivateNIP17 = false },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.width(30.dp),
|
modifier = Modifier.width(30.dp),
|
||||||
onClick = {
|
onClick = { },
|
||||||
if (
|
enabled = false,
|
||||||
!accountViewModel.account.settings.hideNIP17WarningDialog &&
|
|
||||||
!channelScreenModel.nip17 &&
|
|
||||||
!channelScreenModel.requiresNIP17
|
|
||||||
) {
|
|
||||||
wantsToActivateNIP17 = true
|
|
||||||
} else {
|
|
||||||
channelScreenModel.toggleNIP04And24()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
if (channelScreenModel.nip17) {
|
Icon(
|
||||||
Icon(
|
painter = painterRes(R.drawable.incognito, 2),
|
||||||
painter = painterRes(R.drawable.incognito, 2),
|
contentDescription = stringRes(id = R.string.accessibility_turn_off_sealed_message),
|
||||||
contentDescription = stringRes(id = R.string.accessibility_turn_off_sealed_message),
|
modifier = IncognitoIconButtonModifier,
|
||||||
modifier = IncognitoIconButtonModifier,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Icon(
|
|
||||||
painter = painterRes(R.drawable.incognito_off, 2),
|
|
||||||
contentDescription = stringRes(id = R.string.accessibility_turn_on_sealed_message),
|
|
||||||
modifier = IncognitoIconButtonModifier,
|
|
||||||
tint = MaterialTheme.colorScheme.placeholderText,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun NewFeatureNIP17AlertDialog(
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
onConfirm: () -> Unit,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
QuickActionAlertDialog(
|
|
||||||
title = stringRes(R.string.new_feature_nip17_might_not_be_available_title),
|
|
||||||
textContent = stringRes(R.string.new_feature_nip17_might_not_be_available_description),
|
|
||||||
buttonIconResource = R.drawable.incognito,
|
|
||||||
buttonIconReference = 3,
|
|
||||||
buttonText = stringRes(R.string.new_feature_nip17_activate),
|
|
||||||
onClickDoOnce = {
|
|
||||||
scope.launch { onConfirm() }
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
onClickDontShowAgain = {
|
|
||||||
scope.launch {
|
|
||||||
onConfirm()
|
|
||||||
accountViewModel.account.settings.setHideNIP17WarningDialog()
|
|
||||||
}
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
onDismiss = onDismiss,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-42
@@ -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,47 +99,7 @@ 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>) {
|
||||||
uploads.forEach {
|
sendNIP17(uploads)
|
||||||
if (it.cipher != null) {
|
|
||||||
sendNIP17(it.result, it.caption, it.contentWarningReason, it.cipher)
|
|
||||||
} else {
|
|
||||||
sendNIP04(it.result, it.caption, it.contentWarningReason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1351,6 +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 1–3 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 1–3 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">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>
|
||||||
|
|||||||
+20
-66
@@ -28,13 +28,11 @@ import com.vitorpamplona.amethyst.commons.model.User
|
|||||||
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||||
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.nip10Notes.content.findHashtags
|
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
|
||||||
import com.vitorpamplona.quartz.nip10Notes.content.findNostrEventUris
|
import com.vitorpamplona.quartz.nip10Notes.content.findNostrEventUris
|
||||||
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
|
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent
|
import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
|
|
||||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||||
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
|
||||||
@@ -62,9 +60,6 @@ class ChatNewMessageState(
|
|||||||
private val _message = MutableStateFlow(TextFieldValue(""))
|
private val _message = MutableStateFlow(TextFieldValue(""))
|
||||||
val message: StateFlow<TextFieldValue> = _message.asStateFlow()
|
val message: StateFlow<TextFieldValue> = _message.asStateFlow()
|
||||||
|
|
||||||
private val _nip17 = MutableStateFlow(false)
|
|
||||||
val nip17: StateFlow<Boolean> = _nip17.asStateFlow()
|
|
||||||
|
|
||||||
private val _replyTo = MutableStateFlow<Note?>(null)
|
private val _replyTo = MutableStateFlow<Note?>(null)
|
||||||
val replyTo: StateFlow<Note?> = _replyTo.asStateFlow()
|
val replyTo: StateFlow<Note?> = _replyTo.asStateFlow()
|
||||||
|
|
||||||
@@ -74,38 +69,37 @@ class ChatNewMessageState(
|
|||||||
private val _room = MutableStateFlow<ChatroomKey?>(null)
|
private val _room = MutableStateFlow<ChatroomKey?>(null)
|
||||||
val room: StateFlow<ChatroomKey?> = _room.asStateFlow()
|
val room: StateFlow<ChatroomKey?> = _room.asStateFlow()
|
||||||
|
|
||||||
/** Whether NIP-17 is required (group chat with >1 recipient) */
|
/** Whether any recipients are missing DM relay lists, preventing message delivery */
|
||||||
private val _requiresNip17 = MutableStateFlow(false)
|
private val _recipientsMissingDmRelays = MutableStateFlow(false)
|
||||||
val requiresNip17: StateFlow<Boolean> = _requiresNip17.asStateFlow()
|
val recipientsMissingDmRelays: StateFlow<Boolean> = _recipientsMissingDmRelays.asStateFlow()
|
||||||
|
|
||||||
/** Whether a message can be sent (non-blank text + room set) */
|
/** Whether a message can be sent (non-blank text + room set + all recipients have DM relays) */
|
||||||
val canSend: Boolean
|
val canSend: Boolean
|
||||||
get() = _message.value.text.isNotBlank() && _room.value != null
|
get() = _message.value.text.isNotBlank() && _room.value != null && !_recipientsMissingDmRelays.value
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a chatroom. Sets the room key, formats toUsers display,
|
* Load a chatroom. Sets the room key and checks recipient DM relay availability.
|
||||||
* and auto-detects NIP-17 requirement (group chats require NIP-17).
|
|
||||||
*/
|
*/
|
||||||
fun load(roomKey: ChatroomKey) {
|
fun load(roomKey: ChatroomKey) {
|
||||||
_room.value = roomKey
|
_room.value = roomKey
|
||||||
updateNip17FromRoom()
|
updateRecipientRelayStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-detect NIP-17 based on room:
|
* Check if all recipients have DM relay lists.
|
||||||
* - Group chats (>1 recipient) always require NIP-17
|
* Messages can only be sent via NIP-17, so recipients must have
|
||||||
* - Single recipient: NIP-17 off by default (can be toggled)
|
* either a DM inbox relay list (kind 10050) or NIP-65 inbox relays.
|
||||||
*/
|
*/
|
||||||
fun updateNip17FromRoom() {
|
fun updateRecipientRelayStatus() {
|
||||||
val currentRoom = _room.value
|
val currentRoom = _room.value
|
||||||
if (currentRoom != null) {
|
if (currentRoom != null) {
|
||||||
_requiresNip17.value = currentRoom.users.size > 1
|
_recipientsMissingDmRelays.value =
|
||||||
if (_requiresNip17.value) {
|
currentRoom.users.any { hexKey ->
|
||||||
_nip17.value = true
|
val user = cache.getOrCreateUser(hexKey) as? User
|
||||||
}
|
user?.dmInboxRelays().isNullOrEmpty()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_requiresNip17.value = false
|
_recipientsMissingDmRelays.value = false
|
||||||
_nip17.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,27 +120,7 @@ class ChatNewMessageState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle NIP-04/NIP-17 mode.
|
* Send the current message as NIP-17. NIP-04 is deprecated for sending.
|
||||||
* If NIP-17 is required (group chat), stays on NIP-17.
|
|
||||||
*/
|
|
||||||
fun toggleNip17() {
|
|
||||||
if (_requiresNip17.value) {
|
|
||||||
_nip17.value = true
|
|
||||||
} else {
|
|
||||||
_nip17.value = !_nip17.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable NIP-17 (e.g., when recipient has DM relay list).
|
|
||||||
*/
|
|
||||||
fun enableNip17() {
|
|
||||||
_nip17.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send the current message. Builds the appropriate event template
|
|
||||||
* (NIP-04 or NIP-17) and delegates to IAccount for signing/broadcasting.
|
|
||||||
*
|
*
|
||||||
* @return true if send was initiated, false if preconditions not met
|
* @return true if send was initiated, false if preconditions not met
|
||||||
*/
|
*/
|
||||||
@@ -154,12 +128,9 @@ class ChatNewMessageState(
|
|||||||
val currentRoom = _room.value ?: return false
|
val currentRoom = _room.value ?: return false
|
||||||
val messageText = _message.value.text
|
val messageText = _message.value.text
|
||||||
if (messageText.isBlank()) return false
|
if (messageText.isBlank()) return false
|
||||||
|
if (_recipientsMissingDmRelays.value) return false
|
||||||
|
|
||||||
if (_nip17.value || currentRoom.users.size > 1 || _replyTo.value?.event is NIP17Group) {
|
sendNip17(currentRoom, messageText)
|
||||||
sendNip17(currentRoom, messageText)
|
|
||||||
} else {
|
|
||||||
sendNip04(currentRoom, messageText)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -193,23 +164,6 @@ class ChatNewMessageState(
|
|||||||
account.sendNip17PrivateMessage(template)
|
account.sendNip17PrivateMessage(template)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sendNip04(
|
|
||||||
room: ChatroomKey,
|
|
||||||
messageText: String,
|
|
||||||
) {
|
|
||||||
val toUser = (cache.getOrCreateUser(room.users.first()) as? User)?.toPTag() ?: return
|
|
||||||
|
|
||||||
val template =
|
|
||||||
PrivateDmEvent.build(
|
|
||||||
toUser = toUser,
|
|
||||||
message = messageText,
|
|
||||||
replyingTo = _replyTo.value?.toEventHint<PrivateDmEvent>(),
|
|
||||||
signer = account.signer,
|
|
||||||
)
|
|
||||||
|
|
||||||
account.sendNip04PrivateMessage(template)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all composition state after sending or cancelling.
|
* Clear all composition state after sending or cancelling.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+17
-44
@@ -150,25 +150,18 @@ fun ChatPane(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val feedState by feedViewModel.feedState.feedContent.collectAsState()
|
val feedState by feedViewModel.feedState.feedContent.collectAsState()
|
||||||
val messageText by messageState.message.collectAsState()
|
val messageText by messageState.message.collectAsState()
|
||||||
val isNip17 by messageState.nip17.collectAsState()
|
val recipientsMissingRelays by messageState.recipientsMissingDmRelays.collectAsState()
|
||||||
val requiresNip17 by messageState.requiresNip17.collectAsState()
|
|
||||||
|
|
||||||
// File attachment state
|
// File attachment state
|
||||||
val attachedFiles = remember { mutableStateListOf<File>() }
|
val attachedFiles = remember { mutableStateListOf<File>() }
|
||||||
var isUploading by remember { mutableStateOf(false) }
|
var isUploading by remember { mutableStateOf(false) }
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
// Helper: attach files and auto-force NIP-17 if needed
|
// Helper: attach files
|
||||||
fun attachFiles(files: List<File>) {
|
fun attachFiles(files: List<File>) {
|
||||||
val mediaFiles = files.filter { it.extension.lowercase() in MEDIA_EXTENSIONS }
|
val mediaFiles = files.filter { it.extension.lowercase() in MEDIA_EXTENSIONS }
|
||||||
if (mediaFiles.isEmpty()) return
|
if (mediaFiles.isEmpty()) return
|
||||||
attachedFiles.addAll(mediaFiles)
|
attachedFiles.addAll(mediaFiles)
|
||||||
if (!isNip17) {
|
|
||||||
messageState.toggleNip17()
|
|
||||||
scope.launch {
|
|
||||||
snackbarHostState.showSnackbar("Switched to NIP-17 — file attachments require encrypted messaging")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drag-and-drop target for file attachments (NIP-17 only)
|
// Drag-and-drop target for file attachments (NIP-17 only)
|
||||||
@@ -346,13 +339,11 @@ fun ChatPane(
|
|||||||
// Message input
|
// Message input
|
||||||
MessageInput(
|
MessageInput(
|
||||||
messageText = messageText.text,
|
messageText = messageText.text,
|
||||||
isNip17 = isNip17,
|
recipientsMissingRelays = recipientsMissingRelays,
|
||||||
requiresNip17 = requiresNip17,
|
|
||||||
canSend = messageState.canSend || attachedFiles.isNotEmpty(),
|
canSend = messageState.canSend || attachedFiles.isNotEmpty(),
|
||||||
isUploading = isUploading,
|
isUploading = isUploading,
|
||||||
hasAttachments = attachedFiles.isNotEmpty(),
|
hasAttachments = attachedFiles.isNotEmpty(),
|
||||||
onMessageChange = { messageState.updateMessage(messageText.copy(text = it)) },
|
onMessageChange = { messageState.updateMessage(messageText.copy(text = it)) },
|
||||||
onToggleNip17 = { messageState.toggleNip17() },
|
|
||||||
onAttach = { attachFiles(DesktopFilePicker.pickMediaFiles()) },
|
onAttach = { attachFiles(DesktopFilePicker.pickMediaFiles()) },
|
||||||
onSend = {
|
onSend = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -698,13 +689,11 @@ private fun ReactionBar(onReaction: (String) -> Unit) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun MessageInput(
|
private fun MessageInput(
|
||||||
messageText: String,
|
messageText: String,
|
||||||
isNip17: Boolean,
|
recipientsMissingRelays: Boolean,
|
||||||
requiresNip17: Boolean,
|
|
||||||
canSend: Boolean,
|
canSend: Boolean,
|
||||||
isUploading: Boolean = false,
|
isUploading: Boolean = false,
|
||||||
hasAttachments: Boolean = false,
|
hasAttachments: Boolean = false,
|
||||||
onMessageChange: (String) -> Unit,
|
onMessageChange: (String) -> Unit,
|
||||||
onToggleNip17: () -> Unit,
|
|
||||||
onAttach: () -> Unit = {},
|
onAttach: () -> Unit = {},
|
||||||
onSend: () -> Unit,
|
onSend: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -773,46 +762,30 @@ private fun MessageInput(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NIP-17 indicator
|
// NIP-17 indicator / recipient warning
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(start = 4.dp),
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
) {
|
) {
|
||||||
IconButton(
|
Icon(
|
||||||
onClick = onToggleNip17,
|
imageVector = Icons.Default.Lock,
|
||||||
enabled = !requiresNip17,
|
contentDescription = "NIP-17 (encrypted)",
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
) {
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
Icon(
|
)
|
||||||
imageVector = if (isNip17) Icons.Default.Lock else Icons.Default.LockOpen,
|
|
||||||
contentDescription = if (isNip17) "NIP-17 (encrypted)" else "NIP-04 (legacy)",
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
tint =
|
|
||||||
if (isNip17) {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = if (isNip17) "NIP-17" else "NIP-04",
|
text = "NIP-17",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color =
|
color = MaterialTheme.colorScheme.primary,
|
||||||
if (isNip17) {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if (requiresNip17) {
|
if (recipientsMissingRelays) {
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "(required for groups)",
|
text = "Recipient has no DM relay list — messages cannot be delivered",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
color = MaterialTheme.colorScheme.error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user