PageSourceSearch

https://pupwife.com/projects/chatroom/scripts/presence.js

js pupwife.com collected 2026-09-30 15:07:08 UTC 14,040 bytes, 366 lines download raw bytes

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.