Skip to content

Commit fabd134

Browse files
feat: Multiple select hide/unhide and delete (#620)
1 parent a49fdc8 commit fabd134

File tree

12 files changed

+996
-330
lines changed

12 files changed

+996
-330
lines changed

apps/platform/trpc/routers/convoRouter/convoRouter.ts

Lines changed: 174 additions & 127 deletions
Large diffs are not rendered by default.

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@radix-ui/react-alert-dialog": "^1.1.1",
1919
"@radix-ui/react-avatar": "^1.1.0",
2020
"@radix-ui/react-checkbox": "^1.1.1",
21+
"@radix-ui/react-context-menu": "^2.2.1",
2122
"@radix-ui/react-dialog": "^1.1.0",
2223
"@radix-ui/react-dropdown-menu": "^2.1.1",
2324
"@radix-ui/react-hover-card": "^1.1.1",

apps/web/src/app/[orgShortcode]/convo/[convoId]/_components/top-bar.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ import { type RouterOutputs, platform } from '@/src/lib/trpc';
3636
import { Button } from '@/src/components/shadcn-ui/button';
3737
import { type TypeId } from '@u22n/utils/typeid';
3838
import { Participants } from './participants';
39+
import { shiftKeyPressed } from '../../atoms';
3940
import { useRouter } from 'next/navigation';
4041
import { cn } from '@/src/lib/utils';
42+
import { useAtomValue } from 'jotai';
4143
import { useState } from 'react';
4244
import { toast } from 'sonner';
4345
import Link from 'next/link';
@@ -68,6 +70,7 @@ export default function TopBar({
6870
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
6971
const router = useRouter();
7072
const removeConvoFromList = useDeleteConvo$Cache();
73+
const shiftKey = useAtomValue(shiftKeyPressed);
7174

7275
const { mutateAsync: hideConvo, isPending: hidingConvo } =
7376
platform.convos.hideConvo.useMutation();
@@ -109,11 +112,12 @@ export default function TopBar({
109112
<Tooltip>
110113
<TooltipTrigger asChild>
111114
<Button
112-
variant={'outline'}
115+
variant={shiftKey ? 'destructive' : 'outline'}
113116
size={'icon-sm'}
114-
className={
115-
'hover:bg-red-5 hover:text-red-11 hover:border-red-8'
116-
}
117+
className={cn(
118+
!shiftKey &&
119+
'hover:bg-red-5 hover:text-red-11 hover:border-red-8'
120+
)}
117121
loading={deletingConvo}
118122
onClick={async (e) => {
119123
e.preventDefault();
@@ -129,7 +133,9 @@ export default function TopBar({
129133
<Trash size={16} />
130134
</Button>
131135
</TooltipTrigger>
132-
<TooltipContent>Delete Convo</TooltipContent>
136+
<TooltipContent>
137+
{shiftKey ? 'Delete Convo without confirmation' : 'Delete Convo'}
138+
</TooltipContent>
133139
</Tooltip>
134140
<Tooltip>
135141
<TooltipTrigger asChild>
@@ -243,20 +249,22 @@ function DeleteModal({
243249
<DialogContent>
244250
<DialogHeader>
245251
<DialogTitle>Delete Convo?</DialogTitle>
246-
<DialogDescription>
247-
<div>
252+
<DialogDescription className="flex flex-col">
253+
<span>
248254
This will permanently and immediately delete this conversation for
249255
all the participants.
250-
</div>
251-
<div>Are you sure you want to delete this conversation?</div>
256+
</span>
257+
<span>Are you sure you want to delete this conversation?</span>
252258
{!convoHidden && (
253-
<div className="py-2">You can also choose to hide this Convo</div>
259+
<span className="py-2">
260+
You can also choose to hide this Convo
261+
</span>
254262
)}
255-
<div className="py-3 text-xs font-semibold">
263+
<span className="py-3 text-xs font-semibold">
256264
ProTip: Hold{' '}
257265
<kbd className="bg-base-2 rounded-md border p-1">Shift</kbd> next
258266
time to skip this confirmation prompt
259-
</div>
267+
</span>
260268
</DialogDescription>
261269
</DialogHeader>
262270

Lines changed: 165 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,60 @@
11
'use client';
22

3+
import {
4+
ContextMenu,
5+
ContextMenuContent,
6+
ContextMenuGroup,
7+
ContextMenuTrigger,
8+
ContextMenuItem
9+
} from '@/src/components/shadcn-ui/context-menu';
310
import { useGlobalStore } from '@/src/providers/global-store-provider';
11+
import { formatParticipantData, useDeleteConvo$Cache } from '../utils';
12+
import { Eye, EyeSlash, Trash } from '@phosphor-icons/react/dist/ssr';
13+
import { Checkbox } from '@/src/components/shadcn-ui/checkbox';
14+
import { convoListSelecting, shiftKeyPressed } from '../atoms';
15+
import { platform, type RouterOutputs } from '@/src/lib/trpc';
416
import AvatarPlus from '@/src/components/avatar-plus';
517
import { useTimeAgo } from '@/src/hooks/use-time-ago';
6-
import { type RouterOutputs } from '@/src/lib/trpc';
18+
import { useLongPress } from '@uidotdev/usehooks';
719
import { Avatar } from '@/src/components/avatar';
8-
import { formatParticipantData } from '../utils';
920
import { usePathname } from 'next/navigation';
1021
import { cn } from '@/src/lib/utils';
22+
import { useAtomValue } from 'jotai';
1123
import { useMemo } from 'react';
24+
import { toast } from 'sonner';
1225
import Link from 'next/link';
1326

1427
export function ConvoItem({
15-
convo
28+
convo,
29+
selected,
30+
onSelect,
31+
hidden
1632
}: {
1733
convo: RouterOutputs['convos']['getOrgMemberConvos']['data'][number];
34+
selected: boolean;
35+
onSelect: (shiftKey: boolean) => void;
36+
hidden: boolean;
1837
}) {
1938
const orgShortcode = useGlobalStore((state) => state.currentOrg.shortcode);
39+
const selecting = useAtomValue(convoListSelecting);
40+
const shiftKey = useAtomValue(shiftKeyPressed);
41+
42+
const deleteConvo$ = useDeleteConvo$Cache();
43+
const { mutateAsync: deleteConvo } = platform.convos.deleteConvo.useMutation({
44+
onError: (error) => {
45+
toast.error('Failed to delete convo', {
46+
description: error.message
47+
});
48+
}
49+
});
50+
51+
const { mutateAsync: hideConvo } = platform.convos.hideConvo.useMutation({
52+
onError: (error) => {
53+
toast.error('Failed to hide convo', {
54+
description: error.message
55+
});
56+
}
57+
});
2058

2159
const timeAgo = useTimeAgo(convo.lastUpdatedAt);
2260

@@ -59,50 +97,131 @@ export function ConvoItem({
5997

6098
const isActive = currentPath === link;
6199

100+
const longPressHandlers = useLongPress(
101+
(e) => {
102+
if (selecting) return;
103+
e.preventDefault();
104+
e.stopPropagation();
105+
onSelect(false);
106+
},
107+
{
108+
threshold: 450
109+
}
110+
);
111+
62112
return (
63-
<Link
64-
href={link}
65-
className={cn(
66-
'flex h-full flex-row gap-2 overflow-visible rounded-xl border-2 px-2 py-3',
67-
isActive ? 'border-accent-8' : 'hover:border-base-6 border-transparent'
68-
)}>
69-
<AvatarPlus
70-
size="md"
71-
users={participantData}
72-
/>
73-
<div className="flex w-[90%] flex-1 flex-col">
74-
<div className="flex flex-row items-end justify-between gap-1">
75-
<span className="truncate text-sm font-medium">
76-
{participantNames.join(', ')}
77-
</span>
78-
<span className="text-base-11 min-w-fit text-right text-xs">
79-
{timeAgo}
80-
</span>
81-
</div>
82-
83-
<span className="truncate break-all text-left text-xs font-medium">
84-
{convo.subjects[0]?.subject}
85-
</span>
86-
87-
<div className="flex flex-row items-start justify-start gap-1 text-left text-sm">
88-
<div className="px-0.5">
89-
{authorAvatarData && (
90-
<Avatar
91-
avatarProfilePublicId={authorAvatarData.avatarProfilePublicId}
92-
avatarTimestamp={authorAvatarData.avatarTimestamp}
93-
name={authorAvatarData.name}
94-
size={'sm'}
95-
color={authorAvatarData.color}
96-
key={authorAvatarData.participantPublicId}
97-
/>
98-
)}
99-
</div>
113+
<ContextMenu>
114+
<ContextMenuTrigger>
115+
<Link
116+
href={link}
117+
className={cn(
118+
'flex h-full flex-row gap-2 overflow-visible rounded-xl border-2 px-2 py-3',
119+
isActive
120+
? 'border-accent-8'
121+
: 'hover:border-base-6 border-transparent',
122+
selected && 'bg-accent-3',
123+
shiftKey && !selecting && 'group'
124+
)}
125+
{...longPressHandlers}>
126+
{selecting ? (
127+
<Checkbox
128+
className="size-6 rounded-lg"
129+
checked={selected}
130+
onClick={(e) => {
131+
e.preventDefault();
132+
e.stopPropagation();
133+
onSelect(e.shiftKey);
134+
}}
135+
/>
136+
) : (
137+
<div
138+
className="contents"
139+
onClick={(e) => {
140+
if (!e.shiftKey) return;
141+
e.preventDefault();
142+
e.stopPropagation();
143+
onSelect(false);
144+
}}>
145+
<div className="group-hover:block hidden size-6 rounded-lg border bg-accent-2" />
146+
<div className="group-hover:hidden">
147+
<AvatarPlus
148+
size="md"
149+
users={participantData}
150+
/>
151+
</div>
152+
</div>
153+
)}
154+
<div className="flex w-[90%] flex-1 flex-col">
155+
<div className="flex flex-row items-end justify-between gap-1">
156+
<span className="truncate text-sm font-medium">
157+
{participantNames.join(', ')}
158+
</span>
159+
<span className="text-base-11 min-w-fit text-right text-xs">
160+
{timeAgo}
161+
</span>
162+
</div>
163+
164+
<span className="truncate break-all text-left text-xs font-medium">
165+
{convo.subjects[0]?.subject}
166+
</span>
167+
168+
<div className="flex flex-row items-start justify-start gap-1 text-left text-sm">
169+
<div className="px-0.5">
170+
{authorAvatarData && (
171+
<Avatar
172+
avatarProfilePublicId={
173+
authorAvatarData.avatarProfilePublicId
174+
}
175+
avatarTimestamp={authorAvatarData.avatarTimestamp}
176+
name={authorAvatarData.name}
177+
size={'sm'}
178+
color={authorAvatarData.color}
179+
key={authorAvatarData.participantPublicId}
180+
/>
181+
)}
182+
</div>
100183

101-
<span className="line-clamp-2 overflow-ellipsis whitespace-break-spaces break-words">
102-
{convo.entries[0]?.bodyPlainText ?? ''}
103-
</span>
104-
</div>
105-
</div>
106-
</Link>
184+
<span className="line-clamp-2 overflow-ellipsis whitespace-break-spaces break-words">
185+
{convo.entries[0]?.bodyPlainText ?? ''}
186+
</span>
187+
</div>
188+
</div>
189+
</Link>
190+
</ContextMenuTrigger>
191+
<ContextMenuContent>
192+
<ContextMenuGroup>
193+
<ContextMenuItem
194+
className="gap-2"
195+
onClick={async () =>
196+
await hideConvo({
197+
orgShortcode,
198+
convoPublicId: convo.publicId,
199+
unhide: hidden
200+
})
201+
}>
202+
{hidden ? (
203+
<>
204+
<Eye /> Show
205+
</>
206+
) : (
207+
<>
208+
<EyeSlash /> Hide
209+
</>
210+
)}
211+
</ContextMenuItem>
212+
<ContextMenuItem
213+
className="gap-2"
214+
onClick={async () => {
215+
await deleteConvo({
216+
orgShortcode,
217+
convoPublicId: convo.publicId
218+
});
219+
await deleteConvo$(convo.publicId);
220+
}}>
221+
<Trash /> Delete
222+
</ContextMenuItem>
223+
</ContextMenuGroup>
224+
</ContextMenuContent>
225+
</ContextMenu>
107226
);
108227
}

0 commit comments

Comments
 (0)