mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): add channel archive and delete actions (#2067)
Signed-off-by: Pavlenex <36959754+pavlenex@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import { Archive, ArchiveRestore, Trash2 } from "lucide-react";
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
|
||||
import { ownsAuthorAgent } from "@/features/profile/lib/identity";
|
||||
import { useUsersBatchQuery } from "@/features/profile/hooks";
|
||||
import type { ChannelMember } from "@/shared/api/types";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -34,6 +39,117 @@ type ChannelManagementModerationActionsProps = {
|
||||
unarchiveChannelMutation: ChannelMutation;
|
||||
};
|
||||
|
||||
type ChannelDeleteConfirmationDialogProps = {
|
||||
channelName: string;
|
||||
error: unknown;
|
||||
isPending: boolean;
|
||||
onConfirm: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
open: boolean;
|
||||
trigger?: ReactNode;
|
||||
};
|
||||
|
||||
export function useChannelModerationCapabilities(
|
||||
members: ChannelMember[] | undefined,
|
||||
currentPubkey: string | undefined,
|
||||
enabled: boolean,
|
||||
) {
|
||||
const normalizedCurrentPubkey = currentPubkey
|
||||
? normalizePubkey(currentPubkey)
|
||||
: undefined;
|
||||
const selfRole = members?.find(
|
||||
(member) => normalizePubkey(member.pubkey) === normalizedCurrentPubkey,
|
||||
)?.role;
|
||||
const ownerMemberPubkeys = useMemo(
|
||||
() =>
|
||||
(members ?? [])
|
||||
.filter(
|
||||
(member) =>
|
||||
member.role === "owner" &&
|
||||
normalizePubkey(member.pubkey) !== normalizedCurrentPubkey,
|
||||
)
|
||||
.map((member) => member.pubkey),
|
||||
[members, normalizedCurrentPubkey],
|
||||
);
|
||||
const shouldResolveAgentOwnership =
|
||||
enabled && selfRole !== "owner" && ownerMemberPubkeys.length > 0;
|
||||
const ownerProfilesQuery = useUsersBatchQuery(ownerMemberPubkeys, {
|
||||
enabled: shouldResolveAgentOwnership,
|
||||
});
|
||||
const canManageOwnedAgentChannel = ownerMemberPubkeys.some((pubkey) =>
|
||||
ownsAuthorAgent(
|
||||
ownerProfilesQuery.data?.profiles[normalizePubkey(pubkey)],
|
||||
currentPubkey,
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
canDeleteChannel: selfRole === "owner" || canManageOwnedAgentChannel,
|
||||
canManageChannel:
|
||||
selfRole === "owner" ||
|
||||
selfRole === "admin" ||
|
||||
canManageOwnedAgentChannel,
|
||||
error: shouldResolveAgentOwnership ? ownerProfilesQuery.error : null,
|
||||
isLoading: shouldResolveAgentOwnership && ownerProfilesQuery.isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
export function ChannelDeleteConfirmationDialog({
|
||||
channelName,
|
||||
error,
|
||||
isPending,
|
||||
onConfirm,
|
||||
onOpenChange,
|
||||
open,
|
||||
trigger,
|
||||
}: ChannelDeleteConfirmationDialogProps) {
|
||||
return (
|
||||
<AlertDialog onOpenChange={onOpenChange} open={open}>
|
||||
{trigger ? (
|
||||
<AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>
|
||||
) : null}
|
||||
<AlertDialogContent data-testid="channel-delete-confirmation-dialog">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete channel?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Delete {channelName} from the community list. This action cannot be
|
||||
undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
{error instanceof Error ? (
|
||||
<p className="text-sm text-destructive">{error.message}</p>
|
||||
) : null}
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel asChild>
|
||||
<Button
|
||||
data-testid="channel-delete-cancel"
|
||||
disabled={isPending}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button
|
||||
data-testid="channel-delete-confirm"
|
||||
disabled={isPending}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
onConfirm();
|
||||
}}
|
||||
type="button"
|
||||
variant="destructive"
|
||||
>
|
||||
{isPending ? "Deleting..." : "Delete channel"}
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChannelManagementModerationActions({
|
||||
archiveChannelMutation,
|
||||
canManageChannel,
|
||||
@@ -105,11 +221,16 @@ export function ChannelManagementModerationActions({
|
||||
</Button>
|
||||
)}
|
||||
{canDeleteChannel ? (
|
||||
<AlertDialog
|
||||
<ChannelDeleteConfirmationDialog
|
||||
channelName={resolvedChannelName}
|
||||
error={deleteChannelMutation.error}
|
||||
isPending={deleteChannelMutation.isPending}
|
||||
onConfirm={() => {
|
||||
void handleDeleteChannel();
|
||||
}}
|
||||
onOpenChange={handleDeleteDialogOpenChange}
|
||||
open={isDeleteDialogOpen}
|
||||
>
|
||||
<AlertDialogTrigger asChild>
|
||||
trigger={
|
||||
<Button
|
||||
aria-label="Delete channel"
|
||||
data-testid="channel-management-delete"
|
||||
@@ -121,50 +242,8 @@ export function ChannelManagementModerationActions({
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent data-testid="channel-delete-confirmation-dialog">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete channel?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Delete {resolvedChannelName} from the community list. This
|
||||
action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
{deleteChannelMutation.error instanceof Error ? (
|
||||
<p className="text-sm text-destructive">
|
||||
{deleteChannelMutation.error.message}
|
||||
</p>
|
||||
) : null}
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel asChild>
|
||||
<Button
|
||||
data-testid="channel-delete-cancel"
|
||||
disabled={deleteChannelMutation.isPending}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button
|
||||
data-testid="channel-delete-confirm"
|
||||
disabled={deleteChannelMutation.isPending}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
void handleDeleteChannel();
|
||||
}}
|
||||
type="button"
|
||||
variant="destructive"
|
||||
>
|
||||
{deleteChannelMutation.isPending
|
||||
? "Deleting..."
|
||||
: "Delete channel"}
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -38,11 +38,8 @@ import {
|
||||
formatTtlDuration,
|
||||
parseTtlDuration,
|
||||
} from "@/features/channels/lib/ephemeralChannel";
|
||||
import { ownsAuthorAgent } from "@/features/profile/lib/identity";
|
||||
import { useUsersBatchQuery } from "@/features/profile/hooks";
|
||||
import type { Channel } from "@/shared/api/types";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
import { useTheme } from "@/shared/theme/ThemeProvider";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -83,7 +80,10 @@ import {
|
||||
NarrativeGroup,
|
||||
ToggleRow,
|
||||
} from "./ChannelManagementSheetRows";
|
||||
import { ChannelManagementModerationActions } from "./ChannelManagementModerationActions";
|
||||
import {
|
||||
ChannelManagementModerationActions,
|
||||
useChannelModerationCapabilities,
|
||||
} from "./ChannelManagementModerationActions";
|
||||
import { writeTextToClipboard } from "@/shared/lib/clipboard";
|
||||
|
||||
type ChannelManagementSheetProps = {
|
||||
@@ -137,37 +137,8 @@ export function ChannelManagementSheet({
|
||||
members.find((member) => member.pubkey === currentPubkey) ?? null;
|
||||
const hasResolvedMembership = membersQuery.data !== undefined;
|
||||
|
||||
// Collect owner-role member pubkeys to look up their NIP-OA ownerPubkey.
|
||||
// This is what surfaces the "you own the agent that owns this channel" path.
|
||||
const ownerMemberPubkeys = React.useMemo(
|
||||
() =>
|
||||
members
|
||||
.filter((m) => m.role === "owner" && m.pubkey !== currentPubkey)
|
||||
.map((m) => m.pubkey),
|
||||
[members, currentPubkey],
|
||||
);
|
||||
const ownerProfilesQuery = useUsersBatchQuery(ownerMemberPubkeys, {
|
||||
enabled: open && ownerMemberPubkeys.length > 0,
|
||||
});
|
||||
// True when an owner-role member of this channel is an agent owned by the
|
||||
// current user — mirrors the relay's is_agent_owner gate.
|
||||
const canManageOwnedAgentChannel = React.useMemo(() => {
|
||||
if (!currentPubkey || !ownerProfilesQuery.data) return false;
|
||||
return ownerMemberPubkeys.some((pubkey) =>
|
||||
ownsAuthorAgent(
|
||||
ownerProfilesQuery.data?.profiles[normalizePubkey(pubkey)],
|
||||
currentPubkey,
|
||||
),
|
||||
);
|
||||
}, [currentPubkey, ownerMemberPubkeys, ownerProfilesQuery.data]);
|
||||
|
||||
const isSelfOwner = selfMember?.role === "owner";
|
||||
// Capability: may delete this channel (self-owner OR owns the agent-owner).
|
||||
const canDeleteChannel = isSelfOwner || canManageOwnedAgentChannel;
|
||||
const canManageChannel =
|
||||
selfMember?.role === "owner" ||
|
||||
selfMember?.role === "admin" ||
|
||||
canManageOwnedAgentChannel;
|
||||
const { canDeleteChannel, canManageChannel } =
|
||||
useChannelModerationCapabilities(membersQuery.data, currentPubkey, open);
|
||||
const canEditNarrative =
|
||||
canManageChannel && selfMember !== null && detail?.channelType !== "dm";
|
||||
const isArchived =
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
CreateSectionDialog,
|
||||
DeleteSectionAlertDialog,
|
||||
RenameSectionDialog,
|
||||
useDeleteChannelDialog,
|
||||
useLeaveChannelDialog,
|
||||
type SectionDialogValue,
|
||||
} from "@/features/sidebar/ui/ChannelSectionDialogs";
|
||||
@@ -373,6 +374,10 @@ export function AppSidebar({
|
||||
React.useState<ChannelSection | null>(null);
|
||||
const { requestLeaveChannel, dialog: leaveChannelDialog } =
|
||||
useLeaveChannelDialog();
|
||||
const { requestDeleteChannel, dialog: deleteChannelDialog } =
|
||||
useDeleteChannelDialog((channel) => {
|
||||
if (channel.id === selectedChannelId) onSelectHome();
|
||||
});
|
||||
|
||||
const streamChannels = React.useMemo(
|
||||
() => channels.filter((channel) => channel.channelType === "stream"),
|
||||
@@ -636,6 +641,7 @@ export function AppSidebar({
|
||||
starredChannelIds={starredChannelIds}
|
||||
onStarChannel={onStarChannel}
|
||||
onUnstarChannel={onUnstarChannel}
|
||||
onDeleteChannel={requestDeleteChannel}
|
||||
onLeaveChannel={requestLeaveChannel}
|
||||
/>
|
||||
) : null}
|
||||
@@ -705,6 +711,7 @@ export function AppSidebar({
|
||||
starredChannelIds={starredChannelIds}
|
||||
onStarChannel={onStarChannel}
|
||||
onUnstarChannel={onUnstarChannel}
|
||||
onDeleteChannel={requestDeleteChannel}
|
||||
onLeaveChannel={requestLeaveChannel}
|
||||
/>
|
||||
))}
|
||||
@@ -744,6 +751,7 @@ export function AppSidebar({
|
||||
starredChannelIds={starredChannelIds}
|
||||
onStarChannel={onStarChannel}
|
||||
onUnstarChannel={onUnstarChannel}
|
||||
onDeleteChannel={requestDeleteChannel}
|
||||
onLeaveChannel={requestLeaveChannel}
|
||||
/>
|
||||
</SidebarDndContext>
|
||||
@@ -774,6 +782,7 @@ export function AppSidebar({
|
||||
mutedChannelIds={mutedChannelIds}
|
||||
onMuteChannel={onMuteChannel}
|
||||
onUnmuteChannel={onUnmuteChannel}
|
||||
onDeleteChannel={requestDeleteChannel}
|
||||
/>
|
||||
</FeatureGate>
|
||||
<SidebarSection
|
||||
@@ -961,6 +970,7 @@ export function AppSidebar({
|
||||
setDeleteSectionTarget(null);
|
||||
}}
|
||||
/>
|
||||
{deleteChannelDialog}
|
||||
{leaveChannelDialog}
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Archive,
|
||||
Bell,
|
||||
BellOff,
|
||||
Check,
|
||||
@@ -6,11 +7,19 @@ import {
|
||||
CircleDot,
|
||||
Copy,
|
||||
LogOut,
|
||||
LoaderCircle,
|
||||
Plus,
|
||||
Star,
|
||||
StarOff,
|
||||
Trash2,
|
||||
TriangleAlert,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
useArchiveChannelMutation,
|
||||
useChannelMembersQuery,
|
||||
} from "@/features/channels/hooks";
|
||||
import { useChannelModerationCapabilities } from "@/features/channels/ui/ChannelManagementModerationActions";
|
||||
import type { ChannelSection } from "@/features/sidebar/lib/useChannelSections";
|
||||
import {
|
||||
ContextMenuIconSlot,
|
||||
@@ -18,6 +27,7 @@ import {
|
||||
} from "@/features/sidebar/ui/sidebarMenuHelpers";
|
||||
import { StatusEmoji } from "@/features/user-status/ui/StatusEmoji";
|
||||
import type { Channel } from "@/shared/api/types";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import { copyTextToClipboard } from "@/shared/lib/clipboard";
|
||||
import {
|
||||
ContextMenuItem,
|
||||
@@ -144,6 +154,7 @@ export function ChannelContextMenuItems({
|
||||
onAssignChannel,
|
||||
onUnassignChannel,
|
||||
onCreateSectionForChannel,
|
||||
onDeleteChannel,
|
||||
onLeaveChannel,
|
||||
}: {
|
||||
channel: Channel;
|
||||
@@ -164,8 +175,34 @@ export function ChannelContextMenuItems({
|
||||
onAssignChannel?: (channelId: string, sectionId: string) => void;
|
||||
onUnassignChannel?: (channelId: string) => void;
|
||||
onCreateSectionForChannel?: (channelId: string) => void;
|
||||
onDeleteChannel?: (channel: Channel) => void;
|
||||
onLeaveChannel?: (channel: Channel) => void;
|
||||
}) {
|
||||
const canLoadOwnerActions =
|
||||
channel.channelType !== "dm" && Boolean(onDeleteChannel);
|
||||
const membersQuery = useChannelMembersQuery(channel.id, canLoadOwnerActions);
|
||||
const currentPubkey = useIdentityQuery().data?.pubkey;
|
||||
const archiveChannel = useArchiveChannelMutation(channel.id);
|
||||
const {
|
||||
canDeleteChannel,
|
||||
canManageChannel,
|
||||
error: capabilityError,
|
||||
isLoading: isCapabilityLoading,
|
||||
} = useChannelModerationCapabilities(
|
||||
membersQuery.data,
|
||||
currentPubkey,
|
||||
canLoadOwnerActions,
|
||||
);
|
||||
const ownerActionsError = membersQuery.error ?? capabilityError;
|
||||
const ownerActionsLoading =
|
||||
canLoadOwnerActions && (membersQuery.isLoading || isCapabilityLoading);
|
||||
const showChannelActions = Boolean(
|
||||
onLeaveChannel ||
|
||||
ownerActionsLoading ||
|
||||
ownerActionsError ||
|
||||
canManageChannel ||
|
||||
canDeleteChannel,
|
||||
);
|
||||
const showStar = Boolean(onStarChannel && onUnstarChannel);
|
||||
const showReadToggle = hasUnread
|
||||
? Boolean(onMarkChannelRead)
|
||||
@@ -265,19 +302,56 @@ export function ChannelContextMenuItems({
|
||||
</ContextMenuItem>
|
||||
)
|
||||
) : null}
|
||||
{showChannelActions ? <ContextMenuSeparator /> : null}
|
||||
{onLeaveChannel ? (
|
||||
<>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onSelect={() => deferMenuAction(() => onLeaveChannel(channel))}
|
||||
>
|
||||
<ContextMenuIconSlot>
|
||||
<LogOut className="h-4 w-4" />
|
||||
</ContextMenuIconSlot>
|
||||
<span>Leave channel</span>
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
<ContextMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onSelect={() => deferMenuAction(() => onLeaveChannel(channel))}
|
||||
>
|
||||
<ContextMenuIconSlot>
|
||||
<LogOut className="h-4 w-4" />
|
||||
</ContextMenuIconSlot>
|
||||
<span>Leave channel</span>
|
||||
</ContextMenuItem>
|
||||
) : null}
|
||||
{ownerActionsLoading ? (
|
||||
<ContextMenuItem disabled>
|
||||
<ContextMenuIconSlot>
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
</ContextMenuIconSlot>
|
||||
<span>Loading channel actions...</span>
|
||||
</ContextMenuItem>
|
||||
) : ownerActionsError ? (
|
||||
<ContextMenuItem disabled>
|
||||
<ContextMenuIconSlot>
|
||||
<TriangleAlert className="h-4 w-4" />
|
||||
</ContextMenuIconSlot>
|
||||
<span>Channel actions unavailable</span>
|
||||
</ContextMenuItem>
|
||||
) : null}
|
||||
{canManageChannel ? (
|
||||
<ContextMenuItem
|
||||
data-testid={`archive-channel-${channel.name}`}
|
||||
disabled={archiveChannel.isPending}
|
||||
onSelect={() => deferMenuAction(() => archiveChannel.mutate())}
|
||||
>
|
||||
<ContextMenuIconSlot>
|
||||
<Archive className="h-4 w-4" />
|
||||
</ContextMenuIconSlot>
|
||||
<span>Archive channel</span>
|
||||
</ContextMenuItem>
|
||||
) : null}
|
||||
{canDeleteChannel ? (
|
||||
<ContextMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
data-testid={`delete-channel-${channel.name}`}
|
||||
onSelect={() => deferMenuAction(() => onDeleteChannel?.(channel))}
|
||||
>
|
||||
<ContextMenuIconSlot>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</ContextMenuIconSlot>
|
||||
<span>Delete channel</span>
|
||||
</ContextMenuItem>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -26,7 +26,11 @@ import { StatusEmoji } from "@/features/user-status/ui/StatusEmoji";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import type { Channel } from "@/shared/api/types";
|
||||
import { useLeaveChannelMutation } from "@/features/channels/hooks";
|
||||
import {
|
||||
useDeleteChannelMutation,
|
||||
useLeaveChannelMutation,
|
||||
} from "@/features/channels/hooks";
|
||||
import { ChannelDeleteConfirmationDialog } from "@/features/channels/ui/ChannelManagementModerationActions";
|
||||
|
||||
export type SectionDialogValue = {
|
||||
name: string;
|
||||
@@ -335,3 +339,34 @@ export function useLeaveChannelDialog() {
|
||||
|
||||
return { requestLeaveChannel: setTarget, dialog };
|
||||
}
|
||||
|
||||
export function useDeleteChannelDialog(onDeleted: (channel: Channel) => void) {
|
||||
const [target, setTarget] = React.useState<Channel | null>(null);
|
||||
const deleteChannel = useDeleteChannelMutation(target?.id ?? null);
|
||||
|
||||
const dialog = (
|
||||
<ChannelDeleteConfirmationDialog
|
||||
channelName={target?.name ?? ""}
|
||||
error={deleteChannel.error}
|
||||
isPending={deleteChannel.isPending}
|
||||
onConfirm={() => {
|
||||
const deletedChannel = target;
|
||||
deleteChannel.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
setTarget(null);
|
||||
if (deletedChannel) onDeleted(deletedChannel);
|
||||
},
|
||||
});
|
||||
}}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
deleteChannel.reset();
|
||||
setTarget(null);
|
||||
}
|
||||
}}
|
||||
open={target !== null}
|
||||
/>
|
||||
);
|
||||
|
||||
return { requestDeleteChannel: setTarget, dialog };
|
||||
}
|
||||
|
||||
@@ -371,6 +371,7 @@ export function ChannelGroupSection({
|
||||
starredChannelIds,
|
||||
onStarChannel,
|
||||
onUnstarChannel,
|
||||
onDeleteChannel,
|
||||
onLeaveChannel,
|
||||
}: {
|
||||
browseLabel?: string;
|
||||
@@ -420,6 +421,7 @@ export function ChannelGroupSection({
|
||||
starredChannelIds?: ReadonlySet<string>;
|
||||
onStarChannel?: (channelId: string) => void;
|
||||
onUnstarChannel?: (channelId: string) => void;
|
||||
onDeleteChannel?: (channel: Channel) => void;
|
||||
onLeaveChannel?: (channel: Channel) => void;
|
||||
}) {
|
||||
const contentId = `sidebar-${listTestId}`;
|
||||
@@ -478,6 +480,7 @@ export function ChannelGroupSection({
|
||||
onAssignChannel={onAssignChannel}
|
||||
onUnassignChannel={onUnassignChannel}
|
||||
onCreateSectionForChannel={onCreateSectionForChannel}
|
||||
onDeleteChannel={onDeleteChannel}
|
||||
onLeaveChannel={onLeaveChannel}
|
||||
/>
|
||||
</ContextMenuContent>
|
||||
@@ -572,6 +575,7 @@ export function CustomChannelSection({
|
||||
starredChannelIds,
|
||||
onStarChannel,
|
||||
onUnstarChannel,
|
||||
onDeleteChannel,
|
||||
onLeaveChannel,
|
||||
}: {
|
||||
section: ChannelSection;
|
||||
@@ -611,6 +615,7 @@ export function CustomChannelSection({
|
||||
starredChannelIds?: ReadonlySet<string>;
|
||||
onStarChannel?: (channelId: string) => void;
|
||||
onUnstarChannel?: (channelId: string) => void;
|
||||
onDeleteChannel?: (channel: Channel) => void;
|
||||
onLeaveChannel?: (channel: Channel) => void;
|
||||
}) {
|
||||
const contentId = `sidebar-section-${section.id}`;
|
||||
@@ -770,6 +775,7 @@ export function CustomChannelSection({
|
||||
onCreateSectionForChannel={
|
||||
onCreateSectionForChannel
|
||||
}
|
||||
onDeleteChannel={onDeleteChannel}
|
||||
onLeaveChannel={onLeaveChannel}
|
||||
/>
|
||||
</ContextMenuContent>
|
||||
|
||||
@@ -134,6 +134,87 @@ test("leaving a channel from the context menu never freezes the app", async ({
|
||||
await expectAppClickable(page);
|
||||
});
|
||||
|
||||
test("channel context menu only shows owner actions to the owner", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("channel-general").click({ button: "right" });
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Archive channel" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Delete channel" }),
|
||||
).toBeVisible();
|
||||
const lifecycleOrder = (
|
||||
await page.getByRole("menuitem").allTextContents()
|
||||
).filter((label) =>
|
||||
["Leave channel", "Archive channel", "Delete channel"].includes(label),
|
||||
);
|
||||
expect(lifecycleOrder).toEqual([
|
||||
"Leave channel",
|
||||
"Archive channel",
|
||||
"Delete channel",
|
||||
]);
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await page.getByTestId("channel-random").click({ button: "right" });
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Leave channel" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Loading channel actions..." }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Archive channel" }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Delete channel" }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("channel context menu explains when owner actions are loading", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, { channelMembersReadDelayMs: 500 });
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("channel-general").click({ button: "right" });
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Loading channel actions..." }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Archive channel" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Loading channel actions..." }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("channel owner can archive from the context menu", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("channel-general").click({ button: "right" });
|
||||
await page.getByRole("menuitem", { name: "Archive channel" }).click();
|
||||
|
||||
await expect(page.getByTestId("stream-list")).not.toContainText("general");
|
||||
});
|
||||
|
||||
test("channel owner can delete from the context menu", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
|
||||
await page.getByTestId("channel-general").click({ button: "right" });
|
||||
await page.getByRole("menuitem", { name: "Delete channel" }).click();
|
||||
await expect(
|
||||
page.getByTestId("channel-delete-confirmation-dialog"),
|
||||
).toBeVisible();
|
||||
await page.getByTestId("channel-delete-confirm").click();
|
||||
|
||||
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
|
||||
await expect(page.getByTestId("stream-list")).not.toContainText("general");
|
||||
});
|
||||
|
||||
test("fades the pinned sidebar chrome edges outside the Buzz theme", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user