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 (
|
||||
<LobbyView
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
hasLocalParticipant={hasLocalParticipant}
|
||||
roomName={groupCall.room.name}
|
||||
state={state}
|
||||
|
|
|
@ -1,23 +1,20 @@
|
|||
import React, { useEffect, useRef } from "react";
|
||||
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 { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { useCallFeed } from "../video-grid/useCallFeed";
|
||||
import { useMediaStream } from "../video-grid/useMediaStream";
|
||||
import { getRoomUrl } from "../matrix-utils";
|
||||
import { OverflowMenu } from "./OverflowMenu";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
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 { useMediaHandler } from "../settings/useMediaHandler";
|
||||
import { VideoPreview } from "./VideoPreview";
|
||||
import { AudioPreview } from "./AudioPreview";
|
||||
|
||||
export function LobbyView({
|
||||
client,
|
||||
groupCall,
|
||||
roomName,
|
||||
state,
|
||||
onInitLocalCallFeed,
|
||||
|
@ -32,11 +29,14 @@ export function LobbyView({
|
|||
roomId,
|
||||
}) {
|
||||
const { stream } = useCallFeed(localCallFeed);
|
||||
const { audioOutput } = useMediaHandler();
|
||||
const videoRef = useMediaStream(stream, audioOutput, true);
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
||||
const avatarSize = (previewBounds.height - 66) / 2;
|
||||
const {
|
||||
audioInput,
|
||||
audioInputs,
|
||||
setAudioInput,
|
||||
audioOutput,
|
||||
audioOutputs,
|
||||
setAudioOutput,
|
||||
} = useMediaHandler();
|
||||
|
||||
useEffect(() => {
|
||||
onInitLocalCallFeed();
|
||||
|
@ -64,53 +64,31 @@ export function LobbyView({
|
|||
</Header>
|
||||
<div className={styles.joinRoom}>
|
||||
<div className={styles.joinRoomContent}>
|
||||
<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()}
|
||||
{groupCall.isPtt ? (
|
||||
<AudioPreview
|
||||
roomName={roomName}
|
||||
state={state}
|
||||
audioInput={audioInput}
|
||||
audioInputs={audioInputs}
|
||||
setAudioInput={setAudioInput}
|
||||
audioOutput={audioOutput}
|
||||
audioOutputs={audioOutputs}
|
||||
setAudioOutput={setAudioOutput}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.previewButtons}>
|
||||
<MicButton
|
||||
muted={microphoneMuted}
|
||||
onPress={toggleMicrophoneMuted}
|
||||
/>
|
||||
<VideoButton
|
||||
muted={localVideoMuted}
|
||||
onPress={toggleLocalVideoMuted}
|
||||
/>
|
||||
<OverflowMenu
|
||||
roomId={roomId}
|
||||
) : (
|
||||
<VideoPreview
|
||||
state={state}
|
||||
client={client}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
client={client}
|
||||
stream={stream}
|
||||
audioOutput={audioOutput}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
ref={joinCallButtonRef}
|
||||
className={styles.copyButton}
|
||||
|
|
|
@ -46,58 +46,6 @@ limitations under the License.
|
|||
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 {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
@ -118,17 +66,3 @@ limitations under the License.
|
|||
.copyButton:last-child {
|
||||
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