Merge pull request #356 from robintown/call-type-dropdown

Add a dropdown to choose between video calls and radio calls
This commit is contained in:
Robin 2022-05-27 08:54:38 -04:00 committed by GitHub
commit 5c4bab2a8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 133 additions and 58 deletions

View file

@ -39,3 +39,12 @@
border-bottom-left-radius: 8px; border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px; border-bottom-right-radius: 8px;
} }
.checkIcon {
position: absolute;
right: 16px;
}
.checkIcon * {
stroke: var(--textColor1);
}

View file

@ -25,6 +25,7 @@ import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg"; import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg"; import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg"; import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
import { ReactComponent as ArrowDownIcon } from "../icons/ArrowDown.svg";
import { useButton } from "@react-aria/button"; import { useButton } from "@react-aria/button";
import { mergeProps, useObjectRef } from "@react-aria/utils"; import { mergeProps, useObjectRef } from "@react-aria/utils";
import { TooltipTrigger } from "../Tooltip"; import { TooltipTrigger } from "../Tooltip";
@ -36,9 +37,11 @@ export const variantToClassName = {
icon: [styles.iconButton], icon: [styles.iconButton],
secondary: [styles.secondary], secondary: [styles.secondary],
copy: [styles.copyButton], copy: [styles.copyButton],
secondaryCopy: [styles.secondaryCopy],
iconCopy: [styles.iconCopyButton], iconCopy: [styles.iconCopyButton],
secondaryCopy: [styles.copyButton], secondaryCopy: [styles.copyButton],
secondaryHangup: [styles.secondaryHangup], secondaryHangup: [styles.secondaryHangup],
dropdown: [styles.dropdownButton],
}; };
export const sizeToClassName = { export const sizeToClassName = {
@ -86,13 +89,13 @@ export const Button = forwardRef(
{ {
[styles.on]: on, [styles.on]: on,
[styles.off]: off, [styles.off]: off,
[styles.secondaryCopy]: variant === "secondaryCopy",
} }
)} )}
{...mergeProps(rest, filteredButtonProps)} {...mergeProps(rest, filteredButtonProps)}
ref={buttonRef} ref={buttonRef}
> >
{children} {children}
{variant === "dropdown" && <ArrowDownIcon />}
</button> </button>
); );
} }

View file

@ -21,7 +21,8 @@ limitations under the License.
.iconCopyButton, .iconCopyButton,
.secondary, .secondary,
.secondaryHangup, .secondaryHangup,
.copyButton { .copyButton,
.dropdownButton {
position: relative; position: relative;
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -184,6 +185,25 @@ limitations under the License.
stroke: #0dbd8b; stroke: #0dbd8b;
} }
.dropdownButton {
color: var(--textColor1);
padding: 2px 8px;
border-radius: 8px;
}
.dropdownButton:hover,
.dropdownButton.on {
background-color: var(--bgColor4);
}
.dropdownButton svg {
margin-left: 8px;
}
.dropdownButton svg * {
fill: var(--textColor1);
}
.lg { .lg {
height: 40px; height: 40px;
} }

View file

@ -0,0 +1,3 @@
.label {
margin-bottom: 0;
}

View file

@ -0,0 +1,69 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { FC } from "react";
import { Item } from "@react-stately/collections";
import { Headline } from "../typography/Typography";
import { Button } from "../button";
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
import styles from "./CallTypeDropdown.module.css";
import commonStyles from "./common.module.css";
import menuStyles from "../Menu.module.css";
import { Menu } from "../Menu";
export enum CallType {
Video = "video",
Radio = "radio",
}
interface Props {
callType: CallType;
setCallType: (value: CallType) => void;
}
export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
return (
<PopoverMenuTrigger placement="bottom">
<Button variant="dropdown" className={commonStyles.headline}>
<Headline className={styles.label}>
{callType === CallType.Video ? "Video call" : "Radio call"}
</Headline>
</Button>
{(props) => (
<Menu {...props} label="Call type menu" onAction={setCallType}>
<Item key={CallType.Video} textValue="Video call">
<VideoIcon />
<span>Video call</span>
{callType === CallType.Video && (
<CheckIcon className={menuStyles.checkIcon} />
)}
</Item>
<Item key={CallType.Radio} textValue="Radio call">
<MicIcon />
<span>Radio call</span>
{callType === CallType.Radio && (
<CheckIcon className={menuStyles.checkIcon} />
)}
</Item>
</Menu>
)}
</PopoverMenuTrigger>
);
};

View file

@ -27,21 +27,21 @@ import { UserMenuContainer } from "../UserMenuContainer";
import { useModalTriggerState } from "../Modal"; import { useModalTriggerState } from "../Modal";
import { JoinExistingCallModal } from "./JoinExistingCallModal"; import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Headline, Title } from "../typography/Typography"; import { Title } from "../typography/Typography";
import { Form } from "../form/Form"; import { Form } from "../form/Form";
import { useShouldShowPtt } from "../useShouldShowPtt"; import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
export function RegisteredView({ client }) { export function RegisteredView({ client }) {
const [callType, setCallType] = useState(CallType.Video);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(); const [error, setError] = useState();
const history = useHistory(); const history = useHistory();
const shouldShowPtt = useShouldShowPtt();
const onSubmit = useCallback( const onSubmit = useCallback(
(e) => { (e) => {
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; const ptt = callType === CallType.Radio;
async function submit() { async function submit() {
setError(undefined); setError(undefined);
@ -68,7 +68,7 @@ export function RegisteredView({ client }) {
} }
}); });
}, },
[client] [client, callType]
); );
const recentRooms = useGroupCallRooms(client); const recentRooms = useGroupCallRooms(client);
@ -79,6 +79,9 @@ export function RegisteredView({ client }) {
history.push(`/${existingRoomId}`); history.push(`/${existingRoomId}`);
}, [history, existingRoomId]); }, [history, existingRoomId]);
const callNameLabel =
callType === CallType.Video ? "Video call name" : "Radio call name";
return ( return (
<> <>
<Header> <Header>
@ -92,16 +95,14 @@ export function RegisteredView({ client }) {
<div className={commonStyles.container}> <div className={commonStyles.container}>
<main className={commonStyles.main}> <main className={commonStyles.main}>
<HeaderLogo className={commonStyles.logo} /> <HeaderLogo className={commonStyles.logo} />
<Headline className={commonStyles.headline}> <CallTypeDropdown callType={callType} setCallType={setCallType} />
Enter a call name
</Headline>
<Form className={styles.form} onSubmit={onSubmit}> <Form className={styles.form} onSubmit={onSubmit}>
<FieldRow className={styles.fieldRow}> <FieldRow className={styles.fieldRow}>
<InputField <InputField
id="callName" id="callName"
name="callName" name="callName"
label="Call name" label={callNameLabel}
placeholder="Call name" placeholder={callNameLabel}
type="text" type="text"
required required
autoComplete="off" autoComplete="off"
@ -116,16 +117,6 @@ export function RegisteredView({ client }) {
{loading ? "Loading..." : "Go"} {loading ? "Loading..." : "Go"}
</Button> </Button>
</FieldRow> </FieldRow>
{shouldShowPtt && (
<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>

View file

@ -29,14 +29,14 @@ import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { useRecaptcha } from "../auth/useRecaptcha"; import { useRecaptcha } from "../auth/useRecaptcha";
import { Body, Caption, Link, Headline } from "../typography/Typography"; import { Body, Caption, Link, Headline } from "../typography/Typography";
import { Form } from "../form/Form"; import { Form } from "../form/Form";
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
import styles from "./UnauthenticatedView.module.css"; import styles from "./UnauthenticatedView.module.css";
import commonStyles from "./common.module.css"; import commonStyles from "./common.module.css";
import { generateRandomName } from "../auth/generateRandomName"; import { generateRandomName } from "../auth/generateRandomName";
import { useShouldShowPtt } from "../useShouldShowPtt";
export function UnauthenticatedView() { export function UnauthenticatedView() {
const { setClient } = useClient(); const { setClient } = useClient();
const shouldShowPtt = useShouldShowPtt(); const [callType, setCallType] = useState(CallType.Video);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(); const [error, setError] = useState();
const [{ privacyPolicyUrl, recaptchaKey }, register] = const [{ privacyPolicyUrl, recaptchaKey }, register] =
@ -53,7 +53,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; const ptt = callType === CallType.Radio;
async function submit() { async function submit() {
setError(undefined); setError(undefined);
@ -100,9 +100,12 @@ export function UnauthenticatedView() {
reset(); reset();
}); });
}, },
[register, reset, execute, history] [register, reset, execute, history, callType]
); );
const callNameLabel =
callType === CallType.Video ? "Video call name" : "Radio call name";
return ( return (
<> <>
<Header> <Header>
@ -116,16 +119,14 @@ export function UnauthenticatedView() {
<div className={commonStyles.container}> <div className={commonStyles.container}>
<main className={commonStyles.main}> <main className={commonStyles.main}>
<HeaderLogo className={commonStyles.logo} /> <HeaderLogo className={commonStyles.logo} />
<Headline className={commonStyles.headline}> <CallTypeDropdown callType={callType} setCallType={setCallType} />
Enter a call name
</Headline>
<Form className={styles.form} onSubmit={onSubmit}> <Form className={styles.form} onSubmit={onSubmit}>
<FieldRow> <FieldRow>
<InputField <InputField
id="callName" id="callName"
name="callName" name="callName"
label="Call name" label={callNameLabel}
placeholder="Call name" placeholder={callNameLabel}
type="text" type="text"
required required
autoComplete="off" autoComplete="off"
@ -142,16 +143,6 @@ export function UnauthenticatedView() {
autoComplete="off" autoComplete="off"
/> />
</FieldRow> </FieldRow>
{shouldShowPtt && (
<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>

View file

@ -123,6 +123,7 @@ limitations under the License.
body { body {
background-color: var(--bgColor1); background-color: var(--bgColor1);
color: var(--textColor1); color: var(--textColor1);
color-scheme: dark;
margin: 0; margin: 0;
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",

View file

@ -20,7 +20,7 @@ import { PopoverMenuTrigger } from "../popover/PopoverMenu";
import { ReactComponent as SpotlightIcon } from "../icons/Spotlight.svg"; import { ReactComponent as SpotlightIcon } from "../icons/Spotlight.svg";
import { ReactComponent as FreedomIcon } from "../icons/Freedom.svg"; import { ReactComponent as FreedomIcon } from "../icons/Freedom.svg";
import { ReactComponent as CheckIcon } from "../icons/Check.svg"; import { ReactComponent as CheckIcon } from "../icons/Check.svg";
import styles from "./GridLayoutMenu.module.css"; import menuStyles from "../Menu.module.css";
import { Menu } from "../Menu"; import { Menu } from "../Menu";
import { Item } from "@react-stately/collections"; import { Item } from "@react-stately/collections";
import { Tooltip, TooltipTrigger } from "../Tooltip"; import { Tooltip, TooltipTrigger } from "../Tooltip";
@ -39,13 +39,15 @@ export function GridLayoutMenu({ layout, setLayout }) {
<Item key="freedom" textValue="Freedom"> <Item key="freedom" textValue="Freedom">
<FreedomIcon /> <FreedomIcon />
<span>Freedom</span> <span>Freedom</span>
{layout === "freedom" && <CheckIcon className={styles.checkIcon} />} {layout === "freedom" && (
<CheckIcon className={menuStyles.checkIcon} />
)}
</Item> </Item>
<Item key="spotlight" textValue="Spotlight"> <Item key="spotlight" textValue="Spotlight">
<SpotlightIcon /> <SpotlightIcon />
<span>Spotlight</span> <span>Spotlight</span>
{layout === "spotlight" && ( {layout === "spotlight" && (
<CheckIcon className={styles.checkIcon} /> <CheckIcon className={menuStyles.checkIcon} />
)} )}
</Item> </Item>
</Menu> </Menu>

View file

@ -1,8 +0,0 @@
.checkIcon {
position: absolute;
right: 16px;
}
.checkIcon * {
stroke: var(--textColor1);
}

View file

@ -1,6 +0,0 @@
import { useLocation } from "react-router-dom";
export function useShouldShowPtt() {
const { hash } = useLocation();
return hash.startsWith("#ptt");
}