profile update
This commit is contained in:
@@ -6140,6 +6140,7 @@ video.emoji {
|
||||
height: auto;
|
||||
/* vertical-align: middle; */
|
||||
display: inline-block;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
/* video in emoji autocomplete dropdown */
|
||||
|
||||
@@ -7343,11 +7343,20 @@ class NotificationSystem {
|
||||
}
|
||||
}
|
||||
} else if (data.type === 'profile_update') {
|
||||
const { display_name, user, banner_file, banner_position, banner_size } = data.data;
|
||||
const { display_name, user, banner_file, banner_position, banner_size, avatar_file, username_color, description } = data.data;
|
||||
if (display_name !== undefined) this.applyDisplayNameUpdate(display_name, user);
|
||||
if (banner_file !== undefined || banner_position !== undefined || banner_size !== undefined) {
|
||||
this.applyBannerUpdate(banner_file, banner_position, banner_size, user);
|
||||
}
|
||||
if (avatar_file !== undefined) {
|
||||
this.applyAvatarUpdate(avatar_file, user);
|
||||
}
|
||||
if (username_color !== undefined) {
|
||||
this.applyUsernameColorUpdate(username_color, user);
|
||||
}
|
||||
if (description !== undefined) {
|
||||
this.applyDescriptionUpdate(description, user);
|
||||
}
|
||||
// Sync preferences to global session object for client-side gating (like DND)
|
||||
if (window.f0ckSession && data.data.user_id === window.f0ckSession.id) {
|
||||
for (const key in data.data) {
|
||||
@@ -7377,14 +7386,28 @@ class NotificationSystem {
|
||||
// Capture old value before overwriting — needed for thumb matching below
|
||||
const oldDisplayName = window.f0ckSession?.display_name || null;
|
||||
if (window.f0ckSession) window.f0ckSession.display_name = display_name || null;
|
||||
const navBtn = document.getElementById('nav-user-toggle');
|
||||
if (navBtn) {
|
||||
const nameSpan = document.getElementById('nav-display-name');
|
||||
if (nameSpan) {
|
||||
nameSpan.textContent = display_name || window.f0ckSession?.user || '';
|
||||
}
|
||||
}
|
||||
|
||||
const userName = user || window.f0ckSession?.user;
|
||||
|
||||
// Update ALL nav-display-name instances (handles the profile page having a duplicate ID)
|
||||
document.querySelectorAll('#nav-display-name').forEach(nameSpan => {
|
||||
// preserve the badges (Admin, Mod)
|
||||
for (let i = nameSpan.childNodes.length - 1; i >= 0; i--) {
|
||||
let node = nameSpan.childNodes[i];
|
||||
if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) {
|
||||
// Match and preserve any non-breaking spaces at the start (from admin/mod badges)
|
||||
const match = node.nodeValue.match(new RegExp('^(\\s*)(.*)'));
|
||||
node.nodeValue = (match ? match[1] : '') + (display_name || userName || '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const bracket = document.getElementById('username-bracket');
|
||||
if (bracket) {
|
||||
bracket.style.display = display_name ? 'inline' : 'none';
|
||||
}
|
||||
|
||||
if (userName) {
|
||||
const userHref = `/user/${userName}`;
|
||||
// Sidebar / thread comment author links
|
||||
@@ -7406,6 +7429,105 @@ class NotificationSystem {
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: apply a username color change to relevant DOM nodes
|
||||
this.applyUsernameColorUpdate = (username_color, user) => {
|
||||
const userName = user || window.f0ckSession?.user;
|
||||
if (!userName) return;
|
||||
|
||||
const userHref = `/user/${userName}`;
|
||||
document.querySelectorAll(`a.comment-author[href="${userHref}"]`).forEach(el => {
|
||||
if (username_color) {
|
||||
el.style.color = username_color;
|
||||
} else {
|
||||
el.style.removeProperty('color');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle the item view username link
|
||||
const aUsername = document.getElementById('a_username');
|
||||
if (aUsername && aUsername.getAttribute('href') === userHref) {
|
||||
if (username_color) {
|
||||
aUsername.style.color = username_color;
|
||||
} else {
|
||||
aUsername.style.removeProperty('color');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle favs avatar border color
|
||||
document.querySelectorAll('#favs img').forEach(img => {
|
||||
const link = img.closest('a');
|
||||
if (link && (link.href.includes(`/user/${userName.toLowerCase()}`) || link.href.includes(`/user/${userName}`))) {
|
||||
if (username_color) {
|
||||
img.style.borderColor = username_color;
|
||||
} else {
|
||||
img.style.removeProperty('border-color');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update the infobox block variables for color accents
|
||||
const safeUserName = userName.replace(/"/g, '\\"');
|
||||
document.querySelectorAll(`.user-infobox-block[data-banner-user="${safeUserName}"]`).forEach(block => {
|
||||
if (username_color) {
|
||||
block.style.setProperty('--author-accent', username_color);
|
||||
block.style.setProperty('--author-border', username_color);
|
||||
} else {
|
||||
block.style.setProperty('--author-accent', 'var(--accent)');
|
||||
block.style.setProperty('--author-border', 'var(--accent)');
|
||||
}
|
||||
});
|
||||
|
||||
// Special case for settings page preview block
|
||||
if (window.f0ckSession && userName === window.f0ckSession.user) {
|
||||
const previewBlock = document.getElementById('live-profile-preview-box');
|
||||
if (previewBlock) {
|
||||
if (username_color) {
|
||||
previewBlock.style.setProperty('--author-accent', username_color);
|
||||
previewBlock.style.setProperty('--author-border', username_color);
|
||||
} else {
|
||||
previewBlock.style.setProperty('--author-accent', 'var(--accent)');
|
||||
previewBlock.style.setProperty('--author-border', 'var(--accent)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the nav-display-name span elements (profile header and top navbar)
|
||||
if (window.f0ckSession && userName === window.f0ckSession.user) {
|
||||
document.querySelectorAll('#nav-display-name').forEach(el => {
|
||||
if (username_color) {
|
||||
el.style.color = username_color;
|
||||
} else {
|
||||
el.style.removeProperty('color');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: apply an avatar update to relevant DOM nodes
|
||||
this.applyAvatarUpdate = (avatar_file, user) => {
|
||||
const userName = user || window.f0ckSession?.user;
|
||||
if (!userName) return;
|
||||
|
||||
const newSrc = avatar_file ? `/a/${avatar_file}?t=${Date.now()}` : '/a/default.png';
|
||||
|
||||
// Update Nav Avatar & profile header Avatar (only for current user)
|
||||
if (window.f0ckSession && userName === window.f0ckSession.user) {
|
||||
document.querySelectorAll('.nav-avatar-img, .profile_head_avatar img').forEach(img => img.src = newSrc);
|
||||
}
|
||||
|
||||
// Update globally (comments, sidebar, polls, rank, infoboxes, favs)
|
||||
// We broadly target a[href^="/user/"] img to catch basically any avatar linked to the profile
|
||||
const lowerHref = `/user/${userName.toLowerCase()}`;
|
||||
const exactHref = `/user/${userName}`;
|
||||
|
||||
document.querySelectorAll('.comment-avatar img, .sidebar-avatar, .poll-voter-avatar, .rank-avatar, .user-infobox-avatar img, #favs img, a[href^="/user/"] img').forEach(img => {
|
||||
const link = img.closest('a');
|
||||
if (link && (link.href.includes(lowerHref) || link.href.includes(exactHref))) {
|
||||
img.src = newSrc;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Helper: apply banner update to relevant DOM nodes
|
||||
this.applyBannerUpdate = (banner_file, banner_position, banner_size, user) => {
|
||||
const userName = user || window.f0ckSession?.user;
|
||||
@@ -7439,6 +7561,73 @@ class NotificationSystem {
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: apply description update to relevant DOM nodes
|
||||
this.applyDescriptionUpdate = (description, user) => {
|
||||
const userName = user || window.f0ckSession?.user;
|
||||
console.log('[SSE] applyDescriptionUpdate called for user:', userName, 'description:', description);
|
||||
if (!userName) return;
|
||||
|
||||
// Escape quotes for selector
|
||||
const safeUserName = userName.replace(/"/g, '\\"');
|
||||
// Find the profile header for this user
|
||||
const profileHead = document.querySelector(`.profile_head[data-banner-user="${safeUserName}"]`);
|
||||
console.log('[SSE] applyDescriptionUpdate profileHead found?', !!profileHead);
|
||||
|
||||
if (profileHead) {
|
||||
const descDisplay = profileHead.querySelector('#profile-desc-display div');
|
||||
console.log('[SSE] applyDescriptionUpdate descDisplay found?', !!descDisplay);
|
||||
|
||||
if (descDisplay) {
|
||||
if (!description || description.trim() === '') {
|
||||
descDisplay.innerHTML = '<em style="color:var(--text-muted);">No description</em>';
|
||||
} else {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.textContent = description;
|
||||
descDisplay.innerHTML = tempDiv.innerHTML.replace(/\n/g, '<br>');
|
||||
}
|
||||
}
|
||||
// Also update the edit input if it exists (for the logged in user)
|
||||
if (window.f0ckSession && userName === window.f0ckSession.user) {
|
||||
const input = profileHead.querySelector('#inline-desc-input');
|
||||
if (input) {
|
||||
input.value = description || '';
|
||||
console.log('[SSE] applyDescriptionUpdate input updated');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update any item page infoboxes
|
||||
document.querySelectorAll(`.user-infobox-block[data-banner-user="${safeUserName}"]`).forEach(block => {
|
||||
const infoboxDesc = block.querySelector('.user-infobox-description');
|
||||
if (infoboxDesc) {
|
||||
if (!description || description.trim() === '') {
|
||||
infoboxDesc.innerHTML = '';
|
||||
} else {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.textContent = description;
|
||||
infoboxDesc.innerHTML = tempDiv.innerHTML.replace(/\n/g, '<br>');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Special case for settings page preview block
|
||||
if (window.f0ckSession && userName === window.f0ckSession.user) {
|
||||
const previewBlock = document.getElementById('live-profile-preview-box');
|
||||
if (previewBlock) {
|
||||
const previewDesc = previewBlock.querySelector('.user-infobox-description');
|
||||
if (previewDesc) {
|
||||
if (!description || description.trim() === '') {
|
||||
previewDesc.innerHTML = 'Welcome to my profile! This is a live preview.';
|
||||
} else {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.textContent = description;
|
||||
previewDesc.innerHTML = tempDiv.innerHTML.replace(/\n/g, '<br>');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Re-apply own banner after every PJAX navigation and bfcache restore.
|
||||
// Fixes the case where SSE fired on the settings page before the item page DOM was loaded.
|
||||
const reapplyOwnBanner = () => {
|
||||
|
||||
@@ -162,6 +162,14 @@ export const handleAvatarUpload = async (req, res) => {
|
||||
where user_id = ${+req.session.id}
|
||||
`;
|
||||
|
||||
const payloadStr = JSON.stringify({
|
||||
user_id: req.session.id,
|
||||
user: req.session.user,
|
||||
avatar_file: finalFilename,
|
||||
avatar: null
|
||||
});
|
||||
await db`select pg_notify('profile_update', ${payloadStr})`;
|
||||
|
||||
console.log('[AVATAR HANDLER] Upload complete:', finalFilename);
|
||||
return sendJson(res, {
|
||||
success: true,
|
||||
@@ -225,6 +233,13 @@ export const handleAvatarDelete = async (req, res) => {
|
||||
where user_id = ${+req.session.id}
|
||||
`;
|
||||
|
||||
const payloadStr = JSON.stringify({
|
||||
user_id: req.session.id,
|
||||
user: req.session.user,
|
||||
avatar_file: null
|
||||
});
|
||||
await db`select pg_notify('profile_update', ${payloadStr})`;
|
||||
|
||||
console.log('[AVATAR HANDLER] Delete complete');
|
||||
return sendJson(res, { success: true, msg: 'Custom avatar removed' }, 200);
|
||||
} catch (err) {
|
||||
|
||||
@@ -331,6 +331,14 @@ export default router => {
|
||||
`;
|
||||
// Sync session immediately
|
||||
if (req.session) req.session.username_color = color;
|
||||
|
||||
const payloadStr = JSON.stringify({
|
||||
user_id: req.session.id,
|
||||
user: req.session.user,
|
||||
username_color: color
|
||||
});
|
||||
await db`select pg_notify('profile_update', ${payloadStr})`;
|
||||
|
||||
return res.json({ success: true, color }, 200);
|
||||
} catch (e) {
|
||||
console.error('Update Username Color error:', e);
|
||||
@@ -420,6 +428,14 @@ export default router => {
|
||||
`;
|
||||
// Sync session immediately
|
||||
if (req.session) req.session.display_name = cleanDisplayName;
|
||||
|
||||
const payloadStr = JSON.stringify({
|
||||
user_id: req.session.id,
|
||||
user: req.session.user,
|
||||
display_name: cleanDisplayName
|
||||
});
|
||||
await db`select pg_notify('profile_update', ${payloadStr})`;
|
||||
|
||||
return res.json({ success: true, display_name: cleanDisplayName, msg: 'Display name updated successfully' }, 200);
|
||||
} catch (e) {
|
||||
console.error('Update Display Name error:', e);
|
||||
@@ -448,6 +464,14 @@ export default router => {
|
||||
`;
|
||||
// Sync session immediately
|
||||
if (req.session) req.session.description = cleanDescription;
|
||||
|
||||
const payloadStr = JSON.stringify({
|
||||
user_id: req.session.id,
|
||||
user: req.session.user,
|
||||
description: cleanDescription
|
||||
});
|
||||
await db`select pg_notify('profile_update', ${payloadStr})`;
|
||||
|
||||
return res.json({ success: true, description: cleanDescription }, 200);
|
||||
} catch (e) {
|
||||
console.error('Update Description error:', e);
|
||||
|
||||
@@ -102,7 +102,10 @@ db.listen('profile_update', (payload) => {
|
||||
display_name: data.display_name,
|
||||
banner_file: data.banner_file,
|
||||
banner_position: data.banner_position,
|
||||
banner_size: data.banner_size
|
||||
banner_size: data.banner_size,
|
||||
avatar_file: data.avatar_file,
|
||||
username_color: data.username_color,
|
||||
description: data.description
|
||||
};
|
||||
client.send({ type: 'profile_update', data: publicData });
|
||||
}
|
||||
|
||||
@@ -422,7 +422,37 @@
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
let objectUrl = '/a/' + (data.avatar_file || data.file) + '?t=' + Date.now();
|
||||
let wrapper = document.querySelector('.avatar-image-wrapper');
|
||||
if (wrapper) {
|
||||
let placeholder = wrapper.querySelector('.avatar-placeholder');
|
||||
if (placeholder) {
|
||||
let img = document.createElement('img');
|
||||
img.className = 'profile-avatar-img';
|
||||
img.src = objectUrl;
|
||||
wrapper.insertBefore(img, placeholder);
|
||||
placeholder.remove();
|
||||
} else {
|
||||
let img = wrapper.querySelector('.profile-avatar-img');
|
||||
if (img) img.src = objectUrl;
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.nav-avatar-img').forEach(img => img.src = objectUrl);
|
||||
document.querySelectorAll('.comment-avatar img, .sidebar-avatar, .poll-voter-avatar, .rank-avatar').forEach(img => {
|
||||
// Only replace if it matches the user's old avatar file, or if they didn't have one, just match if it links to their profile
|
||||
const oldFile = '{{ user.avatar_file }}';
|
||||
const oldId = '{{ user.avatar }}';
|
||||
const isOldImage = (oldFile && img.src.includes('/a/' + oldFile)) || (oldId && img.src.includes('/t/' + oldId + '.webp'));
|
||||
const link = img.closest('a');
|
||||
const isUserLink = link && link.href.includes('/user/{{ user.user }}');
|
||||
|
||||
if (isOldImage || isUserLink) {
|
||||
img.src = objectUrl;
|
||||
}
|
||||
});
|
||||
avatarEditBtn.innerHTML = '<i class="fa-solid fa-pen" style="font-size: 0.7em;"></i>';
|
||||
avatarEditBtn.disabled = false;
|
||||
} else {
|
||||
alert(data.msg || 'Failed to upload avatar');
|
||||
avatarEditBtn.innerHTML = '<i class="fa-solid fa-pen" style="font-size: 0.7em;"></i>';
|
||||
@@ -471,7 +501,27 @@
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||
body: JSON.stringify(cPayload)
|
||||
});
|
||||
location.reload();
|
||||
nameSaveBtn.disabled = false;
|
||||
nameContainer.style.display = 'flex';
|
||||
nameEditContainer.style.display = 'none';
|
||||
|
||||
const nameSpan = document.getElementById('nav-display-name');
|
||||
if (nameSpan) {
|
||||
nameSpan.style.color = color;
|
||||
for (let i = nameSpan.childNodes.length - 1; i >= 0; i--) {
|
||||
let node = nameSpan.childNodes[i];
|
||||
if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) {
|
||||
// Match and preserve any non-breaking spaces at the start (from admin/mod badges)
|
||||
const match = node.nodeValue.match(new RegExp('^(\\\\s*)(.*)'));
|
||||
node.nodeValue = (match ? match[1] : '') + (displayName ? displayName : '{{ user.user }}');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const bracket = document.getElementById('username-bracket');
|
||||
if (bracket) {
|
||||
bracket.style.display = displayName ? 'inline' : 'none';
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error saving name/color');
|
||||
nameSaveBtn.disabled = false;
|
||||
@@ -507,7 +557,17 @@
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
descSaveBtn.disabled = false;
|
||||
descDisplay.style.display = 'block';
|
||||
descEditContainer.style.display = 'none';
|
||||
const descTextDiv = descDisplay.querySelector('div');
|
||||
if (description.trim() === '') {
|
||||
descTextDiv.innerHTML = '<em style="color:var(--text-muted);">No description</em>';
|
||||
} else {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.textContent = description;
|
||||
descTextDiv.innerHTML = tempDiv.innerHTML.replace(new RegExp('\\\\n', 'g'), '<br>');
|
||||
}
|
||||
} else {
|
||||
alert(data.msg || 'Failed to update description');
|
||||
descSaveBtn.disabled = false;
|
||||
|
||||
Reference in New Issue
Block a user