feat: real-time user profile updates and propagate avatarColor to all Avatar callsites
Broadcast user_updated events over WebSocket when profile fields change, updating members, DM participants, friends, and cached messages in real time. Widen useVoiceParticipantMeta to return the full user object and add a standalone avatarColor prop to Avatar so all ~16 callsites now resolve the user's chosen gradient color instead of falling back to hash-based colors.
This commit is contained in:
@@ -180,6 +180,50 @@ export async function userRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Broadcast user_updated for profile field changes
|
||||||
|
const profileFields = ['displayName', 'avatar', 'banner', 'accentColor', 'avatarColor', 'bio', 'customStatus'];
|
||||||
|
const hasProfileChange = profileFields.some(f => f in updateData);
|
||||||
|
|
||||||
|
if (hasProfileChange) {
|
||||||
|
const userUpdatedEvent = { type: 'user_updated' as const, user: sanitized };
|
||||||
|
const targetUserIds = new Set<string>();
|
||||||
|
|
||||||
|
// 1. Collect online users who share a space
|
||||||
|
const userSpaces = connectionManager.getUserSpaces(sanitized.id);
|
||||||
|
for (const [uid, spaceIds] of connectionManager.getUserSpaceEntries()) {
|
||||||
|
for (const spaceId of userSpaces) {
|
||||||
|
if (spaceIds.has(spaceId)) { targetUserIds.add(uid); break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Collect DM channel co-members
|
||||||
|
const dmMemberships = db.select().from(schema.dmMembers)
|
||||||
|
.where(eq(schema.dmMembers.userId, sanitized.id)).all();
|
||||||
|
for (const dm of dmMemberships) {
|
||||||
|
const coMembers = db.select().from(schema.dmMembers)
|
||||||
|
.where(eq(schema.dmMembers.dmChannelId, dm.dmChannelId)).all();
|
||||||
|
for (const m of coMembers) targetUserIds.add(m.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Collect friends
|
||||||
|
const friendRows = db.select().from(schema.friends)
|
||||||
|
.where(or(
|
||||||
|
eq(schema.friends.userId, sanitized.id),
|
||||||
|
eq(schema.friends.friendId, sanitized.id),
|
||||||
|
)).all();
|
||||||
|
for (const fr of friendRows) {
|
||||||
|
targetUserIds.add(fr.userId === sanitized.id ? fr.friendId : fr.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Include self (for other tabs/connections)
|
||||||
|
targetUserIds.add(sanitized.id);
|
||||||
|
|
||||||
|
// Send deduplicated — each user gets the event exactly once
|
||||||
|
for (const uid of targetUserIds) {
|
||||||
|
connectionManager.sendToUser(uid, userUpdatedEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return reply.code(200).send(sanitized);
|
return reply.code(200).send(sanitized);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ export type ServerEvent =
|
|||||||
| { type: 'voice_permission_muted'; userId: string; spaceId: string; muted: boolean }
|
| { type: 'voice_permission_muted'; userId: string; spaceId: string; muted: boolean }
|
||||||
| { type: 'voice_moved'; userId: string; oldChannelId: string; newChannelId: string }
|
| { type: 'voice_moved'; userId: string; oldChannelId: string; newChannelId: string }
|
||||||
| { type: 'voice_disconnected'; userId: string; channelId: string }
|
| { type: 'voice_disconnected'; userId: string; channelId: string }
|
||||||
|
| { type: 'user_updated'; user: User }
|
||||||
| { type: 'member_banned'; spaceId: string; reason: string | null }
|
| { type: 'member_banned'; spaceId: string; reason: string | null }
|
||||||
| { type: 'pong' }
|
| { type: 'pong' }
|
||||||
| { type: 'error'; message: string };
|
| { type: 'error'; message: string };
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ function FriendItem({ friend, onRemove, onDm }: { friend: TaggedFriend, onRemove
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between px-3 h-[62px] rounded-[8px] hover:bg-interactive-hover group transition-colors border-t border-interactive-muted mx-2">
|
<div className="flex items-center justify-between px-3 h-[62px] rounded-[8px] hover:bg-interactive-hover group transition-colors border-t border-interactive-muted mx-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Avatar src={friend.avatar} name={friend.displayName ?? friend.username} size={32} status={friend.status} userId={friend.homeUserId ?? friend.id} />
|
<Avatar src={friend.avatar} name={friend.displayName ?? friend.username} size={32} status={friend.status} userId={friend.homeUserId ?? friend.id} avatarColor={friend.avatarColor} />
|
||||||
<div className="flex flex-col leading-tight">
|
<div className="flex flex-col leading-tight">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="text-txt-primary font-semibold text-[15px]">{friend.displayName ?? friend.username}</span>
|
<span className="text-txt-primary font-semibold text-[15px]">{friend.displayName ?? friend.username}</span>
|
||||||
@@ -297,7 +297,7 @@ function RequestItem({ request, type, onAccept, onDecline, onCancel }: {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-interactive-hover group transition-colors border-t border-interactive-muted mx-2">
|
<div className="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-interactive-hover group transition-colors border-t border-interactive-muted mx-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Avatar src={user.avatar} name={user.displayName ?? user.username} size={32} status={user.status as any} userId={user.homeUserId ?? user.id} />
|
<Avatar src={user.avatar} name={user.displayName ?? user.username} size={32} status={user.status as any} userId={user.homeUserId ?? user.id} avatarColor={user.avatarColor} />
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="text-txt-primary font-bold text-sm">{user.displayName ?? user.username}</span>
|
<span className="text-txt-primary font-bold text-sm">{user.displayName ?? user.username}</span>
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export function MentionPopover({ query, selectedIndex, onSelect, anchorRef }: Me
|
|||||||
size={24}
|
size={24}
|
||||||
status={member.user.status}
|
status={member.user.status}
|
||||||
userId={member.user.homeUserId ?? member.user.id}
|
userId={member.user.homeUserId ?? member.user.id}
|
||||||
|
user={member.user}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="text-[14px] font-medium truncate"
|
className="text-[14px] font-medium truncate"
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export function ActivityPanel() {
|
|||||||
status={isOffline ? 'offline' : friend.status}
|
status={isOffline ? 'offline' : friend.status}
|
||||||
className={isOffline ? 'opacity-60' : undefined}
|
className={isOffline ? 'opacity-60' : undefined}
|
||||||
userId={friend.homeUserId ?? friend.id}
|
userId={friend.homeUserId ?? friend.id}
|
||||||
|
avatarColor={friend.avatarColor}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<Username
|
<Username
|
||||||
|
|||||||
@@ -236,12 +236,12 @@ export function ChannelSidebar() {
|
|||||||
zIndex: 2 - i,
|
zIndex: 2 - i,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar src={m.avatar} name={m.displayName ?? parseFederatedUsername(m.username).baseName} size={22} userId={m.homeUserId ?? m.id} />
|
<Avatar src={m.avatar} name={m.displayName ?? parseFederatedUsername(m.username).baseName} size={22} userId={m.homeUserId ?? m.id} user={m} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Avatar src={otherMembers[0]?.avatar} name={otherMembers[0]?.displayName ?? parseFederatedUsername(otherMembers[0]?.username ?? '').baseName} size={32} status={otherMembers[0]?.status as any} userId={otherMembers[0]?.homeUserId ?? otherMembers[0]?.id} />
|
<Avatar src={otherMembers[0]?.avatar} name={otherMembers[0]?.displayName ?? parseFederatedUsername(otherMembers[0]?.username ?? '').baseName} size={32} status={otherMembers[0]?.status as any} userId={otherMembers[0]?.homeUserId ?? otherMembers[0]?.id} user={otherMembers[0]} />
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<Username
|
<Username
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface AvatarProps {
|
|||||||
user?: User;
|
user?: User;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
ring?: { width: number; color: string };
|
ring?: { width: number; color: string };
|
||||||
|
avatarColor?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
@@ -54,12 +55,12 @@ function getDotMetrics(avatarSize: number, ringWidth: number = 0) {
|
|||||||
return { dot, gap, inset: avatarInset + ringWidth };
|
return { dot, gap, inset: avatarInset + ringWidth };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Avatar({ src, name, size = 40, status, className = '', onClick, user, userId, ring }: AvatarProps) {
|
export function Avatar({ src, name, size = 40, status, className = '', onClick, user, userId, ring, avatarColor }: AvatarProps) {
|
||||||
const openUserProfile = useUIStore((s) => s.openUserProfile);
|
const openUserProfile = useUIStore((s) => s.openUserProfile);
|
||||||
const initials = name.charAt(0).toUpperCase();
|
const initials = name.charAt(0).toUpperCase();
|
||||||
// Match prototype: 24px→10px, 32-34px→12px, 40px→15px, 56px+→18px
|
// Match prototype: 24px→10px, 32-34px→12px, 40px→15px, 56px+→18px
|
||||||
const fontPx = size <= 24 ? 10 : size <= 34 ? 12 : size <= 44 ? 15 : 18;
|
const fontPx = size <= 24 ? 10 : size <= 34 ? 12 : size <= 44 ? 15 : 18;
|
||||||
const gradient = getAvatarGradient(userId ?? user?.homeUserId ?? user?.id, name, user?.avatarColor);
|
const gradient = getAvatarGradient(userId ?? user?.homeUserId ?? user?.id, name, avatarColor ?? user?.avatarColor);
|
||||||
|
|
||||||
const ringWidth = ring?.width ?? 0;
|
const ringWidth = ring?.width ?? 0;
|
||||||
const outerSize = size + ringWidth * 2;
|
const outerSize = size + ringWidth * 2;
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ export function PictureInPicture() {
|
|||||||
[selectedStream, fallbackParticipant],
|
[selectedStream, fallbackParticipant],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { displayName: resolvedName, avatar: resolvedAvatar } =
|
const { displayName: resolvedName, avatar: resolvedAvatar, user: resolvedUser } =
|
||||||
useVoiceParticipantMeta(displayParticipant ?? EMPTY_PARTICIPANT);
|
useVoiceParticipantMeta(displayParticipant ?? EMPTY_PARTICIPANT);
|
||||||
|
|
||||||
if (!shouldShow) return null;
|
if (!shouldShow) return null;
|
||||||
@@ -370,6 +370,7 @@ export function PictureInPicture() {
|
|||||||
name={resolvedName}
|
name={resolvedName}
|
||||||
size={64}
|
size={64}
|
||||||
userId={displayParticipant.homeUserId ?? displayParticipant.userId}
|
userId={displayParticipant.homeUserId ?? displayParticipant.userId}
|
||||||
|
user={resolvedUser ?? undefined}
|
||||||
/>
|
/>
|
||||||
{speakingParticipantIds.has(displayParticipant.identity) && (
|
{speakingParticipantIds.has(displayParticipant.identity) && (
|
||||||
<div className="absolute -inset-1 rounded-full ring-2 ring-status-online animate-pulse" />
|
<div className="absolute -inset-1 rounded-full ring-2 ring-status-online animate-pulse" />
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
const isLocal = participant.isLocal;
|
const isLocal = participant.isLocal;
|
||||||
const userId = participant.userId;
|
const userId = participant.userId;
|
||||||
const avatarUserId = participant.homeUserId ?? userId;
|
const avatarUserId = participant.homeUserId ?? userId;
|
||||||
const { displayName, avatar } = useVoiceParticipantMeta(participant);
|
const { displayName, avatar, user } = useVoiceParticipantMeta(participant);
|
||||||
|
|
||||||
const isWatching = watchingStreams.has(userId);
|
const isWatching = watchingStreams.has(userId);
|
||||||
|
|
||||||
@@ -110,7 +110,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={avatar} name={displayName} size={large ? 80 : 48} userId={avatarUserId} />
|
<Avatar src={avatar} name={displayName} size={large ? 80 : 48} userId={avatarUserId} user={user ?? undefined} />
|
||||||
</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">
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export function VoiceChannel({ channelId, channelName, onClick, locked }: VoiceC
|
|||||||
size={24}
|
size={24}
|
||||||
status={status}
|
status={status}
|
||||||
userId={member?.user.homeUserId ?? userId}
|
userId={member?.user.homeUserId ?? userId}
|
||||||
|
user={member?.user}
|
||||||
/>
|
/>
|
||||||
<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 */}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
|||||||
|
|
||||||
const isLocal = participant.isLocal;
|
const isLocal = participant.isLocal;
|
||||||
const avatarUserId = participant.homeUserId ?? participant.userId;
|
const avatarUserId = participant.homeUserId ?? participant.userId;
|
||||||
const { displayName, avatar } = useVoiceParticipantMeta(participant);
|
const { displayName, avatar, user } = useVoiceParticipantMeta(participant);
|
||||||
|
|
||||||
// --- VIDEO & UI ---
|
// --- VIDEO & UI ---
|
||||||
|
|
||||||
@@ -98,6 +98,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
|||||||
name={displayName}
|
name={displayName}
|
||||||
size={large ? 100 : 64}
|
size={large ? 100 : 64}
|
||||||
userId={avatarUserId}
|
userId={avatarUserId}
|
||||||
|
user={user ?? undefined}
|
||||||
/>
|
/>
|
||||||
{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" />
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { useMemo } from 'react';
|
|||||||
import { useSpaceStore } from '../stores/spaceStore';
|
import { useSpaceStore } from '../stores/spaceStore';
|
||||||
import { parseFederatedUsername } from '../utils/identity';
|
import { parseFederatedUsername } from '../utils/identity';
|
||||||
import type { ParticipantInfo } from './useLiveKit';
|
import type { ParticipantInfo } from './useLiveKit';
|
||||||
|
import type { User } from '@backspace/shared';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves display metadata (displayName, avatar) for a voice participant
|
* Resolves display metadata (displayName, avatar, user) for a voice participant
|
||||||
* by looking up member data from the space/DM stores.
|
* by looking up member data from the space/DM stores.
|
||||||
*
|
*
|
||||||
* Reactive — re-renders when member data changes (e.g. user updates avatar mid-call).
|
* Reactive — re-renders when member data changes (e.g. user updates avatar mid-call).
|
||||||
@@ -21,6 +22,7 @@ export function useVoiceParticipantMeta(participant: ParticipantInfo) {
|
|||||||
return {
|
return {
|
||||||
displayName: member.user.displayName ?? baseName,
|
displayName: member.user.displayName ?? baseName,
|
||||||
avatar: member.user.avatar ?? null,
|
avatar: member.user.avatar ?? null,
|
||||||
|
user: member.user as User,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,12 +34,13 @@ export function useVoiceParticipantMeta(participant: ParticipantInfo) {
|
|||||||
return {
|
return {
|
||||||
displayName: dmMember.displayName ?? baseName,
|
displayName: dmMember.displayName ?? baseName,
|
||||||
avatar: dmMember.avatar ?? null,
|
avatar: dmMember.avatar ?? null,
|
||||||
|
user: dmMember as User,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Final fallback — parse username from LiveKit identity
|
// 3. Final fallback — parse username from LiveKit identity
|
||||||
const { baseName } = parseFederatedUsername(participant.username);
|
const { baseName } = parseFederatedUsername(participant.username);
|
||||||
return { displayName: baseName, avatar: null };
|
return { displayName: baseName, avatar: null, user: null as User | null };
|
||||||
}, [members, dmChannels, participant.userId, participant.username]);
|
}, [members, dmChannels, participant.userId, participant.username]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -310,6 +310,21 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'user_updated': {
|
||||||
|
if (!isHome) normalizeUserAssets(event.user, origin);
|
||||||
|
useSpaceStore.getState().updateUserEverywhere(event.user);
|
||||||
|
useSocialStore.getState().updateFriendProfile(event.user);
|
||||||
|
useChatStore.getState().updateUserInMessages(event.user);
|
||||||
|
// If this is the current user (other tab changed profile), update authStore
|
||||||
|
const myId = isHome
|
||||||
|
? useAuthStore.getState().user?.id
|
||||||
|
: getMyUserIdForOrigin(origin);
|
||||||
|
if (event.user.id === myId && isHome) {
|
||||||
|
setUser(event.user);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'voice_state_update':
|
case 'voice_state_update':
|
||||||
if (event.action === 'join') {
|
if (event.action === 'join') {
|
||||||
addVoiceUser(event.channelId, event.userId);
|
addVoiceUser(event.channelId, event.userId);
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ interface ChatState {
|
|||||||
markChannelUnread: (channelId: string) => void;
|
markChannelUnread: (channelId: string) => void;
|
||||||
ackChannel: (channelId: string) => void;
|
ackChannel: (channelId: string) => void;
|
||||||
onChannelAck: (channelId: string, messageId: string) => void;
|
onChannelAck: (channelId: string, messageId: string) => void;
|
||||||
|
updateUserInMessages: (user: { id: string; [key: string]: any }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Find which channel a message belongs to by scanning the message cache. */
|
/** Find which channel a message belongs to by scanning the message cache. */
|
||||||
@@ -593,4 +594,23 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
return { readStates: newReadStates, unreadChannels: newUnread };
|
return { readStates: newReadStates, unreadChannels: newUnread };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateUserInMessages: (user: { id: string; [key: string]: any }) => {
|
||||||
|
set((state) => {
|
||||||
|
const newMessages = new Map(state.messages);
|
||||||
|
let changed = false;
|
||||||
|
for (const [channelId, msgs] of newMessages) {
|
||||||
|
let channelChanged = false;
|
||||||
|
const updated = msgs.map(m => {
|
||||||
|
if (m.userId === user.id) {
|
||||||
|
channelChanged = true;
|
||||||
|
return { ...m, user: { ...m.user, ...user } };
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
if (channelChanged) { newMessages.set(channelId, updated); changed = true; }
|
||||||
|
}
|
||||||
|
return changed ? { messages: newMessages } : {};
|
||||||
|
});
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ interface SocialState {
|
|||||||
addIncomingRequest: (request: FriendRequest, origin: string) => void;
|
addIncomingRequest: (request: FriendRequest, origin: string) => void;
|
||||||
addFriendFromAccepted: (friend: Friend, requestId: string, origin: string) => void;
|
addFriendFromAccepted: (friend: Friend, requestId: string, origin: string) => void;
|
||||||
updateFriendPresence: (userId: string, status: string) => void;
|
updateFriendPresence: (userId: string, status: string) => void;
|
||||||
|
updateFriendProfile: (user: User) => void;
|
||||||
removeFriendLocally: (userId: string, origin: string) => void;
|
removeFriendLocally: (userId: string, origin: string) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
@@ -289,5 +290,19 @@ export const useSocialStore = create<SocialState>((set, get) => ({
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Called from WS handler on user_updated to keep friend profile data live
|
||||||
|
updateFriendProfile: (user: User) => {
|
||||||
|
set((state) => ({
|
||||||
|
friends: state.friends.map(f =>
|
||||||
|
f.id === user.id
|
||||||
|
? { ...f, displayName: user.displayName, avatar: user.avatar,
|
||||||
|
banner: user.banner, accentColor: user.accentColor,
|
||||||
|
avatarColor: user.avatarColor, bio: user.bio,
|
||||||
|
customStatus: user.customStatus, status: user.status }
|
||||||
|
: f
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
reset: () => set({ friends: [], requests: [], isLoading: false, error: null }),
|
reset: () => set({ friends: [], requests: [], isLoading: false, error: null }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ interface SpaceState {
|
|||||||
addSpace: (space: Space) => void;
|
addSpace: (space: Space) => void;
|
||||||
removeSpace: (spaceId: string) => void;
|
removeSpace: (spaceId: string) => void;
|
||||||
updateMemberPresence: (userId: string, status: string) => void;
|
updateMemberPresence: (userId: string, status: string) => void;
|
||||||
|
updateUserEverywhere: (user: User) => void;
|
||||||
addMember: (member: MemberWithUser) => void;
|
addMember: (member: MemberWithUser) => void;
|
||||||
removeMember: (userId: string) => void;
|
removeMember: (userId: string) => void;
|
||||||
populateFromReady: (origin: string, spaces: SpaceWithChannelsAndMembers[], folders?: SpaceFolder[], dmChannels?: DmChannel[]) => void;
|
populateFromReady: (origin: string, spaces: SpaceWithChannelsAndMembers[], folders?: SpaceFolder[], dmChannels?: DmChannel[]) => void;
|
||||||
@@ -304,6 +305,20 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateUserEverywhere: (user: User) => {
|
||||||
|
set((state) => ({
|
||||||
|
members: state.members.map(m =>
|
||||||
|
m.userId === user.id ? { ...m, user: { ...m.user, ...user } } : m
|
||||||
|
),
|
||||||
|
dmChannels: state.dmChannels.map(dm => ({
|
||||||
|
...dm,
|
||||||
|
members: dm.members.map(m =>
|
||||||
|
m.id === user.id ? { ...m, ...user } : m
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
addMember: (member: MemberWithUser) => {
|
addMember: (member: MemberWithUser) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
members: [...state.members.filter(m => m.userId !== member.userId), member],
|
members: [...state.members.filter(m => m.userId !== member.userId), member],
|
||||||
|
|||||||
Reference in New Issue
Block a user