From c70a31a682577bf57bf963964215299755a0a6e5 Mon Sep 17 00:00:00 2001 From: Coracle-Bot Date: Mon, 28 Sep 2026 22:08:14 +0000 Subject: [PATCH] Add a create post button to the home page and to a profile (#660) --- e2e/USER_STORIES.md | 12 +++ e2e/specs/people.spec.ts | 57 +++++++++++- src/app/components/HomeNetwork.svelte | 13 +++ src/app/components/NoteCreate.svelte | 127 ++++++++++++++++++++++++++ src/app/components/ProfilePage.svelte | 14 ++- 5 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 src/app/components/NoteCreate.svelte diff --git a/e2e/USER_STORIES.md b/e2e/USER_STORIES.md index 9649d486..5f15a169 100644 --- a/e2e/USER_STORIES.md +++ b/e2e/USER_STORIES.md @@ -1281,6 +1281,18 @@ Acceptance: - A note alice publishes from her own session appears at the top of bob's open view without a reload. +### US-134 — Write a public post + +As alice, I want to write a post from my own profile or from my network feed, so +that the people who follow me and the spaces I favorited both see it. + +Acceptance: + +- "Create post" on her profile and at the top of home's Network section both + open the same composer. +- Submitting it publishes the post to her own relays and to every space she + favorited, and it appears at the top of her profile's notes. + ### US-080 — Preview a profile from anywhere As alice, I want to click a name or avatar wherever it appears, so that I can diff --git a/e2e/specs/people.spec.ts b/e2e/specs/people.spec.ts index 898d8c4f..f0db3244 100644 --- a/e2e/specs/people.spec.ts +++ b/e2e/specs/people.spec.ts @@ -1,7 +1,7 @@ import {createHash} from "node:crypto" import type {Locator, Page} from "@playwright/test" import {npubEncode} from "nostr-tools/nip19" -import {HOUR, MINUTE, MONTH} from "@welshman/lib" +import {HOUR, MINUTE, MONTH, uniq} from "@welshman/lib" import {NOTE, STATUS, makeEvent} from "@welshman/util" import type {SignedEvent} from "@welshman/util" import {FollowList, Note, PinList, Profile} from "@welshman/domain" @@ -10,7 +10,9 @@ import { GIF, WEBP, expect, + getPublished, mockBlossom, + noteEditor, profilePath, spacePath, test, @@ -397,6 +399,59 @@ test("US-079 read a person's notes", async ({seed, as}) => { await expect(list.filter({hasText: "REPLY"})).toHaveCount(0) }) +test("US-134 write a public post", async ({seed, as}) => { + const post = "the wind has finally turned" + + const scenario = await seed(({relay, user}) => { + const space = relay("space") + const other = relay("other") + + space.room("general", {name: "General"}) + space.join(user.alice, "general") + space.profile(user.alice, {name: "Alice Anderson"}) + space.relayList(user.alice) + + // Her second space is in no relay list of hers, so a post reaching it was routed there as a space. + other.room("general", {name: "General"}) + other.join(user.alice, "general") + }) + + const page = await as(users.alice, profilePath(users.alice.pubkey), {context: DESKTOP}) + + await page.getByRole("button", {name: "Create post"}).click() + + const composer = topDialog(page) + + await expect(composer.getByText("Create a Public Post")).toBeVisible() + + await noteEditor(composer).click() + await noteEditor(composer).pressSequentially(post) + await composer.getByRole("button", {name: "Post"}).click() + + await expect(notes(page).filter({hasText: post})).toBeVisible() + + await expect + .poll(() => + uniq( + getPublished(page.context()) + .filter(({event}) => event.content === post) + .map(({url}) => url), + ).sort(), + ) + .toEqual([scenario.space("other").url, scenario.space("space").url].sort()) + + // The same composer is how a post starts from home, out of the feed it posts into. + await page.goto("/home") + + const network = page + .locator("section") + .filter({has: page.getByRole("heading", {name: "Network"})}) + + await network.getByRole("button", {name: "Create post"}).click() + + await expect(topDialog(page).getByText("Create a Public Post")).toBeVisible() +}) + test("US-080 preview a profile from anywhere", async ({seed, as}) => { const scenario = await seed(({relay, user}) => { const space = relay("space") diff --git a/src/app/components/HomeNetwork.svelte b/src/app/components/HomeNetwork.svelte index 00295379..6bcde69d 100644 --- a/src/app/components/HomeNetwork.svelte +++ b/src/app/components/HomeNetwork.svelte @@ -8,13 +8,18 @@ import type {TrustedEvent} from "@welshman/util" import {getReplyTags} from "@welshman/domain" import Planet from "@assets/icons/planet.svg?dataurl" + import PenNewSquare from "@assets/icons/pen-new-square.svg?dataurl" + import Icon from "@lib/components/Icon.svelte" import Link from "@lib/components/Link.svelte" + import Button from "@lib/components/Button.svelte" import Spinner from "@lib/components/Spinner.svelte" import HomeSection from "@app/components/HomeSection.svelte" import HomeNetworkItem from "@app/components/HomeNetworkItem.svelte" + import NoteCreate from "@app/components/NoteCreate.svelte" import {CONTENT_KINDS} from "@app/content" import {followLists, relayLists, router, user} from "@app/core" import {isFeedLoading, makeFeed, makeFeedContext, makeScrollLoader} from "@app/feeds" + import {pushModal} from "@app/modal" type Props = { class?: string @@ -39,6 +44,8 @@ let started = false let stop: Maybe<() => void> + const createPost = () => pushModal(NoteCreate) + // Kind 1 replies read as half a conversation without their parent. const isRoot = (event: TrustedEvent) => event.kind !== NOTE || getReplyTags(event.tags).replies.length === 0 @@ -85,6 +92,12 @@ + {#snippet action()} + + {/snippet}
{#if isEmpty}
diff --git a/src/app/components/NoteCreate.svelte b/src/app/components/NoteCreate.svelte new file mode 100644 index 00000000..42c0d100 --- /dev/null +++ b/src/app/components/NoteCreate.svelte @@ -0,0 +1,127 @@ + + + + + + Create a Public Post + Published to your outbox and all favorited spaces. + +
+
+ +
+ +
+
+ + + + +
diff --git a/src/app/components/ProfilePage.svelte b/src/app/components/ProfilePage.svelte index 88ef8968..ee89f5be 100644 --- a/src/app/components/ProfilePage.svelte +++ b/src/app/components/ProfilePage.svelte @@ -6,6 +6,7 @@ import LinkRound from "@assets/icons/link-round.svg?dataurl" import Letter from "@assets/icons/letter-opened.svg?dataurl" import UserPlus from "@assets/icons/user-plus.svg?dataurl" + import Pen2 from "@assets/icons/pen-2.svg?dataurl" import PenNewSquare from "@assets/icons/pen-new-square.svg?dataurl" import GallerySend from "@assets/icons/gallery-send.svg?dataurl" import Icon from "@lib/components/Icon.svelte" @@ -21,6 +22,7 @@ import ProfilePageNotes from "@app/components/ProfilePageNotes.svelte" import ProfileEdit from "@app/components/ProfileEdit.svelte" import ProfileMenu from "@app/components/ProfileMenu.svelte" + import NoteCreate from "@app/components/NoteCreate.svelte" import WotScore from "@app/components/WotScore.svelte" import {compressFileForUpload, uploadFile} from "@app/uploads" import {pushModal} from "@app/modal" @@ -53,6 +55,8 @@ const startEdit = () => pushModal(ProfileEdit) + const createPost = () => pushModal(NoteCreate) + const openChat = () => goToChat([target]) const toggleFollow = async () => { @@ -154,11 +158,17 @@
{#if isSelf} + {:else}