1/** 2 * User Presence System 3 * Tracks online/offline status of users in the chatroom 4 */ 5 6window.ChatroomPresence = (function() { 7 'use strict'; 8 9 let presenceRef = null; 10 let currentUsername = null; 11 let userPresenceRef = null; 12 let onlineUsersRef = null; 13 let onlineUsers = new Map(); 14 let onlineUsersValueListener = null; 15 let presenceBooted = false; 16 17 /** 18 * Initialize presence system 19 */ 20 function init() { 21 if (typeof firebase === 'undefined' || !firebase.database) { 22 console.error('Firebase not available for presence'); 23 return false; 24 } 25 26 // Wait for username to be available 27 const checkUsername = setInterval(() => { 28 if (window.__sharedUsername) { 29 currentUsername = window.__sharedUsername.get(); 30 if (currentUsername) { 31 clearInterval(checkUsername); 32 setupPresence(); 33 } 34 } 35 }, 100); 36 37 // Stop checking after 5 seconds 38 setTimeout(() => { 39 clearInterval(checkUsername); 40 if (!currentUsername) { 41 currentUsername = 'guest_' + Date.now(); 42 setupPresence(); 43 } 44 }, 5000); 45 46 return true; 47 } 48 49 let disposeTabPresence = null; 50 51 function waitForPresenceClient(done) { 52 if (window.PupwifePresenceClient && window.PupwifePresenceClient.startTabPresence) { 53 done(); 54 return; 55 } 56 let n = 0; 57 const t = setInterval(() => { 58 n++; 59 if (window.PupwifePresenceClient && window.PupwifePresenceClient.startTabPresence) { 60 clearInterval(t); 61 done(); 62 } else if (n > 100) { 63 clearInterval(t); 64 console.warn('PupwifePresenceClient not loaded; presence may be incomplete'); 65 done(); 66 } 67 }, 50); 68 } 69 70 /** 71 * Setup presence tracking (multi-tab via PupwifePresenceClient) 72 */ 73 function setupPresence() { 74 if (!currentUsername) return; 75 76 presenceRef = firebase.database().ref('chatroom/presence'); 77 userPresenceRef = presenceRef.child(currentUsername); 78 onlineUsersRef = presenceRef; 79 80 waitForPresenceClient(() => { 81 if (window.PupwifePresenceClient && window.PupwifePresenceClient.startTabPresence) { 82 if (disposeTabPresence) { 83 try { disposeTabPresence(); } catch (e) {} 84 } 85 disposeTabPresence = window.PupwifePresenceClient.startTabPresence({ username: currentUsername }); 86 window.addEventListener('beforeunload', () => { 87 if (disposeTabPresence) { 88 try { disposeTabPresence(); } catch (e) {} 89 } 90 }); 91 } 92 }); 93 94 const aggregate = () => (window.PupwifePresenceClient && window.PupwifePresenceClient.aggregateUserPresence) 95 ? window.PupwifePresenceClient.aggregateUserPresence 96 : null; 97 98 if (onlineUsersRef && onlineUsersValueListener) { 99 onlineUsersRef.off('value', onlineUsersValueListener); 100 } 101 102 // Listen for online / idle users (aggregated from tabs per user) 103 onlineUsersValueListener = (snapshot) => { 104 onlineUsers.clear(); 105 const users = snapshot.val(); 106 const aggFn = aggregate(); 107 108 if (users && aggFn) { 109 Object.keys(users).forEach(username => { 110 const userData = users[username]; 111 const a = aggFn(userData); 112 if (a.presenceState === 'online' || a.presenceState === 'idle') { 113 onlineUsers.set(username, { 114 currentPage: a.currentPage || 'Unknown', 115 pageSummary: a.pageSummary || a.currentPage || 'Unknown', 116 presenceState: a.presenceState, 117 lastSeen: a.lastSeen 118 }); 119 } 120 }); 121 } else if (users) {
122 Object.keys(users).forEach(username => { 123 const userData = users[username]; 124 if (userData.status === 'online' && Date.now() - userData.lastSeen < 60000) { 125 const cp = userData.currentPage || 'Unknown'; 126 onlineUsers.set(username, { 127 currentPage: cp, 128 pageSummary: cp, 129 presenceState: 'online', 130 lastSeen: userData.lastSeen 131 }); 132 } 133 }); 134 } 135 136 updateOnlineUsersDisplay(); 137 }; 138 onlineUsersRef.on('value', onlineUsersValueListener); 139 presenceBooted = true; 140 141 // Re-render when profile data (e.g. status) changes 142 window.addEventListener('userProfilesUpdated', () => { 143 updateOnlineUsersDisplay(); 144 }); 145 146 console.log('â Presence system initialized for:', currentUsername); 147 } 148 149 /** 150 * Update online users display 151 */ 152 function updateOnlineUsersDisplay() { 153 const membersList = document.getElementById('members-list'); 154 const onlineCount = document.getElementById('online-count'); 155 const onlineMembersCount = document.getElementById('online-members-count'); 156 const membersTotal = document.getElementById('members-total'); 157 158 if (!membersList) return; 159 160 // Get all registered users from Firebase 161 if (!firebase.database) return; 162 163 const usernamesRef = firebase.database().ref('usernames'); 164 usernamesRef.once('value', (snapshot) => { 165 const registeredUsers = new Set(); 166 if (snapshot.exists()) { 167 snapshot.forEach((childSnapshot) => { 168 registeredUsers.add(childSnapshot.key); 169 }); 170 } 171 172 // Clear existing 173 membersList.innerHTML = ''; 174 175 // Separate online and offline users 176 const onlineUserList = []; 177 const offlineUserList = []; 178 179 // Add online users (with userData for page info) 180 onlineUsers.forEach((userData, username) => { 181 onlineUserList.push({ username, userData, isOnline: true }); 182 registeredUsers.delete(username); // Remove from registered set 183 }); 184 185 // Add offline registered users 186 registeredUsers.forEach(username => { 187 offlineUserList.push({ username, isOnline: false }); 188 }); 189 190 // Sort both lists alphabetically 191 onlineUserList.sort((a, b) => a.username.localeCompare(b.username)); 192 offlineUserList.sort((a, b) => a.username.localeCompare(b.username)); 193 194 // Update counts 195 const totalOnline = onlineUserList.length; 196 const totalRegistered = onlineUserList.length + offlineUserList.length; 197 198 if (onlineCount) { 199 onlineCount.textContent = totalOnline; 200 } 201 if (onlineMembersCount) { 202 onlineMembersCount.textContent = totalOnline; 203 } 204 if (membersTotal) { 205 membersTotal.textContent = totalRegistered; 206 } 207 208 // Add online users first (includes idle â still "here") 209 if (onlineUserList.length > 0) { 210 onlineUserList.forEach(({ username, userData }) => { 211 const profilePicture = window.ChatroomProfilePicture && window.ChatroomProfilePicture.getProfilePicture(username); 212 const avatarDisplay = profilePicture 213 ? `<img src="${escapeHtml(profilePicture)}" alt="${escapeHtml(username)}" class="profile-picture-avatar">` 214 : `<span>${getAvatar(username)}</span>`; 215 216 const pageLine = userData?.pageSummary || userData?.currentPage || 'Unknown'; 217 const presenceState = userData?.presenceState || 'online'; 218 const rowClass = presenceState === 'idle' ? 'member-item member-online member-idle' : 'member-item member-online'; 219 const statusText = (window.ChatroomProfilePicture && window.ChatroomProfilePicture.getStatus && window.ChatroomProfilePicture.getStatus(username)) || ''; 220 const statusLine = statusText ? `<span class="member-status-text">${escapeHtml(statusText)}</span>` : ''; 221 const memberItem = document.createElement('div'); 222 memberItem.className = rowClass; 223 memberItem.dataset.username = username; 224 memberItem.dataset.presence = presenceState; 225 memberItem.innerHTML = ` 226 <div class="member-avatar"> 227 ${avatarDisplay}
228 <span class="member-status ${presenceState === 'idle' ? 'idle' : 'online'}"></span> 229 </div> 230 <div class="member-info"> 231 <span class="member-name">${escapeHtml(username)}</span> 232 <span class="member-page">${escapeHtml(pageLine)}${presenceState === 'idle' ? '<span class="member-idle-label"> · idle</span>' : ''}</span> 233 ${statusLine} 234 </div> 235 `; 236 237 membersList.appendChild(memberItem); 238 }); 239 } 240 241 // Add divider between online and offline users if both exist 242 if (onlineUserList.length > 0 && offlineUserList.length > 0) { 243 const divider = document.createElement('div'); 244 divider.className = 'members-divider'; 245 divider.innerHTML = '<span class="divider-text">Offline</span>'; 246 membersList.appendChild(divider); 247 } 248 249 // Add offline users 250 offlineUserList.forEach(({ username }) => { 251 const profilePicture = window.ChatroomProfilePicture && window.ChatroomProfilePicture.getProfilePicture(username); 252 const avatarDisplay = profilePicture 253 ? `<img src="${escapeHtml(profilePicture)}" alt="${escapeHtml(username)}" class="profile-picture-avatar">` 254 : `<span>${getAvatar(username)}</span>`; 255 256 const memberItem = document.createElement('div'); 257 memberItem.className = 'member-item member-offline'; 258 memberItem.dataset.username = username; 259 memberItem.innerHTML = ` 260 <div class="member-avatar"> 261 ${avatarDisplay} 262 <span class="member-status offline"></span> 263 </div> 264 <div class="member-info"> 265 <span class="member-name">${escapeHtml(username)}</span> 266 </div> 267 `; 268 269 membersList.appendChild(memberItem); 270 }); 271 272 try { 273 document.dispatchEvent(new CustomEvent('chatroom-members-rendered')); 274 } catch (e) { /* ignore */ } 275 }); 276 } 277 278 /** 279 * Get avatar for username 280 */ 281 function getAvatar(username) { 282 if (!username) return 'ð¤'; 283 const avatars = ['ð¾', 'ð±', 'ð¶', 'ð¦', 'ð°', 'ð»', 'ð¼', 'ð¨', 'ð¯', 'ð¦']; 284 let hash = 0; 285 for (let i = 0; i < username.length; i++) { 286 hash = username.charCodeAt(i) + ((hash << 5) - hash); 287 } 288 return avatars[Math.abs(hash) % avatars.length]; 289 } 290 291 /** 292 * Get current username 293 */ 294 function getUsername() { 295 return currentUsername; 296 } 297 298 /** 299 * Get online users 300 */ 301 function getOnlineUsers() { 302 return Array.from(onlineUsers.keys()); 303 } 304 305 /** 306 * Check if user is online (includes idle â still on the site) 307 */ 308 function isUserOnline(username) { 309 return onlineUsers.has(username); 310 } 311 312 /** 313 * 'online' | 'idle' | 'offline' 314 */ 315 function getPresenceState(username) { 316 const row = onlineUsers.get(username); 317 if (!row) return 'offline'; 318 return row.presenceState || 'online'; 319 } 320 321 /** 322 * Escape HTML 323 */ 324 function escapeHtml(text) { 325 const div = document.createElement('div'); 326 div.textContent = text; 327 return div.innerHTML; 328 } 329 330 function autoInitIfChatroomDom() { 331 if (!document.getElementById('members-list')) return; 332 init(); 333 } 334 if (document.readyState === 'loading') { 335 document.addEventListener('DOMContentLoaded', autoInitIfChatroomDom); 336 } else { 337 autoInitIfChatroomDom(); 338 } 339 340 function refresh() { 341 if (!document.getElementById('members-list')) return; 342 if (presenceBooted) { 343 updateOnlineUsersDisplay(); 344 return; 345 } 346 init(); 347 } 348 349 function teardown() { 350 if (onlineUsersRef && onlineUsersValueListener) { 351 onlineUsersRef.off('value', onlineUsersValueListener); 352 } 353 onlineUsersValueListener = null; 354 presenceBooted = false; 355 } 356 357 return { 358 getUsername, 359 getOnlineUsers, 360 isUserOnline, 361 getPresenceState, 362 init: init, 363 refresh: refresh, 364 teardown: teardown 365 }; 366})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.