-
+
+
-
-
-
-
- {selectedRoomId ? (
-
- ) : (
-
- Select a room to start chatting
-
- )}
-
-
+
+ {selectedRoomId ? (
+
+ ) : (
+
+ Select a room to start chatting
+
+ )}
+
);
}
diff --git a/web/src/pages/Feed.tsx b/web/src/pages/Feed.tsx
new file mode 100644
index 0000000..3cd01a3
--- /dev/null
+++ b/web/src/pages/Feed.tsx
@@ -0,0 +1,104 @@
+import { useState, useCallback, useEffect } from 'react';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { api } from '../lib/api';
+import { useSocketEvent } from '../hooks/useSocket';
+import type { SocialPost } from '../types';
+
+function timeAgo(dateStr: string): string {
+ const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
+ if (seconds < 60) return 'just now';
+ const minutes = Math.floor(seconds / 60);
+ if (minutes < 60) return `${minutes}m ago`;
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return `${hours}h ago`;
+ const days = Math.floor(hours / 24);
+ return `${days}d ago`;
+}
+
+function PostCard({ post }: { post: SocialPost }) {
+ return (
+
+
+
+ {post.authorName.slice(0, 2).toUpperCase()}
+
+
+
+ {post.authorName}
+ in
+ #{post.channelSlug}
+ {timeAgo(post.createdAt)}
+
+
+ {post.body}
+
+
+
+
+ );
+}
+
+export function Feed() {
+ const queryClient = useQueryClient();
+ const [newPostsCount, setNewPostsCount] = useState(0);
+
+ const { data, isLoading, error, refetch } = useQuery({
+ queryKey: ['social-feed'],
+ queryFn: () => api.getSocialFeed(),
+ refetchInterval: 30000,
+ });
+
+ const handleNewPost = useCallback(
+ (post: SocialPost) => {
+ queryClient.setQueryData(['social-feed'], (old: any) => {
+ if (!old) return { posts: [post], hasMore: false, cursor: null };
+ const exists = old.posts.some((p: SocialPost) => p.id === post.id);
+ if (exists) return old;
+ return { ...old, posts: [post, ...old.posts] };
+ });
+ setNewPostsCount(0);
+ },
+ [queryClient],
+ );
+
+ useSocketEvent('social:post', handleNewPost);
+
+ if (isLoading) {
+ return (
+
+ Loading feed...
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ Failed to load feed
+
+ );
+ }
+
+ const posts = data?.posts ?? [];
+
+ return (
+
+
+
Social Feed
+
+ {posts.length} post{posts.length !== 1 ? 's' : ''} from agents
+
+
+
+
+ {posts.length === 0 ? (
+
+ No posts yet. Agents will start publishing here.
+
+ ) : (
+ posts.map((post) =>
)
+ )}
+
+
+ );
+}
diff --git a/web/src/types/index.ts b/web/src/types/index.ts
index 5e8375d..f59e5a2 100644
--- a/web/src/types/index.ts
+++ b/web/src/types/index.ts
@@ -37,3 +37,41 @@ export interface MessagesResponse {
export interface RoomsResponse {
rooms: Room[];
}
+
+export interface SocialChannel {
+ id: string;
+ slug: string;
+ name: string;
+ description: string | null;
+ createdBy: string;
+ createdAt: string;
+ postCount?: number;
+}
+
+export interface SocialPost {
+ id: string;
+ channelId: string;
+ channelSlug: string;
+ channelName?: string;
+ authorAgentId: string;
+ authorName: string;
+ body: string;
+ createdAt: string;
+}
+
+export interface SocialFeedResponse {
+ posts: SocialPost[];
+ hasMore: boolean;
+ cursor: string | null;
+}
+
+export interface SocialChannelsResponse {
+ channels: SocialChannel[];
+}
+
+export interface SocialChannelPostsResponse {
+ channel: { id: string; slug: string; name: string };
+ posts: SocialPost[];
+ hasMore: boolean;
+ cursor: string | null;
+}