Finish first pass at PTT lobby UI
This commit is contained in:
parent
38f9a79bd3
commit
c430ebb3a3
7 changed files with 271 additions and 125 deletions
61
src/room/AudioPreview.jsx
Normal file
61
src/room/AudioPreview.jsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import React from "react";
|
||||||
|
import styles from "./AudioPreview.module.css";
|
||||||
|
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
import { SelectInput } from "../input/SelectInput";
|
||||||
|
import { Item } from "@react-stately/collections";
|
||||||
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
|
export function AudioPreview({
|
||||||
|
state,
|
||||||
|
roomName,
|
||||||
|
audioInput,
|
||||||
|
audioInputs,
|
||||||
|
setAudioInput,
|
||||||
|
audioOutput,
|
||||||
|
audioOutputs,
|
||||||
|
setAudioOutput,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>{`${roomName} - Radio Call`}</h1>
|
||||||
|
<div className={styles.preview}>
|
||||||
|
{state === GroupCallState.LocalCallFeedUninitialized && (
|
||||||
|
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
|
||||||
|
Microphone permissions needed to join the call.
|
||||||
|
</Body>
|
||||||
|
)}
|
||||||
|
{state === GroupCallState.InitializingLocalCallFeed && (
|
||||||
|
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
|
||||||
|
Accept microphone permissions to join the call.
|
||||||
|
</Body>
|
||||||
|
)}
|
||||||
|
{state === GroupCallState.LocalCallFeedInitialized && (
|
||||||
|
<>
|
||||||
|
<SelectInput
|
||||||
|
label="Microphone"
|
||||||
|
selectedKey={audioInput}
|
||||||
|
onSelectionChange={setAudioInput}
|
||||||
|
className={styles.inputField}
|
||||||
|
>
|
||||||
|
{audioInputs.map(({ deviceId, label }) => (
|
||||||
|
<Item key={deviceId}>{label}</Item>
|
||||||
|
))}
|
||||||
|
</SelectInput>
|
||||||
|
{audioOutputs.length > 0 && (
|
||||||
|
<SelectInput
|
||||||
|
label="Speaker"
|
||||||
|
selectedKey={audioOutput}
|
||||||
|
onSelectionChange={setAudioOutput}
|
||||||
|
className={styles.inputField}
|
||||||
|
>
|
||||||
|
{audioOutputs.map(({ deviceId, label }) => (
|
||||||
|
<Item key={deviceId}>{label}</Item>
|
||||||
|
))}
|
||||||
|
</SelectInput>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
27
src/room/AudioPreview.module.css
Normal file
27
src/room/AudioPreview.module.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.preview {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 24px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
max-width: 414px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphonePermissions {
|
||||||
|
margin: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
.preview {
|
||||||
|
margin-top: 40px;
|
||||||
|
background-color: #21262C;
|
||||||
|
}
|
||||||
|
}
|
|
@ -126,6 +126,7 @@ export function GroupCallView({
|
||||||
return (
|
return (
|
||||||
<LobbyView
|
<LobbyView
|
||||||
client={client}
|
client={client}
|
||||||
|
groupCall={groupCall}
|
||||||
hasLocalParticipant={hasLocalParticipant}
|
hasLocalParticipant={hasLocalParticipant}
|
||||||
roomName={groupCall.room.name}
|
roomName={groupCall.room.name}
|
||||||
state={state}
|
state={state}
|
||||||
|
|
|
@ -1,23 +1,20 @@
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import styles from "./LobbyView.module.css";
|
import styles from "./LobbyView.module.css";
|
||||||
import { Button, CopyButton, MicButton, VideoButton } from "../button";
|
import { Button, CopyButton } from "../button";
|
||||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { useCallFeed } from "../video-grid/useCallFeed";
|
import { useCallFeed } from "../video-grid/useCallFeed";
|
||||||
import { useMediaStream } from "../video-grid/useMediaStream";
|
|
||||||
import { getRoomUrl } from "../matrix-utils";
|
import { getRoomUrl } from "../matrix-utils";
|
||||||
import { OverflowMenu } from "./OverflowMenu";
|
|
||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
import { Body, Link } from "../typography/Typography";
|
import { Body, Link } from "../typography/Typography";
|
||||||
import { Avatar } from "../Avatar";
|
|
||||||
import { useProfile } from "../profile/useProfile";
|
|
||||||
import useMeasure from "react-use-measure";
|
|
||||||
import { ResizeObserver } from "@juggle/resize-observer";
|
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
import { useMediaHandler } from "../settings/useMediaHandler";
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
|
import { VideoPreview } from "./VideoPreview";
|
||||||
|
import { AudioPreview } from "./AudioPreview";
|
||||||
|
|
||||||
export function LobbyView({
|
export function LobbyView({
|
||||||
client,
|
client,
|
||||||
|
groupCall,
|
||||||
roomName,
|
roomName,
|
||||||
state,
|
state,
|
||||||
onInitLocalCallFeed,
|
onInitLocalCallFeed,
|
||||||
|
@ -32,11 +29,14 @@ export function LobbyView({
|
||||||
roomId,
|
roomId,
|
||||||
}) {
|
}) {
|
||||||
const { stream } = useCallFeed(localCallFeed);
|
const { stream } = useCallFeed(localCallFeed);
|
||||||
const { audioOutput } = useMediaHandler();
|
const {
|
||||||
const videoRef = useMediaStream(stream, audioOutput, true);
|
audioInput,
|
||||||
const { displayName, avatarUrl } = useProfile(client);
|
audioInputs,
|
||||||
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
setAudioInput,
|
||||||
const avatarSize = (previewBounds.height - 66) / 2;
|
audioOutput,
|
||||||
|
audioOutputs,
|
||||||
|
setAudioOutput,
|
||||||
|
} = useMediaHandler();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onInitLocalCallFeed();
|
onInitLocalCallFeed();
|
||||||
|
@ -64,53 +64,31 @@ export function LobbyView({
|
||||||
</Header>
|
</Header>
|
||||||
<div className={styles.joinRoom}>
|
<div className={styles.joinRoom}>
|
||||||
<div className={styles.joinRoomContent}>
|
<div className={styles.joinRoomContent}>
|
||||||
<div className={styles.preview} ref={previewRef}>
|
{groupCall.isPtt ? (
|
||||||
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
<AudioPreview
|
||||||
{state === GroupCallState.LocalCallFeedUninitialized && (
|
roomName={roomName}
|
||||||
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
|
state={state}
|
||||||
Webcam/microphone permissions needed to join the call.
|
audioInput={audioInput}
|
||||||
</Body>
|
audioInputs={audioInputs}
|
||||||
)}
|
setAudioInput={setAudioInput}
|
||||||
{state === GroupCallState.InitializingLocalCallFeed && (
|
audioOutput={audioOutput}
|
||||||
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
|
audioOutputs={audioOutputs}
|
||||||
Accept webcam/microphone permissions to join the call.
|
setAudioOutput={setAudioOutput}
|
||||||
</Body>
|
/>
|
||||||
)}
|
) : (
|
||||||
{state === GroupCallState.LocalCallFeedInitialized && (
|
<VideoPreview
|
||||||
<>
|
state={state}
|
||||||
{localVideoMuted && (
|
client={client}
|
||||||
<div className={styles.avatarContainer}>
|
microphoneMuted={microphoneMuted}
|
||||||
<Avatar
|
localVideoMuted={localVideoMuted}
|
||||||
style={{
|
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||||
width: avatarSize,
|
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||||
height: avatarSize,
|
setShowInspector={setShowInspector}
|
||||||
borderRadius: avatarSize,
|
showInspector={showInspector}
|
||||||
fontSize: Math.round(avatarSize / 2),
|
stream={stream}
|
||||||
}}
|
audioOutput={audioOutput}
|
||||||
src={avatarUrl}
|
/>
|
||||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
)}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.previewButtons}>
|
|
||||||
<MicButton
|
|
||||||
muted={microphoneMuted}
|
|
||||||
onPress={toggleMicrophoneMuted}
|
|
||||||
/>
|
|
||||||
<VideoButton
|
|
||||||
muted={localVideoMuted}
|
|
||||||
onPress={toggleLocalVideoMuted}
|
|
||||||
/>
|
|
||||||
<OverflowMenu
|
|
||||||
roomId={roomId}
|
|
||||||
setShowInspector={setShowInspector}
|
|
||||||
showInspector={showInspector}
|
|
||||||
client={client}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
ref={joinCallButtonRef}
|
ref={joinCallButtonRef}
|
||||||
className={styles.copyButton}
|
className={styles.copyButton}
|
||||||
|
|
|
@ -46,58 +46,6 @@ limitations under the License.
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview {
|
|
||||||
position: relative;
|
|
||||||
min-height: 280px;
|
|
||||||
height: 50vh;
|
|
||||||
border-radius: 24px;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: var(--bgColor3);
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview video {
|
|
||||||
width: calc(100% + 1px);
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
background-color: black;
|
|
||||||
/* transform scale doesn't perfectly match width, so make -1.01 border issues */
|
|
||||||
transform: scaleX(-1.01);
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatarContainer {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 66px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: var(--bgColor3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.webcamPermissions {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
margin: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.previewButtons {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 66px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: rgba(23, 25, 28, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.joinCallButton {
|
.joinCallButton {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -118,17 +66,3 @@ limitations under the License.
|
||||||
.copyButton:last-child {
|
.copyButton:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.previewButtons > * {
|
|
||||||
margin-right: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.previewButtons > :last-child {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
.preview {
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
80
src/room/VideoPreview.jsx
Normal file
80
src/room/VideoPreview.jsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import React from "react";
|
||||||
|
import { MicButton, VideoButton } from "../button";
|
||||||
|
import { useMediaStream } from "../video-grid/useMediaStream";
|
||||||
|
import { OverflowMenu } from "./OverflowMenu";
|
||||||
|
import { Avatar } from "../Avatar";
|
||||||
|
import { useProfile } from "../profile/useProfile";
|
||||||
|
import useMeasure from "react-use-measure";
|
||||||
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
|
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
import styles from "./VideoPreview.module.css";
|
||||||
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
|
export function VideoPreview({
|
||||||
|
client,
|
||||||
|
state,
|
||||||
|
roomId,
|
||||||
|
microphoneMuted,
|
||||||
|
localVideoMuted,
|
||||||
|
toggleLocalVideoMuted,
|
||||||
|
toggleMicrophoneMuted,
|
||||||
|
setShowInspector,
|
||||||
|
showInspector,
|
||||||
|
audioOutput,
|
||||||
|
stream,
|
||||||
|
}) {
|
||||||
|
const videoRef = useMediaStream(stream, audioOutput, true);
|
||||||
|
const { displayName, avatarUrl } = useProfile(client);
|
||||||
|
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
|
const avatarSize = (previewBounds.height - 66) / 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.preview} ref={previewRef}>
|
||||||
|
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
||||||
|
{state === GroupCallState.LocalCallFeedUninitialized && (
|
||||||
|
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
|
||||||
|
Webcam/microphone permissions needed to join the call.
|
||||||
|
</Body>
|
||||||
|
)}
|
||||||
|
{state === GroupCallState.InitializingLocalCallFeed && (
|
||||||
|
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
|
||||||
|
Accept webcam/microphone permissions to join the call.
|
||||||
|
</Body>
|
||||||
|
)}
|
||||||
|
{state === GroupCallState.LocalCallFeedInitialized && (
|
||||||
|
<>
|
||||||
|
{localVideoMuted && (
|
||||||
|
<div className={styles.avatarContainer}>
|
||||||
|
<Avatar
|
||||||
|
style={{
|
||||||
|
width: avatarSize,
|
||||||
|
height: avatarSize,
|
||||||
|
borderRadius: avatarSize,
|
||||||
|
fontSize: Math.round(avatarSize / 2),
|
||||||
|
}}
|
||||||
|
src={avatarUrl}
|
||||||
|
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.previewButtons}>
|
||||||
|
<MicButton
|
||||||
|
muted={microphoneMuted}
|
||||||
|
onPress={toggleMicrophoneMuted}
|
||||||
|
/>
|
||||||
|
<VideoButton
|
||||||
|
muted={localVideoMuted}
|
||||||
|
onPress={toggleLocalVideoMuted}
|
||||||
|
/>
|
||||||
|
<OverflowMenu
|
||||||
|
roomId={roomId}
|
||||||
|
setShowInspector={setShowInspector}
|
||||||
|
showInspector={showInspector}
|
||||||
|
client={client}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
65
src/room/VideoPreview.module.css
Normal file
65
src/room/VideoPreview.module.css
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
.preview {
|
||||||
|
position: relative;
|
||||||
|
min-height: 280px;
|
||||||
|
height: 50vh;
|
||||||
|
border-radius: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--bgColor3);
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview video {
|
||||||
|
width: calc(100% + 1px);
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
background-color: black;
|
||||||
|
/* transform scale doesn't perfectly match width, so make -1.01 border issues */
|
||||||
|
transform: scaleX(-1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarContainer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 66px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--bgColor3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.webcamPermissions {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewButtons {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 66px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(23, 25, 28, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewButtons > * {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewButtons > :last-child {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
.preview {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue