An open source supporter broker powered by high-fives.
high-five.atprotofans.com/
1{{define "title"}}High-Five Room{{end}}
2
3{{define "content"}}
4<article>
5 <header>
6 <h2>High-Five Room <span id="connection-status" class="status status-offline">CONNECTING...</span></h2>
7 </header>
8
9 <div class="grid">
10 <div>
11 <h3>People in the room (<span id="user-count">0</span>)</h3>
12 <ul id="users">
13 <li><em>Waiting for people to join...</em></li>
14 </ul>
15 </div>
16 <div>
17 <h3>Activity</h3>
18 <ul id="activities">
19 <li><em>No activity yet...</em></li>
20 </ul>
21 </div>
22 </div>
23
24</article>
25
26<style>
27#users, #activities {
28 max-height: 400px;
29 overflow-y: auto;
30 padding-left: 1rem;
31}
32#users li, #activities li {
33 margin-bottom: 0.5rem;
34}
35#users button {
36 margin-left: 0.5rem;
37 padding: 0.25rem 0.5rem;
38 font-size: 0.875rem;
39}
40.activity-time {
41 color: var(--pico-muted-color);
42 font-size: 0.75rem;
43}
44</style>
45{{end}}
46
47{{define "scripts"}}
48<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js"></script>
49<script>
50const myDID = "{{.Session.DID | js}}";
51const myHandle = "{{.Session.Handle | js}}";
52let ws = null;
53let users = new Map(); // DID -> {did, handle}
54let isReconnect = false;
55let activityCount = 0;
56let sessionExpired = false;
57
58function connect() {
59 const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
60 let wsUrl = protocol + '//' + window.location.host + '/ws';
61 if (isReconnect) {
62 wsUrl += '?reconnect=true';
63 }
64 ws = new WebSocket(wsUrl);
65
66 ws.onopen = function() {
67 document.getElementById('connection-status').textContent = 'CONNECTED';
68 document.getElementById('connection-status').className = 'status status-live';
69 if (!isReconnect) {
70 addActivity('Connected to the high-five room');
71 }
72 };
73
74 ws.onmessage = function(event) {
75 const msg = JSON.parse(event.data);
76 handleMessage(msg);
77 };
78
79 ws.onclose = function() {
80 document.getElementById('connection-status').textContent = 'DISCONNECTED';
81 document.getElementById('connection-status').className = 'status status-offline';
82 if (!sessionExpired) {
83 addActivity('Disconnected - reconnecting...');
84 isReconnect = true;
85 setTimeout(connect, 2000);
86 }
87 };
88
89 ws.onerror = function(err) {
90 console.error('WebSocket error:', err);
91 };
92}
93
94function handleMessage(msg) {
95 switch (msg.type) {
96 case 'identity_joined':
97 addUser(msg.payload.did, msg.payload.handle);
98 addActivity('@' + msg.payload.handle + ' joined the room');
99 break;
100
101 case 'identity_left':
102 removeUser(msg.payload.did);
103 addActivity('@' + msg.payload.handle + ' left the room');
104 break;
105
106 case 'populate_room':
107 const identities = msg.payload.identities || [];
108 identities.forEach(u => addUser(u.did, u.handle));
109 break;
110
111 case 'high_five_event':
112 addActivity('@' + msg.payload.from.handle + ' gave @' + msg.payload.to.handle + ' a high-five!');
113 fireHighFiveConfetti();
114 break;
115
116 case 'error':
117 addActivity('Error: ' + msg.payload.error);
118 break;
119
120 case 'session_timeout':
121 handleSessionTimeout(msg.payload.message);
122 break;
123
124 default:
125 console.log('Unhandled message type:', msg.type, msg);
126 }
127}
128
129function addUser(did, handle) {
130 if (!did || did === myDID) return; // Don't show self
131 if (users.has(did)) return;
132
133 users.set(did, {did, handle});
134 renderUsers();
135}
136
137function removeUser(did) {
138 users.delete(did);
139 renderUsers();
140}
141
142function renderUsers() {
143 const list = document.getElementById('users');
144 const count = users.size;
145 document.getElementById('user-count').textContent = count;
146
147 if (count === 0) {
148 list.innerHTML = '<li><em>Waiting for people to join...</em></li>';
149 return;
150 }
151
152 list.innerHTML = '';
153 users.forEach((user, did) => {
154 const li = document.createElement('li');
155 const span = document.createElement('span');
156 span.textContent = '@' + user.handle;
157
158 const button = document.createElement('button');
159 button.textContent = 'High Five!';
160 button.addEventListener('click', () => sendHighFive(did));
161
162 li.appendChild(span);
163 li.appendChild(document.createTextNode(' '));
164 li.appendChild(button);
165 list.appendChild(li);
166 });
167}
168
169function sendHighFive(subjectDID) {
170 if (!ws || ws.readyState !== WebSocket.OPEN) {
171 addActivity('Error: Not connected');
172 return;
173 }
174 ws.send(JSON.stringify({type: 'high_five', payload: {subject: subjectDID}}));
175}
176
177// Fire confetti with raising hands and tada emoji when a high-five occurs
178function fireHighFiveConfetti() {
179 const emojis = ['\u{1F64C}', '\u{1F389}']; // Raising hands and tada
180 const defaults = {
181 spread: 360,
182 ticks: 100,
183 gravity: 0.5,
184 decay: 0.94,
185 startVelocity: 30,
186 colors: ['#ff6b6b', '#feca57', '#48dbfb', '#ff9ff3', '#54a0ff']
187 };
188
189 // Fire emoji confetti from multiple positions
190 function fireEmoji() {
191 confetti({
192 ...defaults,
193 particleCount: 30,
194 scalar: 2,
195 shapes: ['emoji'],
196 shapeOptions: {
197 emoji: {
198 value: emojis
199 }
200 },
201 origin: { x: Math.random(), y: Math.random() - 0.2 }
202 });
203 }
204
205 // Fire regular confetti burst
206 function fireBurst() {
207 confetti({
208 ...defaults,
209 particleCount: 50,
210 origin: { x: 0.5, y: 0.5 }
211 });
212 }
213
214 // Sequence of effects
215 fireBurst();
216 setTimeout(fireEmoji, 100);
217 setTimeout(fireEmoji, 200);
218 setTimeout(fireEmoji, 300);
219}
220
221function handleSessionTimeout(message) {
222 sessionExpired = true;
223 if (ws) {
224 ws.close();
225 }
226
227 // Update status
228 document.getElementById('connection-status').textContent = 'SESSION EXPIRED';
229 document.getElementById('connection-status').className = 'status status-offline';
230
231 // Clear users list
232 users.clear();
233 renderUsers();
234
235 // Show session expired modal
236 showSessionExpiredModal(message);
237}
238
239function showSessionExpiredModal(message) {
240 // Create modal overlay
241 const overlay = document.createElement('div');
242 overlay.id = 'session-expired-overlay';
243 overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:1000;';
244
245 // Create modal content
246 const modal = document.createElement('article');
247 modal.style.cssText = 'max-width:400px;margin:1rem;text-align:center;';
248 modal.innerHTML = '<header><h3>Session Expired</h3></header>' +
249 '<p>' + escapeHtml(message) + '</p>' +
250 '<footer><a href="/info" role="button">Start New Session</a></footer>';
251
252 overlay.appendChild(modal);
253 document.body.appendChild(overlay);
254}
255
256function addActivity(message) {
257 const list = document.getElementById('activities');
258
259 // Remove the "no activity" placeholder
260 if (activityCount === 0) {
261 list.innerHTML = '';
262 }
263
264 const li = document.createElement('li');
265 const time = new Date().toLocaleTimeString();
266 li.innerHTML = '<span class="activity-time">[' + time + ']</span> ' + escapeHtml(message);
267 list.prepend(li);
268 activityCount++;
269
270 // Keep only last 50 activities
271 while (list.children.length > 50) {
272 list.removeChild(list.lastChild);
273 }
274}
275
276function escapeHtml(text) {
277 const div = document.createElement('div');
278 div.textContent = text;
279 return div.innerHTML;
280}
281
282// Start connection when page loads
283connect();
284</script>
285{{end}}