Initial PTT designs
This commit is contained in:
parent
fc1aaf02bf
commit
38f9a79bd3
13 changed files with 277 additions and 24 deletions
|
@ -20,6 +20,7 @@ export const variantToClassName = {
|
|||
copy: [styles.copyButton],
|
||||
iconCopy: [styles.iconCopyButton],
|
||||
secondaryCopy: [styles.copyButton],
|
||||
secondaryHangup: [styles.secondaryHangup],
|
||||
};
|
||||
|
||||
export const sizeToClassName = {
|
||||
|
|
|
@ -20,6 +20,7 @@ limitations under the License.
|
|||
.iconButton,
|
||||
.iconCopyButton,
|
||||
.secondary,
|
||||
.secondaryHangup,
|
||||
.copyButton {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
@ -34,6 +35,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.secondary,
|
||||
.secondaryHangup,
|
||||
.button,
|
||||
.copyButton {
|
||||
padding: 7px 15px;
|
||||
|
@ -53,6 +55,7 @@ limitations under the License.
|
|||
.iconButton:focus,
|
||||
.iconCopyButton:focus,
|
||||
.secondary:focus,
|
||||
.secondaryHangup:focus,
|
||||
.copyButton:focus {
|
||||
outline: auto;
|
||||
}
|
||||
|
@ -119,6 +122,12 @@ limitations under the License.
|
|||
background-color: transparent;
|
||||
}
|
||||
|
||||
.secondaryHangup {
|
||||
color: #ff5b55;
|
||||
border: 2px solid #ff5b55;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.copyButton.secondaryCopy {
|
||||
color: var(--textColor1);
|
||||
border-color: var(--textColor1);
|
||||
|
|
|
@ -23,12 +23,13 @@ export function RegisteredView({ client }) {
|
|||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const roomName = data.get("callName");
|
||||
const ptt = data.get("ptt") !== null;
|
||||
|
||||
async function submit() {
|
||||
setError(undefined);
|
||||
setLoading(true);
|
||||
|
||||
const roomIdOrAlias = await createRoom(client, roomName);
|
||||
const roomIdOrAlias = await createRoom(client, roomName, ptt);
|
||||
|
||||
if (roomIdOrAlias) {
|
||||
history.push(`/room/${roomIdOrAlias}`);
|
||||
|
@ -87,6 +88,7 @@ export function RegisteredView({ client }) {
|
|||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
|
@ -96,6 +98,14 @@ export function RegisteredView({ client }) {
|
|||
{loading ? "Loading..." : "Go"}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<InputField
|
||||
id="ptt"
|
||||
name="ptt"
|
||||
label="Push to Talk"
|
||||
type="checkbox"
|
||||
/>
|
||||
</FieldRow>
|
||||
{error && (
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
}
|
||||
|
||||
.fieldRow {
|
||||
margin-bottom: 24px;;
|
||||
}
|
||||
|
||||
.fieldRow:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ export function UnauthenticatedView() {
|
|||
const data = new FormData(e.target);
|
||||
const roomName = data.get("callName");
|
||||
const displayName = data.get("displayName");
|
||||
const ptt = data.get("ptt") !== null;
|
||||
|
||||
async function submit() {
|
||||
setError(undefined);
|
||||
|
@ -41,7 +42,7 @@ export function UnauthenticatedView() {
|
|||
recaptchaResponse,
|
||||
true
|
||||
);
|
||||
const roomIdOrAlias = await createRoom(client, roomName);
|
||||
const roomIdOrAlias = await createRoom(client, roomName, ptt);
|
||||
|
||||
if (roomIdOrAlias) {
|
||||
history.push(`/room/${roomIdOrAlias}`);
|
||||
|
@ -111,6 +112,14 @@ export function UnauthenticatedView() {
|
|||
autoComplete="off"
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="ptt"
|
||||
name="ptt"
|
||||
label="Push to Talk"
|
||||
type="checkbox"
|
||||
/>
|
||||
</FieldRow>
|
||||
<Caption>
|
||||
By clicking "Go", you agree to our{" "}
|
||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||
|
|
|
@ -76,7 +76,7 @@ export function isLocalRoomId(roomId) {
|
|||
return parts[1] === defaultHomeserverHost;
|
||||
}
|
||||
|
||||
export async function createRoom(client, name) {
|
||||
export async function createRoom(client, name, isPtt = false) {
|
||||
const { room_id, room_alias } = await client.createRoom({
|
||||
visibility: "private",
|
||||
preset: "public_chat",
|
||||
|
@ -107,9 +107,12 @@ export async function createRoom(client, name) {
|
|||
},
|
||||
});
|
||||
|
||||
console.log({ isPtt });
|
||||
|
||||
await client.createGroupCall(
|
||||
room_id,
|
||||
GroupCallType.Video,
|
||||
isPtt,
|
||||
GroupCallIntent.Prompt
|
||||
);
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useGroupCall } from "./useGroupCall";
|
|||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
import { LobbyView } from "./LobbyView";
|
||||
import { InCallView } from "./InCallView";
|
||||
import { PTTCallView } from "./PTTCallView";
|
||||
import { CallEndedView } from "./CallEndedView";
|
||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||
import { useLocationNavigation } from "../useLocationNavigation";
|
||||
|
@ -47,6 +48,7 @@ export function GroupCallView({
|
|||
localScreenshareFeed,
|
||||
screenshareFeeds,
|
||||
hasLocalParticipant,
|
||||
participants,
|
||||
} = useGroupCall(groupCall);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -72,27 +74,46 @@ export function GroupCallView({
|
|||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
} else if (state === GroupCallState.Entered) {
|
||||
return (
|
||||
<InCallView
|
||||
groupCall={groupCall}
|
||||
client={client}
|
||||
roomName={groupCall.room.name}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
userMediaFeeds={userMediaFeeds}
|
||||
activeSpeaker={activeSpeaker}
|
||||
onLeave={onLeave}
|
||||
toggleScreensharing={toggleScreensharing}
|
||||
isScreensharing={isScreensharing}
|
||||
localScreenshareFeed={localScreenshareFeed}
|
||||
screenshareFeeds={screenshareFeeds}
|
||||
setShowInspector={onChangeShowInspector}
|
||||
showInspector={showInspector}
|
||||
roomId={roomId}
|
||||
/>
|
||||
);
|
||||
if (groupCall.isPtt) {
|
||||
return (
|
||||
<PTTCallView
|
||||
groupCall={groupCall}
|
||||
participants={participants}
|
||||
client={client}
|
||||
roomName={groupCall.room.name}
|
||||
microphoneMuted={microphoneMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
userMediaFeeds={userMediaFeeds}
|
||||
activeSpeaker={activeSpeaker}
|
||||
onLeave={onLeave}
|
||||
setShowInspector={onChangeShowInspector}
|
||||
showInspector={showInspector}
|
||||
roomId={roomId}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<InCallView
|
||||
groupCall={groupCall}
|
||||
client={client}
|
||||
roomName={groupCall.room.name}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
userMediaFeeds={userMediaFeeds}
|
||||
activeSpeaker={activeSpeaker}
|
||||
onLeave={onLeave}
|
||||
toggleScreensharing={toggleScreensharing}
|
||||
isScreensharing={isScreensharing}
|
||||
localScreenshareFeed={localScreenshareFeed}
|
||||
screenshareFeeds={screenshareFeeds}
|
||||
setShowInspector={onChangeShowInspector}
|
||||
showInspector={showInspector}
|
||||
roomId={roomId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (state === GroupCallState.Entering) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
|
|
44
src/room/PTTButton.jsx
Normal file
44
src/room/PTTButton.jsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import styles from "./PTTButton.module.css";
|
||||
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||
import { Avatar } from "../Avatar";
|
||||
|
||||
export function PTTButton({ client, activeSpeaker }) {
|
||||
const size = 232;
|
||||
|
||||
const isLocal = client.userId === activeSpeaker;
|
||||
const avatarUrl = activeSpeaker?.user?.avatarUrl;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames(styles.pttButton, {
|
||||
[styles.speaking]: !!activeSpeaker,
|
||||
})}
|
||||
>
|
||||
{isLocal || !avatarUrl || !activeSpeaker ? (
|
||||
<MicIcon
|
||||
classNames={styles.micIcon}
|
||||
width={size / 3}
|
||||
height={size / 3}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
key={activeSpeaker.userId}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size,
|
||||
fontSize: Math.round(size / 2),
|
||||
}}
|
||||
src={
|
||||
activeSpeaker.user.avatarUrl &&
|
||||
getAvatarUrl(client, activeSpeaker.user.avatarUrl, size)
|
||||
}
|
||||
fallback={activeSpeaker.name.slice(0, 1).toUpperCase()}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
11
src/room/PTTButton.module.css
Normal file
11
src/room/PTTButton.module.css
Normal file
|
@ -0,0 +1,11 @@
|
|||
.pttButton {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
max-height: 232px;
|
||||
max-width: 232px;
|
||||
border-radius: 116px;
|
||||
|
||||
color: ##fff;
|
||||
border: 6px solid #0dbd8b;
|
||||
background-color: #21262C;
|
||||
}
|
88
src/room/PTTCallView.jsx
Normal file
88
src/room/PTTCallView.jsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
import React from "react";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { SettingsModal } from "../settings/SettingsModal";
|
||||
import { InviteModal } from "./InviteModal";
|
||||
import { Button } from "../button";
|
||||
import { Header, LeftNav, RightNav, RoomSetupHeaderInfo } from "../Header";
|
||||
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
|
||||
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
|
||||
import styles from "./PTTCallView.module.css";
|
||||
import { Facepile } from "../Facepile";
|
||||
import { PTTButton } from "./PTTButton";
|
||||
import { PTTFeed } from "./PTTFeed";
|
||||
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||
|
||||
export function PTTCallView({
|
||||
groupCall,
|
||||
participants,
|
||||
client,
|
||||
roomName,
|
||||
microphoneMuted,
|
||||
toggleMicrophoneMuted,
|
||||
userMediaFeeds,
|
||||
activeSpeaker,
|
||||
onLeave,
|
||||
setShowInspector,
|
||||
showInspector,
|
||||
roomId,
|
||||
}) {
|
||||
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
||||
useModalTriggerState();
|
||||
const { modalState: settingsModalState, modalProps: settingsModalProps } =
|
||||
useModalTriggerState();
|
||||
const { audioOutput } = useMediaHandler();
|
||||
|
||||
return (
|
||||
<div className={styles.pttCallView}>
|
||||
<Header className={styles.header}>
|
||||
<LeftNav>
|
||||
<RoomSetupHeaderInfo roomName={roomName} onPress={onLeave} />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<Button variant="secondaryHangup" onPress={onLeave}>
|
||||
Leave
|
||||
</Button>
|
||||
<Button variant="icon" onPress={() => inviteModalState.open()}>
|
||||
<AddUserIcon />
|
||||
</Button>
|
||||
<Button variant="icon" onPress={() => settingsModalState.open()}>
|
||||
<SettingsIcon />
|
||||
</Button>
|
||||
</RightNav>
|
||||
</Header>
|
||||
<div className={styles.headerSeparator} />
|
||||
<div className={styles.participants}>
|
||||
<p>{`${participants.length} user${
|
||||
participants.length > 1 ? "s" : ""
|
||||
} connected`}</p>
|
||||
<Facepile client={client} participants={participants} />
|
||||
</div>
|
||||
<div className={styles.center}>
|
||||
<PTTButton
|
||||
client={client}
|
||||
activeSpeaker={activeSpeaker}
|
||||
groupCall={groupCall}
|
||||
/>
|
||||
<p className={styles.actionTip}>Press and hold spacebar to talk</p>
|
||||
{userMediaFeeds.map((callFeed) => (
|
||||
<PTTFeed
|
||||
key={callFeed.userId}
|
||||
callFeed={callFeed}
|
||||
audioOutputDevice={audioOutput}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{settingsModalState.isOpen && (
|
||||
<SettingsModal
|
||||
{...settingsModalProps}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
/>
|
||||
)}
|
||||
{inviteModalState.isOpen && (
|
||||
<InviteModal roomId={roomId} {...inviteModalProps} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
40
src/room/PTTCallView.module.css
Normal file
40
src/room/PTTCallView.module.css
Normal file
|
@ -0,0 +1,40 @@
|
|||
.pttCallView {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 100%;
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.headerSeparator {
|
||||
width: calc(100% - 40px);
|
||||
height: 1px;
|
||||
margin: 0 20px;
|
||||
background-color: #21262C;
|
||||
}
|
||||
|
||||
.center {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actionTip {
|
||||
margin-top: 42px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.participants {
|
||||
margin: 24px 20px 0 20px;
|
||||
}
|
||||
|
||||
.participants > p {
|
||||
color: #A9B2BC;
|
||||
margin-bottom: 8px;
|
||||
}
|
10
src/room/PTTFeed.jsx
Normal file
10
src/room/PTTFeed.jsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import React from "react";
|
||||
import { useCallFeed } from "../video-grid/useCallFeed";
|
||||
import { useMediaStream } from "../video-grid/useMediaStream";
|
||||
import styles from "./PTTFeed.module.css";
|
||||
|
||||
export function PTTFeed({ callFeed, audioOutputDevice }) {
|
||||
const { isLocal, stream } = useCallFeed(callFeed);
|
||||
const mediaRef = useMediaStream(stream, audioOutputDevice, isLocal);
|
||||
return <audio ref={mediaRef} className={styles.audioFeed} playsInline />;
|
||||
}
|
3
src/room/PTTFeed.module.css
Normal file
3
src/room/PTTFeed.module.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.audioFeed {
|
||||
display: none;
|
||||
}
|
Loading…
Reference in a new issue