Finish first pass at PTT lobby UI

This commit is contained in:
Robert Long 2022-04-27 15:18:55 -07:00
parent 38f9a79bd3
commit c430ebb3a3
7 changed files with 271 additions and 125 deletions

61
src/room/AudioPreview.jsx Normal file
View 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>
</>
);
}

View 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;
}
}

View file

@ -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}

View file

@ -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 && (
<>
{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> ) : (
)} <VideoPreview
<div className={styles.previewButtons}> state={state}
<MicButton client={client}
muted={microphoneMuted} microphoneMuted={microphoneMuted}
onPress={toggleMicrophoneMuted} localVideoMuted={localVideoMuted}
/> toggleLocalVideoMuted={toggleLocalVideoMuted}
<VideoButton toggleMicrophoneMuted={toggleMicrophoneMuted}
muted={localVideoMuted}
onPress={toggleLocalVideoMuted}
/>
<OverflowMenu
roomId={roomId}
setShowInspector={setShowInspector} setShowInspector={setShowInspector}
showInspector={showInspector} showInspector={showInspector}
client={client} stream={stream}
audioOutput={audioOutput}
/> />
</div>
</>
)} )}
</div>
<Button <Button
ref={joinCallButtonRef} ref={joinCallButtonRef}
className={styles.copyButton} className={styles.copyButton}

View file

@ -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
View 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>
);
}

View 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;
}
}