Files
buzz/desktop/src/shared/useMessageDeepLinks.ts
Taylor Ho 4bf80978f5 fix(messages): route edits to the owning composer (#6575)
**Category:** fix
**User Impact:** Editing a channel message now opens in the main
composer, while editing a thread reply stays in the thread composer,
with focus ready for typing.

**Problem:** When a thread was open, Buzz treated its root message as
thread-owned and opened edits in the thread composer. Menu-driven edits
also lacked regression coverage for immediate focus.

**Solution:** Carry the message's semantic root/reply classification
into the edit target, route only actual replies to the thread composer,
and use the menu primitive's selection event for a reliable handoff.
End-to-end tests cover placement and focus for both paths.

<details>
<summary>File changes</summary>

**desktop/src/features/channels/ui/ChannelPane.tsx**
Routes edit targets by semantic thread ownership rather than membership
in the open thread panel.

**desktop/src/features/channels/ui/ChannelPane.types.ts**
Uses the shared composer edit-target type so routing metadata stays
attached to the target.

**desktop/src/features/messages/lib/draftMentionRefs.ts**
Classifies each edit target as a root or true thread reply from its
event tags.

**desktop/src/features/messages/lib/draftMentionRefs.test.mjs**
Covers semantic ownership for root and reply edit targets.

**desktop/src/features/messages/ui/MessageActionBar.tsx**
Handles Edit through the dropdown menu's selection event so focus
restoration and edit startup share the intended lifecycle.

**desktop/src/features/messages/ui/MessageComposer.types.ts**
Adds semantic thread ownership to the edit-target contract.

**desktop/tests/e2e/messaging.spec.ts**
Verifies root edits use and focus the main composer, while reply edits
use and focus the thread composer.

</details>

### Reproduction Steps

1. Send a channel message and open its thread.
2. From the thread panel, edit the root message; confirm its content
loads in the main composer and the editor is focused.
3. Send a reply in that thread.
4. Edit the reply; confirm its content loads in the thread composer and
the editor is focused.


### Screenshots

**Editing a channel-root message uses the main composer**

![Channel-root message editing in the main composer, dark theme with
purple
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6575/message-edit-root-main-dark.png)

**Editing an actual thread reply uses the thread composer**

![Thread reply editing in the thread composer, dark theme with purple
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6575/message-edit-reply-thread-dark.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-24 13:06:00 -07:00

47 lines
1.6 KiB
TypeScript

import * as React from "react";
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
import { listenForNavigationDeepLinks } from "@/shared/deep-link";
/**
* Subscribe to `buzz://message` deep links emitted by the Tauri backend
* and route them through the app's navigation helpers.
*
* Lives in a hook (not inline in `AppShell`) so it can be unit-tested
* without the entire shell, and so the shell file stays under its line cap.
*
* Mirrors the cold-start race handling of the `connect` listener in
* `App.tsx`: late-arriving payloads from a fresh launch are picked up the
* first time the listener mounts. Routing matches the in-app buzz://
* handler in `markdown.tsx`: always `goChannel` with `messageId` and let
* the channel route's existing scroll-into-view + getEventById backfill
* resolve the target (works for both stream replies and forum threads).
*/
export function useMessageDeepLinks(enabled = true) {
const { goChannel } = useAppNavigation();
React.useEffect(() => {
if (!enabled) return;
let cancelled = false;
const unlistenPromise = listenForNavigationDeepLinks(
async (payload) => {
if (cancelled) return false;
await goChannel(payload.channelId);
return true;
},
async (payload) => {
if (cancelled) return false;
return goChannel(payload.channelId, {
messageId: payload.messageId,
threadRootId: payload.threadRootId,
});
},
);
return () => {
cancelled = true;
void unlistenPromise.then((unlisten) => unlisten());
};
}, [enabled, goChannel]);
}