Offer Send Message in the profile menu where there is no Message button (#432)

This commit is contained in:
Coracle-Bot 2026-09-08 18:04:37 +00:00 committed by hodlbod
parent 9810695782
commit 90ba6cff4c
3 changed files with 61 additions and 13 deletions

View file

@ -455,6 +455,8 @@ Acceptance:
- The conversation appears at the top of her list, labeled with bob's name. - 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; - Bob's profile page offers a "Message" button that opens the same conversation;
her own profile offers none. 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 ### US-030 — Start a group chat

View file

@ -60,6 +60,14 @@ const unreadDots = (scope: Locator) => scope.locator(".rounded-full.bg-primary")
const pageBar = (page: Page) => page.locator('[data-component="PageBar"]') 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. // A modal's body is the only scroll container carrying its title.
const modalBody = (page: Page, title: string) => const modalBody = (page: Page, title: string) =>
page page
@ -68,8 +76,13 @@ const modalBody = (page: Page, title: string) =>
const composer = (page: Page) => page.locator(".chat-editor [contenteditable=true]") const composer = (page: Page) => page.locator(".chat-editor [contenteditable=true]")
// The send button is named by the shortcut it advertises, which differs by platform. // The editor is where the composer says whether it is ready. The send button is not there to
const sendButton = (page: Page) => page.locator('.room__compose [data-tip$="enter to send"]') // 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 // Both things the composer puts above itself — the message being replied to and the editing
// indicator — are the same bordered strip. // 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 // 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. // on it is part of sending rather than a wait for a wait's sake.
const send = async (page: Page, content: string) => { const send = async (page: Page, content: string) => {
await expect(sendButton(page)).toBeEnabled() await composerEnabled(page)
await composer(page).pressSequentially(content) await composer(page).pressSequentially(content)
await composer(page).press("Enter") 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(page).toHaveURL(pathPattern(chatPath(users.bob.pubkey)))
await expect(composer(page)).toBeVisible() await expect(composer(page)).toBeVisible()
await expect(sendButton(page)).toBeEnabled() await composerEnabled(page)
await send(page, "hi bob") 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(page).toHaveURL(pathPattern(chatPath(users.bob.pubkey)))
await expect(bubble(page, "hi bob")).toBeVisible() 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 // Her own profile offers no way to message herself
await page.goto(profilePath(users.alice.pubkey)) 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 // One conversation holding both of them, rather than one apiece
await expect(page).toHaveURL(pathPattern(chatPath(users.bob.pubkey, users.carol.pubkey))) 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 // Labeled with both of them rather than with one
await expect(pageBar(page)).toContainText("Bob Barnacle") 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"}) const banner = alice.locator(".card").filter({hasText: "Direct messages are not enabled"})
await expect(banner).toContainText("Bob Barnacle") await expect(banner).toContainText("Bob Barnacle")
await expect(sendButton(alice)).toBeDisabled() await composerDisabled(alice)
const bob = await as(users.bob, "/") 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 // 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(alice).toHaveURL(pathPattern(chatPath(users.bob.pubkey)))
await expect(enablePrompt(alice)).toHaveCount(0) await expect(enablePrompt(alice)).toHaveCount(0)
await expect(sendButton(alice)).toBeEnabled() await composerEnabled(alice)
await expect(banner).toHaveCount(0) await expect(banner).toHaveCount(0)
await send(alice, "finally") await send(alice, "finally")

View file

@ -5,6 +5,7 @@
import UserCircle from "@assets/icons/user-circle.svg?dataurl" import UserCircle from "@assets/icons/user-circle.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import UserMinus from "@assets/icons/user-minus.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 Restart from "@assets/icons/restart.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Confirm from "@lib/components/Confirm.svelte" import Confirm from "@lib/components/Confirm.svelte"
@ -16,11 +17,11 @@
import ProfileAbout from "@app/components/ProfileAbout.svelte" import ProfileAbout from "@app/components/ProfileAbout.svelte"
import ProfileBadges from "@app/components/ProfileBadges.svelte" import ProfileBadges from "@app/components/ProfileBadges.svelte"
import ProfileMenu from "@app/components/ProfileMenu.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 {deriveUserIsSpaceAdmin} from "@app/management"
import {navigate, pushModal} from "@app/modal" import {navigate, popModal, pushModal} from "@app/modal"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {makeProfilePath} from "@app/routes" import {goToChat, makeProfilePath} from "@app/routes"
export type Props = { export type Props = {
pubkey: string pubkey: string
@ -31,10 +32,17 @@
const userIsAdmin = deriveUserIsSpaceAdmin(url) const userIsAdmin = deriveUserIsSpaceAdmin(url)
const isSelf = $derived($user.pubkey === pubkey)
const back = () => history.back() const back = () => history.back()
const viewProfile = () => navigate(makeProfilePath(pubkey), {replaceState: true}) const viewProfile = () => navigate(makeProfilePath(pubkey), {replaceState: true})
const sendMessage = () => {
popModal()
goToChat([pubkey])
}
const report = (error: string | undefined, message: string) => { const report = (error: string | undefined, message: string) => {
if (error) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
@ -92,24 +100,32 @@
<Profile showPubkey avatarSize={14} {pubkey} {url} /> <Profile showPubkey avatarSize={14} {pubkey} {url} />
<ProfileMenu {pubkey} {url}> <ProfileMenu {pubkey} {url}>
{#snippet customActions()} {#snippet customActions()}
{#if !isSelf}
<li>
<Button onclick={sendMessage}>
<Icon size={4} icon={Letter} />
Send Message
</Button>
</li>
{/if}
{#if $userIsAdmin} {#if $userIsAdmin}
{#if isBanned} {#if isBanned}
<li> <li>
<Button onclick={restoreMember}> <Button onclick={restoreMember}>
<Icon icon={Restart} /> <Icon size={4} icon={Restart} />
Restore Membership Restore Membership
</Button> </Button>
</li> </li>
{:else} {:else}
<li> <li>
<Button onclick={removeMember}> <Button onclick={removeMember}>
<Icon icon={UserMinus} /> <Icon size={4} icon={UserMinus} />
Remove Member Remove Member
</Button> </Button>
</li> </li>
<li> <li>
<Button class="text-error" onclick={banMember}> <Button class="text-error" onclick={banMember}>
<Icon icon={MinusCircle} /> <Icon size={4} icon={MinusCircle} />
Ban User Ban User
</Button> </Button>
</li> </li>