Skip to content

Commit b17e681

Browse files
committed
feat: infinite notifications
1 parent 257a4f0 commit b17e681

File tree

2 files changed

+58
-19
lines changed

2 files changed

+58
-19
lines changed
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1-
import { useQuery } from '@tanstack/react-query';
1+
import { useInfiniteQuery } from '@tanstack/react-query';
22
import { useBlueskyStore } from '../store';
33
import { BSkyNotification } from '../types/BSkyNotification';
44

5+
type Notifications = {
6+
cursor: string;
7+
notifications: BSkyNotification[];
8+
};
9+
510
export function useNotifications() {
611
const { agent, isAuthenticated } = useBlueskyStore();
712

8-
return useQuery({
13+
return useInfiniteQuery<Notifications>({
914
queryKey: ['notifications'],
10-
queryFn: async () => {
15+
queryFn: async ({ pageParam: cursor }) => {
1116
if (!agent || !isAuthenticated) throw new Error('Not authenticated');
1217

13-
const response = await agent.api.app.bsky.notification.listNotifications();
14-
return response.data.notifications as BSkyNotification[];
18+
const response = await agent.api.app.bsky.notification.listNotifications({ cursor: cursor as string });
19+
return response.data as Notifications;
1520
},
21+
getNextPageParam: (lastPage) => lastPage.cursor,
22+
initialPageParam: undefined,
1623
enabled: !!agent,
1724
});
1825
}

src/routes/notifications.tsx

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,24 @@ import { createFileRoute } from '@tanstack/react-router';
33
import { useTranslation } from 'react-i18next';
44
import { useNotifications } from '../lib/bluesky/hooks/useNotifications';
55
import { Debug } from '../components/ui/Debug';
6-
import { useState } from 'react';
6+
import { forwardRef, useState } from 'react';
77
import { cn } from '../lib/utils';
88
import { BSkyNotification } from '../lib/bluesky/types/BSkyNotification';
99
import { Link } from '../components/ui/Link';
1010
import { useBlueskyStore } from '../lib/bluesky/store';
1111
import { Image } from '../components/ui/Image';
1212
import { Handle } from '../components/ui/Handle';
13+
import { Virtuoso } from 'react-virtuoso';
14+
import { Button } from '../components/ui/Button';
1315

1416
export const Route = createFileRoute('/notifications')({
1517
component: RouteComponent,
1618
});
1719

1820
function RouteComponent() {
1921
const { t } = useTranslation(['app', 'notifications']);
20-
const { data: notifications, isLoading } = useNotifications();
22+
const { data, isLoading } = useNotifications();
23+
const notifications = data?.pages.flatMap((page) => page.notifications);
2124
const mentions = notifications?.filter(
2225
(notification) =>
2326
notification.reason === 'mention' || notification.reason === 'reply' || notification.reason === 'quote',
@@ -65,9 +68,7 @@ function RouteComponent() {
6568
</Ariakit.Tab>
6669
</Ariakit.TabList>
6770
<div className="p-2">
68-
<Ariakit.TabPanel tabId="grouped">
69-
{notifications && <GroupedNotifications notifications={notifications} />}
70-
</Ariakit.TabPanel>
71+
<Ariakit.TabPanel tabId="grouped">{notifications && <GroupedNotifications />}</Ariakit.TabPanel>
7172
<Ariakit.TabPanel tabId="all">
7273
{notifications?.map((notification) => <Notification key={notification.uri} notification={notification} />)}
7374
</Ariakit.TabPanel>
@@ -81,8 +82,11 @@ function RouteComponent() {
8182
}
8283

8384
// group notifications by uri
84-
function GroupedNotifications({ notifications }: { notifications: BSkyNotification[] }) {
85+
function GroupedNotifications() {
8586
const { t } = useTranslation('notifications');
87+
const { data, isLoading, fetchNextPage, isFetching } = useNotifications();
88+
const notifications = data?.pages.flatMap((page) => page.notifications);
89+
if (!notifications) return null;
8690
const grouped = notifications.reduce(
8791
(acc, notification) => {
8892
if (notification.reason === 'like') {
@@ -97,15 +101,43 @@ function GroupedNotifications({ notifications }: { notifications: BSkyNotificati
97101
{} as Record<string, BSkyNotification[]>,
98102
);
99103

104+
if (isLoading) return <div>{t('loading')}</div>;
105+
106+
const list = Object.values(grouped);
107+
100108
return (
101-
<div>
102-
{Object.entries(grouped).map(([uri, notifications]) => (
103-
<div key={uri} className="p-2 bg-neutral-800 rounded-lg mb-2">
104-
<div className="text-sm text-neutral-400">{t('groupedNotifications')}</div>
105-
<GroupNotification key={notifications[0]?.uri} notifications={notifications} />
106-
</div>
107-
))}
108-
</div>
109+
<Virtuoso
110+
useWindowScroll
111+
totalCount={list.length}
112+
endReached={() => fetchNextPage()}
113+
components={{
114+
List: forwardRef((props, ref) => <div ref={ref} {...props} className="flex flex-col gap-2" />),
115+
Footer: () => {
116+
return isFetching ? (
117+
<div className="p-2 text-center">{t('loading')}</div>
118+
) : (
119+
<div className="p-2 text-center">
120+
<Button
121+
onClick={() => {
122+
fetchNextPage();
123+
}}
124+
>
125+
load more
126+
</Button>
127+
</div>
128+
);
129+
},
130+
}}
131+
itemContent={(index: number) => {
132+
if (!list[index]) return null;
133+
return (
134+
<div key={list[index][0]?.uri} className="p-2 bg-neutral-800 rounded-lg mb-2">
135+
<div className="text-sm text-neutral-400">{t('groupedNotifications')}</div>
136+
<GroupNotification key={list[index][0]?.uri} notifications={list[index]} />
137+
</div>
138+
);
139+
}}
140+
/>
109141
);
110142
}
111143

0 commit comments

Comments
 (0)