Feat v0.6.15 user display audit (#50)
All checks were successful
CI/CD / detect-changes (push) Successful in 3s
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m51s
CI/CD / test-sqlite (push) Successful in 3m10s
CI/CD / build-and-deploy (push) Successful in 1m31s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #50.
This commit is contained in:
2026-04-01 14:19:48 +00:00
committed by xcaliber
parent c9b9e68c18
commit d9802df2af
21 changed files with 542 additions and 35 deletions

View File

@@ -91,6 +91,9 @@ def create(title, type="group", participants=None, creator_id="", creator_displa
cid = conv["id"]
# Add creator as admin participant
# NOTE: display_name is a snapshot captured at creation time.
# DEPRECATED v0.6.15 — UI resolves display names from users table via sw.users.resolve().
# Column retained for backward compatibility; UI no longer relies on this value for display.
if creator_id:
db.insert("participants", {
"conversation_id": cid,
@@ -110,7 +113,7 @@ def create(title, type="group", participants=None, creator_id="", creator_displa
"conversation_id": cid,
"participant_id": pid,
"participant_type": _str(p.get("type", "user")),
"display_name": _str(p.get("display_name", "")),
"display_name": _str(p.get("display_name", "")), # DEPRECATED v0.6.15 — snapshot only
"role": _str(p.get("role", "member")),
"joined_at": "",
})
@@ -213,7 +216,7 @@ def add_participant(conversation_id, participant_id, participant_type="user", di
"conversation_id": cid,
"participant_id": pid,
"participant_type": _str(participant_type),
"display_name": _str(display_name),
"display_name": _str(display_name), # DEPRECATED v0.6.15 — snapshot only
"role": _str(role),
"joined_at": "",
})

View File

@@ -243,9 +243,9 @@
onMouseEnter=${() => setHover(true)}
onMouseLeave=${() => setHover(false)}>
${!isOwn && html`
<${Avatar} name=${msg._display_name || msg.participant_id} size="sm" />`}
<${Avatar} name=${msg._display_name || 'Unknown'} size="sm" />`}
<div class="ext-chat-msg__body">
${!isOwn && html`<span class="ext-chat-msg__name">${msg._display_name || msg.participant_id}</span>`}
${!isOwn && html`<span class="ext-chat-msg__name">${msg._display_name || 'Unknown'}</span>`}
${editing ? html`
<div class="ext-chat-msg__edit">
<textarea class="ext-chat-msg__edit-input"
@@ -285,20 +285,32 @@
var [hasMore, setHasMore] = useState(false);
var [nextCursor, setNextCursor] = useState('');
var [typingUsers, setTypingUsers] = useState({});
var [resolvedNames, setResolvedNames] = useState({});
var bottomRef = useRef(null);
var listRef = useRef(null);
var userId = currentUserId();
// Build participant lookup
// Resolve participant display names from users table (not snapshot)
useEffect(() => {
var ids = (participants || []).map(p => p.participant_id).filter(Boolean);
if (ids.length === 0) return;
sw.users.resolveMany(ids).then(map => {
var names = {};
map.forEach((user, id) => { names[id] = sw.users.displayName(user); });
setResolvedNames(names);
});
}, [participants]);
// Build participant lookup from resolved names
var partMap = useMemo(() => {
var m = {};
(participants || []).forEach(p => { m[p.participant_id] = p.display_name || p.participant_id; });
(participants || []).forEach(p => { m[p.participant_id] = resolvedNames[p.participant_id] || p.display_name || 'Unknown'; });
return m;
}, [participants]);
}, [participants, resolvedNames]);
// Enrich messages with display names
function enrichMessages(msgs) {
return msgs.map(m => ({ ...m, _display_name: partMap[m.participant_id] || m.participant_id }));
return msgs.map(m => ({ ...m, _display_name: partMap[m.participant_id] || 'Unknown' }));
}
// Load initial messages
@@ -361,7 +373,7 @@
var unsubs = [
sw.realtime.subscribe(channel, 'message', (payload) => {
var msg = { ...payload, _display_name: partMap[payload.participant_id] || payload.participant_id };
var msg = { ...payload, _display_name: partMap[payload.participant_id] || 'Unknown' };
setMessages(prev => [...prev, msg]);
setTimeout(() => scrollToBottom(), 50);
// Auto mark read if from someone else
@@ -386,7 +398,8 @@
unsubs.push(sw.realtime.subscribe(channel, 'typing', (payload) => {
var pid = payload.participant_id;
if (pid === userId) return;
setTypingUsers(prev => ({ ...prev, [pid]: payload.display_name || pid }));
var typingName = resolvedNames[pid] || payload.display_name || 'Someone';
setTypingUsers(prev => ({ ...prev, [pid]: typingName }));
clearTimeout(typingTimers[pid]);
typingTimers[pid] = setTimeout(() => {
setTypingUsers(prev => {
@@ -553,6 +566,18 @@
function ParticipantSidebar({ conversationId, participants, onRefresh, isAdmin }) {
var [addOpen, setAddOpen] = useState(false);
var [presence, setPresence] = useState({});
var [resolvedNames, setResolvedNames] = useState({});
// Resolve display names from users table
useEffect(() => {
var ids = (participants || []).map(p => p.participant_id).filter(Boolean);
if (ids.length === 0) return;
sw.users.resolveMany(ids).then(map => {
var names = {};
map.forEach((user, id) => { names[id] = sw.users.displayName(user); });
setResolvedNames(names);
});
}, [participants]);
// Query presence
useEffect(() => {
@@ -588,9 +613,9 @@
<div class="ext-chat-participants__list">
${(participants || []).map(p => html`
<div key=${p.participant_id} class="ext-chat-participants__item">
<${Avatar} name=${p.display_name || p.participant_id} size="sm" />
<${Avatar} name=${resolvedNames[p.participant_id] || p.display_name || 'Unknown'} size="sm" />
<span class="ext-chat-participants__name">
${p.display_name || p.participant_id}
${resolvedNames[p.participant_id] || p.display_name || 'Unknown'}
${p.role === 'admin' && html`<span class="ext-chat-participants__badge">admin</span>`}
</span>
<span class=${'chat-participants__status' + (presence[p.participant_id] ? ' ext-chat-participants__status--online' : '')} />

View File

@@ -156,7 +156,7 @@
const user = sw.user;
const greeting = document.createElement('div');
greeting.className = 'ext-dashboard-greeting';
greeting.textContent = 'Welcome back' + (user ? ', ' + (user.display_name || user.username) : '');
greeting.textContent = 'Welcome back' + (user ? ', ' + (user.display_name || user.username || 'Unknown') : '');
main.appendChild(greeting);
const subtitle = document.createElement('div');

View File

@@ -181,7 +181,7 @@
<div key=${e.id} class="ext-team-activity-log-entry">
<div class="ext-team-activity-log-entry__header">
<span class="ext-team-activity-log-entry__category badge">${e.category}</span>
<span class="ext-team-activity-log-entry__user">${esc(e.username || 'unknown')}</span>
<span class="ext-team-activity-log-entry__user">${esc(e.username || 'Unknown')}</span>
<span class="ext-team-activity-log-entry__time">${timeAgo(e.created_at)}</span>
<button class="ext-team-activity-log-entry__delete" onClick=${() => onDelete(e.id)}
title="Delete">×</button>