Skip to content

Commit 878b543

Browse files
committed
feat: Hide/Unhide convos with updating lists
1 parent 2c80c60 commit 878b543

File tree

2 files changed

+163
-72
lines changed

2 files changed

+163
-72
lines changed

Diff for: apps/web/src/app/[orgShortCode]/convo/_components/convo-list.tsx

+49-34
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22

33
import { type RouterOutputs, api } from '@/src/lib/trpc';
44
import { useGlobalStore } from '@/src/providers/global-store-provider';
5-
import { useEffect, useMemo, useRef } from 'react';
5+
import { useState, useEffect, useMemo, useRef } from 'react';
66
import { useVirtualizer } from '@tanstack/react-virtual';
77
import useTimeAgo from '@/src/hooks/use-time-ago';
88
import { formatParticipantData } from '../utils';
99
import Link from 'next/link';
1010
import AvatarPlus from '@/src/components/avatar-plus';
11+
import { Button } from '@/src/components/shadcn-ui/button';
12+
import { ms } from '@u22n/utils/ms';
1113

1214
export default function ConvoList() {
1315
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode);
1416
const scrollableRef = useRef(null);
17+
const [showHidden, setShowHidden] = useState(false);
1518

1619
const {
1720
data: convos,
@@ -21,10 +24,12 @@ export default function ConvoList() {
2124
isFetchingNextPage
2225
} = api.convos.getOrgMemberConvos.useInfiniteQuery(
2326
{
24-
orgShortCode
27+
orgShortCode,
28+
includeHidden: showHidden ? true : undefined
2529
},
2630
{
27-
getNextPageParam: (lastPage) => lastPage.cursor ?? undefined
31+
getNextPageParam: (lastPage) => lastPage.cursor ?? undefined,
32+
staleTime: ms('1 hour')
2833
}
2934
);
3035

@@ -57,43 +62,53 @@ export default function ConvoList() {
5762
]);
5863

5964
return (
60-
<div className="bg-sand-1 flex h-full w-full flex-col border-r p-4">
65+
<div className="bg-sand-1 flex h-full w-full flex-col border-r p-4">
6166
{isLoading ? (
6267
<div className="w-full text-center font-bold">Loading...</div>
6368
) : (
64-
<div
65-
className="h-full max-h-full w-full max-w-full overflow-y-auto overflow-x-hidden"
66-
ref={scrollableRef}>
69+
<>
70+
{/* TODO: Replace this according to designs later */}
71+
<div className="flex w-full pb-2">
72+
<Button
73+
onClick={() => setShowHidden((prev) => !prev)}
74+
variant="secondary">
75+
{showHidden ? 'Show Normal Convos' : 'Show Hidden Convos'}
76+
</Button>
77+
</div>
6778
<div
68-
className="relative flex w-full max-w-full flex-col overflow-hidden"
69-
style={{ height: `${convosVirtualizer.getTotalSize()}px` }}>
70-
{convosVirtualizer.getVirtualItems().map((virtualItem) => {
71-
const isLoader = virtualItem.index > allConvos.length - 1;
72-
const convo = allConvos[virtualItem.index]!;
79+
className="h-full max-h-full w-full max-w-full overflow-y-auto overflow-x-hidden"
80+
ref={scrollableRef}>
81+
<div
82+
className="relative flex w-full max-w-full flex-col overflow-hidden"
83+
style={{ height: `${convosVirtualizer.getTotalSize()}px` }}>
84+
{convosVirtualizer.getVirtualItems().map((virtualItem) => {
85+
const isLoader = virtualItem.index > allConvos.length - 1;
86+
const convo = allConvos[virtualItem.index]!;
7387

74-
return (
75-
<div
76-
key={virtualItem.index}
77-
data-index={virtualItem.index}
78-
className="absolute left-0 top-0 w-full"
79-
ref={convosVirtualizer.measureElement}
80-
style={{
81-
transform: `translateY(${virtualItem.start}px)`
82-
}}>
83-
{isLoader ? (
84-
<div className="w-full text-center font-bold">
85-
{hasNextPage ? 'Loading...' : ''}
86-
</div>
87-
) : (
88-
<div className="h-full w-full">
89-
<ConvoItem convo={convo} />
90-
</div>
91-
)}
92-
</div>
93-
);
94-
})}
88+
return (
89+
<div
90+
key={virtualItem.index}
91+
data-index={virtualItem.index}
92+
className="absolute left-0 top-0 w-full"
93+
ref={convosVirtualizer.measureElement}
94+
style={{
95+
transform: `translateY(${virtualItem.start}px)`
96+
}}>
97+
{isLoader ? (
98+
<div className="w-full text-center font-bold">
99+
{hasNextPage ? 'Loading...' : ''}
100+
</div>
101+
) : (
102+
<div className="h-full w-full">
103+
<ConvoItem convo={convo} />
104+
</div>
105+
)}
106+
</div>
107+
);
108+
})}
109+
</div>
95110
</div>
96-
</div>
111+
</>
97112
)}
98113
</div>
99114
);

Diff for: apps/web/src/app/[orgShortCode]/convo/utils.ts

+114-38
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { api, type RouterOutputs } from '@/src/lib/trpc';
22
import { useGlobalStore } from '@/src/providers/global-store-provider';
33
import { type TypeId } from '@u22n/utils/typeid';
4+
import { type InfiniteData } from '@tanstack/react-query';
5+
import { useCallback } from 'react';
46

57
export function formatParticipantData(
68
participant: RouterOutputs['convos']['getOrgMemberConvos']['data'][number]['participants'][number]
@@ -56,9 +58,9 @@ export function formatParticipantData(
5658

5759
export function useAddSingleConvo$Cache() {
5860
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode);
59-
const convoListApi = api.useUtils().convos.getOrgMemberConvos;
60-
const getOrgMemberSpecificConvoApi =
61-
api.useUtils().convos.getOrgMemberSpecificConvo;
61+
const utils = api.useUtils();
62+
const convoListApi = utils.convos.getOrgMemberConvos;
63+
const getOrgMemberSpecificConvoApi = utils.convos.getOrgMemberSpecificConvo;
6264

6365
return async (convoId: TypeId<'convos'>) => {
6466
const convo = await getOrgMemberSpecificConvoApi.fetch({
@@ -78,10 +80,11 @@ export function useAddSingleConvo$Cache() {
7880
export function useDeleteConvo$Cache() {
7981
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode);
8082
const convoListApi = api.useUtils().convos.getOrgMemberConvos;
81-
82-
return async (convoId: TypeId<'convos'>) => {
83-
await convoListApi.cancel({ orgShortCode });
84-
convoListApi.setInfiniteData({ orgShortCode }, (updater) => {
83+
const deleteFn = useCallback(
84+
(
85+
convoId: TypeId<'convos'>,
86+
updater?: InfiniteData<RouterOutputs['convos']['getOrgMemberConvos']>
87+
) => {
8588
if (!updater) return;
8689
const clonedUpdater = structuredClone(updater);
8790
for (const page of clonedUpdater.pages) {
@@ -93,52 +96,58 @@ export function useDeleteConvo$Cache() {
9396
break;
9497
}
9598
return clonedUpdater;
96-
});
99+
},
100+
[]
101+
);
102+
103+
return async (convoId: TypeId<'convos'>) => {
104+
await convoListApi.cancel({ orgShortCode });
105+
await convoListApi.cancel({ orgShortCode, includeHidden: true });
106+
107+
convoListApi.setInfiniteData({ orgShortCode }, (updater) =>
108+
deleteFn(convoId, updater)
109+
);
110+
convoListApi.setInfiniteData(
111+
{ orgShortCode, includeHidden: true },
112+
(updater) => deleteFn(convoId, updater)
113+
);
97114
};
98115
}
99116

117+
// TODO: Simplify this function later, its too complex
100118
export function useToggleConvoHidden$Cache() {
101119
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode);
102-
const convoApi = api.useUtils().convos.getConvo;
103-
const convoListApi = api.useUtils().convos.getOrgMemberConvos;
104-
const specificConvoApi = api.useUtils().convos.getOrgMemberSpecificConvo;
120+
const utils = api.useUtils();
121+
const convoApi = utils.convos.getConvo;
122+
const convoListApi = utils.convos.getOrgMemberConvos;
123+
const specificConvoApi = utils.convos.getOrgMemberSpecificConvo;
105124

106-
return async (convoId: TypeId<'convos'>, hide = false) => {
107-
const convoToAdd = !hide
108-
? await specificConvoApi.fetch({
109-
convoPublicId: convoId,
110-
orgShortCode
111-
})
112-
: null;
113-
114-
await convoApi.cancel({ convoPublicId: convoId, orgShortCode });
115-
convoApi.setData({ convoPublicId: convoId, orgShortCode }, (updater) => {
125+
// 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
126+
const convoListUpdaterFn = useCallback(
127+
(
128+
hideFromList: boolean,
129+
convoToAdd: RouterOutputs['convos']['getOrgMemberSpecificConvo'] | null,
130+
convoToRemove: TypeId<'convos'> | null,
131+
updater?: InfiniteData<RouterOutputs['convos']['getOrgMemberConvos']>
132+
) => {
116133
if (!updater) return;
117134
const clonedUpdater = structuredClone(updater);
118-
const participantIndex = clonedUpdater.data.participants.findIndex(
119-
(participant) => participant.publicId === updater.ownParticipantPublicId
120-
);
121-
if (participantIndex === -1) return;
122-
clonedUpdater.data.participants[participantIndex]!.hidden = hide;
123-
return clonedUpdater;
124-
});
125135

126-
await convoListApi.cancel({ orgShortCode });
127-
convoListApi.setInfiniteData({ orgShortCode }, (updater) => {
128-
if (!updater) return;
129-
const clonedUpdater = structuredClone(updater);
130-
131-
if (hide) {
136+
if (hideFromList) {
132137
for (const page of clonedUpdater.pages) {
133138
const convoIndex = page.data.findIndex(
134-
(convo) => convo.publicId === convoId
139+
(convo) => convo.publicId === convoToRemove
135140
);
136141
if (convoIndex === -1) continue;
137142
page.data.splice(convoIndex, 1);
138143
break;
139144
}
140145
} else {
141-
const clonedConvo = structuredClone(convoToAdd)!; // We know it's not null as we are not hiding
146+
if (!convoToAdd)
147+
throw new Error(
148+
'Trying to unhide from convo list without providing the convo to add'
149+
);
150+
const clonedConvo = structuredClone(convoToAdd);
142151
let convoAlreadyAdded = false;
143152
for (const page of clonedUpdater.pages) {
144153
const insertIndex = page.data.findIndex(
@@ -159,14 +168,81 @@ export function useToggleConvoHidden$Cache() {
159168
}
160169
}
161170
return clonedUpdater;
171+
},
172+
[]
173+
);
174+
175+
return async (convoId: TypeId<'convos'>, hide = false) => {
176+
await convoApi.cancel({ convoPublicId: convoId, orgShortCode });
177+
convoApi.setData({ convoPublicId: convoId, orgShortCode }, (updater) => {
178+
if (!updater) return;
179+
const clonedUpdater = structuredClone(updater);
180+
const participantIndex = clonedUpdater.data.participants.findIndex(
181+
(participant) => participant.publicId === updater.ownParticipantPublicId
182+
);
183+
if (participantIndex === -1) return;
184+
clonedUpdater.data.participants[participantIndex]!.hidden = hide;
185+
return clonedUpdater;
162186
});
187+
188+
const convoToAdd = await specificConvoApi.fetch({
189+
convoPublicId: convoId,
190+
orgShortCode
191+
});
192+
193+
// Update both hidden and non-hidden convo lists
194+
await convoListApi.cancel({ orgShortCode, includeHidden: true });
195+
await convoListApi.cancel({ orgShortCode });
196+
197+
// if we are hiding a convo, we need to remove it from the non-hidden list and add to hidden list
198+
if (hide) {
199+
convoListApi.setInfiniteData({ orgShortCode }, (updater) =>
200+
convoListUpdaterFn(
201+
/* hide from non-hidden */ true,
202+
null,
203+
convoId,
204+
updater
205+
)
206+
);
207+
convoListApi.setInfiniteData(
208+
{ orgShortCode, includeHidden: true },
209+
(updater) =>
210+
convoListUpdaterFn(
211+
/* add from hidden */ false,
212+
convoToAdd,
213+
null,
214+
updater
215+
)
216+
);
217+
} else {
218+
// if we are un-hiding a convo, we need to remove it from the hidden list and add to non-hidden list
219+
convoListApi.setInfiniteData({ orgShortCode }, (updater) =>
220+
convoListUpdaterFn(
221+
/* add to non-hidden */ false,
222+
convoToAdd,
223+
null,
224+
updater
225+
)
226+
);
227+
convoListApi.setInfiniteData(
228+
{ orgShortCode, includeHidden: true },
229+
(updater) =>
230+
convoListUpdaterFn(
231+
/* hide from hidden */ true,
232+
null,
233+
convoId,
234+
updater
235+
)
236+
);
237+
}
163238
};
164239
}
165240

166241
export function useUpdateConvoMessageList$Cache() {
167242
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode);
168-
const convoEntiresApi = api.useUtils().convos.entries.getConvoEntries;
169-
const singleConvoEntryApi = api.useUtils().convos.entries.getConvoSingleEntry;
243+
const utils = api.useUtils();
244+
const convoEntiresApi = utils.convos.entries.getConvoEntries;
245+
const singleConvoEntryApi = utils.convos.entries.getConvoSingleEntry;
170246

171247
// TODO: make the reply mutation return the new convo entry, to save one API call
172248
return async (

0 commit comments

Comments
 (0)