extract actions from amethyst

This commit is contained in:
nrobi144
2026-01-02 13:29:55 +02:00
parent 6581fe18a3
commit 2751855186
9 changed files with 529 additions and 178 deletions
@@ -23,33 +23,17 @@ package com.vitorpamplona.amethyst.model.nip18Reposts
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.amethyst.commons.actions.RepostAction as CommonsRepostAction
/**
* Android wrapper - pure proxy to commons implementation.
* All logic and validation handled in commons.
*/
class RepostAction {
companion object {
suspend fun repost(
note: Note,
signer: NostrSigner,
): Event? {
val noteEvent = note.event ?: return null
if (note.hasBoostedInTheLast5Minutes(signer.pubKey)) {
// has already bosted in the past 5mins
return null
}
val noteHint = note.relayHintUrl()
val authorHint = note.author?.bestRelayHint()
val template =
if (noteEvent.kind == 1) {
RepostEvent.build(noteEvent, noteHint, authorHint)
} else {
GenericRepostEvent.build(noteEvent, noteHint, authorHint)
}
return signer.sign(template)
}
): Event? = CommonsRepostAction.repost(note, signer)
}
}
@@ -22,14 +22,15 @@ package com.vitorpamplona.amethyst.model.nip25Reactions
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.amethyst.commons.actions.ReactionAction as CommonsReactionAction
/**
* Android wrapper - pure proxy to commons implementation.
* All logic and validation handled in commons.
*/
class ReactionAction {
companion object {
suspend fun reactTo(
@@ -39,65 +40,6 @@ class ReactionAction {
signer: NostrSigner,
onPublic: (ReactionEvent) -> Unit,
onPrivate: suspend (NIP17Factory.Result) -> Unit,
) {
if (!signer.isWriteable()) return
if (note.hasReacted(by, reaction)) {
// has already liked this note
return
}
val noteEvent = note.event
if (noteEvent is NIP17Group) {
val users = noteEvent.groupMembers().toList()
if (reaction.startsWith(":")) {
val emojiUrl = EmojiUrlTag.decode(reaction)
if (emojiUrl != null) {
note.toEventHint<Event>()?.let {
onPrivate(
NIP17Factory().createReactionWithinGroup(
emojiUrl = emojiUrl,
originalNote = it,
to = users,
signer = signer,
),
)
}
return
}
}
note.toEventHint<Event>()?.let {
onPrivate(
NIP17Factory().createReactionWithinGroup(
content = reaction,
originalNote = it,
to = users,
signer = signer,
),
)
}
return
} else {
if (reaction.startsWith(":")) {
val emojiUrl = EmojiUrlTag.decode(reaction)
if (emojiUrl != null) {
note.event?.let {
val template = ReactionEvent.build(emojiUrl, EventHintBundle(it, note.relayHintUrl()))
onPublic(signer.sign(template))
}
return
}
}
note.toEventHint<Event>()?.let {
onPublic(signer.sign(ReactionEvent.build(reaction, it)))
}
}
}
) = CommonsReactionAction.reactTo(note, reaction, by, signer, onPublic, onPrivate)
}
}
@@ -0,0 +1,123 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.actions
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip02FollowList.ReadWrite
import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
/**
* Shared action for following/unfollowing users.
* Creates NIP-02 ContactList events (kind 3).
*/
object FollowAction {
/**
* Follows a user by creating an updated ContactListEvent.
*
* @param pubKeyHex The hex public key of the user to follow
* @param signer The NostrSigner to sign the event
* @param currentContactList The current contact list event (if any)
* @return Signed ContactListEvent with the user added
* @throws IllegalStateException if signer is not writeable
*/
suspend fun follow(
pubKeyHex: String,
signer: NostrSigner,
currentContactList: ContactListEvent? = null,
): ContactListEvent {
if (!signer.isWriteable()) {
throw IllegalStateException("Cannot follow: signer is not writeable")
}
val updatedEvent =
if (currentContactList != null) {
// Add to existing contact list
ContactListEvent.followUser(
earlierVersion = currentContactList,
pubKeyHex = pubKeyHex,
signer = signer,
)
} else {
// Create new contact list from scratch
ContactListEvent.createFromScratch(
followUsers = listOf(ContactTag(pubKeyHex)),
relayUse = emptyMap<String, ReadWrite>(),
signer = signer,
)
}
return updatedEvent
}
/**
* Unfollows a user by creating an updated ContactListEvent.
*
* @param pubKeyHex The hex public key of the user to unfollow
* @param signer The NostrSigner to sign the event
* @param currentContactList The current contact list event (required for unfollow)
* @return Signed ContactListEvent with the user removed
* @throws IllegalStateException if signer is not writeable
* @throws IllegalArgumentException if currentContactList is null
*/
suspend fun unfollow(
pubKeyHex: String,
signer: NostrSigner,
currentContactList: ContactListEvent?,
): ContactListEvent {
if (!signer.isWriteable()) {
throw IllegalStateException("Cannot unfollow: signer is not writeable")
}
requireNotNull(currentContactList) {
"Cannot unfollow: current contact list is required"
}
// Remove from existing contact list
val updatedEvent =
ContactListEvent.unfollowUser(
earlierVersion = currentContactList,
pubKeyHex = pubKeyHex,
signer = signer,
)
return updatedEvent
}
/**
* Checks if a user is currently followed in the contact list.
*
* @param pubKeyHex The hex public key to check
* @param currentContactList The current contact list event (if any)
* @return true if the user is followed, false otherwise
*/
fun isFollowing(
pubKeyHex: String,
currentContactList: ContactListEvent?,
): Boolean {
if (currentContactList == null) return false
// Check if the pubKeyHex is in the contact list's p-tags
return currentContactList.tags.any { tag ->
tag.size >= 2 && tag[0] == "p" && tag[1] == pubKeyHex
}
}
}
@@ -0,0 +1,85 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.actions
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.events.eTag
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.pTag
import com.vitorpamplona.quartz.nip01Core.tags.references.references
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
/**
* Shared action for publishing new text notes.
* Handles replies, hashtags, and URL references.
*/
object PublishAction {
/**
* Publishes a text note (NIP-01 kind 1).
*
* @param content The text content of the note
* @param signer The NostrSigner to sign the event
* @param replyTo Optional event to reply to (adds e-tag and p-tag)
* @return Signed TextNoteEvent ready to broadcast
* @throws IllegalStateException if signer is not writeable
*/
suspend fun publishTextNote(
content: String,
signer: NostrSigner,
replyTo: Event? = null,
): TextNoteEvent {
if (!signer.isWriteable()) {
throw IllegalStateException("Cannot publish: signer is not writeable")
}
val template =
TextNoteEvent.build(content) {
// If replying, add e-tag and p-tag
if (replyTo != null) {
val etag = ETag(replyTo.id)
etag.relay = null
etag.author = replyTo.pubKey
eTag(etag)
pTag(PTag(replyTo.pubKey, relayHint = null))
}
// Extract hashtags and URLs from content
hashtags(findHashtags(content))
references(findURLs(content))
}
return signer.sign(template)
}
/**
* Publishes a reply to an existing note.
*/
suspend fun publishReply(
content: String,
replyTo: Event,
signer: NostrSigner,
): TextNoteEvent = publishTextNote(content, signer, replyTo)
}
@@ -0,0 +1,193 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.actions
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
/**
* Shared action for reacting to events (likes, emoji reactions).
* Supports public reactions, custom emoji reactions, and NIP-17 private group reactions.
*
* Full-featured version extracted from Android for maximum code reuse across platforms.
*/
object ReactionAction {
/**
* Creates a signed reaction event.
*
* @param event The event to react to
* @param reaction The reaction content (e.g., "+", "❤️", ":custom_emoji:")
* @param signer The NostrSigner to sign the event
* @param relayHint Optional relay hint URL where the original event was seen
* @return Signed ReactionEvent ready to broadcast
* @throws IllegalStateException if signer is not writeable
*/
suspend fun reactTo(
event: Event,
reaction: String,
signer: NostrSigner,
relayHint: String? = null,
): ReactionEvent {
if (!signer.isWriteable()) {
throw IllegalStateException("Cannot react: signer is not writeable")
}
val normalizedRelayHint = relayHint?.let { RelayUrlNormalizer.normalizeOrNull(it) }
val eventHint = EventHintBundle(event, normalizedRelayHint)
// Handle custom emoji reactions (format: ":emoji_name:")
val template =
if (reaction.startsWith(":")) {
val emojiUrl = EmojiUrlTag.decode(reaction)
if (emojiUrl != null) {
ReactionEvent.build(emojiUrl, eventHint)
} else {
// Fallback to text if emoji decode fails
ReactionEvent.build(reaction, eventHint)
}
} else {
ReactionEvent.build(reaction, eventHint)
}
return signer.sign(template)
}
/**
* Creates a "like" reaction ("+").
*/
suspend fun like(
event: Event,
signer: NostrSigner,
relayHint: String? = null,
): ReactionEvent = reactTo(event, "+", signer, relayHint)
/**
* Advanced: React to an event with support for NIP-17 private groups.
*
* This method handles both public and private group reactions:
* - For NIP17Group events: Creates private reactions within the group
* - For regular events: Creates public reactions
*
* @param event The event to react to
* @param eventHint EventHintBundle with relay information
* @param reaction The reaction content (e.g., "+", "❤️", ":custom_emoji:")
* @param signer The NostrSigner to sign the event
* @param onPublic Callback for public reactions (returns signed ReactionEvent)
* @param onPrivate Callback for private group reactions (returns NIP17Factory.Result)
*/
suspend fun reactToWithGroupSupport(
event: Event,
eventHint: EventHintBundle<Event>,
reaction: String,
signer: NostrSigner,
onPublic: suspend (ReactionEvent) -> Unit,
onPrivate: suspend (NIP17Factory.Result) -> Unit,
) {
if (!signer.isWriteable()) {
throw IllegalStateException("Cannot react: signer is not writeable")
}
// Check if this is a NIP-17 private group event
if (event is NIP17Group) {
val users = event.groupMembers().toList()
// Handle custom emoji reactions in groups
if (reaction.startsWith(":")) {
val emojiUrl = EmojiUrlTag.decode(reaction)
if (emojiUrl != null) {
onPrivate(
NIP17Factory().createReactionWithinGroup(
emojiUrl = emojiUrl,
originalNote = eventHint,
to = users,
signer = signer,
),
)
return
}
}
// Regular text reaction in group
onPrivate(
NIP17Factory().createReactionWithinGroup(
content = reaction,
originalNote = eventHint,
to = users,
signer = signer,
),
)
} else {
// Public reaction
val template =
if (reaction.startsWith(":")) {
val emojiUrl = EmojiUrlTag.decode(reaction)
if (emojiUrl != null) {
ReactionEvent.build(emojiUrl, eventHint)
} else {
ReactionEvent.build(reaction, eventHint)
}
} else {
ReactionEvent.build(reaction, eventHint)
}
onPublic(signer.sign(template))
}
}
/**
* Android-compatible overload: React to a note with full validation.
*
* This accepts Note/User directly and handles all validation including
* duplicate checking and writeability checks.
*
* @param note The note to react to
* @param reaction The reaction content
* @param by The user reacting
* @param signer The NostrSigner to sign the event
* @param onPublic Callback for public reactions
* @param onPrivate Callback for private group reactions
*/
suspend fun reactTo(
note: Note,
reaction: String,
by: User,
signer: NostrSigner,
onPublic: (ReactionEvent) -> Unit,
onPrivate: suspend (NIP17Factory.Result) -> Unit,
) {
// All validation in commons
if (!signer.isWriteable()) return
if (note.hasReacted(by, reaction)) return
val noteEvent = note.event ?: return
val eventHint = note.toEventHint<Event>() ?: return
reactToWithGroupSupport(noteEvent, eventHint, reaction, signer, onPublic, onPrivate)
}
}
@@ -0,0 +1,95 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.actions
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
/**
* Shared action for reposting (boosting) events.
* Supports both NIP-18 reposts (kind 6 for kind 1) and generic reposts (kind 16 for other kinds).
*/
object RepostAction {
/**
* Creates a signed repost event.
*
* @param event The event to repost
* @param signer The NostrSigner to sign the event
* @param noteHint Optional relay hint where the note was seen
* @param authorHint Optional relay hint for the author's outbox
* @return Signed repost event ready to broadcast
* @throws IllegalStateException if signer is not writeable
*/
suspend fun repost(
event: Event,
signer: NostrSigner,
noteHint: String? = null,
authorHint: String? = null,
): Event {
if (!signer.isWriteable()) {
throw IllegalStateException("Cannot repost: signer is not writeable")
}
// Normalize relay hints
val normalizedNoteHint = noteHint?.let { RelayUrlNormalizer.normalizeOrNull(it) }
val normalizedAuthorHint = authorHint?.let { RelayUrlNormalizer.normalizeOrNull(it) }
// Use NIP-18 RepostEvent (kind 6) for text notes (kind 1)
// Use GenericRepostEvent (kind 16) for all other kinds
val template =
if (event.kind == 1) {
RepostEvent.build(event, normalizedNoteHint, normalizedAuthorHint)
} else {
GenericRepostEvent.build(event, normalizedNoteHint, normalizedAuthorHint)
}
return signer.sign(template)
}
/**
* Android-compatible overload: Repost a note with full validation.
*
* This accepts Note directly and handles all validation including
* duplicate checking (5-minute window) and writeability checks.
*
* @param note The note to repost
* @param signer The NostrSigner to sign the event
* @return Signed repost event, or null if validation fails
*/
suspend fun repost(
note: Note,
signer: NostrSigner,
): Event? {
// All validation in commons
if (!signer.isWriteable()) return null
if (note.hasBoostedInTheLast5Minutes(signer.pubKey)) return null
val noteEvent = note.event ?: return null
val noteHint = note.relayHintUrl()?.url
val authorHint = note.author?.bestRelayHint()?.url
return repost(noteEvent, signer, noteHint, authorHint)
}
}
@@ -44,16 +44,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.vitorpamplona.amethyst.commons.account.AccountState
import com.vitorpamplona.amethyst.commons.actions.PublishAction
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.events.eTag
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.pTag
import com.vitorpamplona.quartz.nip01Core.tags.references.references
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -187,35 +179,16 @@ private suspend fun publishNote(
withContext(Dispatchers.IO) {
println("[ComposeNoteDialog] Starting publishNote: content length=${content.length}")
// Build TextNoteEvent using Quartz
val template =
TextNoteEvent.build(content) {
// If replying, add e-tag and p-tag
if (replyTo != null) {
val etag = ETag(replyTo.id)
etag.relay = null
etag.author = replyTo.pubKey
eTag(etag)
pTag(PTag(replyTo.pubKey, relayHint = null))
}
// Check read-only mode
if (account.isReadOnly) {
println("[ComposeNoteDialog] Error: Cannot post in read-only mode")
throw IllegalStateException("Cannot post in read-only mode")
}
// Extract hashtags and URLs from content
hashtags(findHashtags(content))
references(findURLs(content))
}
println("[ComposeNoteDialog] Signing event with pubkey: ${account.pubKeyHex.take(8)}...")
// Sign the event
val signedEvent =
if (account.isReadOnly) {
// Read-only mode - can't sign events
println("[ComposeNoteDialog] Error: Cannot post in read-only mode")
throw IllegalStateException("Cannot post in read-only mode")
} else {
// Sign with nsec (full key)
val signer = account.signer
println("[ComposeNoteDialog] Signing event with pubkey: ${account.pubKeyHex.take(8)}...")
signer.sign(template)
}
// Use shared PublishAction from commons
val signedEvent = PublishAction.publishTextNote(content, account.signer, replyTo)
println("[ComposeNoteDialog] Event signed successfully, ID: ${signedEvent.id.take(8)}...")
@@ -40,14 +40,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.account.AccountState
import com.vitorpamplona.amethyst.commons.actions.ReactionAction
import com.vitorpamplona.amethyst.commons.actions.RepostAction
import com.vitorpamplona.amethyst.commons.icons.Reply
import com.vitorpamplona.amethyst.commons.icons.Repost
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -163,12 +161,8 @@ private suspend fun reactToNote(
relayManager: DesktopRelayConnectionManager,
) {
withContext(Dispatchers.IO) {
// Build reaction event
val reactedTo = EventHintBundle(event, null)
val template = ReactionEvent.like(reactedTo)
// Sign with user's key
val signedEvent = account.signer.sign(template)
// Use shared ReactionAction from commons
val signedEvent = ReactionAction.reactTo(event, reaction, account.signer)
// Broadcast to all relays
relayManager.broadcastToAll(signedEvent)
@@ -184,26 +178,8 @@ private suspend fun repostNote(
relayManager: DesktopRelayConnectionManager,
) {
withContext(Dispatchers.IO) {
// Build repost event
val template =
if (event.kind == 1) {
// Text note - use RepostEvent (kind 6)
RepostEvent.build(
boostedPost = event,
eventSourceRelay = null,
authorHomeRelay = null,
)
} else {
// Other kinds - use GenericRepostEvent (kind 16)
GenericRepostEvent.build(
boostedPost = event,
eventSourceRelay = null,
authorHomeRelay = null,
)
}
// Sign with user's key
val signedEvent = account.signer.sign(template)
// Use shared RepostAction from commons
val signedEvent = RepostAction.repost(event, account.signer)
// Broadcast to all relays
relayManager.broadcastToAll(signedEvent)
@@ -62,6 +62,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.account.AccountState
import com.vitorpamplona.amethyst.commons.actions.FollowAction
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
import com.vitorpamplona.quartz.nip01Core.core.Event
@@ -71,8 +72,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip02FollowList.ReadWrite
import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import kotlinx.coroutines.Dispatchers
@@ -531,24 +530,8 @@ private suspend fun followUser(
withContext(Dispatchers.IO) {
println("[UserProfile] Starting followUser: target=${pubKeyHex.take(8)}...")
val updatedEvent =
if (currentContactList != null) {
println("[UserProfile] Adding to existing contact list")
// Add to existing contact list
ContactListEvent.followUser(
earlierVersion = currentContactList,
pubKeyHex = pubKeyHex,
signer = account.signer,
)
} else {
println("[UserProfile] Creating new contact list")
// Create new contact list with this user
ContactListEvent.createFromScratch(
followUsers = listOf(ContactTag(pubKeyHex)),
relayUse = emptyMap<String, ReadWrite>(),
signer = account.signer,
)
}
// Use shared FollowAction from commons
val updatedEvent = FollowAction.follow(pubKeyHex, account.signer, currentContactList)
println("[UserProfile] ContactListEvent created, broadcasting...")
relayManager.broadcastToAll(updatedEvent)
@@ -570,12 +553,9 @@ private suspend fun unfollowUser(
if (currentContactList != null) {
println("[UserProfile] Removing from existing contact list")
val updatedEvent =
ContactListEvent.unfollowUser(
earlierVersion = currentContactList,
pubKeyHex = pubKeyHex,
signer = account.signer,
)
// Use shared FollowAction from commons
val updatedEvent = FollowAction.unfollow(pubKeyHex, account.signer, currentContactList)
println("[UserProfile] ContactListEvent updated, broadcasting...")
relayManager.broadcastToAll(updatedEvent)