/** * Profile Modal Component * Reusable modal for displaying user profiles across plugins * * Usage: * 1. Include the CSS: * 2. Include the HTML: (include profile-modal.html content or use iframe/component loader) * 3. Include this JS: * 4. Use: window.ProfileModal.open(peerId) */ (function() { 'use strict'; // Profile cache with TTL const profileCache = new Map(); const CACHE_TTL = 5 * 60 * 1000; // 5 minutes // Global profile domain const GLOBAL_PROFILE_DOMAIN = 'global.profile'; /** * Get profile from cache or fetch from API */ async function getProfile(peerId) { // Check cache first const cached = profileCache.get(peerId); if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) { return cached.profile; } try { // Fetch from global.profile API // Use absolute URL to access global.profile domain const protocol = window.location.protocol; // Construct URL - use absolute URL for cross-domain access const profileUrl = `${protocol}//${GLOBAL_PROFILE_DOMAIN}/api/profile/${encodeURIComponent(peerId)}`; const response = await fetch(profileUrl); if (!response.ok) { if (response.status === 404) { return null; // Profile doesn't exist } throw new Error(`Failed to fetch profile: ${response.status}`); } const profile = await response.json(); // Cache the profile profileCache.set(peerId, { profile, timestamp: Date.now() }); return profile; } catch (err) { console.error('Error fetching profile:', err); return null; } } /** * Get avatar URL for a peer */ function getAvatarUrl(peerId, size = 128) { if (!peerId) return ''; const protocol = window.location.protocol; return `${protocol}//${GLOBAL_PROFILE_DOMAIN}/api/profile/avatar/${encodeURIComponent(peerId)}/${size}`; } /** * Escape HTML to prevent XSS */ function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Open profile modal for a peer */ async function openProfileModal(peerId) { if (!peerId) { console.error('ProfileModal: peerId is required'); return; } const modal = document.getElementById('profile-modal'); if (!modal) { console.error('ProfileModal: Modal element not found. Make sure profile-modal.html is included.'); return; } // Show modal and loading state modal.style.display = 'flex'; document.getElementById('profile-modal-loading').style.display = 'block'; document.getElementById('profile-modal-error').style.display = 'none'; document.getElementById('profile-modal-body').style.display = 'none'; try { // Fetch profile const profile = await getProfile(peerId); if (!profile) { // Show error state document.getElementById('profile-modal-loading').style.display = 'none'; document.getElementById('profile-modal-error').style.display = 'block'; return; } // Populate modal with profile data const displayName = profile.displayName || 'Anonymous'; const isBot = profile.customFields && (profile.customFields.isBot === true || profile.customFields.botTag === 'BOT'); const nameElement = document.getElementById('profile-modal-name'); if (isBot) { nameElement.innerHTML = escapeHtml(displayName) + ' BOT'; } else { nameElement.textContent = displayName; } document.getElementById('profile-modal-peerid').textContent = profile.peerId || peerId; // Avatar const avatarImg = document.getElementById('profile-modal-avatar'); const avatarPlaceholder = document.getElementById('profile-modal-avatar-placeholder'); const avatarUrl = getAvatarUrl(peerId, 128); avatarImg.src = avatarUrl + '?t=' + Date.now(); // Add timestamp to bust cache avatarImg.style.display = 'block'; avatarPlaceholder.style.display = 'none'; avatarImg.onerror = () => { avatarImg.style.display = 'none'; avatarPlaceholder.style.display = 'flex'; }; // Bio const bioContainer = document.getElementById('profile-modal-bio-container'); const bioText = document.getElementById('profile-modal-bio'); if (profile.bio && profile.bio.trim()) { bioText.textContent = profile.bio; bioContainer.style.display = 'block'; } else { bioContainer.style.display = 'none'; } // Additional details - check if fields are enabled (via customFields or direct flags) const emailItem = document.getElementById('profile-modal-email-item'); const emailLink = document.getElementById('profile-modal-email'); const emailEnabled = profile.email || (profile.customFields && profile.customFields.emailEnabled); if (emailEnabled && profile.email) { emailLink.href = `mailto:${escapeHtml(profile.email)}`; emailLink.textContent = escapeHtml(profile.email); emailItem.style.display = 'flex'; } else { emailItem.style.display = 'none'; } const websiteItem = document.getElementById('profile-modal-website-item'); const websiteLink = document.getElementById('profile-modal-website'); const websiteEnabled = profile.website || (profile.customFields && profile.customFields.websiteEnabled); if (websiteEnabled && profile.website) { let websiteUrl = profile.website; if (!websiteUrl.startsWith('http://') && !websiteUrl.startsWith('https://')) { websiteUrl = 'https://' + websiteUrl; } websiteLink.href = websiteUrl; websiteLink.textContent = escapeHtml(profile.website); websiteItem.style.display = 'flex'; } else { websiteItem.style.display = 'none'; } const xItem = document.getElementById('profile-modal-x-item'); const xLink = document.getElementById('profile-modal-x'); const xEnabled = profile.xUsername || (profile.customFields && profile.customFields.xHandleEnabled); if (xEnabled && profile.xUsername) { const xHandle = profile.xUsername.replace('@', ''); const xUrl = `https://x.com/${xHandle}`; xLink.href = xUrl; xLink.textContent = escapeHtml(profile.xUsername); xItem.style.display = 'flex'; } else { xItem.style.display = 'none'; } // GitHub const githubItem = document.getElementById('profile-modal-github-item'); const githubLink = document.getElementById('profile-modal-github'); if (githubItem && githubLink) { if (profile.github) { const githubHandle = profile.github.replace('@', ''); const githubUrl = `https://github.com/${githubHandle}`; githubLink.href = githubUrl; githubLink.textContent = escapeHtml(profile.github); githubItem.style.display = 'flex'; } else { githubItem.style.display = 'none'; } } // Discord const discordItem = document.getElementById('profile-modal-discord-item'); const discordText = document.getElementById('profile-modal-discord'); if (discordItem && discordText) { if (profile.discord) { discordText.textContent = escapeHtml(profile.discord); discordItem.style.display = 'flex'; } else { discordItem.style.display = 'none'; } } // Location const locationItem = document.getElementById('profile-modal-location-item'); const locationText = document.getElementById('profile-modal-location'); if (locationItem && locationText) { if (profile.location) { locationText.textContent = escapeHtml(profile.location); locationItem.style.display = 'flex'; } else { locationItem.style.display = 'none'; } } // Tags const tagsContainer = document.getElementById('profile-modal-tags-container'); const tagsList = document.getElementById('profile-modal-tags-list'); if (tagsContainer && tagsList) { let tagsArray = []; if (Array.isArray(profile.tags)) { tagsArray = profile.tags; } else if (typeof profile.tags === 'string') { try { const parsed = JSON.parse(profile.tags); tagsArray = Array.isArray(parsed) ? parsed : []; } catch { // If it's a comma-separated string, split it if (profile.tags.trim()) { tagsArray = profile.tags.split(',').map(t => t.trim()).filter(t => t); } } } if (tagsArray && tagsArray.length > 0) { tagsList.innerHTML = ''; tagsArray.forEach(tag => { const tagElement = document.createElement('span'); tagElement.className = 'profile-modal-tag'; tagElement.textContent = escapeHtml(tag); tagsList.appendChild(tagElement); }); tagsContainer.style.display = 'block'; } else { tagsContainer.style.display = 'none'; } } // Show/hide block button (if blocking functions are available from peer.chat or SDK) const actionsContainer = document.getElementById('profile-modal-actions'); const blockBtn = document.getElementById('profile-modal-block-btn'); const unblockBtn = document.getElementById('profile-modal-unblock-btn'); if (actionsContainer && blockBtn && unblockBtn && typeof window.isPeerBlocked === 'function') { const isBlocked = window.isPeerBlocked(peerId); const isOwnProfile = window.localPeerId && peerId === window.localPeerId; if (!isOwnProfile) { blockBtn.style.display = isBlocked ? 'none' : 'flex'; unblockBtn.style.display = isBlocked ? 'flex' : 'none'; actionsContainer.style.display = 'flex'; // Update button handlers blockBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); if (typeof window.blockPeer === 'function') { window.blockPeer(peerId); // Update button state blockBtn.style.display = 'none'; unblockBtn.style.display = 'flex'; } }; unblockBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); if (typeof window.unblockPeer === 'function') { window.unblockPeer(peerId); // Update button state unblockBtn.style.display = 'none'; blockBtn.style.display = 'flex'; } }; } else { actionsContainer.style.display = 'none'; } } else if (actionsContainer) { actionsContainer.style.display = 'none'; } // Show body document.getElementById('profile-modal-loading').style.display = 'none'; document.getElementById('profile-modal-body').style.display = 'block'; } catch (err) { console.error('Error loading profile:', err); document.getElementById('profile-modal-loading').style.display = 'none'; document.getElementById('profile-modal-error').style.display = 'block'; } } /** * Close profile modal */ function closeProfileModal() { const modal = document.getElementById('profile-modal'); if (modal) { modal.style.display = 'none'; } } // Track if listeners are already attached let listenersAttached = false; /** * Initialize modal event listeners * Uses event delegation so it works even if modal is added dynamically */ function initModal() { // Only attach document-level listeners once if (listenersAttached) { // But always try to attach direct listeners to the close button if modal exists attachDirectListeners(); return; } listenersAttached = true; // Use event delegation for close button (check if clicked element or its parent is the close button) document.addEventListener('click', (e) => { const closeBtn = e.target.closest('.profile-modal-close'); if (closeBtn) { e.preventDefault(); e.stopPropagation(); closeProfileModal(); return; } // Check backdrop click if (e.target && e.target.classList.contains('profile-modal-backdrop')) { closeProfileModal(); } }); // ESC key - check if modal is visible document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { const modal = document.getElementById('profile-modal'); if (modal && modal.style.display === 'flex') { closeProfileModal(); } } }); // Also attach direct listeners if modal already exists attachDirectListeners(); } /** * Attach direct event listeners to the close button */ function attachDirectListeners() { const modal = document.getElementById('profile-modal'); if (modal) { const closeBtn = modal.querySelector('.profile-modal-close'); if (closeBtn && !closeBtn.hasAttribute('data-listener-attached')) { closeBtn.setAttribute('data-listener-attached', 'true'); closeBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); closeProfileModal(); }); // Also add onclick as fallback closeBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); closeProfileModal(); }; } } } /** * Helper function to make avatar clickable * Call this on avatar elements to make them open the profile modal when clicked */ function makeAvatarClickable(avatarElement, peerId) { if (!avatarElement || !peerId) return; avatarElement.style.cursor = 'pointer'; avatarElement.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openProfileModal(peerId); }); } // Initialize immediately (event delegation will work even if modal doesn't exist yet) initModal(); // Also try to initialize when DOM is ready (in case modal is already there) if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', attachDirectListeners); } // Export to global scope window.ProfileModal = { open: openProfileModal, close: closeProfileModal, getProfile: getProfile, getAvatarUrl: getAvatarUrl, makeAvatarClickable: makeAvatarClickable, init: initModal // Export init so it can be called after HTML is loaded }; })();