mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
[codex] hide archived channels from sidebar (#260)
This commit is contained in:
@@ -137,6 +137,10 @@ export function AppShell() {
|
||||
() => channels.filter((channel) => channel.isMember),
|
||||
[channels],
|
||||
);
|
||||
const sidebarChannels = React.useMemo(
|
||||
() => memberChannels.filter((channel) => channel.archivedAt === null),
|
||||
[memberChannels],
|
||||
);
|
||||
const activeChannel = React.useMemo(
|
||||
() =>
|
||||
selectedChannelId
|
||||
@@ -379,7 +383,7 @@ export function AppShell() {
|
||||
</Button>
|
||||
</div>
|
||||
<AppSidebar
|
||||
channels={memberChannels}
|
||||
channels={sidebarChannels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
errorMessage={
|
||||
channelsQuery.error instanceof Error
|
||||
|
||||
@@ -86,6 +86,25 @@ async function invalidateChannelState(
|
||||
]);
|
||||
}
|
||||
|
||||
function setChannelArchivedState(
|
||||
queryClient: ReturnType<typeof useQueryClient>,
|
||||
channelId: string,
|
||||
archivedAt: string | null,
|
||||
) {
|
||||
queryClient.setQueryData<Channel[]>(channelsQueryKey, (current = []) =>
|
||||
sortChannels(
|
||||
current.map((channel) =>
|
||||
channel.id === channelId ? { ...channel, archivedAt } : channel,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
queryClient.setQueryData<ChannelDetail | undefined>(
|
||||
channelDetailQueryKey(channelId),
|
||||
(current) => (current ? { ...current, archivedAt } : current),
|
||||
);
|
||||
}
|
||||
|
||||
export function useChannelsQuery() {
|
||||
return useQuery({
|
||||
queryKey: channelsQueryKey,
|
||||
@@ -273,6 +292,13 @@ export function useArchiveChannelMutation(channelId: string | null) {
|
||||
|
||||
await archiveChannel(channelId);
|
||||
},
|
||||
onSuccess: () => {
|
||||
if (!channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setChannelArchivedState(queryClient, channelId, new Date().toISOString());
|
||||
},
|
||||
onSettled: async () => {
|
||||
await invalidateChannelState(queryClient, channelId);
|
||||
},
|
||||
@@ -290,6 +316,13 @@ export function useUnarchiveChannelMutation(channelId: string | null) {
|
||||
|
||||
await unarchiveChannel(channelId);
|
||||
},
|
||||
onSuccess: () => {
|
||||
if (!channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setChannelArchivedState(queryClient, channelId, null);
|
||||
},
|
||||
onSettled: async () => {
|
||||
await invalidateChannelState(queryClient, channelId);
|
||||
},
|
||||
|
||||
@@ -108,8 +108,9 @@ export function ChannelBrowserDialog({
|
||||
const filtered = channels.filter(
|
||||
(channel) =>
|
||||
channel.channelType !== "dm" &&
|
||||
channel.visibility === "open" &&
|
||||
!channel.archivedAt &&
|
||||
(channel.archivedAt
|
||||
? channel.isMember
|
||||
: channel.visibility === "open") &&
|
||||
(channelTypeFilter ? channel.channelType === channelTypeFilter : true),
|
||||
);
|
||||
|
||||
@@ -133,6 +134,10 @@ export function ChannelBrowserDialog({
|
||||
() => browsableChannels.filter((channel) => channel.isMember),
|
||||
[browsableChannels],
|
||||
);
|
||||
const hasArchivedJoinedChannels = React.useMemo(
|
||||
() => joined.some((channel) => channel.archivedAt !== null),
|
||||
[joined],
|
||||
);
|
||||
|
||||
// Flat list for keyboard navigation: not-joined first, then joined
|
||||
const allItems = React.useMemo(
|
||||
@@ -324,7 +329,9 @@ export function ChannelBrowserDialog({
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/80 bg-card/50 px-6 py-3 text-xs text-muted-foreground">
|
||||
Showing open {entityLabel}s. Private {entityLabel}s require an invite.
|
||||
{hasArchivedJoinedChannels
|
||||
? `Showing open ${entityLabel}s and your archived ${entityLabel}s. Private ${entityLabel}s require an invite.`
|
||||
: `Showing open ${entityLabel}s. Private ${entityLabel}s require an invite.`}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -375,6 +382,11 @@ function ChannelCard({
|
||||
<p className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.16em] text-muted-foreground">
|
||||
{channel.channelType}
|
||||
</p>
|
||||
{channel.archivedAt ? (
|
||||
<p className="rounded-full bg-amber-500/15 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.16em] text-amber-700 dark:text-amber-300">
|
||||
archived
|
||||
</p>
|
||||
) : null}
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Users className="h-3 w-3" />
|
||||
|
||||
@@ -571,15 +571,26 @@ test("manage channel can archive and unarchive a stream", async ({ page }) => {
|
||||
await expect(page.getByTestId("channel-management-unarchive")).toBeVisible();
|
||||
|
||||
await closeChannelManagement(page);
|
||||
await expect(page.getByTestId("stream-list")).not.toContainText("general");
|
||||
await expect(page.getByTestId("message-input")).toBeDisabled();
|
||||
await expect(page.getByTestId("send-message")).toBeDisabled();
|
||||
|
||||
await page.getByTestId("browse-channels").click();
|
||||
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
|
||||
await expect(page.getByTestId("browse-channel-general")).toContainText(
|
||||
"archived",
|
||||
);
|
||||
await page.getByTestId("browse-channel-general").click();
|
||||
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
await page.getByTestId("channel-management-trigger").click();
|
||||
await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
|
||||
await page.getByTestId("channel-management-unarchive").click();
|
||||
await expect(page.getByTestId("channel-management-archive")).toBeVisible();
|
||||
|
||||
await closeChannelManagement(page);
|
||||
await expect(page.getByTestId("stream-list")).toContainText("general");
|
||||
await expect(page.getByTestId("message-input")).toBeEnabled();
|
||||
});
|
||||
|
||||
|
||||
@@ -542,12 +542,20 @@ test("manage sheet archive and unarchive survives a reload through the relay", a
|
||||
await expect(page.getByTestId("channel-management-unarchive")).toBeVisible();
|
||||
await closeChannelManagement(page);
|
||||
|
||||
await expect(page.getByTestId("stream-list")).not.toContainText(channelName);
|
||||
await expect(page.getByTestId("message-input")).toBeDisabled();
|
||||
await expect(page.getByTestId("send-message")).toBeDisabled();
|
||||
|
||||
await page.reload();
|
||||
|
||||
await page.getByTestId(`channel-${channelName}`).click();
|
||||
await expect(page.getByTestId("stream-list")).not.toContainText(channelName);
|
||||
await page.getByTestId("browse-channels").click();
|
||||
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
|
||||
await expect(page.getByTestId(`browse-channel-${channelName}`)).toContainText(
|
||||
"archived",
|
||||
);
|
||||
await page.getByTestId(`browse-channel-${channelName}`).click();
|
||||
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
||||
await expect(page.getByTestId("message-input")).toBeDisabled();
|
||||
|
||||
@@ -556,5 +564,6 @@ test("manage sheet archive and unarchive survives a reload through the relay", a
|
||||
await expect(page.getByTestId("channel-management-archive")).toBeVisible();
|
||||
await closeChannelManagement(page);
|
||||
|
||||
await expect(page.getByTestId("stream-list")).toContainText(channelName);
|
||||
await expect(page.getByTestId("message-input")).toBeEnabled();
|
||||
});
|
||||
|
||||
@@ -3,6 +3,15 @@ import { expect, test, type Browser, type Page } from "@playwright/test";
|
||||
import { installRelayBridge } from "../helpers/bridge";
|
||||
import { assertRelaySeeded } from "../helpers/seed";
|
||||
|
||||
const isCi = Boolean(process.env.CI);
|
||||
const relayDeliveryTimeoutMs = isCi ? 15_000 : 5_000;
|
||||
|
||||
async function expectTimelineToContain(page: Page, text: string) {
|
||||
await expect(page.getByTestId("message-timeline")).toContainText(text, {
|
||||
timeout: relayDeliveryTimeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
async function getTimelineMetrics(page: Page) {
|
||||
return page.getByTestId("message-timeline").evaluate((element) => {
|
||||
const timeline = element as HTMLDivElement;
|
||||
@@ -36,9 +45,7 @@ async function ensureTimelineScrollable(
|
||||
await expect(input).toBeEnabled();
|
||||
await input.fill(message);
|
||||
await sendButton.click();
|
||||
await expect(receiverPage.getByTestId("message-timeline")).toContainText(
|
||||
message,
|
||||
);
|
||||
await expectTimelineToContain(receiverPage, message);
|
||||
}
|
||||
|
||||
const metrics = await getTimelineMetrics(receiverPage);
|
||||
@@ -143,7 +150,7 @@ test("sends a message through the real relay", async ({ page }) => {
|
||||
await page.getByTestId("message-input").fill(message);
|
||||
await page.getByTestId("send-message").click();
|
||||
|
||||
await expect(page.getByTestId("message-timeline")).toContainText(message);
|
||||
await expectTimelineToContain(page, message);
|
||||
});
|
||||
|
||||
test("delivers a message to a second browser context in real time", async ({
|
||||
@@ -169,9 +176,7 @@ test("delivers a message to a second browser context in real time", async ({
|
||||
await pageOne.getByTestId("message-input").fill(message);
|
||||
await pageOne.getByTestId("send-message").click();
|
||||
|
||||
await expect(pageTwo.getByTestId("message-timeline")).toContainText(
|
||||
message,
|
||||
);
|
||||
await expectTimelineToContain(pageTwo, message);
|
||||
} finally {
|
||||
await contextOne.close();
|
||||
await contextTwo.close();
|
||||
@@ -183,6 +188,8 @@ test("stays pinned to the latest message when new messages arrive at the bottom"
|
||||
}: {
|
||||
browser: Browser;
|
||||
}) => {
|
||||
test.slow();
|
||||
|
||||
const channelName = `pinned-bottom-${Date.now()}`;
|
||||
const contextOne = await browser.newContext();
|
||||
const contextTwo = await browser.newContext();
|
||||
@@ -207,9 +214,7 @@ test("stays pinned to the latest message when new messages arrive at the bottom"
|
||||
await pageOne.getByTestId("message-input").fill(incomingMessage);
|
||||
await pageOne.getByTestId("send-message").click();
|
||||
|
||||
await expect(pageTwo.getByTestId("message-timeline")).toContainText(
|
||||
incomingMessage,
|
||||
);
|
||||
await expectTimelineToContain(pageTwo, incomingMessage);
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeLessThan(8);
|
||||
@@ -227,6 +232,8 @@ test("stays pinned after you send a message and a remote reply arrives right aft
|
||||
}: {
|
||||
browser: Browser;
|
||||
}) => {
|
||||
test.slow();
|
||||
|
||||
const channelName = `reply-shared-${Date.now()}`;
|
||||
const contextOne = await browser.newContext();
|
||||
const contextTwo = await browser.newContext();
|
||||
@@ -251,16 +258,12 @@ test("stays pinned after you send a message and a remote reply arrives right aft
|
||||
|
||||
await pageTwo.getByTestId("message-input").fill(localMessage);
|
||||
await pageTwo.getByTestId("send-message").click();
|
||||
await expect(pageTwo.getByTestId("message-timeline")).toContainText(
|
||||
localMessage,
|
||||
);
|
||||
await expectTimelineToContain(pageTwo, localMessage);
|
||||
|
||||
await pageOne.getByTestId("message-input").fill(incomingMessage);
|
||||
await pageOne.getByTestId("send-message").click();
|
||||
|
||||
await expect(pageTwo.getByTestId("message-timeline")).toContainText(
|
||||
incomingMessage,
|
||||
);
|
||||
await expectTimelineToContain(pageTwo, incomingMessage);
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeLessThan(8);
|
||||
@@ -278,6 +281,8 @@ test("keeps bottom-pinned scrolling after the composer grows", async ({
|
||||
}: {
|
||||
browser: Browser;
|
||||
}) => {
|
||||
test.slow();
|
||||
|
||||
const channelName = `composer-shared-${Date.now()}`;
|
||||
const contextOne = await browser.newContext();
|
||||
const contextTwo = await browser.newContext();
|
||||
@@ -315,9 +320,7 @@ test("keeps bottom-pinned scrolling after the composer grows", async ({
|
||||
await pageOne.getByTestId("message-input").fill(incomingMessage);
|
||||
await pageOne.getByTestId("send-message").click();
|
||||
|
||||
await expect(pageTwo.getByTestId("message-timeline")).toContainText(
|
||||
incomingMessage,
|
||||
);
|
||||
await expectTimelineToContain(pageTwo, incomingMessage);
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeLessThan(8);
|
||||
@@ -335,6 +338,8 @@ test("keeps scroll position when new messages arrive above the fold", async ({
|
||||
}: {
|
||||
browser: Browser;
|
||||
}) => {
|
||||
test.slow();
|
||||
|
||||
const channelName = `scroll-shared-${Date.now()}`;
|
||||
const contextOne = await browser.newContext();
|
||||
const contextTwo = await browser.newContext();
|
||||
@@ -370,9 +375,7 @@ test("keeps scroll position when new messages arrive above the fold", async ({
|
||||
|
||||
await pageTwo.getByTestId("message-scroll-to-latest").click();
|
||||
|
||||
await expect(pageTwo.getByTestId("message-timeline")).toContainText(
|
||||
incomingMessage,
|
||||
);
|
||||
await expectTimelineToContain(pageTwo, incomingMessage);
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeLessThan(8);
|
||||
|
||||
@@ -2,14 +2,15 @@ import { request } from "@playwright/test";
|
||||
|
||||
const tylerPubkey =
|
||||
"e5ebc6cdb579be112e336cc319b5989b4bb6af11786ea90dbe52b5f08d741b34";
|
||||
const isCi = Boolean(process.env.CI);
|
||||
const relayBaseUrl =
|
||||
process.env.SPROUT_E2E_RELAY_URL ?? "http://localhost:3000";
|
||||
process.env.SPROUT_E2E_RELAY_URL ?? "http://127.0.0.1:3000";
|
||||
const seedTimeoutMs = Number.parseInt(
|
||||
process.env.SPROUT_E2E_SEED_TIMEOUT_MS ?? "25000",
|
||||
process.env.SPROUT_E2E_SEED_TIMEOUT_MS ?? (isCi ? "60000" : "25000"),
|
||||
10,
|
||||
);
|
||||
const requestTimeoutMs = Number.parseInt(
|
||||
process.env.SPROUT_E2E_SEED_REQUEST_TIMEOUT_MS ?? "2000",
|
||||
process.env.SPROUT_E2E_SEED_REQUEST_TIMEOUT_MS ?? (isCi ? "5000" : "2000"),
|
||||
10,
|
||||
);
|
||||
const retryDelayMs = Number.parseInt(
|
||||
|
||||
Reference in New Issue
Block a user