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">×</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.