refactor: remove USE_VOICE_ACTIVITY dead code and enforce STREAM permission in VoiceControlBar
Remove the unused USE_VOICE_ACTIVITY permission bit (was bit 25) and shift STREAM to bit 25, DISCONNECT_MEMBERS to bit 26. Add a database migration to remap stored permission values. Gate camera and screen share buttons in VoiceControlBar behind canSpeak/canStream, matching VoiceControls behavior.
This commit is contained in:
@@ -615,9 +615,8 @@ Bitwise permission engine defined in `packages/shared/src/permissions.ts`. Store
|
|||||||
| 22 | MUTE_MEMBERS | Server-mute other members |
|
| 22 | MUTE_MEMBERS | Server-mute other members |
|
||||||
| 23 | DEAFEN_MEMBERS | Server-deafen other members |
|
| 23 | DEAFEN_MEMBERS | Server-deafen other members |
|
||||||
| 24 | MOVE_MEMBERS | Move members between voice channels |
|
| 24 | MOVE_MEMBERS | Move members between voice channels |
|
||||||
| 25 | USE_VOICE_ACTIVITY | Use voice activity detection |
|
| 25 | STREAM | Share screen in voice channels |
|
||||||
| 26 | STREAM | Share screen in voice channels |
|
| 26 | DISCONNECT_MEMBERS | Disconnect members from voice channels |
|
||||||
| 27 | DISCONNECT_MEMBERS | Disconnect members from voice channels |
|
|
||||||
|
|
||||||
**Resolution order:** Owner → @everyone role → Assigned roles (OR'd) → ADMINISTRATOR shortcut → Channel overrides (@everyone → role overrides → member override).
|
**Resolution order:** Owner → @everyone role → Assigned roles (OR'd) → ADMINISTRATOR shortcut → Channel overrides (@everyone → role overrides → member override).
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ export function runMigrations(db: Database.Database): void {
|
|||||||
// ─── Admin flag: ensure at least one admin exists (first registered user) ──
|
// ─── Admin flag: ensure at least one admin exists (first registered user) ──
|
||||||
migrateFirstAdmin(db);
|
migrateFirstAdmin(db);
|
||||||
|
|
||||||
|
// ─── Remove USE_VOICE_ACTIVITY bit and shift STREAM/DISCONNECT_MEMBERS down ─
|
||||||
|
migrateRemoveVoiceActivityBit(db);
|
||||||
|
|
||||||
// ─── Clean up corrupted read_states (temp_ IDs leaked from optimistic messages) ─
|
// ─── Clean up corrupted read_states (temp_ IDs leaked from optimistic messages) ─
|
||||||
migrateCorruptedReadStates(db);
|
migrateCorruptedReadStates(db);
|
||||||
|
|
||||||
@@ -303,6 +306,94 @@ function migrateEveryoneRoles(db: Database.Database): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the USE_VOICE_ACTIVITY bit (was bit 25) and shift STREAM (26→25)
|
||||||
|
* and DISCONNECT_MEMBERS (27→26) down. Idempotent: uses a sentinel flag in
|
||||||
|
* instance_settings metadata to avoid re-running.
|
||||||
|
*/
|
||||||
|
function migrateRemoveVoiceActivityBit(db: Database.Database): void {
|
||||||
|
// Use a pragma-style check: if STREAM is already at bit 25 in DEFAULT_EVERYONE_PERMISSIONS
|
||||||
|
// of the @everyone roles, the migration has already run. But for robustness, use a flag column.
|
||||||
|
// We'll check if any role still has bit 25 set AND bit 26 set (old layout had both USE_VOICE_ACTIVITY
|
||||||
|
// and STREAM). Simplest approach: track via a one-time marker.
|
||||||
|
const OLD_VOICE_ACTIVITY = 1n << 25n; // old USE_VOICE_ACTIVITY
|
||||||
|
const OLD_STREAM = 1n << 26n; // old STREAM
|
||||||
|
const OLD_DISCONNECT = 1n << 27n; // old DISCONNECT_MEMBERS
|
||||||
|
|
||||||
|
// Check if any role still uses the old bit layout (has bit 26 or 27 set)
|
||||||
|
const roles = db.prepare('SELECT id, permissions FROM roles WHERE permissions IS NOT NULL').all() as { id: string; permissions: string }[];
|
||||||
|
const overrides = db.prepare('SELECT channel_id, target_type, target_id, allow, deny FROM channel_overrides').all() as {
|
||||||
|
channel_id: string; target_type: string; target_id: string; allow: string; deny: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
let needsMigration = false;
|
||||||
|
for (const role of roles) {
|
||||||
|
try {
|
||||||
|
const p = BigInt(role.permissions);
|
||||||
|
if ((p & OLD_STREAM) !== 0n || (p & OLD_DISCONNECT) !== 0n || (p & OLD_VOICE_ACTIVITY) !== 0n) {
|
||||||
|
needsMigration = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch { /* skip invalid */ }
|
||||||
|
}
|
||||||
|
if (!needsMigration) {
|
||||||
|
for (const ov of overrides) {
|
||||||
|
try {
|
||||||
|
const a = BigInt(ov.allow);
|
||||||
|
const d = BigInt(ov.deny);
|
||||||
|
if ((a & OLD_STREAM) !== 0n || (a & OLD_DISCONNECT) !== 0n || (a & OLD_VOICE_ACTIVITY) !== 0n ||
|
||||||
|
(d & OLD_STREAM) !== 0n || (d & OLD_DISCONNECT) !== 0n || (d & OLD_VOICE_ACTIVITY) !== 0n) {
|
||||||
|
needsMigration = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch { /* skip invalid */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needsMigration) return;
|
||||||
|
|
||||||
|
function shiftPermBits(p: bigint): bigint {
|
||||||
|
const hasStream = (p & OLD_STREAM) !== 0n;
|
||||||
|
const hasDisconnect = (p & OLD_DISCONNECT) !== 0n;
|
||||||
|
// Clear bits 25, 26, 27
|
||||||
|
p = p & ~(OLD_VOICE_ACTIVITY | OLD_STREAM | OLD_DISCONNECT);
|
||||||
|
// Re-set at new positions
|
||||||
|
if (hasStream) p |= (1n << 25n); // STREAM now at 25
|
||||||
|
if (hasDisconnect) p |= (1n << 26n); // DISCONNECT_MEMBERS now at 26
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Migrating: Shifting permission bits (removing USE_VOICE_ACTIVITY)...');
|
||||||
|
|
||||||
|
const updateRole = db.prepare('UPDATE roles SET permissions = ? WHERE id = ?');
|
||||||
|
for (const role of roles) {
|
||||||
|
try {
|
||||||
|
const old = BigInt(role.permissions);
|
||||||
|
const shifted = shiftPermBits(old);
|
||||||
|
if (shifted !== old) {
|
||||||
|
updateRole.run(shifted.toString(), role.id);
|
||||||
|
}
|
||||||
|
} catch { /* skip invalid */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOverride = db.prepare(
|
||||||
|
'UPDATE channel_overrides SET allow = ?, deny = ? WHERE channel_id = ? AND target_type = ? AND target_id = ?'
|
||||||
|
);
|
||||||
|
for (const ov of overrides) {
|
||||||
|
try {
|
||||||
|
const oldAllow = BigInt(ov.allow);
|
||||||
|
const oldDeny = BigInt(ov.deny);
|
||||||
|
const newAllow = shiftPermBits(oldAllow);
|
||||||
|
const newDeny = shiftPermBits(oldDeny);
|
||||||
|
if (newAllow !== oldAllow || newDeny !== oldDeny) {
|
||||||
|
updateOverride.run(newAllow.toString(), newDeny.toString(), ov.channel_id, ov.target_type, ov.target_id);
|
||||||
|
}
|
||||||
|
} catch { /* skip invalid */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Migrating: Permission bit shift complete.');
|
||||||
|
}
|
||||||
|
|
||||||
/** Delete corrupted read_states rows where last_read_message_id is not a valid snowflake (numeric string) */
|
/** Delete corrupted read_states rows where last_read_message_id is not a valid snowflake (numeric string) */
|
||||||
function migrateCorruptedReadStates(db: Database.Database): void {
|
function migrateCorruptedReadStates(db: Database.Database): void {
|
||||||
const deleted = db.prepare(
|
const deleted = db.prepare(
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ export const PermissionBits = {
|
|||||||
MUTE_MEMBERS: 1n << 22n,
|
MUTE_MEMBERS: 1n << 22n,
|
||||||
DEAFEN_MEMBERS: 1n << 23n,
|
DEAFEN_MEMBERS: 1n << 23n,
|
||||||
MOVE_MEMBERS: 1n << 24n,
|
MOVE_MEMBERS: 1n << 24n,
|
||||||
USE_VOICE_ACTIVITY: 1n << 25n,
|
STREAM: 1n << 25n,
|
||||||
STREAM: 1n << 26n,
|
DISCONNECT_MEMBERS: 1n << 26n,
|
||||||
DISCONNECT_MEMBERS: 1n << 27n,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type PermissionBit = (typeof PermissionBits)[keyof typeof PermissionBits];
|
export type PermissionBit = (typeof PermissionBits)[keyof typeof PermissionBits];
|
||||||
@@ -39,8 +38,7 @@ export const DEFAULT_EVERYONE_PERMISSIONS =
|
|||||||
PermissionBits.ATTACH_FILES |
|
PermissionBits.ATTACH_FILES |
|
||||||
PermissionBits.READ_MESSAGE_HISTORY |
|
PermissionBits.READ_MESSAGE_HISTORY |
|
||||||
PermissionBits.ADD_REACTIONS |
|
PermissionBits.ADD_REACTIONS |
|
||||||
PermissionBits.STREAM |
|
PermissionBits.STREAM;
|
||||||
PermissionBits.USE_VOICE_ACTIVITY;
|
|
||||||
|
|
||||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ const PERMISSION_GROUPS: { name: string; perms: PermDef[] }[] = [
|
|||||||
{ bit: PermissionBits.DEAFEN_MEMBERS, label: 'Deafen Members' },
|
{ bit: PermissionBits.DEAFEN_MEMBERS, label: 'Deafen Members' },
|
||||||
{ bit: PermissionBits.MOVE_MEMBERS, label: 'Move Members' },
|
{ bit: PermissionBits.MOVE_MEMBERS, label: 'Move Members' },
|
||||||
{ bit: PermissionBits.DISCONNECT_MEMBERS, label: 'Disconnect Members' },
|
{ bit: PermissionBits.DISCONNECT_MEMBERS, label: 'Disconnect Members' },
|
||||||
{ bit: PermissionBits.USE_VOICE_ACTIVITY, label: 'Voice Activity' },
|
|
||||||
{ bit: PermissionBits.STREAM, label: 'Stream' },
|
{ bit: PermissionBits.STREAM, label: 'Stream' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useSpaceStore, getChannelOrigin, getMyUserIdForOrigin } from '../../sto
|
|||||||
import { ScreenShareSettingsPopover } from './ScreenShareSettingsPopover';
|
import { ScreenShareSettingsPopover } from './ScreenShareSettingsPopover';
|
||||||
import { CAMERA_PRESET, startScreenShare, stopScreenShare } from '../../utils/screenShare';
|
import { CAMERA_PRESET, startScreenShare, stopScreenShare } from '../../utils/screenShare';
|
||||||
import { broadcastVoiceStatus, broadcastDeafenViaLiveKit } from '../../utils/voice';
|
import { broadcastVoiceStatus, broadcastDeafenViaLiveKit } from '../../utils/voice';
|
||||||
|
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
||||||
|
|
||||||
const btnBase = 'w-10 h-10 flex items-center justify-center rounded-full transition-colors';
|
const btnBase = 'w-10 h-10 flex items-center justify-center rounded-full transition-colors';
|
||||||
const btnDefault = `${btnBase} bg-surface-channel text-txt-secondary hover:bg-surface-elevated hover:text-txt-primary`;
|
const btnDefault = `${btnBase} bg-surface-channel text-txt-secondary hover:bg-surface-elevated hover:text-txt-primary`;
|
||||||
@@ -35,6 +36,11 @@ export function VoiceControlBar() {
|
|||||||
const serverDeafenedUserIds = useVoiceStore((s) => s.serverDeafenedUserIds);
|
const serverDeafenedUserIds = useVoiceStore((s) => s.serverDeafenedUserIds);
|
||||||
const isServerMuted = !!(myOriginId && spaceId && serverMutedUserIds.has(`${spaceId}:${myOriginId}`));
|
const isServerMuted = !!(myOriginId && spaceId && serverMutedUserIds.has(`${spaceId}:${myOriginId}`));
|
||||||
const isServerDeafened = !!(myOriginId && spaceId && serverDeafenedUserIds.has(`${spaceId}:${myOriginId}`));
|
const isServerDeafened = !!(myOriginId && spaceId && serverDeafenedUserIds.has(`${spaceId}:${myOriginId}`));
|
||||||
|
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
|
||||||
|
const channelPerms = useSpaceStore((s) => currentVoiceChannelId ? s.channelPermissions.get(currentVoiceChannelId) : undefined);
|
||||||
|
const isDmCall = !!activeDmCall;
|
||||||
|
const canSpeak = isDmCall || hasPermissionBit(channelPerms, PermissionBits.SPEAK);
|
||||||
|
const canStream = isDmCall || hasPermissionBit(channelPerms, PermissionBits.STREAM);
|
||||||
const [qualityOpen, setQualityOpen] = useState(false);
|
const [qualityOpen, setQualityOpen] = useState(false);
|
||||||
const qualityBtnRef = useRef<HTMLButtonElement>(null);
|
const qualityBtnRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
@@ -174,6 +180,7 @@ export function VoiceControlBar() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Camera */}
|
{/* Camera */}
|
||||||
|
{canSpeak && (
|
||||||
<button
|
<button
|
||||||
onClick={handleCamera}
|
onClick={handleCamera}
|
||||||
className={isCameraOn ? btnGreen : btnDefault}
|
className={isCameraOn ? btnGreen : btnDefault}
|
||||||
@@ -190,8 +197,10 @@ export function VoiceControlBar() {
|
|||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Screen Share */}
|
{/* Screen Share */}
|
||||||
|
{canStream && (
|
||||||
<button
|
<button
|
||||||
onClick={handleScreenShare}
|
onClick={handleScreenShare}
|
||||||
className={isScreenSharing ? btnGreen : btnDefault}
|
className={isScreenSharing ? btnGreen : btnDefault}
|
||||||
@@ -202,6 +211,7 @@ export function VoiceControlBar() {
|
|||||||
<path d="M15 11L11 14V12H9V10H11V8L15 11Z" />
|
<path d="M15 11L11 14V12H9V10H11V8L15 11Z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Video Quality */}
|
{/* Video Quality */}
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user