mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
**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**  **Editing an actual thread reply uses the thread composer**  --------- 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>
47 lines
1.6 KiB
TypeScript
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]);
|
|
}
|