1.1.2
Some checks failed
Build and Release / build-and-release (push) Has been cancelled

This commit is contained in:
Bryan1029384756
2026-04-17 21:44:01 -05:00
parent a3f6e29d6a
commit ad54f60225
30 changed files with 2517 additions and 475 deletions

View File

@@ -1,250 +0,0 @@
.container {
position: fixed;
top: 0;
left: 0;
z-index: calc(var(--z-index-modal, 10000) + 1);
border-radius: 12px;
overflow: hidden;
box-shadow:
0 8px 32px var(--voice-shadow-strong),
0 2px 8px var(--voice-shadow-soft);
cursor: grab;
user-select: none;
touch-action: none;
background-color: var(--voice-surface-2);
will-change: transform;
}
.container:active {
cursor: grabbing;
}
.videoWrapper {
position: absolute;
inset: 0;
overflow: hidden;
border-radius: inherit;
}
.videoWrapper video {
width: 100%;
height: 100%;
object-fit: contain;
background-color: var(--voice-surface-0);
border-radius: inherit;
display: block;
}
.screenShareVideo {
object-fit: contain;
}
/* ── Top gradient + return-to-call button ───────────────────── */
.headerGradient {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 48px;
background: linear-gradient(to bottom, var(--voice-overlay-strong) 0%, transparent 100%);
display: flex;
align-items: flex-start;
padding: 8px 10px;
pointer-events: auto;
z-index: 3;
}
.returnToCallButton {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--voice-text-strong);
cursor: pointer;
background: none;
border: none;
padding: 0;
font-size: 0.75rem;
font-weight: 600;
border-bottom: 1px solid transparent;
transition: border-color 150ms;
}
.returnToCallButton:hover {
border-bottom-color: currentColor;
}
/* ── Bottom gradient + streamer name ───────────────────────── */
.footerGradient {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 48px;
background: linear-gradient(to top, var(--voice-overlay-strong) 0%, transparent 100%);
display: flex;
align-items: flex-end;
padding: 8px 10px;
pointer-events: none;
z-index: 3;
}
.streamerName {
font-size: 0.75rem;
font-weight: 500;
color: var(--voice-text-strong);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── Resize handles ─────────────────────────────────────────── */
.resizeHandleTop,
.resizeHandleBottom,
.resizeHandleLeft,
.resizeHandleRight,
.resizeHandleTopLeft,
.resizeHandleTopRight,
.resizeHandleBottomLeft,
.resizeHandleBottomRight {
position: absolute;
border: none;
background: transparent;
z-index: 4;
pointer-events: auto;
padding: 0;
}
.resizeHandleTop {
top: 0;
left: 16px;
right: 16px;
height: 8px;
cursor: n-resize;
}
.resizeHandleBottom {
bottom: 0;
left: 16px;
right: 16px;
height: 8px;
cursor: s-resize;
}
.resizeHandleLeft {
top: 16px;
left: 0;
bottom: 16px;
width: 8px;
cursor: w-resize;
}
.resizeHandleRight {
top: 16px;
right: 0;
bottom: 16px;
width: 8px;
cursor: e-resize;
}
.resizeHandleTopLeft {
top: 0;
left: 0;
width: 16px;
height: 16px;
cursor: nw-resize;
}
.resizeHandleTopRight {
top: 0;
right: 0;
width: 16px;
height: 16px;
cursor: ne-resize;
}
.resizeHandleBottomLeft {
bottom: 0;
left: 0;
width: 16px;
height: 16px;
cursor: sw-resize;
}
.resizeHandleBottomRight {
bottom: 0;
right: 0;
width: 16px;
height: 16px;
cursor: se-resize;
}
.pipContent {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
padding: 12px 14px;
gap: 4px;
}
.pipLabel {
font-size: 11px;
font-weight: 600;
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.pipChannel {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pipActions {
display: flex;
gap: 8px;
margin-top: 6px;
}
.pipPrimary {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 10px;
border: none;
border-radius: 4px;
background-color: var(--brand-primary, #4641d9);
color: #fff;
font: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.pipPrimary:hover {
filter: brightness(1.1);
}
.pipDanger {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 4px;
background-color: var(--status-danger, #ed4245);
color: #fff;
cursor: pointer;
}
.pipDanger:hover {
filter: brightness(1.1);
}

View File

@@ -1,133 +0,0 @@
/**
* PiPOverlay — floating voice-call status widget shown when the user
* is connected to a voice channel but has navigated to a different
* channel / DM / route. Presents the channel name, a "Return to
* channel" button, and a disconnect button. The widget is draggable
* via pointer events.
*
* The new UI's screen-share video PiP required live LiveKit track
* binding from a MobX store; our Convex/React voice context doesn't
* expose that the same way, so this is a simpler status widget
* focused on the common case: "I'm in a call, I want to get back."
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { PhoneDisconnect, ArrowRight } from '@phosphor-icons/react';
import { useVoice } from '../../contexts/VoiceContext';
import styles from './PiPOverlay.module.css';
interface Box {
x: number;
y: number;
}
const WIDTH = 260;
const HEIGHT = 96;
function clamp(v: number, min: number, max: number): number {
return Math.max(min, Math.min(max, v));
}
export function PiPOverlay() {
const navigate = useNavigate();
const location = useLocation();
const voice = useVoice() as any;
const [box, setBox] = useState<Box>(() => ({
x: Math.max(16, window.innerWidth - WIDTH - 24),
y: Math.max(16, window.innerHeight - HEIGHT - 24),
}));
const dragRef = useRef<{ startX: number; startY: number; startBox: Box } | null>(null);
useEffect(() => {
const handleMove = (e: PointerEvent) => {
const drag = dragRef.current;
if (!drag) return;
const dx = e.clientX - drag.startX;
const dy = e.clientY - drag.startY;
setBox({
x: clamp(drag.startBox.x + dx, 0, window.innerWidth - WIDTH),
y: clamp(drag.startBox.y + dy, 0, window.innerHeight - HEIGHT),
});
};
const handleUp = () => {
dragRef.current = null;
};
window.addEventListener('pointermove', handleMove);
window.addEventListener('pointerup', handleUp);
return () => {
window.removeEventListener('pointermove', handleMove);
window.removeEventListener('pointerup', handleUp);
};
}, []);
const startDrag = useCallback(
(e: React.PointerEvent) => {
// Don't initiate a drag when the pointer lands on a button.
const target = e.target as HTMLElement;
if (target.closest('button')) return;
e.preventDefault();
dragRef.current = {
startX: e.clientX,
startY: e.clientY,
startBox: { ...box },
};
},
[box],
);
const activeChannelId: string | null = voice?.activeChannelId ?? null;
const activeChannelName: string | null = voice?.activeChannelName ?? null;
// Hide the overlay when not in a call or when the user is already
// looking at the active voice channel.
if (!activeChannelId) return null;
const onActiveChannelRoute =
location.pathname.includes(`/${activeChannelId}`);
if (onActiveChannelRoute) return null;
const handleReturn = () => {
navigate(`/channels/home/${activeChannelId}`);
};
const handleDisconnect = () => {
voice?.disconnect?.();
};
return (
<div
className={styles.container}
style={{
transform: `translate(${box.x}px, ${box.y}px)`,
width: WIDTH,
height: HEIGHT,
}}
onPointerDown={startDrag}
>
<div className={styles.pipContent}>
<div className={styles.pipLabel}>In voice</div>
<div className={styles.pipChannel}>
#{activeChannelName || 'channel'}
</div>
<div className={styles.pipActions}>
<button
type="button"
className={styles.pipPrimary}
onClick={handleReturn}
aria-label="Return to channel"
>
<ArrowRight size={14} weight="bold" />
<span>Return</span>
</button>
<button
type="button"
className={styles.pipDanger}
onClick={handleDisconnect}
aria-label="Disconnect"
>
<PhoneDisconnect size={14} weight="bold" />
</button>
</div>
</div>
</div>
);
}

View File

@@ -60,6 +60,24 @@
color: var(--voice-text-strong);
}
.backButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: transparent;
border: none;
color: var(--voice-text-strong);
cursor: pointer;
border-radius: 8px;
-webkit-tap-highlight-color: transparent;
}
.backButton:active {
background-color: rgba(255, 255, 255, 0.08);
}
.channelIcon {
color: var(--voice-text-muted);
}

View File

@@ -1,5 +1,7 @@
import { Hash } from '@phosphor-icons/react';
import { ArrowLeft, Hash } from '@phosphor-icons/react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useVoice } from '../../contexts/VoiceContext';
import { useIsMobile } from '../../hooks/useIsMobile';
import { ScreenSharePreview } from './ScreenSharePreview';
import { VoiceGridLayout } from './VoiceGridLayout';
import { VoiceControlBar } from './VoiceControlBar';
@@ -22,6 +24,21 @@ interface RawParticipant {
export function VoiceCallView({ channelId }: VoiceCallViewProps) {
const voice = useVoice();
const isMobile = useIsMobile();
const navigate = useNavigate();
const location = useLocation();
// On mobile the call surface fills the whole viewport and the
// channel list isn't visible, so expose an explicit way back.
// Drop the last route segment (the channelId) so we land on the
// server's nav column (e.g. /channels/home/xyz → /channels/home),
// which `GuildsLayout` renders as the sidebar on mobile.
const handleBack = () => {
const segments = location.pathname.split('/').filter(Boolean);
if (segments[0] === 'channels' && segments.length >= 3) {
navigate(`/${segments[0]}/${segments[1]}`);
}
};
const rawParticipants: RawParticipant[] =
(voice?.voiceStates && voice.voiceStates[channelId]) || [];
@@ -42,6 +59,16 @@ export function VoiceCallView({ channelId }: VoiceCallViewProps) {
<div className={`${styles.root} ${styles.pointerActive}`} data-voice-call-root>
<div className={styles.voiceHeader}>
<div className={styles.headerLeftSection}>
{isMobile && (
<button
type="button"
className={styles.backButton}
onClick={handleBack}
aria-label="Back to channel list"
>
<ArrowLeft size={22} weight="bold" />
</button>
)}
<div className={styles.channelInfoContainer}>
<Hash size={20} weight="bold" className={styles.channelIcon} />
<span className={styles.channelName}>{channelName}</span>