reorg
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user