Skip to content

Commit 8ddb51b

Browse files
committed
refactor: data refetching improvements
1 parent 9d3f703 commit 8ddb51b

7 files changed

Lines changed: 96 additions & 80 deletions

File tree

apps/website/src/app/dashboard/[id]/ama/amas/new/_components/CreateAMAForm.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ function validateURL(value: string): string | undefined {
4242
}
4343
}
4444

45+
const allowedChannelTypes = [ChannelType.GuildText, ...threadTypes];
46+
4547
export function CreateAMAForm() {
4648
const router = useRouter();
4749
const params = useParams<{ id: string }>();
@@ -63,8 +65,8 @@ export function CreateAMAForm() {
6365
});
6466
const [errors, setErrors] = useState<FormErrors>({});
6567

66-
const { data: guildInfo } = client.guilds.useInfo(guildId, { for_bot: 'AMA', force_fresh: 'false' });
67-
const createAMA = client.guilds.ama.createAMA(guildId);
68+
const { data: guildInfo, isLoading } = client.guilds.useInfo(guildId, { for_bot: 'AMA', force_fresh: 'false' });
69+
const createAMA = client.guilds.ama.useCreateAMA(guildId);
6870

6971
const validateForm = (): boolean => {
7072
const newErrors: FormErrors = {};
@@ -157,7 +159,7 @@ export function CreateAMAForm() {
157159
setTimeout(() => formatJSON(), 50);
158160
};
159161

160-
if (!guildInfo) {
162+
if (isLoading) {
161163
return (
162164
<div className="mt-8 space-y-6">
163165
<div className="space-y-4">
@@ -198,8 +200,8 @@ export function CreateAMAForm() {
198200
{errors.title && <p className="mt-1 text-sm text-misc-danger">{errors.title}</p>}
199201
</div>
200202
<ChannelSelect
201-
allowedTypes={[ChannelType.GuildText, ChannelType.GuildAnnouncement, ...threadTypes]}
202-
channels={guildInfo.channels}
203+
allowedTypes={allowedChannelTypes}
204+
channels={guildInfo!.channels}
203205
error={errors.answersChannelId}
204206
label="Answers Channel"
205207
onChange={(value) => setFormData({ ...formData, answersChannelId: value })}
@@ -209,8 +211,8 @@ export function CreateAMAForm() {
209211
value={formData.answersChannelId}
210212
/>{' '}
211213
<ChannelSelect
212-
allowedTypes={[ChannelType.GuildText, ChannelType.GuildAnnouncement]}
213-
channels={guildInfo.channels}
214+
allowedTypes={allowedChannelTypes}
215+
channels={guildInfo!.channels}
214216
error={errors.promptChannelId}
215217
label="Prompt Channel"
216218
onChange={(value) => setFormData({ ...formData, promptChannelId: value })}
@@ -220,8 +222,8 @@ export function CreateAMAForm() {
220222
value={formData.promptChannelId}
221223
/>{' '}
222224
<ChannelSelect
223-
allowedTypes={[ChannelType.GuildText, ChannelType.GuildAnnouncement]}
224-
channels={guildInfo.channels}
225+
allowedTypes={allowedChannelTypes}
226+
channels={guildInfo!.channels}
225227
error={errors.modQueueId}
226228
label="Mod Queue (optional)"
227229
onChange={(value) => setFormData({ ...formData, modQueueId: value })}
@@ -230,8 +232,8 @@ export function CreateAMAForm() {
230232
value={formData.modQueueId}
231233
/>{' '}
232234
<ChannelSelect
233-
allowedTypes={[ChannelType.GuildText, ChannelType.GuildAnnouncement]}
234-
channels={guildInfo.channels}
235+
allowedTypes={allowedChannelTypes}
236+
channels={guildInfo!.channels}
235237
error={errors.flaggedQueueId}
236238
label="Flagged Queue (optional)"
237239
onChange={(value) => setFormData({ ...formData, flaggedQueueId: value })}
@@ -240,8 +242,8 @@ export function CreateAMAForm() {
240242
value={formData.flaggedQueueId}
241243
/>{' '}
242244
<ChannelSelect
243-
allowedTypes={[ChannelType.GuildText, ChannelType.GuildAnnouncement]}
244-
channels={guildInfo.channels}
245+
allowedTypes={allowedChannelTypes}
246+
channels={guildInfo!.channels}
245247
error={errors.guestQueueId}
246248
label="Guest Queue (optional)"
247249
onChange={(value) => setFormData({ ...formData, guestQueueId: value })}

apps/website/src/components/common/ChannelSelect.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
// TODO(DD): use Button
2-
31
'use client';
42

53
import type { GuildChannelInfo } from '@chatsift/api';
64
import { sortChannels } from '@chatsift/discord-utils';
75
import { ChannelType } from 'discord-api-types/v10';
86
import { useEffect, useMemo, useRef, useState } from 'react';
97
import { SvgChevronDown } from '../icons/SvgChevronDown';
8+
import { Button } from './Button';
109
import { getChannelIcon } from '@/utils/channels';
1110
import { cn } from '@/utils/util';
1211

@@ -69,9 +68,9 @@ export function ChannelSelect({
6968
{label} {required && '*'}
7069
</label>
7170
<div className="relative" ref={selectRef}>
72-
<button
71+
<Button
7372
className={cn(
74-
'w-full px-3 py-2 border border-on-secondary dark:border-on-secondary-dark rounded-md bg-card dark:bg-card-dark text-primary dark:text-primary-dark focus:outline-none focus:ring-2 focus:ring-misc-accent focus:border-misc-accent text-left flex items-center justify-between',
73+
'text-base w-full px-3 py-2 border border-on-secondary dark:border-on-secondary-dark rounded-md bg-card dark:bg-card-dark text-primary dark:text-primary-dark focus:outline-none focus:ring-2 focus:ring-misc-accent focus:border-misc-accent text-left flex items-center justify-between',
7574
error && 'border-misc-danger focus:ring-misc-danger',
7675
)}
7776
id={selectedId}
@@ -92,7 +91,7 @@ export function ChannelSelect({
9291
)}
9392
size={16}
9493
/>
95-
</button>
94+
</Button>
9695

9796
{isOpen && (
9897
<div className="absolute z-50 w-full mt-1 bg-card dark:bg-card-dark border border-on-secondary dark:border-on-secondary-dark rounded-md shadow-lg max-h-80 overflow-y-auto">
@@ -128,7 +127,7 @@ export function ChannelSelect({
128127
}
129128

130129
return (
131-
<button
130+
<Button
132131
className={cn(
133132
'w-full px-3 py-2 text-left transition-colors',
134133
isSelectable && 'hover:bg-on-tertiary dark:hover:bg-on-tertiary-dark cursor-pointer',
@@ -137,13 +136,12 @@ export function ChannelSelect({
137136
isThread && 'pl-8',
138137
!isThread && hasParent && 'pl-6',
139138
)}
140-
disabled={!isSelectable}
139+
isDisabled={!isSelectable}
141140
key={channel.id}
142141
onClick={() => handleSelect(channel.id, isSelectable)}
143-
type="button"
144142
>
145143
<ChannelItem channel={channel} />
146-
</button>
144+
</Button>
147145
);
148146
})}
149147
</div>

apps/website/src/data/client.tsx

Lines changed: 63 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,93 +8,105 @@ import { routesInfo } from './common';
88
import { APIError, clientSideErrorHandler, useClientSideFetcher } from '@/utils/fetcher';
99
import { exponentialBackOff, retryWrapper } from '@/utils/util';
1010

11-
function make<Options extends MakeOptions & { path: GettableRoutes }>({ path, queryKey, params, query }: Options) {
11+
function buildPath(path: string, params?: Record<string, string>, query?: Record<string, any>) {
1212
const substitutedParams = params
1313
? (Object.entries(params) as [string, string][]).reduce<string>(
1414
(acc, [key, value]) => acc.replace(`:${key}`, encodeURIComponent(value)),
1515
path,
1616
)
1717
: path;
18-
const finalPath = query ? `${substitutedParams}?${new URLSearchParams(query as any).toString()}` : substitutedParams;
1918

20-
function useQueryIt() {
21-
// TODO: Investigate wether this is a react compiler bug or not
22-
// eslint-disable-next-line react-compiler/react-compiler
23-
'use no memo';
19+
return (
20+
query ? `${substitutedParams}?${new URLSearchParams(query as any).toString()}` : substitutedParams
21+
) as `/${string}`;
22+
}
23+
24+
function useQueryIt<Options extends MakeOptions & { path: GettableRoutes }>(
25+
{ path: initialPath, queryKey, params, query }: Options,
26+
doForceFresh = false,
27+
) {
28+
const path = buildPath(initialPath, params, query);
29+
const fetcher = useClientSideFetcher({ path, method: 'GET' });
30+
const queryClient = useQueryClient();
2431

25-
const fetcher = useClientSideFetcher({ path: finalPath as `/${string}`, method: 'GET' });
26-
return useQuery({
27-
queryKey,
32+
const { data, isLoading, error, refetch } = useQuery({
33+
// Dirty method to make sure other queries that wanna pass in force_fresh don't overwrite the others'
34+
// queryFn (effectively causing everything to always pass it in HTTP)
35+
queryKey: doForceFresh ? [...queryKey, 'force_fresh'] : queryKey,
36+
queryFn: async () => {
2837
// @ts-expect-error - This won't ever compile
29-
queryFn: async () => fetcher() as Promise<InferAPIRouteResult<Options['path'], 'GET'> | null>,
30-
throwOnError: clientSideErrorHandler({ throwOverride: false }),
31-
refetchOnWindowFocus: false,
32-
retry: retryWrapper((retries, error) => {
33-
if (error instanceof APIError) {
34-
return retries < 5 && error.payload.statusCode !== 401;
35-
}
38+
const data = (await fetcher()) as Promise<InferAPIRouteResult<Options['path'], 'GET'> | null>;
39+
if (doForceFresh) {
40+
queryClient.setQueryData(queryKey, data);
41+
}
42+
43+
return data;
44+
},
45+
throwOnError: clientSideErrorHandler({ throwOverride: false }),
46+
refetchOnWindowFocus: false,
47+
retry: retryWrapper((retries, error) => {
48+
if (error instanceof APIError) {
49+
return retries < 5 && error.payload.statusCode !== 401;
50+
}
3651

37-
return retries < 3;
38-
}),
39-
retryDelay: exponentialBackOff,
40-
});
41-
}
52+
return retries < 3;
53+
}),
54+
// Prevents double requests when a refresh button of sorts exists that passes doForceFresh
55+
enabled: !doForceFresh,
56+
retryDelay: exponentialBackOff,
57+
});
4258

43-
return useQueryIt;
59+
return { data, isLoading, error, refetch };
4460
}
4561

46-
function makeMutation<Options extends MakeOptions, Method extends 'DELETE' | 'PATCH' | 'POST' | 'PUT'>(
47-
{ path }: Options,
62+
function useMutateIt<Options extends MakeOptions, Method extends 'DELETE' | 'PATCH' | 'POST' | 'PUT'>(
63+
{ path: initialPath, params }: Options,
4864
method: Method,
4965
onSuccess?: (
5066
queryClient: QueryClient,
5167
// @ts-expect-error - We can't get it to compile on the Method
5268
data: InferAPIRouteResult<Options['path'], Method>,
5369
) => Promise<unknown>,
5470
) {
55-
function useMutateIt() {
56-
// TODO: Investigate wether this is a react compiler bug or not
57-
// eslint-disable-next-line react-compiler/react-compiler
58-
'use no memo';
71+
const queryClient = useQueryClient();
72+
const path = buildPath(initialPath, params);
73+
const fetcher = useClientSideFetcher({ path, method });
5974

60-
const queryClient = useQueryClient();
61-
const fetcher = useClientSideFetcher({ path, method });
62-
63-
return useMutation<
64-
// @ts-expect-error - We can't get it to compile on the Method
65-
InferAPIRouteResult<Options['path'], Method>,
66-
APIError,
67-
// @ts-expect-error - We can't get it to compile on the Method
68-
Path<Options['path'], Method>
69-
>({
70-
mutationFn: fetcher,
71-
onSuccess: async (data) => onSuccess?.(queryClient, data),
72-
});
73-
}
74-
75-
return useMutateIt;
75+
return useMutation<
76+
// @ts-expect-error - We can't get it to compile on the Method
77+
InferAPIRouteResult<Options['path'], Method>,
78+
APIError,
79+
// @ts-expect-error - We can't get it to compile on the Method
80+
Path<Options['path'], Method>
81+
>({
82+
mutationFn: fetcher,
83+
onSuccess: async (data) => onSuccess?.(queryClient, data),
84+
});
7685
}
7786

7887
export const client = {
7988
auth: {
80-
useMe: (query?: GetAuthMeQuery) => make(routesInfo.auth.me(query ?? { force_fresh: 'false' }))(),
81-
useLogout: makeMutation(routesInfo.auth.logout, 'POST', async (queryClient) => queryClient.invalidateQueries()),
89+
useMe: (query?: GetAuthMeQuery) =>
90+
useQueryIt(routesInfo.auth.me(query ?? { force_fresh: 'false' }), query?.force_fresh === 'true'),
91+
useLogout: () =>
92+
useMutateIt(routesInfo.auth.logout, 'POST', async (queryClient) => queryClient.invalidateQueries()),
8293
},
8394

8495
guilds: {
85-
useInfo: (guildId: string, query: GetGuildQuery) => make(routesInfo.guilds(guildId).info(query))(),
96+
useInfo: (guildId: string, query: GetGuildQuery) =>
97+
useQueryIt(routesInfo.guilds(guildId).info(query), query?.force_fresh === 'true'),
8698

8799
ama: {
88-
createAMA: (guildId: string) =>
89-
makeMutation(routesInfo.guilds(guildId).ama.amas(), 'POST', async (queryClient) => {
100+
useCreateAMA: (guildId: string) =>
101+
useMutateIt(routesInfo.guilds(guildId).ama.amas(), 'POST', async (queryClient) => {
90102
await queryClient.invalidateQueries({
91103
queryKey: [
92104
routesInfo.guilds(guildId).ama.amas({ include_ended: 'false' }).queryKey,
93105
routesInfo.guilds(guildId).ama.amas({ include_ended: 'true' }).queryKey,
94106
],
95107
});
96-
})(),
97-
useAMAs: (guildId: string, query: GetAMAsQuery) => make(routesInfo.guilds(guildId).ama.amas(query))(),
108+
}),
109+
useAMAs: (guildId: string, query: GetAMAsQuery) => useQueryIt(routesInfo.guilds(guildId).ama.amas(query)),
98110
},
99111
},
100112
} as const;

apps/website/src/data/common.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type MakeOptions<Path extends keyof APIRoutes = keyof APIRoutes> = Path e
1919
readonly queryKey: readonly [string, ...string[]];
2020
}
2121
: {
22+
readonly params: { [ParameterName in ParseHTTPParameters<Path>[number]]: string };
2223
readonly path: Path;
2324
readonly queryKey: readonly [string, ...string[]];
2425
};
@@ -43,20 +44,21 @@ export const routesInfo = {
4344
logout: {
4445
queryKey: ['auth', 'logout'],
4546
path: '/v3/auth/logout',
47+
params: {},
4648
},
4749
},
4850

4951
guilds: (guildId: string) => ({
5052
info: (query: GetGuildQuery) => ({
5153
queryKey: ['guilds', guildId],
5254
path: '/v3/guilds/:guildId',
53-
params: { guildId },
5455
query,
56+
params: { guildId },
5557
}),
5658

5759
ama: {
5860
amas: (query?: GetAMAsQuery) => ({
59-
queryKey: ['guilds', guildId, 'ama', 'amas', String(query?.include_ended ?? false)],
61+
queryKey: ['guilds', guildId, 'ama', 'amas', query?.include_ended ?? 'false'],
6062
path: '/v3/guilds/:guildId/ama/amas',
6163
query: { include_ended: query?.include_ended ?? 'false' },
6264
params: { guildId },

packages/public/discord-utils/src/sortChannels.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ export function sortChannels(unsorted: APIChannel[]): SortableChannel[] {
5555
for (const channel of categoryChannels) {
5656
const channelThreads = threads
5757
.filter((thread) => thread.parent_id === channel.id)
58-
.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
58+
.sort((a, b) => Number(BigInt(b.id) - BigInt(a.id)));
5959
channels.push(...channelThreads);
6060
}
6161
} else {
6262
// Add threads for top-level channels
6363
const channelThreads = threads
6464
.filter((thread) => thread.parent_id === top.id)
65-
.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
65+
.sort((a, b) => Number(BigInt(b.id) - BigInt(a.id)));
6666
channels.push(...channelThreads);
6767
}
6868
}

services/api/src/util/channels.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ export async function fetchGuildChannels(guild: MeGuild, api: API, force = false
4242
position: 0, // Threads don't have a position, this should be good enough
4343
}));
4444

45-
CACHE.set(guild.id, channels);
45+
const allChannels = channels.concat(threads);
46+
47+
CACHE.set(guild.id, allChannels);
4648
if (CACHE_TIMEOUTS.has(guild.id)) {
4749
const timeout = CACHE_TIMEOUTS.get(guild.id)!;
4850
timeout.refresh();
@@ -55,5 +57,5 @@ export async function fetchGuildChannels(guild: MeGuild, api: API, force = false
5557
CACHE_TIMEOUTS.set(guild.id, timeout);
5658
}
5759

58-
return channels.concat(threads);
60+
return allChannels;
5961
}

services/api/src/util/stateCookie.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export class StateCookie {
55
public static from(data: string): StateCookie {
66
const bytes = Buffer.from(data, 'base64');
77
const nonce = bytes.subarray(0, 16);
8-
const createdAt = new Date(bytes.readUInt32LE(16));
8+
const createdAt = new Date(bytes.readUInt32LE(16) * 1_000);
99
const redirectURI = bytes.subarray(20).toString();
1010

1111
return new this(redirectURI, nonce, createdAt);

0 commit comments

Comments
 (0)