Skip to content

Commit

Permalink
feat: Support for Hide/Unhide convo with smart convo-list update
Browse files Browse the repository at this point in the history
  • Loading branch information
BlankParticle committed May 13, 2024
1 parent 9c60306 commit 52fcd77
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 73 deletions.
1 change: 1 addition & 0 deletions apps/web/app/[orgShortCode]/convo/ConvoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default function ConvoList() {
getNextPageParam: (lastPage) => lastPage.cursor ?? undefined
}
);

const allConvos = convos ? convos.pages.flatMap(({ data }) => data) : [];
const convosVirtualizer = useVirtualizer({
count: allConvos.length + (hasNextPage ? 1 : 0),
Expand Down
134 changes: 79 additions & 55 deletions apps/web/app/[orgShortCode]/convo/[convoId]/ChatSideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ import {
IconButton,
Text,
HoverCard,
Button
Button,
Tooltip
} from '@radix-ui/themes';
import { ChevronUp, ChevronDown, EyeOff, Trash } from 'lucide-react';
import { ChevronUp, ChevronDown, EyeOff, Eye, Trash } from 'lucide-react';
import { useState } from 'react';
import { useRemoveConvoFromList, type formatParticipantData } from '../utils';
import {
useDeleteConvo$Cache,
useToggleConvoHidden$Cache,
type formatParticipantData
} from '../utils';
import { memo } from 'react';
import useAwaitableModal, {
type ModalComponent
Expand All @@ -26,19 +31,22 @@ import { useRouter } from 'next/navigation';

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

return (
<Flex
Expand All @@ -50,30 +58,37 @@ export default function ChatSideBar({
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>
<Tooltip content="Delete Convo">
<IconButton
color="red"
variant="soft"
disabled={convoHidden === null || hideConvo.isLoading}
onClick={() => {
openDeleteModal({ convoHidden })
// 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>
</Tooltip>
<Tooltip content={convoHidden ? 'Unhide Convo' : 'Hide Convo'}>
<IconButton
variant="soft"
loading={hideConvo.isLoading}
disabled={convoHidden === null}
onClick={async () => {
await hideConvo.mutateAsync({
convoPublicId: convoId,
orgShortCode,
unhide: convoHidden ? true : undefined
});
await toggleConvoHiddenState(convoId, !convoHidden);
}}>
{convoHidden ? <Eye size={16} /> : <EyeOff size={16} />}
</IconButton>
</Tooltip>
</Flex>
<Flex className="border-gray-11 h-full w-full border-l">
<Flex
Expand Down Expand Up @@ -176,18 +191,20 @@ function DeleteModal({
onClose,
onResolve,
open,
convoId
}: ModalComponent<{ convoId: TypeId<'convos'> }>) {
convoId,
convoHidden
}: ModalComponent<{ convoId: TypeId<'convos'>; convoHidden: boolean | null }>) {
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode);
const hideConvo = api.convos.hideConvo.useMutation();
const deleteConvo = api.convos.deleteConvo.useMutation();
const removeConvoFromList = useRemoveConvoFromList();
const removeConvoFromList = useDeleteConvo$Cache();
const toggleConvoHiddenState = useToggleConvoHidden$Cache();

return (
<Dialog.Root
open={open}
onOpenChange={(open) => {
if (!open) {
if (!open && !deleteConvo.isLoading && !hideConvo.isLoading) {
onClose();
}
}}>
Expand All @@ -212,12 +229,14 @@ function DeleteModal({
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>
{convoHidden ? null : (
<Text
size="1"
as="div"
color="gray">
Tip: You can also choose to hide this Convo
</Text>
)}
</Flex>

<Flex
Expand All @@ -228,34 +247,39 @@ function DeleteModal({
<Button
size="2"
variant="surface"
disabled={deleteConvo.isLoading || hideConvo.isLoading}
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>
{convoHidden ? null : (
<Button
size="2"
variant="soft"
loading={hideConvo.isLoading}
disabled={deleteConvo.isLoading}
onClick={async () => {
await hideConvo.mutateAsync({
convoPublicId: convoId,
orgShortCode
});
await toggleConvoHiddenState(convoId, true);
onClose();
}}>
Hide Instead
</Button>
)}
<Button
size="2"
variant="solid"
color="red"
loading={deleteConvo.isLoading}
disabled={hideConvo.isLoading}
onClick={async () => {
await deleteConvo.mutateAsync({
convoPublicId: convoId,
orgShortCode
});
removeConvoFromList(convoId);
await removeConvoFromList(convoId);
onResolve(null);
}}>
Delete
Expand Down
10 changes: 10 additions & 0 deletions apps/web/app/[orgShortCode]/convo/[convoId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ function ConvoView({ convoId }: { convoId: TypeId<'convos'> }) {
}, [status]);

const participantOwnPublicId = convoData?.ownParticipantPublicId;
const convoHidden = useMemo(
() =>
convoData
? convoData?.data.participants.find(
(p) => p.publicId === participantOwnPublicId
)?.hidden ?? false
: null,
[convoData, participantOwnPublicId]
);

const allParticipants = useMemo(() => {
const formattedParticipants: NonNullable<
Expand Down Expand Up @@ -217,6 +226,7 @@ function ConvoView({ convoId }: { convoId: TypeId<'convos'> }) {
<ChatSideBar
participants={allParticipants}
convoId={convoId}
convoHidden={convoHidden}
/>
</Flex>
);
Expand Down
94 changes: 76 additions & 18 deletions apps/web/app/[orgShortCode]/convo/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,32 +54,90 @@ export function formatParticipantData(
};
}

export function useRemoveConvoFromList() {
export function useDeleteConvo$Cache() {
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode);
const convoListApi = api.useUtils().convos.getOrgMemberConvos;

return (convoId: TypeId<'convos'>) =>
return async (convoId: TypeId<'convos'>) => {
await convoListApi.cancel({ orgShortCode });
convoListApi.setInfiniteData({ orgShortCode }, (updater) => {
if (!updater) return;
const pageIndex = updater.pages.findIndex((page) =>
page.data.some((convo) => convo.publicId === convoId)
);
const newPage = updater.pages[pageIndex]?.data.filter(
(convo) => convo.publicId !== convoId
);
const clonedUpdater = structuredClone(updater);
for (const page of clonedUpdater.pages) {
const convoIndex = page.data.findIndex(
(convo) => convo.publicId === convoId
);
if (convoIndex === -1) continue;
page.data.splice(convoIndex, 1);
break;
}
return clonedUpdater;
});
};
}

if (!newPage) return;
export function useToggleConvoHidden$Cache() {
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode);
const convoApi = api.useUtils().convos.getConvo;
const convoListApi = api.useUtils().convos.getOrgMemberConvos;
const specificConvoApi = api.useUtils().convos.getOrgMemberSpecificConvo;

const newPages = updater.pages.slice();
return async (convoId: TypeId<'convos'>, hide = false) => {
const convoToAdd = !hide
? await specificConvoApi.fetch({
convoPublicId: convoId,
orgShortCode
})
: null;

newPages[pageIndex] = {
data: newPage,
cursor: updater.pages[pageIndex]?.cursor ?? null
};
await convoApi.cancel({ convoPublicId: convoId, orgShortCode });
convoApi.setData({ convoPublicId: convoId, orgShortCode }, (updater) => {
if (!updater) return;
const clonedUpdater = structuredClone(updater);
const participantIndex = clonedUpdater.data.participants.findIndex(
(participant) => participant.publicId === updater.ownParticipantPublicId
);
if (participantIndex === -1) return;
clonedUpdater.data.participants[participantIndex]!.hidden = hide;
return clonedUpdater;
});

return {
pages: newPages,
pageParams: updater?.pageParams.slice()
};
await convoListApi.cancel({ orgShortCode });
convoListApi.setInfiniteData({ orgShortCode }, (updater) => {
if (!updater) return;
const clonedUpdater = structuredClone(updater);

if (hide) {
for (const page of clonedUpdater.pages) {
const convoIndex = page.data.findIndex(
(convo) => convo.publicId === convoId
);
if (convoIndex === -1) continue;
page.data.splice(convoIndex, 1);
break;
}
} else {
const clonedConvo = structuredClone(convoToAdd)!; // We know it's not null as we are not hiding
let convoAlreadyAdded = false;
for (const page of clonedUpdater.pages) {
const insertIndex = page.data.findIndex(
(convo) => convo.lastUpdatedAt < clonedConvo.lastUpdatedAt
);
if (insertIndex === -1) {
continue;
} else {
page.data.splice(insertIndex, 0, clonedConvo);
}
convoAlreadyAdded = true;
break;
}

// If convo is the oldest, add it to the last page as the last item
if (!convoAlreadyAdded) {
clonedUpdater.pages.at(-1)?.data.push(clonedConvo);
}
}
return clonedUpdater;
});
};
}

0 comments on commit 52fcd77

Please sign in to comment.