From 90ba6cff4cc24ee9c83ec4d1bf83357c44e0d821 Mon Sep 17 00:00:00 2001 From: Coracle-Bot Date: Tue, 8 Sep 2026 18:04:37 +0000 Subject: [PATCH] Offer Send Message in the profile menu where there is no Message button (#432) --- e2e/USER_STORIES.md | 2 ++ e2e/specs/dms.spec.ts | 44 +++++++++++++++++++++---- src/app/components/ProfileDetail.svelte | 28 ++++++++++++---- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/e2e/USER_STORIES.md b/e2e/USER_STORIES.md index 26c1bcd6..61b29d62 100644 --- a/e2e/USER_STORIES.md +++ b/e2e/USER_STORIES.md @@ -455,6 +455,8 @@ Acceptance: - The conversation appears at the top of her list, labeled with bob's name. - Bob's profile page offers a "Message" button that opens the same conversation; her own profile offers none. +- A profile modal has no such button, so its "..." menu offers "Send Message" + instead; the profile page's menu does not repeat it. ### US-030 — Start a group chat diff --git a/e2e/specs/dms.spec.ts b/e2e/specs/dms.spec.ts index e476091e..2ce8abbc 100644 --- a/e2e/specs/dms.spec.ts +++ b/e2e/specs/dms.spec.ts @@ -60,6 +60,14 @@ const unreadDots = (scope: Locator) => scope.locator(".rounded-full.bg-primary") const pageBar = (page: Page) => page.locator('[data-component="PageBar"]') +// A modal is a `.dialog` overlay wrapping a `.dialog` card, and the card is the one with the +// content in it. +const dialog = (page: Page) => page.locator(".dialog").last() + +// The "..." menu on a profile header, which is the only ghost circle button either the profile +// page or the profile modal renders. +const profileMenu = (scope: Page | Locator) => scope.locator("button.button-circle.button-ghost") + // A modal's body is the only scroll container carrying its title. const modalBody = (page: Page, title: string) => page @@ -68,8 +76,13 @@ const modalBody = (page: Page, title: string) => const composer = (page: Page) => page.locator(".chat-editor [contenteditable=true]") -// The send button is named by the shortcut it advertises, which differs by platform. -const sendButton = (page: Page) => page.locator('.room__compose [data-tip$="enter to send"]') +// The editor is where the composer says whether it is ready. The send button is not there to +// ask while the composer is empty, since a dictation button stands in its place. +const composerEnabled = (page: Page) => + expect(page.locator(".room__compose .chat-editor")).toHaveAttribute("aria-disabled", "false") + +const composerDisabled = (page: Page) => + expect(page.locator(".room__compose .chat-editor")).toHaveAttribute("aria-disabled", "true") // Both things the composer puts above itself — the message being replied to and the editing // indicator — are the same bordered strip. @@ -84,7 +97,7 @@ const enablePrompt = (page: Page) => page.getByRole("heading", {name: "Enable di // The composer stays disabled until every recipient's messaging relays have been read, so waiting // on it is part of sending rather than a wait for a wait's sake. const send = async (page: Page, content: string) => { - await expect(sendButton(page)).toBeEnabled() + await composerEnabled(page) await composer(page).pressSequentially(content) await composer(page).press("Enter") } @@ -183,7 +196,7 @@ test("US-029 start a one-on-one chat", async ({seed, as}) => { await expect(page).toHaveURL(pathPattern(chatPath(users.bob.pubkey))) await expect(composer(page)).toBeVisible() - await expect(sendButton(page)).toBeEnabled() + await composerEnabled(page) await send(page, "hi bob") @@ -204,6 +217,23 @@ test("US-029 start a one-on-one chat", async ({seed, as}) => { await expect(page).toHaveURL(pathPattern(chatPath(users.bob.pubkey))) await expect(bubble(page, "hi bob")).toBeVisible() + // The profile page's menu leaves messaging to the button beside it + await page.goto(profilePath(users.bob.pubkey)) + await profileMenu(page).click() + + await expect(page.getByRole("button", {name: "Profile Info"})).toBeVisible() + await expect(page.getByRole("button", {name: "Send Message"})).toHaveCount(0) + + // The profile modal, which has no such button, carries the action in its menu instead + await page.goto("/chat") + await chatItems(page).filter({hasText: "welcome aboard"}).click() + await bubble(page, "welcome aboard").getByRole("button", {name: "Carol Cutter"}).first().click() + await profileMenu(dialog(page)).click() + await page.getByRole("button", {name: "Send Message"}).click() + + await expect(page).toHaveURL(pathPattern(chatPath(users.carol.pubkey))) + await expect(pageBar(page)).toContainText("Carol Cutter") + // Her own profile offers no way to message herself await page.goto(profilePath(users.alice.pubkey)) @@ -241,7 +271,7 @@ test("US-030 start a group chat", async ({seed, as}) => { // One conversation holding both of them, rather than one apiece await expect(page).toHaveURL(pathPattern(chatPath(users.bob.pubkey, users.carol.pubkey))) - await expect(sendButton(page)).toBeEnabled() + await composerEnabled(page) // Labeled with both of them rather than with one await expect(pageBar(page)).toContainText("Bob Barnacle") @@ -294,7 +324,7 @@ test("US-031 direct messaging has to be switched on", async ({seed, as}) => { const banner = alice.locator(".card").filter({hasText: "Direct messages are not enabled"}) await expect(banner).toContainText("Bob Barnacle") - await expect(sendButton(alice)).toBeDisabled() + await composerDisabled(alice) const bob = await as(users.bob, "/") @@ -319,7 +349,7 @@ test("US-031 direct messaging has to be switched on", async ({seed, as}) => { // The prompt is behind her for good, and his end of it works now await expect(alice).toHaveURL(pathPattern(chatPath(users.bob.pubkey))) await expect(enablePrompt(alice)).toHaveCount(0) - await expect(sendButton(alice)).toBeEnabled() + await composerEnabled(alice) await expect(banner).toHaveCount(0) await send(alice, "finally") diff --git a/src/app/components/ProfileDetail.svelte b/src/app/components/ProfileDetail.svelte index 05c40a87..3c3b6f68 100644 --- a/src/app/components/ProfileDetail.svelte +++ b/src/app/components/ProfileDetail.svelte @@ -5,6 +5,7 @@ import UserCircle from "@assets/icons/user-circle.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" import UserMinus from "@assets/icons/user-minus.svg?dataurl" + import Letter from "@assets/icons/letter-opened.svg?dataurl" import Restart from "@assets/icons/restart.svg?dataurl" import Icon from "@lib/components/Icon.svelte" import Confirm from "@lib/components/Confirm.svelte" @@ -16,11 +17,11 @@ import ProfileAbout from "@app/components/ProfileAbout.svelte" import ProfileBadges from "@app/components/ProfileBadges.svelte" import ProfileMenu from "@app/components/ProfileMenu.svelte" - import {messagingRelayLists, profiles, relayManagement} from "@app/core" + import {messagingRelayLists, profiles, relayManagement, user} from "@app/core" import {deriveUserIsSpaceAdmin} from "@app/management" - import {navigate, pushModal} from "@app/modal" + import {navigate, popModal, pushModal} from "@app/modal" import {pushToast} from "@app/toast" - import {makeProfilePath} from "@app/routes" + import {goToChat, makeProfilePath} from "@app/routes" export type Props = { pubkey: string @@ -31,10 +32,17 @@ const userIsAdmin = deriveUserIsSpaceAdmin(url) + const isSelf = $derived($user.pubkey === pubkey) + const back = () => history.back() const viewProfile = () => navigate(makeProfilePath(pubkey), {replaceState: true}) + const sendMessage = () => { + popModal() + goToChat([pubkey]) + } + const report = (error: string | undefined, message: string) => { if (error) { pushToast({theme: "error", message: error}) @@ -92,24 +100,32 @@ {#snippet customActions()} + {#if !isSelf} +
  • + +
  • + {/if} {#if $userIsAdmin} {#if isBanned}
  • {:else}