PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:07:09 UTC 12,547 bytes, 295 lines download raw bytes

1/**
2 * User Profile System (Discord-inspired)
3 * Shows user profile when clicking on username
4 */
5
6window.ChatroomUserProfile = (function() {
7    'use strict';
8
9    let profileModal = null;
10
11    /**
12     * Members list: one delegated handler (capture phase) so any hit inside the row
13     * (.member-page, status line, img, etc.) opens the profile — bubble-only handlers missed those.
14     */
15    function setupMembersListDelegation() {
16        const membersList = document.getElementById('members-list');
17        if (!membersList || membersList.dataset.profileDelegation === '1') return;
18        membersList.dataset.profileDelegation = '1';
19
20        function openFromMemberRow(e) {
21            const memberItem = e.target.closest('.member-item');
22            if (!memberItem || !memberItem.dataset.username) return;
23            e.preventDefault();
24            e.stopPropagation();
25            showProfile(memberItem.dataset.username);
26        }
27
28        membersList.addEventListener('click', openFromMemberRow, true);
29        membersList.addEventListener('contextmenu', openFromMemberRow, true);
30    }
31
32    /**
33     * Initialize user profile system
34     */
35    function init() {
36        setupMembersListDelegation();
37
38        document.addEventListener('click', (e) => {
39            if (e.target.classList.contains('message-username') || e.target.closest('.message-username')) {
40                const usernameEl = e.target.classList.contains('message-username') ? e.target : e.target.closest('.message-username');
41                const username = usernameEl.textContent.replace('👑', '').trim();
42                if (username) {
43                    e.preventDefault();
44                    showProfile(username);
45                }
46            }
47        });
48    }
49
50    /**
51     * Show user profile
52     */
53    function showProfile(username) {
54        if (!username) return;
55
56        // Remove existing modal
57        if (profileModal) {
58            profileModal.remove();
59        }
60
61        // Get user data
62        const isModerator = window.ChatroomModerator && window.ChatroomModerator.isUserModerator(username);
63        const presenceState = window.ChatroomPresence && window.ChatroomPresence.getPresenceState
64            ? window.ChatroomPresence.getPresenceState(username)
65            : ((window.ChatroomPresence && window.ChatroomPresence.isUserOnline(username)) ? 'online' : 'offline');
66        const currentUser = window.__sharedUsername ? window.__sharedUsername.get() : '';
67        const isOwnProfile = username === currentUser;
68
69        // Get profile picture if available
70        const profilePicture = window.ChatroomProfilePicture && window.ChatroomProfilePicture.getProfilePicture(username);
71        const avatarDisplay = profilePicture 
72            ? `<img src="${escapeHtml(profilePicture)}" alt="${escapeHtml(username)}" class="profile-picture-avatar-large">`
73            : getAvatar(username);
74
75        const statusText = (window.ChatroomProfilePicture && window.ChatroomProfilePicture.getStatus && window.ChatroomProfilePicture.getStatus(username)) || '';
76        const statusDisplay = statusText
77            ? `<div class="user-profile-bio">${escapeHtml(statusText)}</div>`
78            : (isOwnProfile ? '<div class="user-profile-bio user-profile-bio-empty">No status set — add one below or on the account page!</div>' : '');
79        const statusEditSection = isOwnProfile && window.ChatroomProfilePicture && window.ChatroomProfilePicture.setStatus
80            ? `<div class="user-profile-edit-status">
81                <label class="user-profile-edit-label" for="profile-status-input">Your status (others can see this)</label>
82                <input type="text" class="user-profile-status-input" id="profile-status-input" maxlength="200" placeholder="e.g. vibing with the fish 🐟" value="${escapeHtml(statusText)}">
83                <button type="button" class="profile-status-save-btn" id="profile-status-save">Save status</button>
84              </div>`
85            : '';
86
87        // Create profile modal
88        profileModal = document.createElement('div');
89        profileModal.className = 'user-profile-modal';
90        profileModal.innerHTML = `
91            <div class="user-profile-content">
92                <div class="user-profile-header">
93                    <div class="user-profile-avatar-large">${avatarDisplay}</div>
94                    <div class="user-profile-info">
95                        <h3>${escapeHtml(username)} ${isModerator ? '👑' : ''}</h3>
96                        <div class="user-profile-status">
97                            <span class="status-indicator ${presenceState === 'online' ? 'online' : presenceState === 'idle' ? 'idle' : 'offline'}"></span>
98                            <span>${presenceState === 'online' ? 'Online' : presenceState === 'idle' ? 'Idle' : 'Offline'}</span>
99                        </div>
100                        ${statusDisplay}
101                    </div>
102                    <button type="button" class="user-profile-close cr-icon-btn" aria-label="Close profile">&times;</button>
103                </div>
104                <div class="user-profile-body">
105                    ${statusEditSection}
106                    ${isOwnProfile && window.ChatroomProfilePicture ? '<div id="profile-picture-upload-container"></div>' : ''}
107                    <div class="user-profile-actions">
108                        <button type="button" class="profile-action-btn" id="profile-dm-btn">💌 Send Message</button>
109                        ${!isOwnProfile ? '<button type="button" class="profile-action-btn" id="profile-block-btn">🚫 Block User</button>' : ''}
110                    </div>
111                    <div class="user-profile-stats">
112                        <div class="profile-stat">
113                            <span class="stat-label">Messages</span>
114                            <span class="stat-value" id="profile-message-count">-</span>
115                        </div>
116                        <div class="profile-stat">
117                            <span class="stat-label">Joined</span>
118                            <span class="stat-value" id="profile-joined-date">-</span>
119                        </div>
120                    </div>
121                </div>
122            </div>
123        `;
124
125        document.body.appendChild(profileModal);
126
127        const profileCloseBtn = profileModal.querySelector('.user-profile-close');
128        if (profileCloseBtn && window.ChatroomIcons) {
129            window.ChatroomIcons.set(profileCloseBtn, 'xmark');
130        }
131
132        // Close button
133        profileCloseBtn.addEventListener('click', () => {
134            hideProfile();
135        });
136
137        // Close on outside click
138        profileModal.addEventListener('click', (e) => {
139            if (e.target === profileModal) {
140                hideProfile();
141            }
142        });
143
144        // Action buttons
145        const dmBtn = document.getElementById('profile-dm-btn');
146        if (dmBtn) {
147            dmBtn.addEventListener('click', () => {
148                if (window.ChatroomDMs) {
149                    window.ChatroomDMs.openDM(username);
150                }
151                hideProfile();
152            });
153        }
154
155        const blockBtn = document.getElementById('profile-block-btn');
156        if (blockBtn) {
157            const alreadyBlocked = window.ChatroomBlocks && window.ChatroomBlocks.isBlocked(username);
158            if (alreadyBlocked) {
159                blockBtn.textContent = '✅ Unblock User';
160            }
161            blockBtn.addEventListener('click', async () => {
162                if (!window.ChatroomBlocks) return;
163                if (window.ChatroomBlocks.isBlocked(username)) {
164                    const ok = await window.ChatroomBlocks.unblockUser(username);
165                    showToast(ok ? `Unblocked ${username}.` : 'Could not unblock user.');
166                    hideProfile();
167                    return;
168                }
169                if (confirm(`Are you sure you want to block ${username}? You will not see their messages.`)) {
170                    const ok = await window.ChatroomBlocks.blockUser(username);
171                    showToast(ok ? `Blocked ${username}.` : 'Could not block user.');
172                    hideProfile();
173                }
174            });
175        }
176
177        const statusSaveBtn = document.getElementById('profile-status-save');
178        const statusInput = document.getElementById('profile-status-input');
179        if (statusSaveBtn && statusInput && window.ChatroomProfilePicture && window.ChatroomProfilePicture.setStatus) {
180            statusSaveBtn.addEventListener('click', async () => {
181                const text = (statusInput.value || '').trim().substring(0, 200);
182                const ok = await window.ChatroomProfilePicture.setStatus(text);
183                if (ok) {
184                    const bioEl = profileModal.querySelector('.user-profile-bio');
185                    if (bioEl) {
186                        bioEl.textContent = text || '';
187                        bioEl.classList.toggle('user-profile-bio-empty', !text);
188                        if (!text) bioEl.textContent = 'No status set — add one below or on the account page!';
189                    }
190                    if (typeof showToast === 'function') showToast('Status saved!');
191                }
192            });
193        }
194
195        // Load user stats (placeholder)
196        loadUserStats(username);
197
198        // Add profile picture upload UI if own profile
199        if (isOwnProfile && window.ChatroomProfilePicture) {
200            const uploadContainer = document.getElementById('profile-picture-upload-container');
201            if (uploadContainer) {
202                window.ChatroomProfilePicture.createUploadUI(uploadContainer);
203            }
204        }
205
206        // Show modal
207        setTimeout(() => {
208            profileModal.classList.add('show');
209        }, 10);
210    }
211
212    /**
213     * Load user statistics
214     */
215    function loadUserStats(username) {
216        const countEl = document.getElementById('profile-message-count');
217        const joinedEl = document.getElementById('profile-joined-date');
218        if (!countEl || !joinedEl) return;
219
220        if (window.ChatroomUserStats && window.ChatroomUserStats.loadStats) {
221            window.ChatroomUserStats.loadStats(username).then(function (stats) {
222                countEl.textContent = String(stats.messageCount || 0);
223                joinedEl.textContent = window.ChatroomUserStats.formatJoinedDate
224                    ? window.ChatroomUserStats.formatJoinedDate(stats.joinedAt)
225                    : 'Unknown';
226            });
227            return;
228        }
229
230        countEl.textContent = '0';
231        joinedEl.textContent = 'Unknown';
232    }
233
234    /**
235     * Hide user profile
236     */
237    function hideProfile() {
238        if (profileModal) {
239            profileModal.classList.remove('show');
240            setTimeout(() => {
241                if (profileModal) {
242                    profileModal.remove();
243                    profileModal = null;
244                }
245            }, 300);
246        }
247    }
248
249    /**
250     * Get avatar emoji
251     */
252    function getAvatar(username) {
253        const avatars = ['🐾', '🐶', '🐕', '🦴', '🎾', '🧸', '⭐', '💫', '✨', '🌟'];
254        const hash = username.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
255        return avatars[hash % avatars.length];
256    }
257
258    /**
259     * Escape HTML
260     */
261    function escapeHtml(text) {
262        const div = document.createElement('div');
263        div.textContent = text;
264        return div.innerHTML;
265    }
266
267    /**
268     * Show toast
269     */
270    function showToast(message) {
271        const toast = document.createElement('div');
272        toast.className = 'toast';
273        toast.textContent = message;
274        document.body.appendChild(toast);
275
276        setTimeout(() => toast.classList.add('show'), 10);
277        setTimeout(() => {
278            toast.classList.remove('show');
279            setTimeout(() => toast.remove(), 300);
280        }, 2000);
281    }
282
283    // Initialize on load
284    if (document.readyState === 'loading') {
285        document.addEventListener('DOMContentLoaded', init);
286    } else {
287        init();
288    }
289
290    return {
291        showProfile,
292        hideProfile,
293        init
294    };
295})();

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.