mirror of
https://github.com/block/buzz.git
synced 2026-11-04 00:12:02 +00:00
Keep channel management in home inbox
This commit is contained in:
@@ -102,6 +102,9 @@ export function AppShell() {
|
||||
|
||||
const [isChannelManagementOpen, setIsChannelManagementOpen] =
|
||||
React.useState(false);
|
||||
const [managedChannelId, setManagedChannelId] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [searchFocusRequest, setSearchFocusRequest] = React.useState(0);
|
||||
const [topbarSearchHidden, setTopbarSearchHidden] = React.useState(false);
|
||||
const [topbarSearchLoading, setTopbarSearchLoading] = React.useState(false);
|
||||
@@ -223,6 +226,12 @@ export function AppShell() {
|
||||
: null,
|
||||
[channels, selectedChannelId],
|
||||
);
|
||||
const managedChannel = React.useMemo(() => {
|
||||
const targetChannelId = managedChannelId ?? selectedChannelId;
|
||||
return targetChannelId
|
||||
? (channels.find((channel) => channel.id === targetChannelId) ?? null)
|
||||
: null;
|
||||
}, [channels, managedChannelId, selectedChannelId]);
|
||||
|
||||
const handleThreadReplyDesktopNotification = React.useEffectEvent(
|
||||
(channelId: string, event: RelayEvent) => {
|
||||
@@ -665,7 +674,10 @@ export function AppShell() {
|
||||
markChannelRead,
|
||||
markChannelUnread,
|
||||
openCreateChannel: handleOpenCreateChannel,
|
||||
openChannelManagement: () => {
|
||||
openChannelManagement: (channelId?: string) => {
|
||||
setManagedChannelId(
|
||||
typeof channelId === "string" ? channelId : null,
|
||||
);
|
||||
setIsChannelManagementOpen(true);
|
||||
},
|
||||
getChannelReadAt,
|
||||
@@ -887,16 +899,22 @@ export function AppShell() {
|
||||
)}
|
||||
|
||||
<AppShellOverlays
|
||||
activeChannel={activeChannel}
|
||||
activeChannel={managedChannel}
|
||||
browseDialogType={browseDialogType}
|
||||
channels={channels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
isChannelManagementOpen={isChannelManagementOpen}
|
||||
onBrowseChannelJoin={handleBrowseChannelJoin}
|
||||
onBrowseDialogOpenChange={handleBrowseDialogOpenChange}
|
||||
onChannelManagementOpenChange={setIsChannelManagementOpen}
|
||||
onChannelManagementOpenChange={(open) => {
|
||||
setIsChannelManagementOpen(open);
|
||||
if (!open) {
|
||||
setManagedChannelId(null);
|
||||
}
|
||||
}}
|
||||
onDeleteActiveChannel={() => {
|
||||
setIsChannelManagementOpen(false);
|
||||
setManagedChannelId(null);
|
||||
void goHome({ replace: true });
|
||||
}}
|
||||
onSelectChannel={(channelId) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ type AppShellContextValue = {
|
||||
) => void;
|
||||
markChannelUnread: (channelId: string) => void;
|
||||
openCreateChannel: () => void;
|
||||
openChannelManagement: () => void;
|
||||
openChannelManagement: (channelId?: string) => void;
|
||||
// NIP-RS read marker for a channel as a unix-seconds timestamp, or null
|
||||
// when unknown. Backed by the single AppShell-mounted ReadStateManager so
|
||||
// every surface (sidebar, home, badges) projects from the same source.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { useAppShell } from "@/app/AppShellContext";
|
||||
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
||||
import { useChannelsQuery } from "@/features/channels/hooks";
|
||||
import { HomeScreen } from "@/features/home/ui/HomeScreen";
|
||||
@@ -16,6 +17,7 @@ export const Route = createFileRoute("/")({
|
||||
|
||||
function HomeRouteComponent() {
|
||||
const { goChannel } = useAppNavigation();
|
||||
const { openChannelManagement } = useAppShell();
|
||||
const channelsQuery = useChannelsQuery();
|
||||
const identityQuery = useIdentityQuery();
|
||||
const channels = channelsQuery.data ?? [];
|
||||
@@ -65,12 +67,7 @@ function HomeRouteComponent() {
|
||||
<HomeScreen
|
||||
availableChannelIds={availableChannelIds}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
onOpenChannel={(channelId) => {
|
||||
void goChannel(channelId);
|
||||
}}
|
||||
onOpenContext={(channelId, messageId) => {
|
||||
void goChannel(channelId, { messageId });
|
||||
}}
|
||||
onOpenChannel={openChannelManagement}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,14 +14,12 @@ type HomeScreenProps = {
|
||||
availableChannelIds: ReadonlySet<string>;
|
||||
currentPubkey?: string;
|
||||
onOpenChannel: (channelId: string) => void;
|
||||
onOpenContext: (channelId: string, messageId: string) => void;
|
||||
};
|
||||
|
||||
export function HomeScreen({
|
||||
availableChannelIds,
|
||||
currentPubkey,
|
||||
onOpenChannel,
|
||||
onOpenContext,
|
||||
}: HomeScreenProps) {
|
||||
const homeFeedQuery = useHomeFeedQuery();
|
||||
const { threadActivityItems } = useAppShell();
|
||||
@@ -73,7 +71,6 @@ export function HomeScreen({
|
||||
feed={augmentedFeed}
|
||||
isLoading={homeFeedQuery.isLoading}
|
||||
onOpenChannel={onOpenChannel}
|
||||
onOpenContext={onOpenContext}
|
||||
onRefresh={() => {
|
||||
void homeFeedQuery.refetch();
|
||||
}}
|
||||
|
||||
@@ -53,7 +53,6 @@ type HomeViewProps = {
|
||||
currentPubkey?: string;
|
||||
availableChannelIds: ReadonlySet<string>;
|
||||
onOpenChannel: (channelId: string) => void;
|
||||
onOpenContext: (channelId: string, messageId: string) => void;
|
||||
onRefresh: () => void;
|
||||
};
|
||||
|
||||
@@ -64,7 +63,6 @@ export function HomeView({
|
||||
currentPubkey,
|
||||
availableChannelIds,
|
||||
onOpenChannel,
|
||||
onOpenContext,
|
||||
onRefresh,
|
||||
}: HomeViewProps) {
|
||||
const [homeInboxRef, homeInboxWidthPx] = useElementWidth<HTMLDivElement>();
|
||||
@@ -433,7 +431,6 @@ export function HomeView({
|
||||
});
|
||||
}}
|
||||
onOpenChannel={onOpenChannel}
|
||||
onOpenContext={onOpenContext}
|
||||
onSendReply={async ({
|
||||
content,
|
||||
mediaTags,
|
||||
|
||||
@@ -60,7 +60,6 @@ type InboxDetailPaneProps = {
|
||||
onBack?: () => void;
|
||||
onDelete: () => void;
|
||||
onOpenChannel: (channelId: string) => void;
|
||||
onOpenContext?: (channelId: string, messageId: string) => void;
|
||||
onSendReply: (input: {
|
||||
content: string;
|
||||
mediaTags?: string[][];
|
||||
@@ -94,7 +93,6 @@ export function InboxDetailPane({
|
||||
onBack,
|
||||
onDelete,
|
||||
onOpenChannel,
|
||||
onOpenContext,
|
||||
onSendReply,
|
||||
onToggleReaction,
|
||||
onToggleDone,
|
||||
@@ -256,10 +254,10 @@ export function InboxDetailPane({
|
||||
</Button>
|
||||
) : null}
|
||||
<div className="min-w-0">
|
||||
{canOpenChannel && contextChannelId && onOpenContext ? (
|
||||
{canOpenChannel && contextChannelId ? (
|
||||
<button
|
||||
className="flex min-w-0 items-center gap-[4px] text-left text-sm font-semibold leading-5 tracking-tight text-foreground hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
onClick={() => onOpenContext(contextChannelId, item.id)}
|
||||
onClick={() => onOpenChannel(contextChannelId)}
|
||||
title={item.fullTimestampLabel}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -3,8 +3,23 @@ import { expect, test } from "@playwright/test";
|
||||
import { KIND_TYPING_INDICATOR } from "../../src/shared/constants/kinds";
|
||||
import { TEST_IDENTITIES, installMockBridge } from "../helpers/bridge";
|
||||
|
||||
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
|
||||
const MOCK_IDENTITY_PUBKEY = "deadbeef".repeat(8);
|
||||
|
||||
type MockFeedWindow = Window & {
|
||||
__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?: (item: {
|
||||
category: "mention" | "needs_action" | "activity" | "agent_activity";
|
||||
channel_id: string | null;
|
||||
channel_name: string;
|
||||
content: string;
|
||||
created_at: number;
|
||||
id: string;
|
||||
kind: number;
|
||||
pubkey: string;
|
||||
tags: string[][];
|
||||
}) => unknown;
|
||||
};
|
||||
|
||||
async function openChannelManagement(
|
||||
page: import("@playwright/test").Page,
|
||||
channelName: string,
|
||||
@@ -1242,6 +1257,64 @@ test("manage channel keeps canvas near the top of the sheet", async ({
|
||||
expect(canvasBox?.y).toBeLessThan(nameBox?.y);
|
||||
});
|
||||
|
||||
test("home inbox channel label opens management without leaving home", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
typeof (window as MockFeedWindow).__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ ===
|
||||
"function",
|
||||
);
|
||||
|
||||
await page.evaluate(
|
||||
({ channelId, createdAt, currentPubkey, senderPubkey }) => {
|
||||
const pushFeedItem = (window as MockFeedWindow)
|
||||
.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__;
|
||||
if (!pushFeedItem) {
|
||||
throw new Error("Mock feed injection helper is not installed.");
|
||||
}
|
||||
|
||||
pushFeedItem({
|
||||
id: "mock-feed-home-channel-panel",
|
||||
kind: 9,
|
||||
pubkey: senderPubkey,
|
||||
content: "Please review the home panel routing.",
|
||||
created_at: createdAt,
|
||||
channel_id: channelId,
|
||||
channel_name: "general",
|
||||
tags: [
|
||||
["e", channelId],
|
||||
["p", currentPubkey],
|
||||
],
|
||||
category: "mention",
|
||||
});
|
||||
},
|
||||
{
|
||||
channelId: GENERAL_CHANNEL_ID,
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
currentPubkey: TEST_IDENTITIES.tyler.pubkey,
|
||||
senderPubkey: TEST_IDENTITIES.alice.pubkey,
|
||||
},
|
||||
);
|
||||
|
||||
await page
|
||||
.getByTestId("home-inbox-item-mock-feed-home-channel-panel")
|
||||
.click();
|
||||
await page
|
||||
.getByTestId("home-inbox-detail")
|
||||
.getByRole("button", { exact: true, name: "general" })
|
||||
.click();
|
||||
|
||||
await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
|
||||
await expect(page.getByTestId("channel-management-name-row")).toContainText(
|
||||
"general",
|
||||
);
|
||||
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
|
||||
await expect(page).not.toHaveURL(/#\/channels\//);
|
||||
});
|
||||
|
||||
test("members sidebar can invite and remove members", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await openMembersSidebar(page, "general");
|
||||
|
||||
Reference in New Issue
Block a user