diff --git a/apps/web/src/app/[orgShortCode]/convo/_components/convo-list.tsx b/apps/web/src/app/[orgShortCode]/convo/_components/convo-list.tsx index bcb2535f..84471e78 100644 --- a/apps/web/src/app/[orgShortCode]/convo/_components/convo-list.tsx +++ b/apps/web/src/app/[orgShortCode]/convo/_components/convo-list.tsx @@ -2,16 +2,19 @@ import { type RouterOutputs, api } from '@/src/lib/trpc'; import { useGlobalStore } from '@/src/providers/global-store-provider'; -import { useEffect, useMemo, useRef } from 'react'; +import { useState, useEffect, useMemo, useRef } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import useTimeAgo from '@/src/hooks/use-time-ago'; import { formatParticipantData } from '../utils'; import Link from 'next/link'; import AvatarPlus from '@/src/components/avatar-plus'; +import { Button } from '@/src/components/shadcn-ui/button'; +import { ms } from '@u22n/utils/ms'; export default function ConvoList() { const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode); const scrollableRef = useRef(null); + const [showHidden, setShowHidden] = useState(false); const { data: convos, @@ -21,10 +24,12 @@ export default function ConvoList() { isFetchingNextPage } = api.convos.getOrgMemberConvos.useInfiniteQuery( { - orgShortCode + orgShortCode, + includeHidden: showHidden ? true : undefined }, { - getNextPageParam: (lastPage) => lastPage.cursor ?? undefined + getNextPageParam: (lastPage) => lastPage.cursor ?? undefined, + staleTime: ms('1 hour') } ); @@ -57,43 +62,53 @@ export default function ConvoList() { ]); return ( -
+
{isLoading ? (
Loading...
) : ( -
+ <> + {/* TODO: Replace this according to designs later */} +
+ +
- {convosVirtualizer.getVirtualItems().map((virtualItem) => { - const isLoader = virtualItem.index > allConvos.length - 1; - const convo = allConvos[virtualItem.index]!; + className="h-full max-h-full w-full max-w-full overflow-y-auto overflow-x-hidden" + ref={scrollableRef}> +
+ {convosVirtualizer.getVirtualItems().map((virtualItem) => { + const isLoader = virtualItem.index > allConvos.length - 1; + const convo = allConvos[virtualItem.index]!; - return ( -
- {isLoader ? ( -
- {hasNextPage ? 'Loading...' : ''} -
- ) : ( -
- -
- )} -
- ); - })} + return ( +
+ {isLoader ? ( +
+ {hasNextPage ? 'Loading...' : ''} +
+ ) : ( +
+ +
+ )} +
+ ); + })} +
-
+ )}
); diff --git a/apps/web/src/app/[orgShortCode]/convo/utils.ts b/apps/web/src/app/[orgShortCode]/convo/utils.ts index 432a28db..f72c271e 100644 --- a/apps/web/src/app/[orgShortCode]/convo/utils.ts +++ b/apps/web/src/app/[orgShortCode]/convo/utils.ts @@ -1,6 +1,8 @@ import { api, type RouterOutputs } from '@/src/lib/trpc'; import { useGlobalStore } from '@/src/providers/global-store-provider'; import { type TypeId } from '@u22n/utils/typeid'; +import { type InfiniteData } from '@tanstack/react-query'; +import { useCallback } from 'react'; export function formatParticipantData( participant: RouterOutputs['convos']['getOrgMemberConvos']['data'][number]['participants'][number] @@ -56,9 +58,9 @@ export function formatParticipantData( export function useAddSingleConvo$Cache() { const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode); - const convoListApi = api.useUtils().convos.getOrgMemberConvos; - const getOrgMemberSpecificConvoApi = - api.useUtils().convos.getOrgMemberSpecificConvo; + const utils = api.useUtils(); + const convoListApi = utils.convos.getOrgMemberConvos; + const getOrgMemberSpecificConvoApi = utils.convos.getOrgMemberSpecificConvo; return async (convoId: TypeId<'convos'>) => { const convo = await getOrgMemberSpecificConvoApi.fetch({ @@ -78,10 +80,11 @@ export function useAddSingleConvo$Cache() { export function useDeleteConvo$Cache() { const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode); const convoListApi = api.useUtils().convos.getOrgMemberConvos; - - return async (convoId: TypeId<'convos'>) => { - await convoListApi.cancel({ orgShortCode }); - convoListApi.setInfiniteData({ orgShortCode }, (updater) => { + const deleteFn = useCallback( + ( + convoId: TypeId<'convos'>, + updater?: InfiniteData + ) => { if (!updater) return; const clonedUpdater = structuredClone(updater); for (const page of clonedUpdater.pages) { @@ -93,52 +96,58 @@ export function useDeleteConvo$Cache() { break; } return clonedUpdater; - }); + }, + [] + ); + + return async (convoId: TypeId<'convos'>) => { + await convoListApi.cancel({ orgShortCode }); + await convoListApi.cancel({ orgShortCode, includeHidden: true }); + + convoListApi.setInfiniteData({ orgShortCode }, (updater) => + deleteFn(convoId, updater) + ); + convoListApi.setInfiniteData( + { orgShortCode, includeHidden: true }, + (updater) => deleteFn(convoId, updater) + ); }; } +// TODO: Simplify this function later, its too complex 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 utils = api.useUtils(); + const convoApi = utils.convos.getConvo; + const convoListApi = utils.convos.getOrgMemberConvos; + const specificConvoApi = utils.convos.getOrgMemberSpecificConvo; - return async (convoId: TypeId<'convos'>, hide = false) => { - const convoToAdd = !hide - ? await specificConvoApi.fetch({ - convoPublicId: convoId, - orgShortCode - }) - : null; - - await convoApi.cancel({ convoPublicId: convoId, orgShortCode }); - convoApi.setData({ convoPublicId: convoId, orgShortCode }, (updater) => { + // This function is a bit complex, but basically what it does is updates the provided updater by either removing or adding a convo based on the parameters + const convoListUpdaterFn = useCallback( + ( + hideFromList: boolean, + convoToAdd: RouterOutputs['convos']['getOrgMemberSpecificConvo'] | null, + convoToRemove: TypeId<'convos'> | null, + updater?: InfiniteData + ) => { 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; - }); - await convoListApi.cancel({ orgShortCode }); - convoListApi.setInfiniteData({ orgShortCode }, (updater) => { - if (!updater) return; - const clonedUpdater = structuredClone(updater); - - if (hide) { + if (hideFromList) { for (const page of clonedUpdater.pages) { const convoIndex = page.data.findIndex( - (convo) => convo.publicId === convoId + (convo) => convo.publicId === convoToRemove ); 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 + if (!convoToAdd) + throw new Error( + 'Trying to unhide from convo list without providing the convo to add' + ); + const clonedConvo = structuredClone(convoToAdd); let convoAlreadyAdded = false; for (const page of clonedUpdater.pages) { const insertIndex = page.data.findIndex( @@ -159,14 +168,81 @@ export function useToggleConvoHidden$Cache() { } } return clonedUpdater; + }, + [] + ); + + return async (convoId: TypeId<'convos'>, hide = false) => { + 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; }); + + const convoToAdd = await specificConvoApi.fetch({ + convoPublicId: convoId, + orgShortCode + }); + + // Update both hidden and non-hidden convo lists + await convoListApi.cancel({ orgShortCode, includeHidden: true }); + await convoListApi.cancel({ orgShortCode }); + + // if we are hiding a convo, we need to remove it from the non-hidden list and add to hidden list + if (hide) { + convoListApi.setInfiniteData({ orgShortCode }, (updater) => + convoListUpdaterFn( + /* hide from non-hidden */ true, + null, + convoId, + updater + ) + ); + convoListApi.setInfiniteData( + { orgShortCode, includeHidden: true }, + (updater) => + convoListUpdaterFn( + /* add from hidden */ false, + convoToAdd, + null, + updater + ) + ); + } else { + // if we are un-hiding a convo, we need to remove it from the hidden list and add to non-hidden list + convoListApi.setInfiniteData({ orgShortCode }, (updater) => + convoListUpdaterFn( + /* add to non-hidden */ false, + convoToAdd, + null, + updater + ) + ); + convoListApi.setInfiniteData( + { orgShortCode, includeHidden: true }, + (updater) => + convoListUpdaterFn( + /* hide from hidden */ true, + null, + convoId, + updater + ) + ); + } }; } export function useUpdateConvoMessageList$Cache() { const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode); - const convoEntiresApi = api.useUtils().convos.entries.getConvoEntries; - const singleConvoEntryApi = api.useUtils().convos.entries.getConvoSingleEntry; + const utils = api.useUtils(); + const convoEntiresApi = utils.convos.entries.getConvoEntries; + const singleConvoEntryApi = utils.convos.entries.getConvoSingleEntry; // TODO: make the reply mutation return the new convo entry, to save one API call return async (