Skip to content

Commit

Permalink
feat: Hide/Unhide convos with updating lists
Browse files Browse the repository at this point in the history
  • Loading branch information
BlankParticle committed May 25, 2024
1 parent ea7545d commit d3064db
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 72 deletions.
83 changes: 49 additions & 34 deletions apps/web/src/app/[orgShortCode]/convo/_components/convo-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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')
}
);

Expand Down Expand Up @@ -57,43 +62,53 @@ export default function ConvoList() {
]);

return (
<div className="bg-sand-1 flex h-full w-full flex-col border-r p-4">
<div className="bg-sand-1 flex h-full w-full flex-col border-r p-4">
{isLoading ? (
<div className="w-full text-center font-bold">Loading...</div>
) : (
<div
className="h-full max-h-full w-full max-w-full overflow-y-auto overflow-x-hidden"
ref={scrollableRef}>
<>
{/* TODO: Replace this according to designs later */}
<div className="flex w-full pb-2">
<Button
onClick={() => setShowHidden((prev) => !prev)}
variant="secondary">
{showHidden ? 'Show Normal Convos' : 'Show Hidden Convos'}
</Button>
</div>
<div
className="relative flex w-full max-w-full flex-col overflow-hidden"
style={{ height: `${convosVirtualizer.getTotalSize()}px` }}>
{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}>
<div
className="relative flex w-full max-w-full flex-col overflow-hidden"
style={{ height: `${convosVirtualizer.getTotalSize()}px` }}>
{convosVirtualizer.getVirtualItems().map((virtualItem) => {
const isLoader = virtualItem.index > allConvos.length - 1;
const convo = allConvos[virtualItem.index]!;

return (
<div
key={virtualItem.index}
data-index={virtualItem.index}
className="absolute left-0 top-0 w-full"
ref={convosVirtualizer.measureElement}
style={{
transform: `translateY(${virtualItem.start}px)`
}}>
{isLoader ? (
<div className="w-full text-center font-bold">
{hasNextPage ? 'Loading...' : ''}
</div>
) : (
<div className="h-full w-full">
<ConvoItem convo={convo} />
</div>
)}
</div>
);
})}
return (
<div
key={virtualItem.index}
data-index={virtualItem.index}
className="absolute left-0 top-0 w-full"
ref={convosVirtualizer.measureElement}
style={{
transform: `translateY(${virtualItem.start}px)`
}}>
{isLoader ? (
<div className="w-full text-center font-bold">
{hasNextPage ? 'Loading...' : ''}
</div>
) : (
<div className="h-full w-full">
<ConvoItem convo={convo} />
</div>
)}
</div>
);
})}
</div>
</div>
</div>
</>
)}
</div>
);
Expand Down
152 changes: 114 additions & 38 deletions apps/web/src/app/[orgShortCode]/convo/utils.ts
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -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({
Expand All @@ -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<RouterOutputs['convos']['getOrgMemberConvos']>
) => {
if (!updater) return;
const clonedUpdater = structuredClone(updater);
for (const page of clonedUpdater.pages) {
Expand All @@ -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<RouterOutputs['convos']['getOrgMemberConvos']>
) => {
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(
Expand All @@ -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 (
Expand Down

0 comments on commit d3064db

Please sign in to comment.