Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

useNotifications.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

useNotifications.tsBlame109 lines · 1 contributor
c53cf00Claude1import { useState, useCallback } from 'react';
2import { api, type ActivityEvent } from '../api/client';
3
4export interface Notification {
5 id: string;
6 type: string;
7 title: string;
8 subtitle: string;
9 read: boolean;
10 createdAt: string;
11 repoOwner?: string;
12 repoName?: string;
13}
14
15function activityToNotification(event: ActivityEvent & { repoOwner?: string; repoName?: string }): Notification {
16 const type = event.type || 'activity';
17 const payload = event.payload as Record<string, unknown>;
18
19 let title = 'Activity';
20 let subtitle = '';
21
22 switch (type) {
23 case 'push':
24 title = 'Push';
25 subtitle = `${payload.ref ?? 'branch'} updated`;
26 break;
27 case 'issue.opened':
28 title = 'Issue opened';
29 subtitle = String(payload.title ?? '');
30 break;
31 case 'issue.closed':
32 title = 'Issue closed';
33 subtitle = String(payload.title ?? '');
34 break;
35 case 'pr.opened':
36 title = 'PR opened';
37 subtitle = String(payload.title ?? '');
38 break;
39 case 'pr.merged':
40 title = 'PR merged';
41 subtitle = String(payload.title ?? '');
42 break;
43 case 'star':
44 title = 'New star';
45 subtitle = String(payload.username ?? 'Someone') + ' starred the repo';
46 break;
47 default:
48 title = type.replace(/[._]/g, ' ');
49 subtitle = '';
50 }
51
52 return {
53 id: event.id,
54 type,
55 title,
56 subtitle,
57 read: false,
58 createdAt: event.createdAt,
59 repoOwner: event.repoOwner,
60 repoName: event.repoName,
61 };
62}
63
64// Notifications are synthesized from activity feeds across user repos.
65// The Gluecron REST API doesn't expose a dedicated /notifications endpoint,
66// so we aggregate from the repos the user cares about.
67export function useNotifications(repos: Array<{ owner: string; name: string }>) {
68 const [notifications, setNotifications] = useState<Notification[]>([]);
69 const [loading, setLoading] = useState(false);
70 const [error, setError] = useState<string | null>(null);
71
72 const fetch = useCallback(async () => {
73 if (repos.length === 0) return;
74 setLoading(true);
75 setError(null);
76 try {
77 const results = await Promise.allSettled(
78 repos.slice(0, 5).map((r) =>
79 api.getRepoActivity(r.owner, r.name, 10).then((events) =>
80 events.map((e) => activityToNotification({ ...e, repoOwner: r.owner, repoName: r.name }))
81 )
82 )
83 );
84
85 const allNotifs: Notification[] = [];
86 for (const r of results) {
87 if (r.status === 'fulfilled') {
88 allNotifs.push(...r.value);
89 }
90 }
91
92 // Sort newest first
93 allNotifs.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
94 setNotifications(allNotifs.slice(0, 50));
95 } catch (err) {
96 setError(err instanceof Error ? err.message : 'Failed to load notifications');
97 } finally {
98 setLoading(false);
99 }
100 }, [repos]);
101
102 const markAllRead = useCallback(() => {
103 setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
104 }, []);
105
106 const unreadCount = notifications.filter((n) => !n.read).length;
107
108 return { notifications, loading, error, refresh: fetch, markAllRead, unreadCount };
109}