Feat v0.6.15 user display audit
Some checks failed
CI/CD / detect-changes (pull_request) Successful in 3s
CI/CD / test-frontend (pull_request) Successful in 6s
CI/CD / build-and-deploy (pull_request) Has been cancelled
CI/CD / test-go-pg (pull_request) Has been cancelled
CI/CD / test-sqlite (pull_request) Has been cancelled
Some checks failed
CI/CD / detect-changes (pull_request) Successful in 3s
CI/CD / test-frontend (pull_request) Successful in 6s
CI/CD / build-and-deploy (pull_request) Has been cancelled
CI/CD / test-go-pg (pull_request) Has been cancelled
CI/CD / test-sqlite (pull_request) Has been cancelled
Add batch user resolve endpoint, sw.users SDK module, and migrate all surfaces to show display_name instead of UUIDs. Canonical fallback chain: display_name → username → "Unknown". - GET /api/v1/users/resolve?ids=... (max 100, map response) - sw.users.resolve(), resolveMany(), displayName() with 60s cache - Chat participants resolved from users table, not snapshot - Admin users/teams/groups/team-admin show display_name - Chat-core participants.display_name column deprecated - 5 new handler tests, OpenAPI spec updated Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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' : '')} />
|
||||
|
||||
Reference in New Issue
Block a user