feat: redesign + avatars + theme + call presence fixes (v0.6.0)
Visual:
- Light/dark theme via ThemeContext + CSS vars
- New design tokens (surface/fg/accent/line/etc.) across all pages
- Reusable Avatar component (img + letter fallback) wired into UserBar,
ConversationHeader, ChatsPage, FriendsPage, GroupInfoPanel, IncomingCallPanel
Calls:
- Split call UI: IncomingCallPanel, InCallPanel, CallControls,
CallParticipantTile, ScreenShareViewer
- Active speaker hook (useActiveSpeakers)
- Fix: ActiveCallBanner stayed hidden after hangup while peers in room.
- useCallPresence: bind presence callbacks only when we own subscribe
(Supabase forbids .on() after .subscribe() on shared dedup'd channels)
- useCallPresence: never removeChannel — channel is shared with CallContext
so tearing it down on ConversationHeader unmount killed live tracking
- ActiveCallBanner: lastCallConversationId fallback so banner shows
instantly after hangup, auto-dismiss when room confirmed empty
- Drop unused useAnyActiveCall
Bump tauri version 0.5.0 -> 0.6.0
This commit is contained in:
@@ -90,6 +90,10 @@ export interface RemoteScreenShare {
|
||||
participantName: string;
|
||||
}
|
||||
|
||||
// Visual call modes (Discord-style): grid shows all tiles equally, focus pins
|
||||
// one speaker with others in a strip, fullscreen is cinema mode.
|
||||
export type CallMode = 'grid' | 'focus' | 'fullscreen';
|
||||
|
||||
interface CallContextValue {
|
||||
state: CallState;
|
||||
room: Room | null;
|
||||
@@ -102,6 +106,9 @@ interface CallContextValue {
|
||||
// Remembers the conversation of the last call we left so a sidebar widget
|
||||
// can show "still live — rejoin" while peers stay in the room.
|
||||
lastCallConversationId: string | null;
|
||||
// Clean-Rail UI state — display mode + focused participant id.
|
||||
callMode: CallMode;
|
||||
focusedId: string | null;
|
||||
// Actions:
|
||||
startCall: (conversationId: string, mediaKind?: CallKind) => Promise<void>;
|
||||
joinActiveCall: (conversationId: string, mediaKind?: CallKind) => Promise<void>;
|
||||
@@ -111,6 +118,8 @@ interface CallContextValue {
|
||||
toggleMute: () => void;
|
||||
toggleScreenShare: () => Promise<void>;
|
||||
dismissLastCall: () => void;
|
||||
setCallMode: (mode: CallMode) => void;
|
||||
setFocusedId: (id: string | null) => void;
|
||||
}
|
||||
|
||||
const CallContext = createContext<CallContextValue | null>(null);
|
||||
@@ -135,6 +144,8 @@ export function CallProvider({ children }: { children: ReactNode }) {
|
||||
const [isScreenSharing, setIsScreenSharing] = useState(false);
|
||||
const [remoteScreenShares, setRemoteScreenShares] = useState<RemoteScreenShare[]>([]);
|
||||
const [lastCallConversationId, setLastCallConversationId] = useState<string | null>(null);
|
||||
const [callMode, setCallModeState] = useState<CallMode>('grid');
|
||||
const [focusedId, setFocusedIdState] = useState<string | null>(null);
|
||||
|
||||
const signalChannelRef = useRef<RealtimeChannel | null>(null);
|
||||
const presenceChannelRef = useRef<RealtimeChannel | null>(null);
|
||||
@@ -954,6 +965,36 @@ export function CallProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
}, [myId, clearRingTimer, clearSoloTimer, disconnectRoom, sendSignal, emitCallEvent]);
|
||||
|
||||
const setCallMode = useCallback((mode: CallMode) => {
|
||||
setCallModeState(mode);
|
||||
}, []);
|
||||
|
||||
const setFocusedId = useCallback((id: string | null) => {
|
||||
setFocusedIdState(id);
|
||||
}, []);
|
||||
|
||||
// Reset UI call-mode state when the call leaves any active phase so the next
|
||||
// call starts fresh at grid/unfocused.
|
||||
useEffect(() => {
|
||||
if (state.kind === 'idle' || state.kind === 'error') {
|
||||
setCallModeState('grid');
|
||||
setFocusedIdState(null);
|
||||
}
|
||||
}, [state.kind]);
|
||||
|
||||
// Global Esc: drop out of fullscreen cinema back to grid while in an active
|
||||
// call. Doesn't hangup and doesn't fire when other dialogs would consume it.
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (callMode !== 'fullscreen') return;
|
||||
if (state.kind !== 'connected' && state.kind !== 'connecting') return;
|
||||
setCallModeState('grid');
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [callMode, state.kind]);
|
||||
|
||||
const value = useMemo<CallContextValue>(
|
||||
() => ({
|
||||
state,
|
||||
@@ -964,6 +1005,8 @@ export function CallProvider({ children }: { children: ReactNode }) {
|
||||
isScreenSharing,
|
||||
remoteScreenShares,
|
||||
lastCallConversationId,
|
||||
callMode,
|
||||
focusedId,
|
||||
startCall,
|
||||
joinActiveCall,
|
||||
acceptIncoming,
|
||||
@@ -972,6 +1015,8 @@ export function CallProvider({ children }: { children: ReactNode }) {
|
||||
toggleMute,
|
||||
toggleScreenShare,
|
||||
dismissLastCall,
|
||||
setCallMode,
|
||||
setFocusedId,
|
||||
}),
|
||||
[
|
||||
state,
|
||||
@@ -982,6 +1027,8 @@ export function CallProvider({ children }: { children: ReactNode }) {
|
||||
isScreenSharing,
|
||||
remoteScreenShares,
|
||||
lastCallConversationId,
|
||||
callMode,
|
||||
focusedId,
|
||||
startCall,
|
||||
joinActiveCall,
|
||||
acceptIncoming,
|
||||
@@ -990,6 +1037,8 @@ export function CallProvider({ children }: { children: ReactNode }) {
|
||||
toggleMute,
|
||||
toggleScreenShare,
|
||||
dismissLastCall,
|
||||
setCallMode,
|
||||
setFocusedId,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user