fix: make server mute/deafen state survive page reload
- Client ready handler now builds restriction Sets atomically in a single setState call, eliminating intermediate empty-Set state that caused amber icons to flash rose on reload - buildReadyPayload queries all voice restrictions from DB across the user's spaces instead of relying on in-memory state (which is lost on disconnect timeout) - voice_join early-return path now re-broadcasts restrictions to the reconnecting user, covering fast reload where the user is still in the room
This commit is contained in:
@@ -425,6 +425,35 @@ function handleVoiceJoin(event: Record<string, unknown>, userId: string): void {
|
|||||||
isScreenSharing: status.isScreenSharing,
|
isScreenSharing: status.isScreenSharing,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-broadcast persistent restrictions (covers page reload while in voice)
|
||||||
|
const db = getDb();
|
||||||
|
const restrictions = db.select()
|
||||||
|
.from(schema.voiceRestrictions)
|
||||||
|
.where(and(
|
||||||
|
eq(schema.voiceRestrictions.spaceId, spaceId),
|
||||||
|
eq(schema.voiceRestrictions.userId, userId),
|
||||||
|
))
|
||||||
|
.all();
|
||||||
|
for (const r of restrictions) {
|
||||||
|
if (r.restrictionType === 'mute') {
|
||||||
|
connectionManager.setServerMuted(userId, true);
|
||||||
|
connectionManager.sendToUser(userId, {
|
||||||
|
type: 'voice_server_muted',
|
||||||
|
userId,
|
||||||
|
channelId,
|
||||||
|
muted: true,
|
||||||
|
});
|
||||||
|
} else if (r.restrictionType === 'deafen') {
|
||||||
|
connectionManager.setServerDeafened(userId, true);
|
||||||
|
connectionManager.sendToUser(userId, {
|
||||||
|
type: 'voice_server_deafened',
|
||||||
|
userId,
|
||||||
|
channelId,
|
||||||
|
deafened: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -875,34 +875,21 @@ function buildReadyPayload(userId: string): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build server mute/deafen states for users currently in voice
|
// Build server mute/deafen states from DB (authoritative source for all spaces the user belongs to)
|
||||||
const serverVoiceStates: Record<string, { serverMuted: boolean; serverDeafened: boolean }> = {};
|
const serverVoiceStates: Record<string, { serverMuted: boolean; serverDeafened: boolean }> = {};
|
||||||
for (const chId of Object.keys(voiceStates)) {
|
if (spaceIds.length > 0) {
|
||||||
const usersInChannel = voiceStates[chId];
|
const allRestrictions = db.select()
|
||||||
if (usersInChannel) {
|
.from(schema.voiceRestrictions)
|
||||||
for (const uid of usersInChannel) {
|
.where(inArray(schema.voiceRestrictions.spaceId, spaceIds))
|
||||||
const sm = connectionManager.isServerMuted(uid);
|
.all();
|
||||||
const sd = connectionManager.isServerDeafened(uid);
|
for (const r of allRestrictions) {
|
||||||
if (sm || sd) {
|
const existing = serverVoiceStates[r.userId] ?? { serverMuted: false, serverDeafened: false };
|
||||||
serverVoiceStates[uid] = { serverMuted: sm, serverDeafened: sd };
|
if (r.restrictionType === 'mute') existing.serverMuted = true;
|
||||||
}
|
if (r.restrictionType === 'deafen') existing.serverDeafened = true;
|
||||||
}
|
serverVoiceStates[r.userId] = existing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also include the connecting user's own DB-persisted restrictions
|
|
||||||
// (covers reconnect after disconnect timeout cleared in-memory state)
|
|
||||||
const myRestrictions = db.select()
|
|
||||||
.from(schema.voiceRestrictions)
|
|
||||||
.where(eq(schema.voiceRestrictions.userId, userId))
|
|
||||||
.all();
|
|
||||||
for (const r of myRestrictions) {
|
|
||||||
const existing = serverVoiceStates[userId] ?? { serverMuted: false, serverDeafened: false };
|
|
||||||
if (r.restrictionType === 'mute') existing.serverMuted = true;
|
|
||||||
if (r.restrictionType === 'deafen') existing.serverDeafened = true;
|
|
||||||
serverVoiceStates[userId] = existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch read states for unread tracking
|
// Fetch read states for unread tracking
|
||||||
const readStateRows = db.select()
|
const readStateRows = db.select()
|
||||||
.from(schema.readStates)
|
.from(schema.readStates)
|
||||||
|
|||||||
@@ -167,23 +167,26 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
|||||||
setVoiceUserStatus(uid, status.isMuted, status.isDeafened, status.isCameraOn, status.isScreenSharing);
|
setVoiceUserStatus(uid, status.isMuted, status.isDeafened, status.isCameraOn, status.isScreenSharing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Clear stale server voice states before applying fresh from ready payload
|
// Build new restriction Sets atomically from ready payload, then apply in one setState
|
||||||
{
|
{
|
||||||
const { clearServerVoiceStates, setServerMutedUser, setServerDeafenedUser } = useVoiceStore.getState();
|
const newServerMuted = new Set<string>();
|
||||||
clearServerVoiceStates();
|
const newServerDeafened = new Set<string>();
|
||||||
if (event.serverVoiceStates) {
|
if (event.serverVoiceStates) {
|
||||||
for (const [uid, state] of Object.entries(event.serverVoiceStates as Record<string, { serverMuted: boolean; serverDeafened: boolean }>)) {
|
for (const [uid, state] of Object.entries(event.serverVoiceStates as Record<string, { serverMuted: boolean; serverDeafened: boolean }>)) {
|
||||||
if (state.serverMuted) setServerMutedUser(uid, true);
|
if (state.serverMuted) newServerMuted.add(uid);
|
||||||
if (state.serverDeafened) setServerDeafenedUser(uid, true);
|
if (state.serverDeafened) newServerDeafened.add(uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Single atomic update — no intermediate empty-Set state
|
||||||
|
useVoiceStore.setState({ serverMutedUserIds: newServerMuted, serverDeafenedUserIds: newServerDeafened });
|
||||||
|
|
||||||
// Enforce local mute/deafen to match server restrictions (one-directional: only force-mute, never auto-unmute)
|
// Enforce local mute/deafen to match server restrictions (one-directional: only force-mute, never auto-unmute)
|
||||||
const myReadyId = useAuthStore.getState().user?.id;
|
const myReadyId = useAuthStore.getState().user?.id;
|
||||||
if (myReadyId) {
|
if (myReadyId) {
|
||||||
const vs = useVoiceStore.getState();
|
const vs = useVoiceStore.getState();
|
||||||
if (vs.serverDeafenedUserIds.has(myReadyId) && !vs.isDeafened) {
|
if (newServerDeafened.has(myReadyId) && !vs.isDeafened) {
|
||||||
useVoiceStore.setState({ isMuted: true, isDeafened: true });
|
useVoiceStore.setState({ isMuted: true, isDeafened: true });
|
||||||
} else if (vs.serverMutedUserIds.has(myReadyId) && !vs.isMuted) {
|
} else if (newServerMuted.has(myReadyId) && !vs.isMuted) {
|
||||||
useVoiceStore.setState({ isMuted: true });
|
useVoiceStore.setState({ isMuted: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user