Skip to content

Commit

Permalink
feat: hide and delete convo (#444)
Browse files Browse the repository at this point in the history
  • Loading branch information
BlankParticle committed May 13, 2024
1 parent e22f828 commit 9c60306
Show file tree
Hide file tree
Showing 6 changed files with 370 additions and 37 deletions.
10 changes: 6 additions & 4 deletions apps/platform/trpc/routers/convoRouter/convoRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1291,10 +1291,12 @@ export const convoRouter = router({
.input(
z.object({
includeHidden: z.boolean().default(false),
cursor: z.object({
lastUpdatedAt: z.date().optional(),
lastPublicId: typeIdValidator('convos').optional()
})
cursor: z
.object({
lastUpdatedAt: z.date().optional(),
lastPublicId: typeIdValidator('convos').optional()
})
.default({})
})
)
.query(async ({ ctx, input }) => {
Expand Down
10 changes: 6 additions & 4 deletions apps/platform/trpc/routers/convoRouter/entryRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ export const convoEntryRouter = router({
.input(
z.object({
convoPublicId: typeIdValidator('convos'),
cursor: z.object({
lastCreatedAt: z.date().optional(),
lastPublicId: typeIdValidator('convoEntries').optional()
})
cursor: z
.object({
lastCreatedAt: z.date().optional(),
lastPublicId: typeIdValidator('convoEntries').optional()
})
.default({})
})
)
.query(async ({ ctx, input }) => {
Expand Down
18 changes: 9 additions & 9 deletions apps/web/app/[orgShortCode]/convo/ConvoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@ export default function ConvoList() {
isFetchingNextPage
} = api.convos.getOrgMemberConvos.useInfiniteQuery(
{
includeHidden: false,
orgShortCode
},
{
initialCursor: {},
getNextPageParam: (lastPage) => lastPage.cursor ?? undefined
}
);
Expand Down Expand Up @@ -59,7 +57,7 @@ export default function ConvoList() {
]);

return (
<Flex className="bg-slate-2 dark:bg-slatedark-2 h-full w-[400px] overflow-x-hidden">
<Flex className="bg-slate-2 dark:bg-slatedark-2 h-full w-[400px] max-w-[400px]">
{isLoading ? (
<div className="w-full text-center font-bold">Loading...</div>
) : (
Expand All @@ -85,7 +83,7 @@ export default function ConvoList() {
{hasNextPage ? 'Loading...' : ''}
</div>
) : (
<div className="h-full">
<div className="h-full w-full">
<ConvoItem convo={convo} />
</div>
)}
Expand Down Expand Up @@ -149,26 +147,28 @@ function ConvoItem({
return (
<Link
href={`/${orgShortCode}/convo/${convo.publicId}`}
className="border-gray-12 flex h-full w-full gap-4 border-b p-2">
className="border-gray-12 flex h-full w-full max-w-full gap-4 border-b p-2">
<AvatarPlus
size="4"
imageSize="lg"
users={participantData}
/>
<Flex
direction="column"
className="w-full"
className="w-full flex-1"
gap="1">
<Text
className="w-full overflow-hidden pl-2 text-left"
className="w-full max-w-[320px] truncate pl-2 text-left"
size="1"
weight="bold">
<span className="truncate text-xs">{convo.subjects[0]?.subject}</span>
{convo.subjects[0]?.subject}
</Text>
<div className="dark:bg-graydark-3 dark:hover:bg-graydark-4 dark:text-graydark-11 w-full rounded-lg p-1 text-left text-sm">
<span className="line-clamp-2">
<Text weight="bold">{authorName}</Text>:{' '}
<Text className="w-full overflow-ellipsis break-words">
<Text
className="w-full max-w-full overflow-ellipsis break-words"
style={{ overflowWrap: 'anywhere' }}>
{convo.entries[0]?.bodyPlainText ?? ''}
</Text>
</span>
Expand Down
268 changes: 266 additions & 2 deletions apps/web/app/[orgShortCode]/convo/[convoId]/ChatSideBar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,267 @@
export default function ChatSideBar() {
return <div className="h-full w-[300px]">Chat Side Bar</div>;
'use client';

import { cn, generateAvatarUrl, getInitials } from '@/lib/utils';
import {
Avatar,
Box,
Dialog,
Flex,
Heading,
IconButton,
Text,
HoverCard,
Button
} from '@radix-ui/themes';
import { ChevronUp, ChevronDown, EyeOff, Trash } from 'lucide-react';
import { useState } from 'react';
import { useRemoveConvoFromList, type formatParticipantData } from '../utils';
import { memo } from 'react';
import useAwaitableModal, {
type ModalComponent
} from '@/hooks/use-awaitable-modal';
import { api } from '@/lib/trpc';
import { type TypeId } from '@u22n/utils';
import { useGlobalStore } from '@/providers/global-store-provider';
import { useRouter } from 'next/navigation';

export default function ChatSideBar({
participants,
convoId
}: {
participants: NonNullable<ReturnType<typeof formatParticipantData>>[];
convoId: TypeId<'convos'>;
}) {
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode);
const [participantOpen, setParticipantOpen] = useState(false);
const [ModalRoot, openDeleteModal] = useAwaitableModal(DeleteModal, {
convoId
});
const hideConvo = api.convos.hideConvo.useMutation();
const router = useRouter();
const removeConvoFromList = useRemoveConvoFromList();

return (
<Flex
className="h-full w-[300px]"
direction="column"
gap="2">
<Flex
justify="end"
gap="2"
align="center"
className="border-gray-11 h-12 w-full border-b p-2">
<IconButton
color="red"
variant="soft"
onClick={() => {
openDeleteModal()
// Navigate to empty page on delete
.then(() => router.push(`/${orgShortCode}/convo`))
// Do nothing if Hide is chosen or Modal is Closed
.catch(() => null);
}}>
<Trash size={16} />
</IconButton>
<IconButton
variant="soft"
loading={hideConvo.isLoading}
onClick={async () => {
await hideConvo.mutateAsync({
convoPublicId: convoId,
orgShortCode
});
removeConvoFromList(convoId);
}}>
<EyeOff size={16} />
</IconButton>
</Flex>
<Flex className="border-gray-11 h-full w-full border-l">
<Flex
direction="column"
className="w-full">
<Flex
className="w-full p-1"
justify="between"
align="center"
onClick={() => setParticipantOpen((open) => !open)}>
<Heading
size="3"
className="select-none p-2">
Participants
</Heading>
{participantOpen ? (
<ChevronUp size={14} />
) : (
<ChevronDown size={14} />
)}
</Flex>
{participants.length === 0 && <Text className="p-2">Loading...</Text>}
<Flex
direction={participantOpen ? 'column' : 'row'}
className="px-2"
gap={participantOpen ? '2' : undefined}>
{participants.map((participant, i) => (
<Flex
gap="2"
align="center"
key={participant.participantPublicId}>
<HoverCard.Root>
<HoverCard.Trigger>
<Box
style={{ zIndex: 100 + i }}
className={cn(
!participantOpen && i !== 0 ? '-ml-2' : '',
'dark:outline-graydark-1 dark:bg-graydark-1 w-fit rounded-full outline'
)}>
<Avatar
src={
generateAvatarUrl({
avatarTimestamp: participant.avatarTimestamp,
publicId: participant.avatarProfilePublicId,
size: 'lg'
}) ?? undefined
}
fallback={getInitials(participant.name)}
radius="full"
/>
</Box>
</HoverCard.Trigger>
<HoverCard.Content
className="max-w-[300px] px-2 py-1"
side={participantOpen ? 'left' : 'bottom'}>
<Flex
direction="column"
align="center"
gap="2">
<Text className="truncate">{participant.name}</Text>
{participantOpen && participant.signatureHtml && (
<Flex
direction="column"
align="center"
justify="center"
gap="1">
<Text className="uppercase">Signature</Text>
<SignatureHTML html={participant.signatureHtml} />
</Flex>
)}
</Flex>
</HoverCard.Content>
</HoverCard.Root>
{participantOpen && (
<Text className="truncate">{participant.name}</Text>
)}
</Flex>
))}
</Flex>
</Flex>
</Flex>
<ModalRoot />
</Flex>
);
}

const SignatureHTML = memo(
function SIgnatureHTML({ html }: { html: string }) {
return (
<div
dangerouslySetInnerHTML={{ __html: html }}
className="w-full"
/>
);
},
(prev, curr) => prev.html === curr.html
);

function DeleteModal({
onClose,
onResolve,
open,
convoId
}: ModalComponent<{ convoId: TypeId<'convos'> }>) {
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode);
const hideConvo = api.convos.hideConvo.useMutation();
const deleteConvo = api.convos.deleteConvo.useMutation();
const removeConvoFromList = useRemoveConvoFromList();

return (
<Dialog.Root
open={open}
onOpenChange={(open) => {
if (!open) {
onClose();
}
}}>
<Dialog.Content className="w-full max-w-96 p-4">
<Dialog.Title
className="mx-auto w-fit py-2"
size="2">
Delete Convo?
</Dialog.Title>

<Flex
gap="4"
direction="column">
<Text
size="2"
as="div">
This will permanently and immediately delete this conversation for
all the participants.
</Text>
<Text
size="2"
as="div">
Are you sure you want to delete this conversation?
</Text>
<Text
size="1"
as="div"
color="gray">
Tip: You can also choose to hide this Convo
</Text>
</Flex>

<Flex
gap="3"
align="center"
justify="end"
className="mt-4">
<Button
size="2"
variant="surface"
onClick={() => onClose()}>
Cancel
</Button>
<Button
size="2"
variant="soft"
loading={hideConvo.isLoading}
onClick={async () => {
await hideConvo.mutateAsync({
convoPublicId: convoId,
orgShortCode
});
removeConvoFromList(convoId);
onClose();
}}>
Hide Instead
</Button>
<Button
size="2"
variant="solid"
color="red"
loading={deleteConvo.isLoading}
onClick={async () => {
await deleteConvo.mutateAsync({
convoPublicId: convoId,
orgShortCode
});
removeConvoFromList(convoId);
onResolve(null);
}}>
Delete
</Button>
</Flex>
</Dialog.Content>
</Dialog.Root>
);
}
Loading

0 comments on commit 9c60306

Please sign in to comment.