fix: resolve federated avatar color to home instance identity
Federated users get a different snowflake ID on remote instances, causing avatar gradients to mismatch their home identity. Apply resolveDisplayIdentity/isSelf resolution in VoiceUser, StreamTile, VoiceChannel sidebar, and MemberSidebar so the current user's avatar color is consistent across all views.
This commit is contained in:
@@ -2,6 +2,8 @@ import React, { useMemo } from 'react';
|
|||||||
import type { MemberWithUser } from '@backspace/shared';
|
import type { MemberWithUser } from '@backspace/shared';
|
||||||
import { useServerStore } from '../../stores/serverStore';
|
import { useServerStore } from '../../stores/serverStore';
|
||||||
import { useUIStore } from '../../stores/uiStore';
|
import { useUIStore } from '../../stores/uiStore';
|
||||||
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
|
import { resolveDisplayIdentity } from '../../utils/identity';
|
||||||
import { Avatar } from '../ui/Avatar';
|
import { Avatar } from '../ui/Avatar';
|
||||||
import { Username } from '../ui/Username';
|
import { Username } from '../ui/Username';
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ export function MemberSidebar() {
|
|||||||
const currentServerId = useServerStore((s) => s.currentServerId);
|
const currentServerId = useServerStore((s) => s.currentServerId);
|
||||||
const memberListOpen = useUIStore((s) => s.memberListOpen);
|
const memberListOpen = useUIStore((s) => s.memberListOpen);
|
||||||
const openUserProfile = useUIStore((s) => s.openUserProfile);
|
const openUserProfile = useUIStore((s) => s.openUserProfile);
|
||||||
|
const authUser = useAuthStore((s) => s.user);
|
||||||
|
|
||||||
const server = servers.find(s => s.id === currentServerId);
|
const server = servers.find(s => s.id === currentServerId);
|
||||||
const ownerId = server?.ownerId;
|
const ownerId = server?.ownerId;
|
||||||
@@ -97,6 +100,7 @@ export function MemberSidebar() {
|
|||||||
const renderMember = (member: MemberWithUser, isOffline = false) => {
|
const renderMember = (member: MemberWithUser, isOffline = false) => {
|
||||||
const displayName = member.user.displayName ?? member.user.username;
|
const displayName = member.user.displayName ?? member.user.username;
|
||||||
const colorStyle = isOffline ? undefined : getMemberColor(member);
|
const colorStyle = isOffline ? undefined : getMemberColor(member);
|
||||||
|
const resolvedUser = resolveDisplayIdentity(member.user, authUser ?? null);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={member.userId}
|
key={member.userId}
|
||||||
@@ -110,6 +114,7 @@ export function MemberSidebar() {
|
|||||||
status={isOffline ? 'offline' : member.user.status}
|
status={isOffline ? 'offline' : member.user.status}
|
||||||
className={isOffline ? 'opacity-60' : undefined}
|
className={isOffline ? 'opacity-60' : undefined}
|
||||||
user={member.user}
|
user={member.user}
|
||||||
|
userId={resolvedUser.id}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<Username
|
<Username
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||||
import { Avatar } from '../ui/Avatar';
|
import { Avatar } from '../ui/Avatar';
|
||||||
import { useVoiceStore } from '../../stores/voiceStore';
|
import { useVoiceStore } from '../../stores/voiceStore';
|
||||||
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
import { getActiveRoom, setStreamSubscription } from '../../hooks/useLiveKit';
|
import { getActiveRoom, setStreamSubscription } from '../../hooks/useLiveKit';
|
||||||
import { ScreenShareSettingsPopover } from './ScreenShareSettingsPopover';
|
import { ScreenShareSettingsPopover } from './ScreenShareSettingsPopover';
|
||||||
import { stopScreenShare, changeScreenShare } from '../../utils/screenShare';
|
import { stopScreenShare, changeScreenShare } from '../../utils/screenShare';
|
||||||
@@ -23,6 +24,8 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
const { participant } = tile;
|
const { participant } = tile;
|
||||||
const isLocal = participant.isLocal;
|
const isLocal = participant.isLocal;
|
||||||
const userId = participant.userId;
|
const userId = participant.userId;
|
||||||
|
const homeUser = useAuthStore((s) => s.user);
|
||||||
|
const avatarUserId = isLocal ? (homeUser?.id ?? userId) : userId;
|
||||||
|
|
||||||
const isWatching = watchingStreams.has(userId);
|
const isWatching = watchingStreams.has(userId);
|
||||||
const streamVolume = streamVolumes.get(userId) ?? 100;
|
const streamVolume = streamVolumes.get(userId) ?? 100;
|
||||||
@@ -146,7 +149,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex flex-col items-center justify-center gap-3 bg-surface-channel">
|
<div className="w-full h-full flex flex-col items-center justify-center gap-3 bg-surface-channel">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar src={null} name={participant.username} size={large ? 80 : 48} userId={participant.userId} />
|
<Avatar src={null} name={participant.username} size={large ? 80 : 48} userId={avatarUserId} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center px-4">
|
<div className="text-center px-4">
|
||||||
<p className="text-txt-primary text-sm font-semibold">
|
<p className="text-txt-primary text-sm font-semibold">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useVoiceStore } from '../../stores/voiceStore';
|
import { useVoiceStore } from '../../stores/voiceStore';
|
||||||
import { useAuthStore } from '../../stores/authStore';
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
|
import { isSelf } from '../../utils/identity';
|
||||||
|
|
||||||
const EMPTY_VOICE_USERS: string[] = [];
|
const EMPTY_VOICE_USERS: string[] = [];
|
||||||
import { useServerStore } from '../../stores/serverStore';
|
import { useServerStore } from '../../stores/serverStore';
|
||||||
@@ -19,7 +20,8 @@ export function VoiceChannel({ channelId, channelName, onClick }: VoiceChannelPr
|
|||||||
const localIsDeafened = useVoiceStore((s) => s.isDeafened);
|
const localIsDeafened = useVoiceStore((s) => s.isDeafened);
|
||||||
const localIsMuted = useVoiceStore((s) => s.isMuted);
|
const localIsMuted = useVoiceStore((s) => s.isMuted);
|
||||||
const voiceUserStates = useVoiceStore((s) => s.voiceUserStates);
|
const voiceUserStates = useVoiceStore((s) => s.voiceUserStates);
|
||||||
const currentUserId = useAuthStore((s) => s.user?.id);
|
const authUser = useAuthStore((s) => s.user);
|
||||||
|
const currentUserId = authUser?.id;
|
||||||
const members = useServerStore((s) => s.members);
|
const members = useServerStore((s) => s.members);
|
||||||
const isActive = currentVoiceChannel === channelId;
|
const isActive = currentVoiceChannel === channelId;
|
||||||
|
|
||||||
@@ -73,7 +75,7 @@ export function VoiceChannel({ channelId, channelName, onClick }: VoiceChannelPr
|
|||||||
name={displayName}
|
name={displayName}
|
||||||
size={24}
|
size={24}
|
||||||
status={status}
|
status={status}
|
||||||
userId={userId}
|
userId={(authUser && member?.user && isSelf(member.user, authUser)) ? authUser.id : userId}
|
||||||
/>
|
/>
|
||||||
<span className="text-[13px] text-txt-secondary truncate flex-1 min-w-0">{displayName}</span>
|
<span className="text-[13px] text-txt-secondary truncate flex-1 min-w-0">{displayName}</span>
|
||||||
{/* Status badges */}
|
{/* Status badges */}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||||
import { Avatar } from '../ui/Avatar';
|
import { Avatar } from '../ui/Avatar';
|
||||||
import { useVoiceStore } from '../../stores/voiceStore';
|
import { useVoiceStore } from '../../stores/voiceStore';
|
||||||
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
import type { UserTile } from '../../hooks/useLiveKit';
|
import type { UserTile } from '../../hooks/useLiveKit';
|
||||||
|
|
||||||
interface VoiceUserProps {
|
interface VoiceUserProps {
|
||||||
@@ -20,6 +21,8 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
|||||||
|
|
||||||
const perUserVolume = participantVolumes.get(participant.userId) ?? 100;
|
const perUserVolume = participantVolumes.get(participant.userId) ?? 100;
|
||||||
const isLocal = participant.isLocal;
|
const isLocal = participant.isLocal;
|
||||||
|
const homeUser = useAuthStore((s) => s.user);
|
||||||
|
const avatarUserId = isLocal ? (homeUser?.id ?? participant.userId) : participant.userId;
|
||||||
|
|
||||||
// --- VIDEO & UI ---
|
// --- VIDEO & UI ---
|
||||||
|
|
||||||
@@ -95,7 +98,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
|||||||
src={null}
|
src={null}
|
||||||
name={participant.username}
|
name={participant.username}
|
||||||
size={large ? 100 : 64}
|
size={large ? 100 : 64}
|
||||||
userId={participant.userId}
|
userId={avatarUserId}
|
||||||
/>
|
/>
|
||||||
{isSpeaking && (
|
{isSpeaking && (
|
||||||
<div className="absolute -inset-1.5 rounded-full ring-[3px] ring-status-online animate-pulse" />
|
<div className="absolute -inset-1.5 rounded-full ring-[3px] ring-status-online animate-pulse" />
|
||||||
|
|||||||
Reference in New Issue
Block a user