|
1 | 1 | 'use client';
|
2 | 2 |
|
| 3 | +import { |
| 4 | + ContextMenu, |
| 5 | + ContextMenuContent, |
| 6 | + ContextMenuGroup, |
| 7 | + ContextMenuTrigger, |
| 8 | + ContextMenuItem |
| 9 | +} from '@/src/components/shadcn-ui/context-menu'; |
3 | 10 | 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'; |
4 | 16 | import AvatarPlus from '@/src/components/avatar-plus';
|
5 | 17 | import { useTimeAgo } from '@/src/hooks/use-time-ago';
|
6 |
| -import { type RouterOutputs } from '@/src/lib/trpc'; |
| 18 | +import { useLongPress } from '@uidotdev/usehooks'; |
7 | 19 | import { Avatar } from '@/src/components/avatar';
|
8 |
| -import { formatParticipantData } from '../utils'; |
9 | 20 | import { usePathname } from 'next/navigation';
|
10 | 21 | import { cn } from '@/src/lib/utils';
|
| 22 | +import { useAtomValue } from 'jotai'; |
11 | 23 | import { useMemo } from 'react';
|
| 24 | +import { toast } from 'sonner'; |
12 | 25 | import Link from 'next/link';
|
13 | 26 |
|
14 | 27 | export function ConvoItem({
|
15 |
| - convo |
| 28 | + convo, |
| 29 | + selected, |
| 30 | + onSelect, |
| 31 | + hidden |
16 | 32 | }: {
|
17 | 33 | convo: RouterOutputs['convos']['getOrgMemberConvos']['data'][number];
|
| 34 | + selected: boolean; |
| 35 | + onSelect: (shiftKey: boolean) => void; |
| 36 | + hidden: boolean; |
18 | 37 | }) {
|
19 | 38 | 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 | + }); |
20 | 58 |
|
21 | 59 | const timeAgo = useTimeAgo(convo.lastUpdatedAt);
|
22 | 60 |
|
@@ -59,50 +97,131 @@ export function ConvoItem({
|
59 | 97 |
|
60 | 98 | const isActive = currentPath === link;
|
61 | 99 |
|
| 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 | + |
62 | 112 | 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> |
100 | 183 |
|
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> |
107 | 226 | );
|
108 | 227 | }
|
0 commit comments