Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: basic messages #12

Merged
merged 2 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { useAuth } from '../lib/bluesky/hooks/useAuth';
import { Link } from './ui/Link';
import { useBlueskyStore } from '../lib/bluesky/store';

const MessagesLink = () => {
const { t } = useTranslation('messages');
return <Link to="/messages">{t('messages')}</Link>;
};

const NotificationsLink = () => {
const { t } = useTranslation('notifications');
return <Link to="/notifications">{t('notifications')}</Link>;
Expand Down Expand Up @@ -53,6 +58,7 @@ export const Navbar = () => {
<h1 className="text-2xl font-bold">{t('appName')}</h1>
</Link>
<div className="flex flex-row gap-2">
{isAuthenticated && <MessagesLink />}
{isAuthenticated && <NotificationsLink />}
{isAuthenticated && <ProfileLink />}
<SettingsLink />
Expand Down
9 changes: 7 additions & 2 deletions src/components/ui/Handle.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { useSettings } from '../../hooks/useSetting';
import { useProfile } from '../../lib/bluesky/hooks/useProfile';

export const Handle = ({ handle }: { handle: string }) => {
const { experiments } = useSettings();
const { data: profile } = useProfile({ handle });

const resolvedHandle = profile?.handle || handle;

if (experiments.cleanHandles) {
return `@${handle.replace('.bsky.social', '')}`;
return `@${resolvedHandle.replace('.bsky.social', '')}`;
}

return `@${handle}`;
return `@${resolvedHandle}`;
};
4 changes: 4 additions & 0 deletions src/i18n/lang/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,8 @@ export const en = {
quotedYourPost: 'quoted your post',
joinedYourStarterpack: 'joined your starterpack',
},
messages: {
messages: 'messages',
noMessages: 'no messages',
},
} as const;
26 changes: 26 additions & 0 deletions src/lib/bluesky/hooks/useConversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useQuery } from '@tanstack/react-query';
import { useBlueskyStore } from '../store';
import { useAuth } from './useAuth';

export function useConversation({ convoId }: { convoId: string }) {
const { agent, session } = useBlueskyStore();
const { isAuthenticated } = useAuth();

return useQuery({
queryKey: ['conversations', { convoId }],
queryFn: async () => {
if (!session) return [];

// @ts-expect-error bsky_chat does in fact work
const proxy = agent?.withProxy('bsky_chat', 'did:web:api.bsky.chat');
const response = await proxy?.api.chat.bsky.convo.getMessages({
convoId,
});

return response?.data.messages
.slice(0)
.sort((a, b) => new Date(a.sentAt as string).getTime() - new Date(b.sentAt as string).getTime());
},
enabled: !!agent && isAuthenticated,
});
}
23 changes: 23 additions & 0 deletions src/lib/bluesky/hooks/useConversations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { useBlueskyStore } from '../store';
import { useAuth } from './useAuth';

export function useConversations() {
const { agent, session } = useBlueskyStore();
const { isAuthenticated } = useAuth();

return useQuery({
queryKey: ['conversations'],
queryFn: async () => {
if (!session) return [];
const pdsUrl = session.didDoc?.service[0]?.serviceEndpoint;
if (!pdsUrl) return [];

// @ts-expect-error bsky_chat does in fact work
const proxy = agent?.withProxy('bsky_chat', 'did:web:api.bsky.chat');
const response = await proxy?.api.chat.bsky.convo.listConvos();
return response?.data.convos;
},
enabled: !!agent && isAuthenticated,
});
}
15 changes: 14 additions & 1 deletion src/lib/bluesky/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,22 @@ export type BlueskyCredentials = {
password: string;
};

type Session = AtpSessionData & {
didDoc?:
| {
service: {
id: string;
serviceEndpoint: string;
type: 'AtprotoPersonalDataServer';
}[];
}
| undefined;
};

type BlueskyState = {
agent: BskyAgent | null;
isAuthenticated: boolean;
session: AtpSessionData | null;
session: Session | null;
login: (credentials: BlueskyCredentials) => Promise<void>;
logout: () => void;
restoreSession: () => Promise<void>;
Expand Down Expand Up @@ -40,6 +52,7 @@ export const useBlueskyStore = create<BlueskyState>()(
session: {
...session,
active: true,
didDoc: session.didDoc as Session['didDoc'],
},
});
},
Expand Down
54 changes: 53 additions & 1 deletion src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { createFileRoute } from '@tanstack/react-router'

import { Route as rootRoute } from './routes/__root'
import { Route as NotificationsImport } from './routes/notifications'
import { Route as MessagesIndexImport } from './routes/messages/index'
import { Route as MessagesConvoIdImport } from './routes/messages/$convoId'
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'
Expand Down Expand Up @@ -51,12 +53,24 @@ const IndexLazyRoute = IndexLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))

const MessagesIndexRoute = MessagesIndexImport.update({
id: '/messages/',
path: '/messages/',
getParentRoute: () => rootRoute,
} as any)

const TagTagLazyRoute = TagTagLazyImport.update({
id: '/tag/$tag',
path: '/tag/$tag',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/tag.$tag.lazy').then((d) => d.Route))

const MessagesConvoIdRoute = MessagesConvoIdImport.update({
id: '/messages/$convoId',
path: '/messages/$convoId',
getParentRoute: () => rootRoute,
} as any)

const ProfileHandleRouteRoute = ProfileHandleRouteImport.update({
id: '/profile/$handle',
path: '/profile/$handle',
Expand Down Expand Up @@ -114,13 +128,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProfileHandleRouteImport
parentRoute: typeof rootRoute
}
'/messages/$convoId': {
id: '/messages/$convoId'
path: '/messages/$convoId'
fullPath: '/messages/$convoId'
preLoaderRoute: typeof MessagesConvoIdImport
parentRoute: typeof rootRoute
}
'/tag/$tag': {
id: '/tag/$tag'
path: '/tag/$tag'
fullPath: '/tag/$tag'
preLoaderRoute: typeof TagTagLazyImport
parentRoute: typeof rootRoute
}
'/messages/': {
id: '/messages/'
path: '/messages'
fullPath: '/messages'
preLoaderRoute: typeof MessagesIndexImport
parentRoute: typeof rootRoute
}
'/profile/$handle/': {
id: '/profile/$handle/'
path: '/'
Expand Down Expand Up @@ -159,7 +187,9 @@ export interface FileRoutesByFullPath {
'/login': typeof LoginLazyRoute
'/settings': typeof SettingsLazyRoute
'/profile/$handle': typeof ProfileHandleRouteRouteWithChildren
'/messages/$convoId': typeof MessagesConvoIdRoute
'/tag/$tag': typeof TagTagLazyRoute
'/messages': typeof MessagesIndexRoute
'/profile/$handle/': typeof ProfileHandleIndexRoute
'/profile/$handle/post/$postId': typeof ProfileHandlePostPostIdRoute
}
Expand All @@ -169,7 +199,9 @@ export interface FileRoutesByTo {
'/notifications': typeof NotificationsRoute
'/login': typeof LoginLazyRoute
'/settings': typeof SettingsLazyRoute
'/messages/$convoId': typeof MessagesConvoIdRoute
'/tag/$tag': typeof TagTagLazyRoute
'/messages': typeof MessagesIndexRoute
'/profile/$handle': typeof ProfileHandleIndexRoute
'/profile/$handle/post/$postId': typeof ProfileHandlePostPostIdRoute
}
Expand All @@ -181,7 +213,9 @@ export interface FileRoutesById {
'/login': typeof LoginLazyRoute
'/settings': typeof SettingsLazyRoute
'/profile/$handle': typeof ProfileHandleRouteRouteWithChildren
'/messages/$convoId': typeof MessagesConvoIdRoute
'/tag/$tag': typeof TagTagLazyRoute
'/messages/': typeof MessagesIndexRoute
'/profile/$handle/': typeof ProfileHandleIndexRoute
'/profile/$handle/post/$postId': typeof ProfileHandlePostPostIdRoute
}
Expand All @@ -194,7 +228,9 @@ export interface FileRouteTypes {
| '/login'
| '/settings'
| '/profile/$handle'
| '/messages/$convoId'
| '/tag/$tag'
| '/messages'
| '/profile/$handle/'
| '/profile/$handle/post/$postId'
fileRoutesByTo: FileRoutesByTo
Expand All @@ -203,7 +239,9 @@ export interface FileRouteTypes {
| '/notifications'
| '/login'
| '/settings'
| '/messages/$convoId'
| '/tag/$tag'
| '/messages'
| '/profile/$handle'
| '/profile/$handle/post/$postId'
id:
Expand All @@ -213,7 +251,9 @@ export interface FileRouteTypes {
| '/login'
| '/settings'
| '/profile/$handle'
| '/messages/$convoId'
| '/tag/$tag'
| '/messages/'
| '/profile/$handle/'
| '/profile/$handle/post/$postId'
fileRoutesById: FileRoutesById
Expand All @@ -225,7 +265,9 @@ export interface RootRouteChildren {
LoginLazyRoute: typeof LoginLazyRoute
SettingsLazyRoute: typeof SettingsLazyRoute
ProfileHandleRouteRoute: typeof ProfileHandleRouteRouteWithChildren
MessagesConvoIdRoute: typeof MessagesConvoIdRoute
TagTagLazyRoute: typeof TagTagLazyRoute
MessagesIndexRoute: typeof MessagesIndexRoute
}

const rootRouteChildren: RootRouteChildren = {
Expand All @@ -234,7 +276,9 @@ const rootRouteChildren: RootRouteChildren = {
LoginLazyRoute: LoginLazyRoute,
SettingsLazyRoute: SettingsLazyRoute,
ProfileHandleRouteRoute: ProfileHandleRouteRouteWithChildren,
MessagesConvoIdRoute: MessagesConvoIdRoute,
TagTagLazyRoute: TagTagLazyRoute,
MessagesIndexRoute: MessagesIndexRoute,
}

export const routeTree = rootRoute
Expand All @@ -252,7 +296,9 @@ export const routeTree = rootRoute
"/login",
"/settings",
"/profile/$handle",
"/tag/$tag"
"/messages/$convoId",
"/tag/$tag",
"/messages/"
]
},
"/": {
Expand All @@ -274,9 +320,15 @@ export const routeTree = rootRoute
"/profile/$handle/post/$postId"
]
},
"/messages/$convoId": {
"filePath": "messages/$convoId.tsx"
},
"/tag/$tag": {
"filePath": "tag.$tag.lazy.tsx"
},
"/messages/": {
"filePath": "messages/index.tsx"
},
"/profile/$handle/": {
"filePath": "profile/$handle/index.tsx",
"parent": "/profile/$handle"
Expand Down
30 changes: 30 additions & 0 deletions src/routes/messages/$convoId.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createFileRoute } from '@tanstack/react-router';
import { useConversation } from '../../lib/bluesky/hooks/useConversation';
import { useTranslation } from 'react-i18next';
import { Handle } from '../../components/ui/Handle';
import { Debug } from '../../components/ui/Debug';
import TimeAgo from 'react-timeago-i18n';

export const Route = createFileRoute('/messages/$convoId')({
component: RouteComponent,
});

function RouteComponent() {
const { t } = useTranslation('app');
const { convoId } = Route.useParams();
const { data: messages, isLoading } = useConversation({ convoId });

if (isLoading) return <div>{t('loading')}</div>;

return (
<div className="flex flex-col gap-2 overflow-y-auto h-full">
{messages?.map((message) => (
<div key={message.id as string}>
<Handle handle={(message.sender as { did: string }).did} />: {message.text as string} -{' '}
<TimeAgo date={message.sentAt as string} />
<Debug value={message} />
</div>
))}
</div>
);
}
40 changes: 40 additions & 0 deletions src/routes/messages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { createFileRoute } from '@tanstack/react-router';
import { useConversations } from '../../lib/bluesky/hooks/useConversations';
import { useTranslation } from 'react-i18next';
import { Image } from '../../components/ui/Image';
import { cn } from '../../lib/utils';
import { useBlueskyStore } from '../../lib/bluesky/store';
import { Link } from '../../components/ui/Link';

export const Route = createFileRoute('/messages/')({
component: Messages,
});

function Messages() {
const { session } = useBlueskyStore();
const { data, isLoading } = useConversations();
const { t } = useTranslation('app');

if (isLoading) return <div>{t('loading')}</div>;

return (
<div className="flex flex-col gap-2 overflow-y-auto h-screen">
{data?.map((convo) => (
<Link key={convo.id} to="/messages/$convoId" params={{ convoId: convo.id }}>
{convo.members
.filter((member) => member.did !== session?.did)
.map((member) => (
<div className="flex gap-2" key={member.did}>
<Image
type="avatar"
src={member.avatar}
className={cn('size-24', member.associated?.labeler ? 'aspect-square' : 'rounded-full')}
/>
<div>{convo.lastMessage?.text as string}</div>
</div>
))}
</Link>
))}
</div>
);
}
Loading