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],
|
copy: [styles.copyButton],
|
||||||
iconCopy: [styles.iconCopyButton],
|
iconCopy: [styles.iconCopyButton],
|
||||||
secondaryCopy: [styles.copyButton],
|
secondaryCopy: [styles.copyButton],
|
||||||
|
secondaryHangup: [styles.secondaryHangup],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sizeToClassName = {
|
export const sizeToClassName = {
|
||||||
|
|
|
@ -20,6 +20,7 @@ limitations under the License.
|
||||||
.iconButton,
|
.iconButton,
|
||||||
.iconCopyButton,
|
.iconCopyButton,
|
||||||
.secondary,
|
.secondary,
|
||||||
|
.secondaryHangup,
|
||||||
.copyButton {
|
.copyButton {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -34,6 +35,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary,
|
.secondary,
|
||||||
|
.secondaryHangup,
|
||||||
.button,
|
.button,
|
||||||
.copyButton {
|
.copyButton {
|
||||||
padding: 7px 15px;
|
padding: 7px 15px;
|
||||||
|
@ -53,6 +55,7 @@ limitations under the License.
|
||||||
.iconButton:focus,
|
.iconButton:focus,
|
||||||
.iconCopyButton:focus,
|
.iconCopyButton:focus,
|
||||||
.secondary:focus,
|
.secondary:focus,
|
||||||
|
.secondaryHangup:focus,
|
||||||
.copyButton:focus {
|
.copyButton:focus {
|
||||||
outline: auto;
|
outline: auto;
|
||||||
}
|
}
|
||||||
|
@ -119,6 +122,12 @@ limitations under the License.
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.secondaryHangup {
|
||||||
|
color: #ff5b55;
|
||||||
|
border: 2px solid #ff5b55;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.copyButton.secondaryCopy {
|
.copyButton.secondaryCopy {
|
||||||
color: var(--textColor1);
|
color: var(--textColor1);
|
||||||
border-color: var(--textColor1);
|
border-color: var(--textColor1);
|
||||||
|
|
|
@ -23,12 +23,13 @@ export function RegisteredView({ client }) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = new FormData(e.target);
|
const data = new FormData(e.target);
|
||||||
const roomName = data.get("callName");
|
const roomName = data.get("callName");
|
||||||
|
const ptt = data.get("ptt") !== null;
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const roomIdOrAlias = await createRoom(client, roomName);
|
const roomIdOrAlias = await createRoom(client, roomName, ptt);
|
||||||
|
|
||||||
if (roomIdOrAlias) {
|
if (roomIdOrAlias) {
|
||||||
history.push(`/room/${roomIdOrAlias}`);
|
history.push(`/room/${roomIdOrAlias}`);
|
||||||
|
@ -87,6 +88,7 @@ export function RegisteredView({ client }) {
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
@ -96,6 +98,14 @@ export function RegisteredView({ client }) {
|
||||||
{loading ? "Loading..." : "Go"}
|
{loading ? "Loading..." : "Go"}
|
||||||
</Button>
|
</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
|
<FieldRow className={styles.fieldRow}>
|
||||||
|
<InputField
|
||||||
|
id="ptt"
|
||||||
|
name="ptt"
|
||||||
|
label="Push to Talk"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow className={styles.fieldRow}>
|
<FieldRow className={styles.fieldRow}>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage>{error.message}</ErrorMessage>
|
||||||
|
|
|
@ -7,6 +7,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.fieldRow {
|
.fieldRow {
|
||||||
|
margin-bottom: 24px;;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldRow:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ export function UnauthenticatedView() {
|
||||||
const data = new FormData(e.target);
|
const data = new FormData(e.target);
|
||||||
const roomName = data.get("callName");
|
const roomName = data.get("callName");
|
||||||
const displayName = data.get("displayName");
|
const displayName = data.get("displayName");
|
||||||
|
const ptt = data.get("ptt") !== null;
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
|
@ -41,7 +42,7 @@ export function UnauthenticatedView() {
|
||||||
recaptchaResponse,
|
recaptchaResponse,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
const roomIdOrAlias = await createRoom(client, roomName);
|
const roomIdOrAlias = await createRoom(client, roomName, ptt);
|
||||||
|
|
||||||
if (roomIdOrAlias) {
|
if (roomIdOrAlias) {
|
||||||
history.push(`/room/${roomIdOrAlias}`);
|
history.push(`/room/${roomIdOrAlias}`);
|
||||||
|
@ -111,6 +112,14 @@ export function UnauthenticatedView() {
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="ptt"
|
||||||
|
name="ptt"
|
||||||
|
label="Push to Talk"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
<Caption>
|
<Caption>
|
||||||
By clicking "Go", you agree to our{" "}
|
By clicking "Go", you agree to our{" "}
|
||||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||||
|
|
|
@ -76,7 +76,7 @@ export function isLocalRoomId(roomId) {
|
||||||
return parts[1] === defaultHomeserverHost;
|
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({
|
const { room_id, room_alias } = await client.createRoom({
|
||||||
visibility: "private",
|
visibility: "private",
|
||||||
preset: "public_chat",
|
preset: "public_chat",
|
||||||
|
@ -107,9 +107,12 @@ export async function createRoom(client, name) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log({ isPtt });
|
||||||
|
|
||||||
await client.createGroupCall(
|
await client.createGroupCall(
|
||||||
room_id,
|
room_id,
|
||||||
GroupCallType.Video,
|
GroupCallType.Video,
|
||||||
|
isPtt,
|
||||||
GroupCallIntent.Prompt
|
GroupCallIntent.Prompt
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useGroupCall } from "./useGroupCall";
|
||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||||
import { LobbyView } from "./LobbyView";
|
import { LobbyView } from "./LobbyView";
|
||||||
import { InCallView } from "./InCallView";
|
import { InCallView } from "./InCallView";
|
||||||
|
import { PTTCallView } from "./PTTCallView";
|
||||||
import { CallEndedView } from "./CallEndedView";
|
import { CallEndedView } from "./CallEndedView";
|
||||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
|
@ -47,6 +48,7 @@ export function GroupCallView({
|
||||||
localScreenshareFeed,
|
localScreenshareFeed,
|
||||||
screenshareFeeds,
|
screenshareFeeds,
|
||||||
hasLocalParticipant,
|
hasLocalParticipant,
|
||||||
|
participants,
|
||||||
} = useGroupCall(groupCall);
|
} = useGroupCall(groupCall);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -72,27 +74,46 @@ export function GroupCallView({
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorView error={error} />;
|
return <ErrorView error={error} />;
|
||||||
} else if (state === GroupCallState.Entered) {
|
} else if (state === GroupCallState.Entered) {
|
||||||
return (
|
if (groupCall.isPtt) {
|
||||||
<InCallView
|
return (
|
||||||
groupCall={groupCall}
|
<PTTCallView
|
||||||
client={client}
|
groupCall={groupCall}
|
||||||
roomName={groupCall.room.name}
|
participants={participants}
|
||||||
microphoneMuted={microphoneMuted}
|
client={client}
|
||||||
localVideoMuted={localVideoMuted}
|
roomName={groupCall.room.name}
|
||||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
microphoneMuted={microphoneMuted}
|
||||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||||
userMediaFeeds={userMediaFeeds}
|
userMediaFeeds={userMediaFeeds}
|
||||||
activeSpeaker={activeSpeaker}
|
activeSpeaker={activeSpeaker}
|
||||||
onLeave={onLeave}
|
onLeave={onLeave}
|
||||||
toggleScreensharing={toggleScreensharing}
|
setShowInspector={onChangeShowInspector}
|
||||||
isScreensharing={isScreensharing}
|
showInspector={showInspector}
|
||||||
localScreenshareFeed={localScreenshareFeed}
|
roomId={roomId}
|
||||||
screenshareFeeds={screenshareFeeds}
|
/>
|
||||||
setShowInspector={onChangeShowInspector}
|
);
|
||||||
showInspector={showInspector}
|
} else {
|
||||||
roomId={roomId}
|
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) {
|
} else if (state === GroupCallState.Entering) {
|
||||||
return (
|
return (
|
||||||
<FullScreenView>
|
<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