This commit is contained in:
Raven Scott
2025-12-17 20:05:50 -05:00
commit 742e27d3f7
276 changed files with 89838 additions and 0 deletions
@@ -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
};
})();