Name the message share menu item and offer a link from the share dialog

This commit is contained in:
Coracle-Bot 2026-09-18 18:22:51 +00:00
parent ec835f7cfb
commit f2c9a02756
5 changed files with 42 additions and 8 deletions

View file

@ -439,7 +439,10 @@ that I can pass it along without retyping it.
Acceptance: Acceptance:
- "Share" on a message inside a space opens a picker of that space's rooms. - "Share Message" on a message inside a space opens a picker of that space's
rooms.
- The picker also offers a link to the message, which copies to the clipboard
and opens the room scrolled to it.
- Choosing a destination navigates there with the composer pre-filled with a - Choosing a destination navigates there with the composer pre-filled with a
quote of the shared message. quote of the shared message.
- Sending posts the quoted message in the destination, visible to bob there. - Sending posts the quoted message in the destination, visible to bob there.

View file

@ -1,4 +1,4 @@
import {npubEncode} from "nostr-tools/nip19" import {neventEncode, npubEncode} from "nostr-tools/nip19"
import {DAY, HOUR, MINUTE, WEEK, bech32ToHex} from "@welshman/lib" import {DAY, HOUR, MINUTE, WEEK, bech32ToHex} from "@welshman/lib"
import {getLnUrl} from "@welshman/util" import {getLnUrl} from "@welshman/util"
import {Profile, displayPubkey} from "@welshman/domain" import {Profile, displayPubkey} from "@welshman/domain"
@ -871,6 +871,8 @@ test("US-027a a permalink near the newest end lands at the bottom", async ({seed
}) })
test("US-028 share a message somewhere else", async ({seed, as}) => { test("US-028 share a message somewhere else", async ({seed, as}) => {
let shared!: SeededEvent
const scenario = await seed(({relay, user, at}) => { const scenario = await seed(({relay, user, at}) => {
const space = relay("space") const space = relay("space")
@ -880,7 +882,7 @@ test("US-028 share a message somewhere else", async ({seed, as}) => {
space.join(user.bob, "general", "random") space.join(user.bob, "general", "random")
space.profile(user.alice, {name: "Alice Anchor"}) space.profile(user.alice, {name: "Alice Anchor"})
space.profile(user.bob, {name: "Bob Barnacle"}) space.profile(user.bob, {name: "Bob Barnacle"})
space.message(user.bob, "general", "the dock is closed on sunday", at(2, HOUR)) shared = space.message(user.bob, "general", "the dock is closed on sunday", at(2, HOUR))
seedChatter(space, user.alice) seedChatter(space, user.alice)
seedChatter(space, user.bob) seedChatter(space, user.bob)
@ -888,13 +890,15 @@ test("US-028 share a message somewhere else", async ({seed, as}) => {
const {url} = scenario.space("space") const {url} = scenario.space("space")
const alice = await as(users.alice, roomPath(url, "general")) const alice = await as(users.alice, roomPath(url, "general"), {
context: {permissions: ["clipboard-read", "clipboard-write"]},
})
const bob = await as(users.bob, roomPath(url, "random")) const bob = await as(users.bob, roomPath(url, "random"))
await expect(message(alice, "the dock is closed on sunday")).toBeVisible() await expect(message(alice, "the dock is closed on sunday")).toBeVisible()
await openMessageMenu(alice, "the dock is closed on sunday") await openMessageMenu(alice, "the dock is closed on sunday")
await alice.getByRole("button", {name: "Share"}).click() await alice.getByRole("button", {name: "Share Message"}).click()
// ShareEvent is titled after the noun it was opened with, so its subtitle names it instead. // ShareEvent is titled after the noun it was opened with, so its subtitle names it instead.
const picker = alice const picker = alice
@ -902,6 +906,16 @@ test("US-028 share a message somewhere else", async ({seed, as}) => {
.filter({hasText: "Which room would you like to share this event to?"}) .filter({hasText: "Which room would you like to share this event to?"})
.last() .last()
// The same dialog hands out a link to the message, for anywhere flotilla cannot reach.
await picker.getByRole("button", {name: "Copy link"}).click()
await expect(alice.getByRole("alert")).toContainText("Copied to clipboard!")
const permalink = new URL(await alice.evaluate(() => navigator.clipboard.readText()))
expect(permalink.pathname).toBe(roomPath(url, "general"))
expect(permalink.search).toBe(`?at=${shared.event.created_at}`)
expect(permalink.hash).toBe(`#${neventEncode({id: shared.id, relays: [url]})}`)
await picker.getByRole("button", {name: "Random"}).click() await picker.getByRole("button", {name: "Random"}).click()
await picker.getByRole("button", {name: /^Share/}).click() await picker.getByRole("button", {name: /^Share/}).click()

View file

@ -127,7 +127,7 @@
<li> <li>
<Button onclick={share}> <Button onclick={share}>
<Icon size={4} icon={ShareCircle} /> <Icon size={4} icon={ShareCircle} />
Share Share Message
</Button> </Button>
</li> </li>
<li> <li>

View file

@ -157,7 +157,7 @@
{/if} {/if}
<Button class="button button-neutral w-full" onclick={share}> <Button class="button button-neutral w-full" onclick={share}>
<Icon size={4} icon={ShareCircle} /> <Icon size={4} icon={ShareCircle} />
Share Share Message
</Button> </Button>
<Button class="button button-neutral w-full" onclick={read}> <Button class="button button-neutral w-full" onclick={read}>
<Icon size={4} icon={VolumeLoud} /> <Icon size={4} icon={VolumeLoud} />

View file

@ -3,6 +3,8 @@
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
@ -12,8 +14,9 @@
import Modal from "@lib/components/Modal.svelte" import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte" import ModalBody from "@lib/components/ModalBody.svelte"
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte" import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
import {makeRoomPath} from "@app/routes" import {makeEventPermalink, makeRoomPath} from "@app/routes"
import {shareTo} from "@app/share" import {shareTo} from "@app/share"
import {clip} from "@app/toast"
import {rooms} from "@app/core" import {rooms} from "@app/core"
type Props = { type Props = {
@ -36,6 +39,10 @@
selection = h === selection ? "" : h selection = h === selection ? "" : h
} }
const permalink = makeEventPermalink(event, url)
const copyPermalink = () => clip(permalink)
let selection = $state("") let selection = $state("")
</script> </script>
@ -57,6 +64,16 @@
</Button> </Button>
{/each} {/each}
</div> </div>
<div class="flex flex-col gap-2">
<p class="text-xs uppercase tracking-wide opacity-60">Or copy a link</p>
<label class="input flex min-w-0 items-center gap-2">
<Icon icon={LinkRound} class="shrink-0" />
<input value={permalink} class="min-w-0 flex-1 truncate" type="text" readonly />
<Button class="shrink-0" aria-label="Copy link" onclick={copyPermalink}>
<Icon icon={Copy} />
</Button>
</label>
</div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="button button-link" onclick={back}> <Button class="button button-link" onclick={back}>