mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
feat: add @-mention user search and tagging to Marmot group chat composer
Brings the MLS/Marmot message composer to parity with NIP-17 DMs: typing @ shows the shared user-suggestion dropdown (local cache + NIP-05 resolution), selecting a user inserts @npub…, and on send NewMessageTagger rewrites mentions into nostr: URIs and collects the referenced users as p-tags on the inner kind:9 rumor. Mentions stay inside the MLS ciphertext; the outer kind:445 is unchanged. Also applies MentionPreservingInputTransformation and UrlUserTagOutputTransformation to the field so mentions render highlighted while composing, matching the DM editor. https://claude.ai/code/session_013NWdjCSegsf2FYSPPANX3n
This commit is contained in:
+3
@@ -106,6 +106,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
@@ -1626,6 +1627,7 @@ class AccountViewModel(
|
||||
text: String,
|
||||
replyToInnerEventId: HexKey? = null,
|
||||
replyToInnerAuthorPubKey: HexKey? = null,
|
||||
mentions: List<PTag> = emptyList(),
|
||||
) {
|
||||
// Inner event construction lives on MarmotManager so CLI and UI don't drift.
|
||||
// persistOwn=false because Account.sendMarmotGroupMessage routes the outer
|
||||
@@ -1638,6 +1640,7 @@ class AccountViewModel(
|
||||
replyToEventId = replyToInnerEventId,
|
||||
replyToAuthorPubKey = replyToInnerAuthorPubKey,
|
||||
persistOwn = false,
|
||||
mentions = mentions,
|
||||
)
|
||||
?: return
|
||||
val relays = account.marmotGroupRelays(nostrGroupId)
|
||||
|
||||
+44
-1
@@ -50,12 +50,18 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.currentWord
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
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.marmotGroup.send.MarmotFileSender
|
||||
@@ -69,6 +75,7 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.SuggestionListDefaultHeightChat
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -181,6 +188,15 @@ fun MarmotGroupMessageComposer(
|
||||
)
|
||||
}
|
||||
|
||||
val userSuggestions =
|
||||
remember(nostrGroupId) {
|
||||
UserSuggestionState(accountViewModel.account, accountViewModel.nip05ClientBuilder())
|
||||
}
|
||||
|
||||
DisposableEffect(nostrGroupId) {
|
||||
onDispose { userSuggestions.reset() }
|
||||
}
|
||||
|
||||
// Upload dialog
|
||||
uploadState.multiOrchestrator?.let {
|
||||
MarmotGroupFileUploadDialog(
|
||||
@@ -200,11 +216,33 @@ fun MarmotGroupMessageComposer(
|
||||
}
|
||||
|
||||
Column(modifier = EditFieldModifier) {
|
||||
ShowUserSuggestionList(
|
||||
userSuggestions,
|
||||
onSelect = { user ->
|
||||
userSuggestions.replaceCurrentWord(messageState, messageState.currentWord(), user)
|
||||
userSuggestions.reset()
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = SuggestionListDefaultHeightChat,
|
||||
)
|
||||
|
||||
ThinPaddingTextField(
|
||||
state = messageState,
|
||||
onTextChanged = {
|
||||
if (messageState.selection.collapsed) {
|
||||
val lastWord = messageState.currentWord()
|
||||
if (lastWord.startsWith("@")) {
|
||||
userSuggestions.processCurrentWord(lastWord)
|
||||
} else {
|
||||
userSuggestions.reset()
|
||||
}
|
||||
}
|
||||
},
|
||||
onContentReceived = { uri, mimeType ->
|
||||
uploadState.load(persistentListOf(SelectedMedia(uri, mimeType)))
|
||||
},
|
||||
inputTransformation = MentionPreservingInputTransformation,
|
||||
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = EditFieldBorder,
|
||||
placeholder = {
|
||||
@@ -235,11 +273,16 @@ fun MarmotGroupMessageComposer(
|
||||
val replyAuthor = parentEvent?.pubKey
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// Rewrites @npub…/@nprofile… mentions into nostr: URIs and
|
||||
// collects the referenced users as p-tags for the inner event.
|
||||
val tagger = NewMessageTagger(text, null, null, accountViewModel)
|
||||
tagger.run()
|
||||
accountViewModel.sendMarmotGroupMessage(
|
||||
nostrGroupId = nostrGroupId,
|
||||
text = text,
|
||||
text = tagger.message,
|
||||
replyToInnerEventId = replyId,
|
||||
replyToInnerAuthorPubKey = replyAuthor,
|
||||
mentions = tagger.pTags?.map { it.toPTag() } ?: emptyList(),
|
||||
)
|
||||
messageState.clearText()
|
||||
replyTo.value = null
|
||||
|
||||
+9
@@ -46,6 +46,8 @@ import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.pTags
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.QEventTag
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.quote
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
@@ -178,6 +180,11 @@ class MarmotManager(
|
||||
* `persistOwn = false`. Headless callers (CLI) should leave it at
|
||||
* the default.
|
||||
*
|
||||
* [mentions] become p-tags on the inner kind:9 (users referenced via
|
||||
* `nostr:npub…`/`nostr:nprofile…` in [text]), mirroring how NIP-17
|
||||
* chat messages tag mentioned users. They stay inside the MLS
|
||||
* ciphertext — the outer kind:445 never carries member pubkeys.
|
||||
*
|
||||
* @return the signed kind:445 outer event together with the inner kind:9
|
||||
* rumor id, so the caller can reference it for replies/reactions.
|
||||
*/
|
||||
@@ -187,10 +194,12 @@ class MarmotManager(
|
||||
replyToEventId: HexKey? = null,
|
||||
replyToAuthorPubKey: HexKey? = null,
|
||||
persistOwn: Boolean = true,
|
||||
mentions: List<PTag> = emptyList(),
|
||||
): TextMessageBundle {
|
||||
val template =
|
||||
com.vitorpamplona.quartz.nip01Core.signers
|
||||
.eventTemplate<Event>(kind = 9, description = text) {
|
||||
pTags(mentions)
|
||||
if (replyToEventId != null) {
|
||||
// Mirror ChatEvent.reply(): NIP-18 q-tag references the
|
||||
// parent inner kind:9 by id (+ optional author, no
|
||||
|
||||
Reference in New Issue
Block a user