feat: deprecate NIP-04 DM sending, always use NIP-17
NIP-04 encryption for sending DMs is now deprecated. All new messages are sent using NIP-17 (gift-wrapped sealed messages). Reading NIP-04 messages remains supported for backward compatibility. When a recipient lacks both a DM relay list (kind 10050) and NIP-65 inbox relays, the send button is disabled and a warning is shown explaining that messages cannot be delivered. Changes: - ChatNewMessageState: Remove nip17/requiresNip17 toggles, add recipientsMissingDmRelays state, always send via NIP-17 - ChatNewMessageViewModel: Always use NIP-17, remove NIP-04 send paths, add recipient relay status checking - ToggleNip17Button -> Nip17Indicator: Replace toggle with static NIP-17 indicator (always on) - PrivateMessageEditFieldRow: Show warning when recipients lack DM relay lists, hide message input - ChatFileSender: sendAll() always uses NIP-17 - Desktop ChatPane: Remove NIP-17 toggle, show relay warning - ChatroomView: Reactively check recipient DM relay availability https://claude.ai/code/session_01T7QhUW9cZogk4DxDXbbbJJ
This commit is contained in:
+4
-4
@@ -92,10 +92,10 @@ fun ChatroomView(
|
||||
}
|
||||
}
|
||||
|
||||
if (room.users.size == 1) {
|
||||
// Activates NIP-17 if the user has DM relays
|
||||
ObserveRelayListForDMs(pubkey = room.users.first(), accountViewModel = accountViewModel) {
|
||||
newPostModel.nip17 = !it?.relays().isNullOrEmpty()
|
||||
// Reactively check if recipients have DM relays for NIP-17 delivery
|
||||
for (userHex in room.users) {
|
||||
ObserveRelayListForDMs(pubkey = userHex, accountViewModel = accountViewModel) {
|
||||
newPostModel.updateRecipientRelayStatus()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+81
-129
@@ -88,7 +88,6 @@ import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
@@ -128,7 +127,8 @@ class ChatNewMessageViewModel :
|
||||
|
||||
var room: ChatroomKey? by mutableStateOf(null)
|
||||
|
||||
var requiresNIP17: Boolean = false
|
||||
/** Whether any recipients are missing DM relay lists */
|
||||
var recipientsMissingDmRelays by mutableStateOf(false)
|
||||
|
||||
val replyTo = mutableStateOf<Note?>(null)
|
||||
|
||||
@@ -180,8 +180,8 @@ class ChatNewMessageViewModel :
|
||||
var wantsZapraiser by mutableStateOf(false)
|
||||
override var zapRaiserAmount = mutableStateOf<Long?>(null)
|
||||
|
||||
// NIP17 Wrapped DMs / Group messages
|
||||
var nip17 by mutableStateOf(false)
|
||||
// NIP17 is always enabled - NIP-04 is deprecated for sending
|
||||
val nip17: Boolean get() = true
|
||||
|
||||
fun lnAddress(): String? = account.userProfile().lnAddress()
|
||||
|
||||
@@ -214,19 +214,19 @@ class ChatNewMessageViewModel :
|
||||
room.users.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
|
||||
updateNIP17StatusFromRoom()
|
||||
updateRecipientRelayStatus()
|
||||
}
|
||||
|
||||
fun updateNIP17StatusFromRoom() {
|
||||
fun updateRecipientRelayStatus() {
|
||||
val room = this.room
|
||||
if (room != null) {
|
||||
this.requiresNIP17 = room.users.size > 1
|
||||
if (this.requiresNIP17) {
|
||||
this.nip17 = true
|
||||
}
|
||||
this.recipientsMissingDmRelays =
|
||||
room.users.any { hexKey ->
|
||||
val user = LocalCache.getOrCreateUser(hexKey)
|
||||
user.dmInboxRelays().isNullOrEmpty()
|
||||
}
|
||||
} else {
|
||||
this.requiresNIP17 = false
|
||||
this.nip17 = false
|
||||
this.recipientsMissingDmRelays = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,8 +361,7 @@ class ChatNewMessageViewModel :
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
requiresNIP17 = draftEvent is NIP17Group
|
||||
nip17 = draftEvent is NIP17Group
|
||||
updateRecipientRelayStatus()
|
||||
}
|
||||
|
||||
suspend fun sendPostSync() {
|
||||
@@ -402,27 +401,19 @@ class ChatNewMessageViewModel :
|
||||
val uploadState = uploadState ?: return
|
||||
|
||||
accountViewModel.launchSigner {
|
||||
if (nip17) {
|
||||
ChatFileUploader(account).justUploadNIP17(
|
||||
uploadState,
|
||||
onError,
|
||||
onEncryptedUploadError = { title, message ->
|
||||
encryptedUploadErrorTitle = title
|
||||
encryptedUploadErrorMessage = message
|
||||
pendingRetryMode = RetryMode.HOLD
|
||||
},
|
||||
context,
|
||||
) {
|
||||
uploadsWaitingToBeSent += it
|
||||
draftTag.newVersion()
|
||||
onceUploaded()
|
||||
}
|
||||
} else {
|
||||
ChatFileUploader(account).justUploadNIP04(uploadState, onError, context) {
|
||||
uploadsWaitingToBeSent += it
|
||||
draftTag.newVersion()
|
||||
onceUploaded()
|
||||
}
|
||||
ChatFileUploader(account).justUploadNIP17(
|
||||
uploadState,
|
||||
onError,
|
||||
onEncryptedUploadError = { title, message ->
|
||||
encryptedUploadErrorTitle = title
|
||||
encryptedUploadErrorMessage = message
|
||||
pendingRetryMode = RetryMode.HOLD
|
||||
},
|
||||
context,
|
||||
) {
|
||||
uploadsWaitingToBeSent += it
|
||||
draftTag.newVersion()
|
||||
onceUploaded()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -436,30 +427,22 @@ class ChatNewMessageViewModel :
|
||||
val uploadState = uploadState ?: return
|
||||
|
||||
accountViewModel.launchSigner {
|
||||
if (nip17) {
|
||||
ChatFileUploader(account).justUploadNIP17(
|
||||
uploadState,
|
||||
onError,
|
||||
onEncryptedUploadError = { title, message ->
|
||||
encryptedUploadErrorTitle = title
|
||||
encryptedUploadErrorMessage = message
|
||||
pendingRetryMode = RetryMode.SEND
|
||||
pendingRetryOnError = onError
|
||||
pendingRetryContext = context
|
||||
pendingRetryOnceUploaded = onceUploaded
|
||||
},
|
||||
context,
|
||||
) {
|
||||
ChatFileSender(room, account).sendNIP17(it)
|
||||
draftTag.newVersion()
|
||||
onceUploaded()
|
||||
}
|
||||
} else {
|
||||
ChatFileUploader(account).justUploadNIP04(uploadState, onError, context) {
|
||||
ChatFileSender(room, account).sendNIP04(it)
|
||||
draftTag.newVersion()
|
||||
onceUploaded()
|
||||
}
|
||||
ChatFileUploader(account).justUploadNIP17(
|
||||
uploadState,
|
||||
onError,
|
||||
onEncryptedUploadError = { title, message ->
|
||||
encryptedUploadErrorTitle = title
|
||||
encryptedUploadErrorMessage = message
|
||||
pendingRetryMode = RetryMode.SEND
|
||||
pendingRetryOnError = onError
|
||||
pendingRetryContext = context
|
||||
pendingRetryOnceUploaded = onceUploaded
|
||||
},
|
||||
context,
|
||||
) {
|
||||
ChatFileSender(room, account).sendNIP17(it)
|
||||
draftTag.newVersion()
|
||||
onceUploaded()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,70 +524,49 @@ class ChatNewMessageViewModel :
|
||||
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value 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 =
|
||||
if (replyHint == null) {
|
||||
ChatMessageEvent.build(message, room.users.map { LocalCache.getOrCreateUser(it).toPTag() }) {
|
||||
hashtags(findHashtags(message))
|
||||
references(findURLs(message))
|
||||
quotes(findNostrEventUris(message))
|
||||
val template =
|
||||
if (replyHint == null) {
|
||||
ChatMessageEvent.build(message, room.users.map { LocalCache.getOrCreateUser(it).toPTag() }) {
|
||||
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)
|
||||
}
|
||||
} 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,
|
||||
) {
|
||||
geoHash?.let { geohash(it) }
|
||||
localZapRaiserAmount?.let { zapraiser(it) }
|
||||
zapReceiver?.let { zapSplits(it) }
|
||||
contentWarningReason?.let { contentWarning(it) }
|
||||
localExpirationDate?.let { expiration(it) }
|
||||
}
|
||||
|
||||
if (draftTag != null) {
|
||||
accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template)
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
}
|
||||
} 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) {
|
||||
ChatFileSender(room, accountViewModel.account).sendAll(uploadsWaitingToBeSent)
|
||||
ChatFileSender(room, accountViewModel.account).sendNIP17(uploadsWaitingToBeSent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,11 +662,11 @@ class ChatNewMessageViewModel :
|
||||
val users = toUsersTagger.pTags?.mapTo(mutableSetOf()) { it.pubkeyHex }
|
||||
if (users.isNullOrEmpty()) {
|
||||
room = null
|
||||
updateNIP17StatusFromRoom()
|
||||
updateRecipientRelayStatus()
|
||||
} else {
|
||||
if (users != room?.users) {
|
||||
room = ChatroomKey(users)
|
||||
updateNIP17StatusFromRoom()
|
||||
updateRecipientRelayStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -737,9 +699,6 @@ class ChatNewMessageViewModel :
|
||||
val lastWord = toUsers.currentWord()
|
||||
toUsers = userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
||||
updateRoomFromUsersInput()
|
||||
|
||||
val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(item.pubkeyHex))?.event as? AdvertisedRelayListEvent)?.readRelaysNorm()
|
||||
nip17 = relayList != null
|
||||
}
|
||||
|
||||
userSuggestionsMainMessage = null
|
||||
@@ -783,7 +742,8 @@ class ChatNewMessageViewModel :
|
||||
!wantsInvoice &&
|
||||
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
||||
(toUsers.text.isNotBlank()) &&
|
||||
uploadState?.multiOrchestrator == null
|
||||
uploadState?.multiOrchestrator == null &&
|
||||
!recipientsMissingDmRelays
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
@@ -795,15 +755,7 @@ class ChatNewMessageViewModel :
|
||||
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
||||
}
|
||||
|
||||
fun toggleNIP04And24() {
|
||||
if (requiresNIP17) {
|
||||
nip17 = true
|
||||
} else {
|
||||
nip17 = !nip17
|
||||
}
|
||||
|
||||
draftTag.newVersion()
|
||||
}
|
||||
// NIP-04 sending is deprecated. NIP-17 is always used.
|
||||
|
||||
override fun updateZapPercentage(
|
||||
index: Int,
|
||||
|
||||
+5
-1
@@ -332,6 +332,10 @@ fun GroupDMScreenContent(
|
||||
)
|
||||
}
|
||||
|
||||
if (postViewModel.recipientsMissingDmRelays) {
|
||||
RecipientMissingRelaysWarning()
|
||||
}
|
||||
|
||||
BottomRowActions(postViewModel, accountViewModel)
|
||||
}
|
||||
}
|
||||
@@ -551,7 +555,7 @@ fun SendDirectMessageTo(
|
||||
),
|
||||
)
|
||||
|
||||
ToggleNip17Button(postViewModel, accountViewModel)
|
||||
Nip17Indicator(postViewModel)
|
||||
}
|
||||
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
|
||||
+57
-33
@@ -174,39 +174,63 @@ fun EditField(
|
||||
onSendNewMessage: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
ThinPaddingTextField(
|
||||
value = channelScreenModel.message,
|
||||
onValueChange = { channelScreenModel.updateMessage(it) },
|
||||
keyboardOptions = PostKeyboard,
|
||||
shape = EditFieldBorder,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.reply_here),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
ThinSendButton(
|
||||
isActive = channelScreenModel.canPost(),
|
||||
modifier = EditFieldTrailingIconModifier,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
channelScreenModel.sendPostSync()
|
||||
onSendNewMessage()
|
||||
if (channelScreenModel.recipientsMissingDmRelays) {
|
||||
RecipientMissingRelaysWarning()
|
||||
} else {
|
||||
ThinPaddingTextField(
|
||||
value = channelScreenModel.message,
|
||||
onValueChange = { channelScreenModel.updateMessage(it) },
|
||||
keyboardOptions = PostKeyboard,
|
||||
shape = EditFieldBorder,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.reply_here),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
ThinSendButton(
|
||||
isActive = channelScreenModel.canPost(),
|
||||
modifier = EditFieldTrailingIconModifier,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
channelScreenModel.sendPostSync()
|
||||
onSendNewMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
leadingIcon = {
|
||||
KeyboardLeadingIcon(channelScreenModel, accountViewModel)
|
||||
},
|
||||
colors =
|
||||
TextFieldDefaults.colors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
KeyboardLeadingIcon(channelScreenModel, accountViewModel)
|
||||
},
|
||||
colors =
|
||||
TextFieldDefaults.colors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RecipientMissingRelaysWarning() {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.recipient_missing_dm_relays),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontSize = Font12SP,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -225,7 +249,7 @@ fun KeyboardLeadingIcon(
|
||||
onImageChosen = channelScreenModel::pickedMedia,
|
||||
)
|
||||
|
||||
ToggleNip17Button(channelScreenModel, accountViewModel)
|
||||
Nip17Indicator(channelScreenModel)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-78
@@ -25,94 +25,25 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialog
|
||||
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.theme.IncognitoIconButtonModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ToggleNip17Button(
|
||||
channelScreenModel: ChatNewMessageViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
var wantsToActivateNIP17 by remember { mutableStateOf(false) }
|
||||
|
||||
if (wantsToActivateNIP17) {
|
||||
NewFeatureNIP17AlertDialog(
|
||||
accountViewModel = accountViewModel,
|
||||
onConfirm = { channelScreenModel.toggleNIP04And24() },
|
||||
onDismiss = { wantsToActivateNIP17 = false },
|
||||
)
|
||||
}
|
||||
|
||||
fun Nip17Indicator(channelScreenModel: ChatNewMessageViewModel) {
|
||||
IconButton(
|
||||
modifier = Modifier.width(30.dp),
|
||||
onClick = {
|
||||
if (
|
||||
!accountViewModel.account.settings.hideNIP17WarningDialog &&
|
||||
!channelScreenModel.nip17 &&
|
||||
!channelScreenModel.requiresNIP17
|
||||
) {
|
||||
wantsToActivateNIP17 = true
|
||||
} else {
|
||||
channelScreenModel.toggleNIP04And24()
|
||||
}
|
||||
},
|
||||
onClick = { },
|
||||
enabled = false,
|
||||
) {
|
||||
if (channelScreenModel.nip17) {
|
||||
Icon(
|
||||
painter = painterRes(R.drawable.incognito, 2),
|
||||
contentDescription = stringRes(id = R.string.accessibility_turn_off_sealed_message),
|
||||
modifier = IncognitoIconButtonModifier,
|
||||
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,
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
painter = painterRes(R.drawable.incognito, 2),
|
||||
contentDescription = stringRes(id = R.string.accessibility_turn_off_sealed_message),
|
||||
modifier = IncognitoIconButtonModifier,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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
-7
@@ -135,12 +135,6 @@ class ChatFileSender(
|
||||
}
|
||||
|
||||
suspend fun sendAll(uploads: List<SuccessfulUploads>) {
|
||||
uploads.forEach {
|
||||
if (it.cipher != null) {
|
||||
sendNIP17(it.result, it.caption, it.contentWarningReason, it.cipher)
|
||||
} else {
|
||||
sendNIP04(it.result, it.caption, it.contentWarningReason)
|
||||
}
|
||||
}
|
||||
sendNIP17(uploads)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_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="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="search_relays_title">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.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.findNostrEventUris
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent
|
||||
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.nip18Reposts.quotes.quotes
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
@@ -62,9 +60,6 @@ class ChatNewMessageState(
|
||||
private val _message = MutableStateFlow(TextFieldValue(""))
|
||||
val message: StateFlow<TextFieldValue> = _message.asStateFlow()
|
||||
|
||||
private val _nip17 = MutableStateFlow(false)
|
||||
val nip17: StateFlow<Boolean> = _nip17.asStateFlow()
|
||||
|
||||
private val _replyTo = MutableStateFlow<Note?>(null)
|
||||
val replyTo: StateFlow<Note?> = _replyTo.asStateFlow()
|
||||
|
||||
@@ -74,38 +69,37 @@ class ChatNewMessageState(
|
||||
private val _room = MutableStateFlow<ChatroomKey?>(null)
|
||||
val room: StateFlow<ChatroomKey?> = _room.asStateFlow()
|
||||
|
||||
/** Whether NIP-17 is required (group chat with >1 recipient) */
|
||||
private val _requiresNip17 = MutableStateFlow(false)
|
||||
val requiresNip17: StateFlow<Boolean> = _requiresNip17.asStateFlow()
|
||||
/** Whether any recipients are missing DM relay lists, preventing message delivery */
|
||||
private val _recipientsMissingDmRelays = MutableStateFlow(false)
|
||||
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
|
||||
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,
|
||||
* and auto-detects NIP-17 requirement (group chats require NIP-17).
|
||||
* Load a chatroom. Sets the room key and checks recipient DM relay availability.
|
||||
*/
|
||||
fun load(roomKey: ChatroomKey) {
|
||||
_room.value = roomKey
|
||||
updateNip17FromRoom()
|
||||
updateRecipientRelayStatus()
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect NIP-17 based on room:
|
||||
* - Group chats (>1 recipient) always require NIP-17
|
||||
* - Single recipient: NIP-17 off by default (can be toggled)
|
||||
* Check if all recipients have DM relay lists.
|
||||
* Messages can only be sent via NIP-17, so recipients must have
|
||||
* either a DM inbox relay list (kind 10050) or NIP-65 inbox relays.
|
||||
*/
|
||||
fun updateNip17FromRoom() {
|
||||
fun updateRecipientRelayStatus() {
|
||||
val currentRoom = _room.value
|
||||
if (currentRoom != null) {
|
||||
_requiresNip17.value = currentRoom.users.size > 1
|
||||
if (_requiresNip17.value) {
|
||||
_nip17.value = true
|
||||
}
|
||||
_recipientsMissingDmRelays.value =
|
||||
currentRoom.users.any { hexKey ->
|
||||
val user = cache.getOrCreateUser(hexKey) as? User
|
||||
user?.dmInboxRelays().isNullOrEmpty()
|
||||
}
|
||||
} else {
|
||||
_requiresNip17.value = false
|
||||
_nip17.value = false
|
||||
_recipientsMissingDmRelays.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,27 +120,7 @@ class ChatNewMessageState(
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle NIP-04/NIP-17 mode.
|
||||
* 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.
|
||||
* Send the current message as NIP-17. NIP-04 is deprecated for sending.
|
||||
*
|
||||
* @return true if send was initiated, false if preconditions not met
|
||||
*/
|
||||
@@ -154,12 +128,9 @@ class ChatNewMessageState(
|
||||
val currentRoom = _room.value ?: return false
|
||||
val messageText = _message.value.text
|
||||
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)
|
||||
} else {
|
||||
sendNip04(currentRoom, messageText)
|
||||
}
|
||||
sendNip17(currentRoom, messageText)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -193,23 +164,6 @@ class ChatNewMessageState(
|
||||
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.
|
||||
*/
|
||||
|
||||
+17
-44
@@ -150,25 +150,18 @@ fun ChatPane(
|
||||
val scope = rememberCoroutineScope()
|
||||
val feedState by feedViewModel.feedState.feedContent.collectAsState()
|
||||
val messageText by messageState.message.collectAsState()
|
||||
val isNip17 by messageState.nip17.collectAsState()
|
||||
val requiresNip17 by messageState.requiresNip17.collectAsState()
|
||||
val recipientsMissingRelays by messageState.recipientsMissingDmRelays.collectAsState()
|
||||
|
||||
// File attachment state
|
||||
val attachedFiles = remember { mutableStateListOf<File>() }
|
||||
var isUploading by remember { mutableStateOf(false) }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
// Helper: attach files and auto-force NIP-17 if needed
|
||||
// Helper: attach files
|
||||
fun attachFiles(files: List<File>) {
|
||||
val mediaFiles = files.filter { it.extension.lowercase() in MEDIA_EXTENSIONS }
|
||||
if (mediaFiles.isEmpty()) return
|
||||
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)
|
||||
@@ -346,13 +339,11 @@ fun ChatPane(
|
||||
// Message input
|
||||
MessageInput(
|
||||
messageText = messageText.text,
|
||||
isNip17 = isNip17,
|
||||
requiresNip17 = requiresNip17,
|
||||
recipientsMissingRelays = recipientsMissingRelays,
|
||||
canSend = messageState.canSend || attachedFiles.isNotEmpty(),
|
||||
isUploading = isUploading,
|
||||
hasAttachments = attachedFiles.isNotEmpty(),
|
||||
onMessageChange = { messageState.updateMessage(messageText.copy(text = it)) },
|
||||
onToggleNip17 = { messageState.toggleNip17() },
|
||||
onAttach = { attachFiles(DesktopFilePicker.pickMediaFiles()) },
|
||||
onSend = {
|
||||
scope.launch {
|
||||
@@ -698,13 +689,11 @@ private fun ReactionBar(onReaction: (String) -> Unit) {
|
||||
@Composable
|
||||
private fun MessageInput(
|
||||
messageText: String,
|
||||
isNip17: Boolean,
|
||||
requiresNip17: Boolean,
|
||||
recipientsMissingRelays: Boolean,
|
||||
canSend: Boolean,
|
||||
isUploading: Boolean = false,
|
||||
hasAttachments: Boolean = false,
|
||||
onMessageChange: (String) -> Unit,
|
||||
onToggleNip17: () -> Unit,
|
||||
onAttach: () -> Unit = {},
|
||||
onSend: () -> Unit,
|
||||
) {
|
||||
@@ -773,46 +762,30 @@ private fun MessageInput(
|
||||
}
|
||||
}
|
||||
|
||||
// NIP-17 indicator
|
||||
// NIP-17 indicator / recipient warning
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onToggleNip17,
|
||||
enabled = !requiresNip17,
|
||||
modifier = Modifier.size(20.dp),
|
||||
) {
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.Lock,
|
||||
contentDescription = "NIP-17 (encrypted)",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
text = if (isNip17) "NIP-17" else "NIP-04",
|
||||
text = "NIP-17",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color =
|
||||
if (isNip17) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||
},
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
if (requiresNip17) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
if (recipientsMissingRelays) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "(required for groups)",
|
||||
text = "Recipient has no DM relay list — messages cannot be delivered",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user