forked from snxraven/p2ns
434 lines
15 KiB
JavaScript
434 lines
15 KiB
JavaScript
/**
|
|
* Profile Modal Component
|
|
* Reusable modal for displaying user profiles across plugins
|
|
*
|
|
* Usage:
|
|
* 1. Include the CSS: <link rel="stylesheet" href="https://global.profile/css/profile-modal.css">
|
|
* 2. Include the HTML: (include profile-modal.html content or use iframe/component loader)
|
|
* 3. Include this JS: <script src="https://global.profile/js/profile-modal.js"></script>
|
|
* 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) + ' <span class="bot-badge">BOT</span>';
|
|
} 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
|
|
};
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|