extremely claude-assisted go game based on atproto! working on cleaning up and giving a more unique design, still has a bit of a slop vibe to it.
1import { browser } from '$app/environment';
2
3export class GameNotifications {
4 private originalTitle: string;
5 private titleInterval: ReturnType<typeof setInterval> | null = null;
6 private notificationPermission: NotificationPermission = 'default';
7
8 constructor() {
9 if (browser) {
10 this.originalTitle = document.title;
11 // iOS Safari doesn't support the Notification API
12 this.notificationPermission = ('Notification' in window) ? Notification.permission : 'denied';
13 } else {
14 this.originalTitle = '';
15 }
16 }
17
18 /**
19 * Request notification permission from the user
20 */
21 async requestPermission(): Promise<boolean> {
22 if (!browser || !('Notification' in window)) {
23 return false;
24 }
25
26 if (Notification.permission === 'granted') {
27 this.notificationPermission = 'granted';
28 return true;
29 }
30
31 if (Notification.permission !== 'denied') {
32 const permission = await Notification.requestPermission();
33 this.notificationPermission = permission;
34 return permission === 'granted';
35 }
36
37 return false;
38 }
39
40 /**
41 * Start flashing the page title to get attention
42 */
43 startTitleFlash(message: string) {
44 if (!browser) return;
45
46 this.stopTitleFlash(); // Clear any existing flash
47 this.originalTitle = document.title;
48
49 let isOriginal = true;
50 this.titleInterval = setInterval(() => {
51 document.title = isOriginal ? message : this.originalTitle;
52 isOriginal = !isOriginal;
53 }, 1000);
54 }
55
56 /**
57 * Stop flashing the title and restore the original
58 */
59 stopTitleFlash() {
60 if (!browser) return;
61
62 if (this.titleInterval) {
63 clearInterval(this.titleInterval);
64 this.titleInterval = null;
65 }
66 document.title = this.originalTitle;
67 }
68
69 /**
70 * Show a browser push notification
71 */
72 async showNotification(title: string, options?: NotificationOptions) {
73 if (!browser || !('Notification' in window)) {
74 return;
75 }
76
77 // Auto-request permission if not denied
78 if (this.notificationPermission === 'default') {
79 await this.requestPermission();
80 }
81
82 if (this.notificationPermission === 'granted') {
83 try {
84 const notification = new Notification(title, {
85 icon: '/favicon.png',
86 badge: '/favicon.png',
87 ...options
88 });
89
90 // Auto-close after 5 seconds
91 setTimeout(() => notification.close(), 5000);
92
93 return notification;
94 } catch (err) {
95 console.error('Failed to show notification:', err);
96 }
97 }
98 }
99
100 /**
101 * Notify user of their turn with both title flash and push notification
102 */
103 notifyYourTurn(opponentHandle?: string) {
104 const message = opponentHandle
105 ? `Your turn vs ${opponentHandle}!`
106 : 'Your turn!';
107
108 this.startTitleFlash(`⚫ ${message}`);
109 this.showNotification('Cloud Go - Your Turn', {
110 body: message,
111 tag: 'your-turn',
112 requireInteraction: false
113 });
114 }
115
116 /**
117 * Notify user of a new move in the game they're watching
118 */
119 notifyNewMove(playerHandle?: string) {
120 const message = playerHandle
121 ? `${playerHandle} played a move`
122 : 'New move played';
123
124 this.startTitleFlash(`🔄 ${message}`);
125 this.showNotification('Cloud Go - New Move', {
126 body: message,
127 tag: 'new-move',
128 requireInteraction: false
129 });
130 }
131
132 /**
133 * Clean up when component unmounts
134 */
135 cleanup() {
136 this.stopTitleFlash();
137 }
138}
139
140/**
141 * Check if the page is currently visible/focused
142 */
143export function isPageVisible(): boolean {
144 if (!browser) return false;
145 return document.visibilityState === 'visible';
146}
147
148/**
149 * Listen for visibility changes
150 */
151export function onVisibilityChange(callback: (visible: boolean) => void): () => void {
152 if (!browser) return () => {};
153
154 const handler = () => {
155 callback(document.visibilityState === 'visible');
156 };
157
158 document.addEventListener('visibilitychange', handler);
159
160 return () => {
161 document.removeEventListener('visibilitychange', handler);
162 };
163}