profile update

This commit is contained in:
2026-07-17 16:56:40 +02:00
parent 0c0f0fd2a9
commit 2ff53b6513
6 changed files with 304 additions and 12 deletions

View File

@@ -6140,6 +6140,7 @@ video.emoji {
height: auto; height: auto;
/* vertical-align: middle; */ /* vertical-align: middle; */
display: inline-block; display: inline-block;
max-width: 200px;
} }
/* video in emoji autocomplete dropdown */ /* video in emoji autocomplete dropdown */

View File

@@ -7343,11 +7343,20 @@ class NotificationSystem {
} }
} }
} else if (data.type === 'profile_update') { } 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 (display_name !== undefined) this.applyDisplayNameUpdate(display_name, user);
if (banner_file !== undefined || banner_position !== undefined || banner_size !== undefined) { if (banner_file !== undefined || banner_position !== undefined || banner_size !== undefined) {
this.applyBannerUpdate(banner_file, banner_position, banner_size, user); 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) // Sync preferences to global session object for client-side gating (like DND)
if (window.f0ckSession && data.data.user_id === window.f0ckSession.id) { if (window.f0ckSession && data.data.user_id === window.f0ckSession.id) {
for (const key in data.data) { for (const key in data.data) {
@@ -7377,14 +7386,28 @@ class NotificationSystem {
// Capture old value before overwriting — needed for thumb matching below // Capture old value before overwriting — needed for thumb matching below
const oldDisplayName = window.f0ckSession?.display_name || null; const oldDisplayName = window.f0ckSession?.display_name || null;
if (window.f0ckSession) window.f0ckSession.display_name = 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; 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) { if (userName) {
const userHref = `/user/${userName}`; const userHref = `/user/${userName}`;
// Sidebar / thread comment author links // 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 // Helper: apply banner update to relevant DOM nodes
this.applyBannerUpdate = (banner_file, banner_position, banner_size, user) => { this.applyBannerUpdate = (banner_file, banner_position, banner_size, user) => {
const userName = user || window.f0ckSession?.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. // 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. // Fixes the case where SSE fired on the settings page before the item page DOM was loaded.
const reapplyOwnBanner = () => { const reapplyOwnBanner = () => {

View File

@@ -162,6 +162,14 @@ export const handleAvatarUpload = async (req, res) => {
where user_id = ${+req.session.id} 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); console.log('[AVATAR HANDLER] Upload complete:', finalFilename);
return sendJson(res, { return sendJson(res, {
success: true, success: true,
@@ -225,6 +233,13 @@ export const handleAvatarDelete = async (req, res) => {
where user_id = ${+req.session.id} 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'); console.log('[AVATAR HANDLER] Delete complete');
return sendJson(res, { success: true, msg: 'Custom avatar removed' }, 200); return sendJson(res, { success: true, msg: 'Custom avatar removed' }, 200);
} catch (err) { } catch (err) {

View File

@@ -331,6 +331,14 @@ export default router => {
`; `;
// Sync session immediately // Sync session immediately
if (req.session) req.session.username_color = color; 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); return res.json({ success: true, color }, 200);
} catch (e) { } catch (e) {
console.error('Update Username Color error:', e); console.error('Update Username Color error:', e);
@@ -420,6 +428,14 @@ export default router => {
`; `;
// Sync session immediately // Sync session immediately
if (req.session) req.session.display_name = cleanDisplayName; 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); return res.json({ success: true, display_name: cleanDisplayName, msg: 'Display name updated successfully' }, 200);
} catch (e) { } catch (e) {
console.error('Update Display Name error:', e); console.error('Update Display Name error:', e);
@@ -448,6 +464,14 @@ export default router => {
`; `;
// Sync session immediately // Sync session immediately
if (req.session) req.session.description = cleanDescription; 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); return res.json({ success: true, description: cleanDescription }, 200);
} catch (e) { } catch (e) {
console.error('Update Description error:', e); console.error('Update Description error:', e);

View File

@@ -102,7 +102,10 @@ db.listen('profile_update', (payload) => {
display_name: data.display_name, display_name: data.display_name,
banner_file: data.banner_file, banner_file: data.banner_file,
banner_position: data.banner_position, 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 }); client.send({ type: 'profile_update', data: publicData });
} }

View File

@@ -422,7 +422,37 @@
}); });
const data = await res.json(); const data = await res.json();
if (data.success) { 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 { } else {
alert(data.msg || 'Failed to upload avatar'); alert(data.msg || 'Failed to upload avatar');
avatarEditBtn.innerHTML = '<i class="fa-solid fa-pen" style="font-size: 0.7em;"></i>'; 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 }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
body: JSON.stringify(cPayload) 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) { } catch (err) {
alert('Error saving name/color'); alert('Error saving name/color');
nameSaveBtn.disabled = false; nameSaveBtn.disabled = false;
@@ -507,7 +557,17 @@
}); });
const data = await res.json(); const data = await res.json();
if (data.success) { 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 { } else {
alert(data.msg || 'Failed to update description'); alert(data.msg || 'Failed to update description');
descSaveBtn.disabled = false; descSaveBtn.disabled = false;