Initial PTT designs

This commit is contained in:
Robert Long 2022-04-22 18:05:48 -07:00
parent fc1aaf02bf
commit 38f9a79bd3
13 changed files with 277 additions and 24 deletions

View file

@ -20,6 +20,7 @@ export const variantToClassName = {
copy: [styles.copyButton],
iconCopy: [styles.iconCopyButton],
secondaryCopy: [styles.copyButton],
secondaryHangup: [styles.secondaryHangup],
};
export const sizeToClassName = {

View file

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

View file

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

View file

@ -7,6 +7,10 @@
}
.fieldRow {
margin-bottom: 24px;;
}
.fieldRow:last-child {
margin-bottom: 0;
}

View file

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

View file

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

View file

@ -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,6 +74,24 @@ export function GroupCallView({
if (error) {
return <ErrorView error={error} />;
} else if (state === GroupCallState.Entered) {
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}
@ -93,6 +113,7 @@ export function GroupCallView({
roomId={roomId}
/>
);
}
} else if (state === GroupCallState.Entering) {
return (
<FullScreenView>

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

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

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

View file

@ -0,0 +1,3 @@
.audioFeed {
display: none;
}