Send emoji reactions through the shared reaction helpers and toggle room pins through toggleRoomPin

This commit is contained in:
Jon Staab 2026-09-25 21:37:45 -07:00
parent 8d0c73b364
commit 9c3d77a2f2
9 changed files with 40 additions and 95 deletions

View file

@ -119,7 +119,8 @@ user holds a method for — reports under `banevent`, join requests under `allow
- **Invite** (`publishRoomInvite`): a 9009 with a random `code` tag. The link carries `h` and
`code`, and `joinRoom(url, h, code)` sends the code as the join's `claim`.
- **Pins**: `roomPinLists.setPins(url, h, pins)` sends 9010 and the relay republishes 39005.
`deriveRoomPinnedEvents` (`src/app/roomPins.ts`) loads the pinned events from the room's relay.
`toggleRoomPin(url, h, id)` (`src/app/roomPins.ts`) wraps it for the message menus and toasts
the result. `deriveRoomPinnedEvents` loads the pinned events from the room's relay.
The relay enforces the `RoomMetaReader` flags (`isClosed`, `isHidden`, `isPrivate`,
`isRestricted`); the UI only reflects them. Until membership is `Granted`, `RoomChat` hides a

View file

@ -17,7 +17,7 @@ and NIP-43 tables in [SKILL.md](SKILL.md). Routes are under `src/routes/`.
| Goals | `ZAP_GOAL` (9041) | `ZapGoal` | none | `spaces/[relay]/goals`, `goals/[id]` (`GoalCreate`) |
| Polls | `POLL` (1068), `POLL_RESPONSE` (1018) | `Poll`, `PollResponse` | none | `spaces/[relay]/polls`, `polls/[id]` (`PollCreate`, `PollVotes`) |
| Library | `PINBOARD` (30067), `PIN` (39067) | `Pinboard`, `Pin` | `src/app/pinboards.ts` | `spaces/[relay]/library` (`PinboardEdit`, `PinAdd`); published by any member |
| Room pins | `ROOM_PINS` (39005), `ROOM_UPDATE_PINS` (9010) | `RoomPins`, `RoomUpdatePins` | `src/app/roomPins.ts` | `RoomItemMenu`, `RoomPinnedMessagesAll` |
| Room pins | `ROOM_PINS` (39005), `ROOM_UPDATE_PINS` (9010) | `RoomPins`, `RoomUpdatePins` | `src/app/roomPins.ts` | `RoomItemMenu`, `RoomItemMenuMobile`, `RoomPinnedMessagesAll` |
| Featured content | `APP_DATA` (30078), `d` = `flotilla/featured-content` | `AppData` | `src/app/featured.ts` | `SpaceFeaturedContent`; published as the relay |
| Bot commands (NIP-CD) | `COMMAND` (31992) | `Command` | `src/app/commands.ts` | `RoomCompose`, `ContentCommand` |
| Voice room participants | `LIVEKIT_PARTICIPANTS` (39004, defined in `src/app/call.ts`) | none | `src/app/call.ts` | rooms where `meta.hasLivekit()` |

View file

@ -3,7 +3,7 @@
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import EmojiButton from "@lib/components/EmojiButton.svelte"
import {reactions, wraps} from "@app/core"
import {publishWrappedReaction} from "@app/reactions"
type Props = {
event: TrustedEvent
@ -12,11 +12,8 @@
const {event, pubkeys}: Props = $props()
const onEmoji = async (emoji: string) => {
const reaction = await $reactions.react(event, emoji)
return $wraps.publish({event: reaction.event, recipients: pubkeys, pow: 16})
}
const onEmoji = (emoji: string) =>
publishWrappedReaction(event, {content: emoji, tags: []}, pubkeys)
</script>
<EmojiButton

View file

@ -14,7 +14,7 @@
import Button from "@lib/components/Button.svelte"
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import {reactions, wraps} from "@app/core"
import {publishWrappedReaction} from "@app/reactions"
import {pushModal} from "@app/modal"
import {clip} from "@app/toast"
@ -29,12 +29,9 @@
const tile = "button h-auto flex-col gap-1.5 py-4 text-xs"
const onEmoji = async (emoji: string) => {
const onEmoji = (emoji: string) => {
history.back()
const reaction = await $reactions.react(event, emoji)
return $wraps.publish({event: reaction.event, recipients: pubkeys, pow: 16})
publishWrappedReaction(event, {content: emoji, tags: []}, pubkeys)
}
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})

View file

@ -1,10 +1,9 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {tagSpec, tagValue} from "@welshman/util"
import EmojiButton from "@lib/components/EmojiButton.svelte"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import {reactions, relays} from "@app/core"
import {publishReaction} from "@app/reactions"
type Props = {
url: string
@ -13,22 +12,7 @@
const {url, event}: Props = $props()
const shouldProtect = $relays.hasNip(url, 70)
const onEmoji = async (emoji: string) => {
const protect = await shouldProtect
const room = tagValue(tagSpec("h"), event.tags)
const command = await $reactions.react(event, emoji, writer => {
writer.setProtected(protect)
if (room) {
writer.setRoom(url, room)
}
})
return command.publishToRelays([url])
}
const onEmoji = (emoji: string) => publishReaction(event, {content: emoji, tags: []}, {url})
</script>
<EmojiButton

View file

@ -22,8 +22,8 @@
import {deriveUserAdminDelete, deriveUserIsRoomAdmin} from "@app/rooms"
import {shareEvent} from "@app/share"
import {readAloud} from "@app/speech"
import {toggleRoomPin} from "@app/roomPins"
import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast"
type Props = {
url: string
@ -83,24 +83,9 @@
pushModal(EventAdminDeleteConfirm, {url, noun: "Message", event})
}
const togglePin = async () => {
const togglePin = () => {
onClick()
if (!h) {
return
}
// The optimistic write flips isPinned while the publish is in flight, so read it before it goes out.
const wasPinned = isPinned
const pins = wasPinned ? $pinIds.filter(pin => pin !== event.id) : [...$pinIds, event.id]
const command = await $roomPinLists.setPins(url, h, pins)
const error = await command.publishToRelays([url]).waitForError()
if (error) {
pushToast({theme: "error", message: error})
} else {
pushToast({message: wasPinned ? "Message unpinned" : "Message pinned"})
}
toggleRoomPin(url, h, event.id)
}
</script>

View file

@ -31,14 +31,15 @@
import EventAdminDeleteConfirm from "@app/components/EventAdminDeleteConfirm.svelte"
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
import ThreadCreate from "@app/components/ThreadCreate.svelte"
import {reactions, relays, roomPinLists, user} from "@app/core"
import {roomPinLists, user} from "@app/core"
import {deriveUserAdminDelete, deriveUserIsRoomAdmin} from "@app/rooms"
import {toggleRoomPin} from "@app/roomPins"
import {publishReaction} from "@app/reactions"
import {ENABLE_ZAPS} from "@app/env"
import {makeContentPath} from "@app/routes"
import {shareEvent} from "@app/share"
import {readAloud} from "@app/speech"
import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast"
type Props = {
url: string
@ -57,13 +58,9 @@
const isPinned = $derived($pinIds.includes(event.id))
const tile = "button h-auto flex-col gap-1.5 py-4 text-xs"
const onEmoji = async (emoji: string) => {
const onEmoji = (emoji: string) => {
history.back()
const protect = await $relays.hasNip(url, 70)
const command = await $reactions.react(event, emoji, w => w.setProtected(protect))
command.publishToRelays([url])
publishReaction(event, {content: emoji, tags: []}, {url})
}
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
@ -113,24 +110,9 @@
showMore = !showMore
}
const togglePin = async () => {
if (!h) {
return
}
const togglePin = () => {
history.back()
// The optimistic write flips isPinned while the publish is in flight, so read it before it goes out.
const wasPinned = isPinned
const pins = wasPinned ? $pinIds.filter(pin => pin !== event.id) : [...$pinIds, event.id]
const command = await $roomPinLists.setPins(url, h, pins)
const error = await command.publishToRelays([url]).waitForError()
if (error) {
pushToast({theme: "error", message: error})
} else {
pushToast({message: wasPinned ? "Message unpinned" : "Message pinned"})
}
toggleRoomPin(url, h, event.id)
}
let showMore = $state(false)

View file

@ -10,15 +10,13 @@
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {roomPinLists} from "@app/core"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import RoomItemContent from "@app/components/RoomItemContent.svelte"
import RoomName from "@app/components/RoomName.svelte"
import {deriveUserIsRoomAdmin} from "@app/rooms"
import {deriveRoomPinnedEvents} from "@app/roomPins"
import {deriveRoomPinnedEvents, toggleRoomPin} from "@app/roomPins"
import {goToEvent} from "@app/routes"
import {pushToast} from "@app/toast"
type Props = {
url: string
@ -27,7 +25,6 @@
const {url, h}: Props = $props()
const pinIds = $roomPinLists.pins(url, h).$
const pinnedEvents = deriveRoomPinnedEvents(url, h)
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
@ -37,21 +34,6 @@
back()
goToEvent(event)
}
const unpin = async (event: TrustedEvent) => {
const command = await $roomPinLists.setPins(
url,
h,
$pinIds.filter(pin => pin !== event.id),
)
const error = await command.publishToRelays([url]).waitForError()
if (error) {
pushToast({theme: "error", message: error})
} else {
pushToast({message: "Message unpinned"})
}
}
</script>
<Modal>
@ -64,7 +46,7 @@
</ModalHeader>
<div class="flex flex-col gap-2">
{#each $pinnedEvents as event (event.id)}
{@const onUnpin = () => unpin(event)}
{@const onUnpin = () => toggleRoomPin(url, h, event.id)}
{@const onJump = () => jumpToMessage(event)}
<div class="card card-sm flex flex-col gap-3 p-4">
<div class="flex items-start gap-3">

View file

@ -3,6 +3,7 @@ import type {Readable} from "svelte/store"
import {getIdFilters, getIdOrAddress} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {events, network, roomPinLists} from "@app/core"
import {pushToast} from "@app/toast"
export const deriveRoomPinnedEvents = (url: string, h: string): Readable<TrustedEvent[]> =>
readable<TrustedEvent[]>([], set => {
@ -50,3 +51,19 @@ export const deriveRoomPinnedEvents = (url: string, h: string): Readable<Trusted
unsubscribe()
}
})
export const toggleRoomPin = async (url: string, h: string, id: string) => {
const pins = roomPinLists.get().pins(url, h).get()
// The optimistic write flips the pin while the publish is in flight, so read it before it goes out.
const wasPinned = pins.includes(id)
const command = await roomPinLists
.get()
.setPins(url, h, wasPinned ? pins.filter(pin => pin !== id) : [...pins, id])
const error = await command.publishToRelays([url]).waitForError()
if (error) {
pushToast({theme: "error", message: error})
} else {
pushToast({message: wasPinned ? "Message unpinned" : "Message pinned"})
}
}