feat: update call layout controls
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -262,8 +262,22 @@ function MediaTrack({ track, muted = false, className = '' }) {
|
|||||||
|
|
||||||
|
|
||||||
function RemoteAudioTrack({ track, volume }) {
|
function RemoteAudioTrack({ track, volume }) {
|
||||||
|
const audioRef = useRef(null);
|
||||||
const gainRef = useRef(null);
|
const gainRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!track || !audioRef.current) return undefined;
|
||||||
|
track.attach(audioRef.current);
|
||||||
|
return () => {
|
||||||
|
track.detach(audioRef.current);
|
||||||
|
};
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!audioRef.current) return;
|
||||||
|
audioRef.current.volume = Math.min(1, Math.max(0, volume));
|
||||||
|
}, [volume]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!track?.mediaStreamTrack) return undefined;
|
if (!track?.mediaStreamTrack) return undefined;
|
||||||
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
||||||
@@ -272,10 +286,10 @@ function RemoteAudioTrack({ track, volume }) {
|
|||||||
const stream = new MediaStream([track.mediaStreamTrack]);
|
const stream = new MediaStream([track.mediaStreamTrack]);
|
||||||
const source = context.createMediaStreamSource(stream);
|
const source = context.createMediaStreamSource(stream);
|
||||||
const gain = context.createGain();
|
const gain = context.createGain();
|
||||||
gain.gain.value = volume;
|
gain.gain.value = Math.max(0, volume - 1);
|
||||||
source.connect(gain);
|
source.connect(gain);
|
||||||
gain.connect(context.destination);
|
gain.connect(context.destination);
|
||||||
gainRef.current = gain;
|
gainRef.current = { context, gain };
|
||||||
context.resume().catch(() => {});
|
context.resume().catch(() => {});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -287,14 +301,14 @@ function RemoteAudioTrack({ track, volume }) {
|
|||||||
}, [track]);
|
}, [track]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gainRef.current) gainRef.current.gain.value = volume;
|
if (gainRef.current) gainRef.current.gain.gain.value = Math.max(0, volume - 1);
|
||||||
}, [volume]);
|
}, [volume]);
|
||||||
|
|
||||||
return null;
|
return <audio ref={audioRef} autoPlay playsInline className='hidden-audio'></audio>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function VideoTile({ item, active, volume, onVolumeChange }) {
|
function VideoTile({ item, active, volume, volumeMenuOpen, onScreenFullscreen, onToggleVolumeMenu, onVolumeChange }) {
|
||||||
const camera = item.tracks.find((entry) => entry.kind === 'video' && entry.source !== Track.Source.ScreenShare);
|
const camera = item.tracks.find((entry) => entry.kind === 'video' && entry.source !== Track.Source.ScreenShare);
|
||||||
const screen = item.tracks.find((entry) => entry.kind === 'video' && entry.source === Track.Source.ScreenShare);
|
const screen = item.tracks.find((entry) => entry.kind === 'video' && entry.source === Track.Source.ScreenShare);
|
||||||
const audio = item.tracks.filter((entry) => entry.kind === 'audio');
|
const audio = item.tracks.filter((entry) => entry.kind === 'audio');
|
||||||
@@ -302,24 +316,45 @@ function VideoTile({ item, active, volume, onVolumeChange }) {
|
|||||||
const volumePercent = Math.round(volume * 100);
|
const volumePercent = Math.round(volume * 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`video-tile${active ? ' speaking' : ''}${screen ? ' has-screen' : ''}`}>
|
<div
|
||||||
|
className={`video-tile${active ? ' speaking' : ''}${screen ? ' has-screen' : ''}`}
|
||||||
|
onContextMenu={(event) => {
|
||||||
|
if (item.isLocal) return;
|
||||||
|
event.preventDefault();
|
||||||
|
onToggleVolumeMenu(item.identity);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className='video-label-row'>
|
<div className='video-label-row'>
|
||||||
<span>{item.name}</span>
|
<span>{item.name}</span>
|
||||||
<span className='muted'>{item.isLocal ? 'ты' : ''}</span>
|
<div className='tile-actions'>
|
||||||
|
<span className='muted'>{item.isLocal ? 'ты' : ''}</span>
|
||||||
|
{!item.isLocal ? (
|
||||||
|
<button className='tile-action-btn' type='button' onClick={() => onToggleVolumeMenu(item.identity)} aria-label={`Настройки участника ${item.name}`}>
|
||||||
|
⚙
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{screen ? (
|
||||||
|
<button className='tile-action-btn' type='button' onClick={(event) => onScreenFullscreen(event.currentTarget)} aria-label='Развернуть демонстрацию'>
|
||||||
|
⛶
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!item.isLocal ? (
|
{!item.isLocal && volumeMenuOpen ? (
|
||||||
<label className='participant-volume'>
|
<div className='participant-volume-popover'>
|
||||||
<span>Громкость: {volumePercent}%</span>
|
<label className='participant-volume'>
|
||||||
<input
|
<span>Громкость: {volumePercent}%</span>
|
||||||
aria-label={`Громкость участника ${item.name}`}
|
<input
|
||||||
max='2'
|
aria-label={`Громкость участника ${item.name}`}
|
||||||
min='0'
|
max='2'
|
||||||
onChange={(event) => onVolumeChange(item.identity, Number(event.target.value))}
|
min='0'
|
||||||
step='0.05'
|
onChange={(event) => onVolumeChange(item.identity, Number(event.target.value))}
|
||||||
type='range'
|
step='0.05'
|
||||||
value={volume}
|
type='range'
|
||||||
/>
|
value={volume}
|
||||||
</label>
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{screen ? (
|
{screen ? (
|
||||||
<div className='screen-slot'>
|
<div className='screen-slot'>
|
||||||
@@ -346,6 +381,7 @@ function CallPage({ roomName, showToast }) {
|
|||||||
const [room, setRoom] = useState(null);
|
const [room, setRoom] = useState(null);
|
||||||
const [participants, setParticipants] = useState([]);
|
const [participants, setParticipants] = useState([]);
|
||||||
const [participantVolumes, setParticipantVolumes] = useState({});
|
const [participantVolumes, setParticipantVolumes] = useState({});
|
||||||
|
const [volumeMenuIdentity, setVolumeMenuIdentity] = useState('');
|
||||||
const [activeSpeakers, setActiveSpeakers] = useState([]);
|
const [activeSpeakers, setActiveSpeakers] = useState([]);
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
const [chatText, setChatText] = useState('');
|
const [chatText, setChatText] = useState('');
|
||||||
@@ -353,6 +389,7 @@ function CallPage({ roomName, showToast }) {
|
|||||||
const [elapsed, setElapsed] = useState(0);
|
const [elapsed, setElapsed] = useState(0);
|
||||||
const previewRef = useRef(null);
|
const previewRef = useRef(null);
|
||||||
const previewStreamRef = useRef(null);
|
const previewStreamRef = useRef(null);
|
||||||
|
const meetingCardRef = useRef(null);
|
||||||
const fileRef = useRef(null);
|
const fileRef = useRef(null);
|
||||||
|
|
||||||
const inviteToken = useMemo(getInviteToken, []);
|
const inviteToken = useMemo(getInviteToken, []);
|
||||||
@@ -471,6 +508,43 @@ function CallPage({ roomName, showToast }) {
|
|||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const toggleParticipantVolumeMenu = useCallback((identity) => {
|
||||||
|
setVolumeMenuIdentity((current) => current === identity ? '' : identity);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fullscreenElement = () => document.fullscreenElement || document.webkitFullscreenElement;
|
||||||
|
|
||||||
|
const requestFullscreen = async (element) => {
|
||||||
|
if (!element) return;
|
||||||
|
if (element.requestFullscreen) {
|
||||||
|
await element.requestFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (element.webkitRequestFullscreen) element.webkitRequestFullscreen();
|
||||||
|
};
|
||||||
|
|
||||||
|
const exitFullscreen = async () => {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
await document.exitFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (document.webkitExitFullscreen) document.webkitExitFullscreen();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCallFullscreen = async () => {
|
||||||
|
if (fullscreenElement()) {
|
||||||
|
await exitFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await requestFullscreen(meetingCardRef.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openScreenFullscreen = async (button) => {
|
||||||
|
const tile = button.closest('.video-tile');
|
||||||
|
const target = tile?.querySelector('.screen-slot video') || tile?.querySelector('.screen-slot');
|
||||||
|
await requestFullscreen(target);
|
||||||
|
};
|
||||||
|
|
||||||
const joinCall = async () => {
|
const joinCall = async () => {
|
||||||
const name = displayName.trim();
|
const name = displayName.trim();
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -708,59 +782,65 @@ function CallPage({ roomName, showToast }) {
|
|||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<section className='card meeting-card'>
|
<div className='call-layout'>
|
||||||
<div className='meeting-topbar'>
|
<section className='card meeting-card' ref={meetingCardRef}>
|
||||||
<div>
|
<div className='meeting-topbar'>
|
||||||
<div className='meeting-title'>Созвон</div>
|
<div>
|
||||||
<h1 className='meeting-room-name'>{call.room_title}</h1>
|
<div className='meeting-title'>Созвон</div>
|
||||||
<p className='muted'>В комнате: {participants.length}</p>
|
<h1 className='meeting-room-name'>{call.room_title}</h1>
|
||||||
</div>
|
<p className='muted'>В комнате: {participants.length}</p>
|
||||||
<div className='meeting-controls'>
|
|
||||||
<button className={`icon-btn ${micEnabled ? '' : 'is-off'}`} type='button' onClick={toggleMic} aria-label={micEnabled ? 'Выключить микрофон' : 'Включить микрофон'}>
|
|
||||||
<img className='control-icon' src={micEnabled ? ICONS.micOn : ICONS.micOff} alt='' />
|
|
||||||
</button>
|
|
||||||
<button className={`icon-btn ${cameraEnabled ? '' : 'is-off'}`} type='button' onClick={toggleCamera} aria-label={cameraEnabled ? 'Выключить камеру' : 'Включить камеру'}>
|
|
||||||
<img className='control-icon' src={cameraEnabled ? ICONS.cameraOn : ICONS.cameraOff} alt='' />
|
|
||||||
</button>
|
|
||||||
<button className='icon-btn' type='button' onClick={toggleScreen}>Screen</button>
|
|
||||||
<button className='btn secondary' type='button' onClick={copyInvite}>Ссылка</button>
|
|
||||||
<div className='meeting-timer'>{time}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='videos'>
|
|
||||||
{participants.map((item) => (
|
|
||||||
<VideoTile
|
|
||||||
active={activeSpeakers.includes(item.identity)}
|
|
||||||
item={item}
|
|
||||||
key={item.identity}
|
|
||||||
onVolumeChange={updateParticipantVolume}
|
|
||||||
volume={participantVolumes[item.identity] ?? 1}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<aside className='card chat-card'>
|
|
||||||
<h2>Чат комнаты</h2>
|
|
||||||
<div className='chat-messages'>
|
|
||||||
{messages.map((message) => (
|
|
||||||
<div className={`chat-message${message.own ? ' own' : ''}`} key={message.id}>
|
|
||||||
<div className='chat-author'>{message.author}</div>
|
|
||||||
{message.attachment ? (
|
|
||||||
<a className='file-link' href={`${message.attachment.download_url}?invite_token=${encodeURIComponent(inviteToken)}`} target='_blank' rel='noreferrer'>{message.attachment.file_name}</a>
|
|
||||||
) : <div className='chat-text'>{message.text}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div className='meeting-controls'>
|
||||||
</div>
|
<button className={`icon-btn ${micEnabled ? '' : 'is-off'}`} type='button' onClick={toggleMic} aria-label={micEnabled ? 'Выключить микрофон' : 'Включить микрофон'}>
|
||||||
<div className='chat-form'>
|
<img className='control-icon' src={micEnabled ? ICONS.micOn : ICONS.micOff} alt='' />
|
||||||
<input value={chatText} onChange={(event) => setChatText(event.target.value)} onKeyDown={(event) => event.key === 'Enter' ? sendChat() : null} placeholder='Напиши сообщение...' maxLength='1000' />
|
</button>
|
||||||
<input className='hidden-file-input' ref={fileRef} type='file' onChange={(event) => sendFile(event.target.files?.[0])} />
|
<button className={`icon-btn ${cameraEnabled ? '' : 'is-off'}`} type='button' onClick={toggleCamera} aria-label={cameraEnabled ? 'Выключить камеру' : 'Включить камеру'}>
|
||||||
<button className='icon-btn attachment-btn' type='button' onClick={() => fileRef.current?.click()} aria-label='Прикрепить файл'>
|
<img className='control-icon' src={cameraEnabled ? ICONS.cameraOn : ICONS.cameraOff} alt='' />
|
||||||
<img className='control-icon' src={ICONS.attachment} alt='' />
|
</button>
|
||||||
</button>
|
<button className='icon-btn' type='button' onClick={toggleScreen} aria-label='Демонстрация экрана'>Screen</button>
|
||||||
<button className='btn primary' type='button' onClick={sendChat}>Отправить</button>
|
<button className='btn secondary' type='button' onClick={toggleCallFullscreen}>Во весь экран</button>
|
||||||
</div>
|
<button className='btn secondary' type='button' onClick={copyInvite}>Ссылка</button>
|
||||||
</aside>
|
<div className='meeting-timer'>{time}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='videos'>
|
||||||
|
{participants.map((item) => (
|
||||||
|
<VideoTile
|
||||||
|
active={activeSpeakers.includes(item.identity)}
|
||||||
|
item={item}
|
||||||
|
key={item.identity}
|
||||||
|
onScreenFullscreen={openScreenFullscreen}
|
||||||
|
onToggleVolumeMenu={toggleParticipantVolumeMenu}
|
||||||
|
onVolumeChange={updateParticipantVolume}
|
||||||
|
volume={participantVolumes[item.identity] ?? 1}
|
||||||
|
volumeMenuOpen={volumeMenuIdentity === item.identity}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside className='card chat-card'>
|
||||||
|
<h2>Чат комнаты</h2>
|
||||||
|
<div className='chat-messages'>
|
||||||
|
{messages.map((message) => (
|
||||||
|
<div className={`chat-message${message.own ? ' own' : ''}`} key={message.id}>
|
||||||
|
<div className='chat-author'>{message.author}</div>
|
||||||
|
{message.attachment ? (
|
||||||
|
<a className='file-link' href={`${message.attachment.download_url}?invite_token=${encodeURIComponent(inviteToken)}`} target='_blank' rel='noreferrer'>{message.attachment.file_name}</a>
|
||||||
|
) : <div className='chat-text'>{message.text}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className='chat-form'>
|
||||||
|
<input value={chatText} onChange={(event) => setChatText(event.target.value)} onKeyDown={(event) => event.key === 'Enter' ? sendChat() : null} placeholder='Напиши сообщение...' maxLength='1000' />
|
||||||
|
<input className='hidden-file-input' ref={fileRef} type='file' onChange={(event) => sendFile(event.target.files?.[0])} />
|
||||||
|
<button className='icon-btn attachment-btn' type='button' onClick={() => fileRef.current?.click()} aria-label='Прикрепить файл'>
|
||||||
|
<img className='control-icon' src={ICONS.attachment} alt='' />
|
||||||
|
</button>
|
||||||
|
<button className='btn primary' type='button' onClick={sendChat}>Отправить</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='call-bottom-actions is-visible'>
|
<div className='call-bottom-actions is-visible'>
|
||||||
<button className='btn secondary' type='button' onClick={leaveCall}>Выйти</button>
|
<button className='btn secondary' type='button' onClick={leaveCall}>Выйти</button>
|
||||||
|
|||||||
@@ -355,6 +355,13 @@ input[type='range'] {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.call-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(320px, 380px);
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
.meeting-title {
|
.meeting-title {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -390,6 +397,7 @@ input[type='range'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.video-tile {
|
.video-tile {
|
||||||
|
position: relative;
|
||||||
background: #0b1220;
|
background: #0b1220;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
@@ -406,19 +414,55 @@ input[type='range'] {
|
|||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-action-btn {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.3);
|
||||||
|
border-radius: 9px;
|
||||||
|
background: rgba(15, 23, 42, 0.8);
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-action-btn:hover {
|
||||||
|
background: rgba(37, 99, 235, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.participant-volume {
|
.participant-volume {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 10px 12px 12px;
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
background: rgba(15, 23, 42, 0.72);
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.participant-volume-popover {
|
||||||
|
position: absolute;
|
||||||
|
top: 48px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 20;
|
||||||
|
width: min(260px, calc(100% - 20px));
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(17, 24, 39, 0.98);
|
||||||
|
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
.screen-slot {
|
.screen-slot {
|
||||||
height: 260px;
|
height: 260px;
|
||||||
background: #020617;
|
background: #020617;
|
||||||
@@ -469,12 +513,18 @@ input[type='range'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-card {
|
.chat-card {
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
margin-bottom: 96px;
|
margin-bottom: 96px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
min-height: 260px;
|
flex: 1 1 auto;
|
||||||
max-height: 420px;
|
min-height: 360px;
|
||||||
|
max-height: calc(100vh - 260px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@@ -547,12 +597,45 @@ input[type='range'] {
|
|||||||
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.22);
|
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.meeting-card:fullscreen {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 0;
|
||||||
|
background: var(--bg-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-card:fullscreen .videos {
|
||||||
|
min-height: calc(100vh - 140px);
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-slot:fullscreen,
|
||||||
|
.screen-slot video:fullscreen {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-slot:fullscreen video,
|
||||||
|
.screen-slot video:fullscreen {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.home-grid,
|
.home-grid,
|
||||||
|
.call-layout,
|
||||||
.prejoin-layout {
|
.prejoin-layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-card {
|
||||||
|
position: static;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.meeting-topbar {
|
.meeting-topbar {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user