+
── whispers ──
{whispers.map((w) => (
-
+
🔊
{w.fromUsername}
{w.message || 'whispered'}
-
+
))}
)}
- {/* Video grid */}
+ {/* Video area — fills remaining space, never scrolls */}
{/* Controls */}
-
+
-
- {/* Box-drawn footer */}
-
- └{'─'.repeat(40)}┘
-
);
}
diff --git a/web/src/stores/voice.ts b/web/src/stores/voice.ts
index 9853136..a8977f9 100644
--- a/web/src/stores/voice.ts
+++ b/web/src/stores/voice.ts
@@ -20,6 +20,7 @@ export interface VoiceParticipant {
isMuted: boolean;
isSpeaking: boolean;
hasVideo: boolean;
+ isScreenSharing: boolean;
}
export interface WhisperNotification {
@@ -66,12 +67,15 @@ interface VoiceState {
function participantToVoice(p: Participant): VoiceParticipant {
const micPub = p.getTrackPublication(Track.Source.Microphone);
const camPub = p.getTrackPublication(Track.Source.Camera);
+ const screenPub = p.getTrackPublication(Track.Source.ScreenShare);
+ const screenAudioPub = p.getTrackPublication(Track.Source.ScreenShareAudio);
return {
identity: p.identity,
username: p.name || p.identity,
isMuted: micPub?.isMuted ?? true,
isSpeaking: p.isSpeaking,
hasVideo: !!(camPub && !camPub.isMuted),
+ isScreenSharing: !!(screenPub && !screenPub.isMuted) || !!(screenAudioPub && !screenAudioPub.isMuted),
};
}
@@ -367,8 +371,15 @@ export const useVoiceStore = create
((set, get) => ({
if (!room) return;
const newState = !get().isScreenSharing;
- await room.localParticipant.setScreenShareEnabled(newState);
- set({ isScreenSharing: newState });
+ try {
+ await room.localParticipant.setScreenShareEnabled(newState, { audio: true });
+ set({ isScreenSharing: newState });
+ } catch (err: any) {
+ // user cancelled the browser picker — not an error
+ if (err?.name === 'AbortError' || err?.message?.includes('cancel')) return;
+ console.error('[voice] screenshare toggle failed:', err);
+ set({ error: err instanceof Error ? err.message : 'Screen share failed' });
+ }
},
toggleNoiseSuppression: async () => {
diff --git a/web/vite.config.ts b/web/vite.config.ts
index 01332fe..f6a2e48 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -1,6 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { execSync } from 'child_process'
+import fs from 'fs'
+import path from 'path'
let gitSha = process.env.GIT_SHA
if (!gitSha) {
@@ -13,7 +15,23 @@ if (!gitSha) {
// https://vite.dev/config/
export default defineConfig({
- plugins: [react()],
+ plugins: [
+ react(),
+ // Inject git SHA into service worker cache version so every deploy busts the SW cache
+ {
+ name: 'inject-sw-version',
+ writeBundle() {
+ const swPath = path.resolve(__dirname, 'dist/sw.js');
+ if (!fs.existsSync(swPath)) return;
+ let content = fs.readFileSync(swPath, 'utf-8');
+ content = content.replace(
+ /const CACHE_VERSION = '[^']*'/,
+ `const CACHE_VERSION = 'dumpster-${gitSha}'`
+ );
+ fs.writeFileSync(swPath, content);
+ },
+ },
+ ],
define: {
__APP_VERSION__: JSON.stringify(`v${gitSha}`),
},