From b714e8c596cc697a77853e2532fabf07649620c6 Mon Sep 17 00:00:00 2001 From: Luna <105922175+ImLunaHey@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:03:00 +1030 Subject: [PATCH] feat: basic notifications (#9) --- src/components/Navbar.tsx | 8 +- src/i18n/lang/en.ts | 11 +++ src/lib/bluesky/hooks/useNotifications.ts | 17 ++++ src/routeTree.gen.ts | 26 +++++ src/routes/notifications.tsx | 112 ++++++++++++++++++++++ 5 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/lib/bluesky/hooks/useNotifications.ts create mode 100644 src/routes/notifications.tsx diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 44fdbe0..a0d3bb0 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -3,6 +3,11 @@ import { useAuth } from '../lib/bluesky/hooks/useAuth'; import { Link } from './ui/Link'; import { useBlueskyStore } from '../lib/bluesky/store'; +const NotificationsLink = () => { + const { t } = useTranslation('notifications'); + return {t('notifications')}; +}; + const ProfileLink = () => { const { session } = useBlueskyStore(); const { t } = useTranslation('profile'); @@ -48,7 +53,8 @@ export const Navbar = () => {

{t('appName')}

- + {isAuthenticated && } + {isAuthenticated && } {isAuthenticated ? : }
diff --git a/src/i18n/lang/en.ts b/src/i18n/lang/en.ts index 0cec618..87e35fe 100644 --- a/src/i18n/lang/en.ts +++ b/src/i18n/lang/en.ts @@ -81,4 +81,15 @@ export const en = { debug: { notImplemented: 'NOT IMPLEMENTED: {{value}}', }, + notifications: { + notifications: 'notifications', + noNotifications: 'no notifications', + followedYou: 'followed you', + likedYourPost: 'liked your post', + repostedYourPost: 'reposted your post', + repliedToYourPost: 'replied to your post', + mentionedYou: 'mentioned you', + quotedYourPost: 'quoted your post', + joinedYourStarterpack: 'joined your starterpack', + }, } as const; diff --git a/src/lib/bluesky/hooks/useNotifications.ts b/src/lib/bluesky/hooks/useNotifications.ts new file mode 100644 index 0000000..4c7f146 --- /dev/null +++ b/src/lib/bluesky/hooks/useNotifications.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import { useBlueskyStore } from '../store'; + +export function useNotifications() { + const { agent } = useBlueskyStore(); + + return useQuery({ + queryKey: ['notifications'], + queryFn: async () => { + if (!agent) throw new Error('Not authenticated'); + + const response = await agent.api.app.bsky.notification.listNotifications(); + return response.data.notifications; + }, + enabled: !!agent, + }); +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 7a02ddf..3a1f8a8 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -13,6 +13,7 @@ import { createFileRoute } from '@tanstack/react-router' // Import Routes import { Route as rootRoute } from './routes/__root' +import { Route as NotificationsImport } from './routes/notifications' import { Route as ProfileHandleRouteImport } from './routes/profile/$handle/route' import { Route as ProfileHandleIndexImport } from './routes/profile/$handle/index' import { Route as ProfileHandlePostPostIdImport } from './routes/profile/$handle/post.$postId' @@ -38,6 +39,12 @@ const LoginLazyRoute = LoginLazyImport.update({ getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/login.lazy').then((d) => d.Route)) +const NotificationsRoute = NotificationsImport.update({ + id: '/notifications', + path: '/notifications', + getParentRoute: () => rootRoute, +} as any) + const IndexLazyRoute = IndexLazyImport.update({ id: '/', path: '/', @@ -79,6 +86,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexLazyImport parentRoute: typeof rootRoute } + '/notifications': { + id: '/notifications' + path: '/notifications' + fullPath: '/notifications' + preLoaderRoute: typeof NotificationsImport + parentRoute: typeof rootRoute + } '/login': { id: '/login' path: '/login' @@ -141,6 +155,7 @@ const ProfileHandleRouteRouteWithChildren = export interface FileRoutesByFullPath { '/': typeof IndexLazyRoute + '/notifications': typeof NotificationsRoute '/login': typeof LoginLazyRoute '/settings': typeof SettingsLazyRoute '/profile/$handle': typeof ProfileHandleRouteRouteWithChildren @@ -151,6 +166,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexLazyRoute + '/notifications': typeof NotificationsRoute '/login': typeof LoginLazyRoute '/settings': typeof SettingsLazyRoute '/tag/$tag': typeof TagTagLazyRoute @@ -161,6 +177,7 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexLazyRoute + '/notifications': typeof NotificationsRoute '/login': typeof LoginLazyRoute '/settings': typeof SettingsLazyRoute '/profile/$handle': typeof ProfileHandleRouteRouteWithChildren @@ -173,6 +190,7 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' + | '/notifications' | '/login' | '/settings' | '/profile/$handle' @@ -182,6 +200,7 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/' + | '/notifications' | '/login' | '/settings' | '/tag/$tag' @@ -190,6 +209,7 @@ export interface FileRouteTypes { id: | '__root__' | '/' + | '/notifications' | '/login' | '/settings' | '/profile/$handle' @@ -201,6 +221,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexLazyRoute: typeof IndexLazyRoute + NotificationsRoute: typeof NotificationsRoute LoginLazyRoute: typeof LoginLazyRoute SettingsLazyRoute: typeof SettingsLazyRoute ProfileHandleRouteRoute: typeof ProfileHandleRouteRouteWithChildren @@ -209,6 +230,7 @@ export interface RootRouteChildren { const rootRouteChildren: RootRouteChildren = { IndexLazyRoute: IndexLazyRoute, + NotificationsRoute: NotificationsRoute, LoginLazyRoute: LoginLazyRoute, SettingsLazyRoute: SettingsLazyRoute, ProfileHandleRouteRoute: ProfileHandleRouteRouteWithChildren, @@ -226,6 +248,7 @@ export const routeTree = rootRoute "filePath": "__root.tsx", "children": [ "/", + "/notifications", "/login", "/settings", "/profile/$handle", @@ -235,6 +258,9 @@ export const routeTree = rootRoute "/": { "filePath": "index.lazy.tsx" }, + "/notifications": { + "filePath": "notifications.tsx" + }, "/login": { "filePath": "login.lazy.tsx" }, diff --git a/src/routes/notifications.tsx b/src/routes/notifications.tsx new file mode 100644 index 0000000..127d081 --- /dev/null +++ b/src/routes/notifications.tsx @@ -0,0 +1,112 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { useTranslation } from 'react-i18next'; +import { useNotifications } from '../lib/bluesky/hooks/useNotifications'; +import { Debug } from '../components/ui/Debug'; +import { Notification as BskyNotification } from '@atproto/api/dist/client/types/app/bsky/notification/listNotifications'; + +export const Route = createFileRoute('/notifications')({ + component: RouteComponent, +}); + +function RouteComponent() { + const { t } = useTranslation(['app', 'notifications']); + const { data: notifications, isLoading } = useNotifications(); + + if (isLoading) return
{t('loading')}
; + + return ( +
+ {t('notifications:notifications')} + {notifications?.map((notification) => )} +
+ ); +} + +function Notification({ notification }: { notification: BskyNotification }) { + switch (notification.reason) { + case 'follow': + return ; + case 'like': + return ; + case 'repost': + return ; + case 'reply': + return ; + case 'mention': + return ; + case 'quote': + return ; + case 'starterpack-joined': + return ; + } + + return ( +
+ {notification.author.displayName} + +
+ ); +} + +function FollowNotification({ notification }: { notification: BskyNotification }) { + const { t } = useTranslation('notifications'); + return ( +
+ {notification.author.displayName} {t('followedYou')} +
+ ); +} + +function LikeNotification({ notification }: { notification: BskyNotification }) { + const { t } = useTranslation('notifications'); + return ( +
+ {notification.author.displayName} {t('likedYourPost')} +
+ ); +} + +function RepostNotification({ notification }: { notification: BskyNotification }) { + const { t } = useTranslation('notifications'); + return ( +
+ {notification.author.displayName} {t('repostedYourPost')} +
+ ); +} + +function ReplyNotification({ notification }: { notification: BskyNotification }) { + const { t } = useTranslation('notifications'); + return ( +
+ {notification.author.displayName} {t('repliedToYourPost')} +
+ ); +} + +function MentionNotification({ notification }: { notification: BskyNotification }) { + const { t } = useTranslation('notifications'); + return ( +
+ {notification.author.displayName} {t('mentionedYou')} +
+ ); +} + +function QuoteNotification({ notification }: { notification: BskyNotification }) { + const { t } = useTranslation('notifications'); + return ( +
+ {notification.author.displayName} {t('quotedYourPost')} +
+ ); +} + +function StarterpackJoinedNotification({ notification }: { notification: BskyNotification }) { + const { t } = useTranslation('notifications'); + return ( +
+ {notification.author.displayName} {t('joinedYourStarterpack')} +
+ ); +}