Redesign homepage WIP
This commit is contained in:
parent
eb620e9220
commit
ef8c28f274
18 changed files with 697 additions and 437 deletions
|
@ -29,6 +29,7 @@
|
|||
"events": "^3.3.0",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
|
||||
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#robertlong/group-call",
|
||||
"normalize.css": "^8.0.1",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"re-resizable": "^6.9.0",
|
||||
"react": "^17.0.0",
|
||||
|
|
|
@ -31,10 +31,15 @@ export function LeftNav({ children, className, hideMobile, ...rest }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function RightNav({ children, className, ...rest }) {
|
||||
export function RightNav({ children, className, hideMobile, ...rest }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.nav, styles.rightNav, className)}
|
||||
className={classNames(
|
||||
styles.nav,
|
||||
styles.rightNav,
|
||||
{ [styles.hideMobile]: hideMobile },
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
@ -42,9 +47,9 @@ export function RightNav({ children, className, ...rest }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function HeaderLogo() {
|
||||
export function HeaderLogo({ className }) {
|
||||
return (
|
||||
<Link className={styles.headerLogo} to="/">
|
||||
<Link className={classNames(styles.headerLogo, className)} to="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
);
|
||||
|
|
|
@ -42,6 +42,10 @@
|
|||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.rightNav.hideMobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav > :last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
@ -103,7 +107,8 @@
|
|||
@media (min-width: 800px) {
|
||||
.headerLogo,
|
||||
.roomAvatar,
|
||||
.leftNav.hideMobile {
|
||||
.leftNav.hideMobile,
|
||||
.rightNav.hideMobile {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
394
src/Home.jsx
394
src/Home.jsx
|
@ -14,399 +14,25 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState, useRef, useEffect } from "react";
|
||||
import { useHistory, Link } from "react-router-dom";
|
||||
import {
|
||||
useClient,
|
||||
useGroupCallRooms,
|
||||
usePublicRooms,
|
||||
createRoom,
|
||||
roomAliasFromRoomName,
|
||||
useInteractiveRegistration,
|
||||
} from "./ConferenceCallManagerHooks";
|
||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||
import styles from "./Home.module.css";
|
||||
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
||||
import { UserMenu } from "./UserMenu";
|
||||
import { Button } from "./button";
|
||||
import { CallList } from "./CallList";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { useClient } from "./ConferenceCallManagerHooks";
|
||||
import { ErrorView, LoadingView } from "./FullScreenView";
|
||||
import { useModalTriggerState } from "./Modal";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
||||
import { RecaptchaInput } from "./RecaptchaInput";
|
||||
import { UserMenuContainer } from "./UserMenuContainer";
|
||||
import { UnauthenticatedView } from "./home/UnauthenticatedView";
|
||||
import { RegisteredView } from "./home/RegisteredView";
|
||||
|
||||
export function Home() {
|
||||
const {
|
||||
isAuthenticated,
|
||||
isGuest,
|
||||
isPasswordlessUser,
|
||||
loading,
|
||||
error,
|
||||
client,
|
||||
} = useClient();
|
||||
|
||||
const [{ privacyPolicyUrl, recaptchaKey }, register] =
|
||||
useInteractiveRegistration();
|
||||
const [recaptchaResponse, setRecaptchaResponse] = useState();
|
||||
|
||||
const history = useHistory();
|
||||
const [creatingRoom, setCreatingRoom] = useState(false);
|
||||
const [createRoomError, setCreateRoomError] = useState();
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
const [existingRoomId, setExistingRoomId] = useState();
|
||||
|
||||
const onCreateRoom = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const roomName = data.get("roomName");
|
||||
const userName = data.get("userName");
|
||||
|
||||
async function onCreateRoom() {
|
||||
let _client = client;
|
||||
|
||||
if (!recaptchaResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_client || isGuest) {
|
||||
_client = await register(
|
||||
userName,
|
||||
randomString(16),
|
||||
recaptchaResponse,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const roomIdOrAlias = await createRoom(_client, roomName);
|
||||
|
||||
if (roomIdOrAlias) {
|
||||
history.push(`/room/${roomIdOrAlias}`);
|
||||
}
|
||||
}
|
||||
|
||||
setCreateRoomError(undefined);
|
||||
setCreatingRoom(true);
|
||||
|
||||
return onCreateRoom().catch((error) => {
|
||||
if (error.errcode === "M_ROOM_IN_USE") {
|
||||
setExistingRoomId(roomAliasFromRoomName(roomName));
|
||||
setCreateRoomError(undefined);
|
||||
modalState.open();
|
||||
} else {
|
||||
setCreateRoomError(error);
|
||||
}
|
||||
|
||||
setCreatingRoom(false);
|
||||
});
|
||||
},
|
||||
[client, history, register, isGuest, recaptchaResponse]
|
||||
);
|
||||
|
||||
const onJoinRoom = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const roomId = data.get("roomId");
|
||||
history.push(`/${roomId}`);
|
||||
},
|
||||
[history]
|
||||
);
|
||||
|
||||
const onJoinExistingRoom = useCallback(() => {
|
||||
history.push(`/${existingRoomId}`);
|
||||
}, [history, existingRoomId]);
|
||||
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
|
||||
useClient();
|
||||
|
||||
if (loading) {
|
||||
return <LoadingView />;
|
||||
} else if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{!isAuthenticated || isGuest ? (
|
||||
<UnregisteredView
|
||||
onCreateRoom={onCreateRoom}
|
||||
createRoomError={createRoomError}
|
||||
creatingRoom={creatingRoom}
|
||||
onJoinRoom={onJoinRoom}
|
||||
privacyPolicyUrl={privacyPolicyUrl}
|
||||
recaptchaKey={recaptchaKey}
|
||||
setRecaptchaResponse={setRecaptchaResponse}
|
||||
/>
|
||||
) : (
|
||||
<RegisteredView
|
||||
client={client}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
isGuest={isGuest}
|
||||
onCreateRoom={onCreateRoom}
|
||||
createRoomError={createRoomError}
|
||||
creatingRoom={creatingRoom}
|
||||
onJoinRoom={onJoinRoom}
|
||||
/>
|
||||
)}
|
||||
{modalState.isOpen && (
|
||||
<JoinExistingCallModal onJoin={onJoinExistingRoom} {...modalProps} />
|
||||
)}
|
||||
</>
|
||||
return isAuthenticated ? (
|
||||
<RegisteredView isPasswordlessUser={isPasswordlessUser} client={client} />
|
||||
) : (
|
||||
<UnauthenticatedView />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function UnregisteredView({
|
||||
onCreateRoom,
|
||||
createRoomError,
|
||||
creatingRoom,
|
||||
onJoinRoom,
|
||||
privacyPolicyUrl,
|
||||
recaptchaKey,
|
||||
setRecaptchaResponse,
|
||||
}) {
|
||||
const acceptTermsRef = useRef();
|
||||
const [acceptTerms, setAcceptTerms] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!acceptTermsRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!acceptTerms) {
|
||||
acceptTermsRef.current.setCustomValidity(
|
||||
"You must accept the terms to continue."
|
||||
);
|
||||
} else {
|
||||
acceptTermsRef.current.setCustomValidity("");
|
||||
}
|
||||
}, [acceptTerms]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.home, styles.fullWidth)}>
|
||||
<Header className={styles.header}>
|
||||
<LeftNav>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenuContainer />
|
||||
</RightNav>
|
||||
</Header>
|
||||
<div className={styles.splitContainer}>
|
||||
<div className={styles.left}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.centered}>
|
||||
<form onSubmit={onJoinRoom}>
|
||||
<h1>Join a call</h1>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<InputField
|
||||
id="roomId"
|
||||
name="roomId"
|
||||
label="Call ID"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Call ID"
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<Button className={styles.button} type="submit">
|
||||
Join call
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
<hr />
|
||||
<form onSubmit={onCreateRoom}>
|
||||
<h1>Create a call</h1>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<InputField
|
||||
id="userName"
|
||||
name="userName"
|
||||
label="Username"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Username"
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<InputField
|
||||
id="roomName"
|
||||
name="roomName"
|
||||
label="Room Name"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Room Name"
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="acceptTerms"
|
||||
type="checkbox"
|
||||
name="acceptTerms"
|
||||
onChange={(e) => setAcceptTerms(e.target.checked)}
|
||||
checked={acceptTerms}
|
||||
label="Accept Privacy Policy"
|
||||
ref={acceptTermsRef}
|
||||
/>
|
||||
<a target="_blank" href={privacyPolicyUrl}>
|
||||
Privacy Policy
|
||||
</a>
|
||||
</FieldRow>
|
||||
{recaptchaKey && (
|
||||
<FieldRow>
|
||||
<RecaptchaInput
|
||||
publicKey={recaptchaKey}
|
||||
onResponse={setRecaptchaResponse}
|
||||
/>
|
||||
</FieldRow>
|
||||
)}
|
||||
{createRoomError && (
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<ErrorMessage>{createRoomError.message}</ErrorMessage>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
type="submit"
|
||||
disabled={creatingRoom}
|
||||
>
|
||||
{creatingRoom ? "Creating call..." : "Create call"}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
|
||||
<div className={styles.authLinks}>
|
||||
<p>
|
||||
Not registered yet?{" "}
|
||||
<Link to="/register">Create an account</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RegisteredView({
|
||||
client,
|
||||
isPasswordlessUser,
|
||||
isGuest,
|
||||
onCreateRoom,
|
||||
createRoomError,
|
||||
creatingRoom,
|
||||
onJoinRoom,
|
||||
}) {
|
||||
const publicRooms = usePublicRooms(
|
||||
client,
|
||||
import.meta.env.VITE_PUBLIC_SPACE_ROOM_ID
|
||||
);
|
||||
const recentRooms = useGroupCallRooms(client);
|
||||
|
||||
const hideCallList = publicRooms.length === 0 && recentRooms.length === 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.home, {
|
||||
[styles.fullWidth]: hideCallList,
|
||||
})}
|
||||
>
|
||||
<Header className={styles.header}>
|
||||
<LeftNav className={styles.leftNav}>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenuContainer />
|
||||
</RightNav>
|
||||
</Header>
|
||||
<div className={styles.splitContainer}>
|
||||
<div className={styles.left}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.centered}>
|
||||
<form onSubmit={onJoinRoom}>
|
||||
<h1>Join a call</h1>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<InputField
|
||||
id="roomId"
|
||||
name="roomId"
|
||||
label="Call ID"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Call ID"
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<Button className={styles.button} type="submit">
|
||||
Join call
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
<hr />
|
||||
<form onSubmit={onCreateRoom}>
|
||||
<h1>Create a call</h1>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<InputField
|
||||
id="roomName"
|
||||
name="roomName"
|
||||
label="Room Name"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Room Name"
|
||||
/>
|
||||
</FieldRow>
|
||||
{createRoomError && (
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<ErrorMessage>{createRoomError.message}</ErrorMessage>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
type="submit"
|
||||
disabled={creatingRoom}
|
||||
>
|
||||
{creatingRoom ? "Creating call..." : "Create call"}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
{(isPasswordlessUser || isGuest) && (
|
||||
<div className={styles.authLinks}>
|
||||
<p>
|
||||
Not registered yet?{" "}
|
||||
<Link to="/register">Create an account</Link>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!hideCallList && (
|
||||
<div className={styles.right}>
|
||||
<div className={styles.content}>
|
||||
{publicRooms.length > 0 && (
|
||||
<CallList
|
||||
title="Public Calls"
|
||||
rooms={publicRooms}
|
||||
client={client}
|
||||
/>
|
||||
)}
|
||||
{recentRooms.length > 0 && (
|
||||
<CallList
|
||||
title="Recent Calls"
|
||||
rooms={recentRooms}
|
||||
client={client}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ export function ProfileModal({
|
|||
client,
|
||||
isAuthenticated,
|
||||
isPasswordlessUser,
|
||||
isGuest,
|
||||
...rest
|
||||
}) {
|
||||
const { onClose } = rest;
|
||||
|
@ -66,7 +65,7 @@ export function ProfileModal({
|
|||
onChange={onChangeDisplayName}
|
||||
/>
|
||||
</FieldRow>
|
||||
{isAuthenticated && !isGuest && !isPasswordlessUser && (
|
||||
{isAuthenticated && !isPasswordlessUser && (
|
||||
<FieldRow>
|
||||
<InputField
|
||||
type="file"
|
||||
|
|
46
src/Room.jsx
46
src/Room.jsx
|
@ -60,34 +60,17 @@ const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
|||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
|
||||
export function Room() {
|
||||
const [registeringGuest, setRegisteringGuest] = useState(false);
|
||||
const [registrationError, setRegistrationError] = useState();
|
||||
const {
|
||||
loading,
|
||||
isAuthenticated,
|
||||
error,
|
||||
client,
|
||||
registerGuest,
|
||||
isGuest,
|
||||
isPasswordlessUser,
|
||||
} = useClient();
|
||||
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||
useClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !isAuthenticated) {
|
||||
setRegisteringGuest(true);
|
||||
|
||||
registerGuest()
|
||||
.then(() => {
|
||||
setRegisteringGuest(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setRegistrationError(error);
|
||||
setRegisteringGuest(false);
|
||||
});
|
||||
setRegistrationError(new Error("Must be registered"));
|
||||
}
|
||||
}, [loading, isAuthenticated]);
|
||||
|
||||
if (loading || registeringGuest) {
|
||||
if (loading) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
|
@ -95,16 +78,10 @@ export function Room() {
|
|||
return <ErrorView error={registrationError || error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupCall
|
||||
client={client}
|
||||
isGuest={isGuest}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
/>
|
||||
);
|
||||
return <GroupCall client={client} isPasswordlessUser={isPasswordlessUser} />;
|
||||
}
|
||||
|
||||
export function GroupCall({ client, isGuest, isPasswordlessUser }) {
|
||||
export function GroupCall({ client, isPasswordlessUser }) {
|
||||
const { roomId: maybeRoomId } = useParams();
|
||||
const { hash, search } = useLocation();
|
||||
const [simpleGrid, viaServers] = useMemo(() => {
|
||||
|
@ -132,7 +109,6 @@ export function GroupCall({ client, isGuest, isPasswordlessUser }) {
|
|||
|
||||
return (
|
||||
<GroupCallView
|
||||
isGuest={isGuest}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
client={client}
|
||||
roomId={roomId}
|
||||
|
@ -144,7 +120,6 @@ export function GroupCall({ client, isGuest, isPasswordlessUser }) {
|
|||
|
||||
export function GroupCallView({
|
||||
client,
|
||||
isGuest,
|
||||
isPasswordlessUser,
|
||||
roomId,
|
||||
groupCall,
|
||||
|
@ -201,12 +176,12 @@ export function GroupCallView({
|
|||
const onLeave = useCallback(() => {
|
||||
leave();
|
||||
|
||||
if (!isGuest && !isPasswordlessUser) {
|
||||
if (!isPasswordlessUser) {
|
||||
history.push("/");
|
||||
} else {
|
||||
setLeft(true);
|
||||
}
|
||||
}, [leave, history, isGuest]);
|
||||
}, [leave, history]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
|
@ -215,7 +190,6 @@ export function GroupCallView({
|
|||
<InRoomView
|
||||
groupCall={groupCall}
|
||||
client={client}
|
||||
isGuest={isGuest}
|
||||
roomName={groupCall.room.name}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
|
@ -245,7 +219,6 @@ export function GroupCallView({
|
|||
} else {
|
||||
return (
|
||||
<RoomSetupView
|
||||
isGuest={isGuest}
|
||||
client={client}
|
||||
hasLocalParticipant={hasLocalParticipant}
|
||||
roomName={groupCall.room.name}
|
||||
|
@ -376,7 +349,6 @@ function RoomSetupView({
|
|||
|
||||
function InRoomView({
|
||||
client,
|
||||
isGuest,
|
||||
groupCall,
|
||||
roomName,
|
||||
microphoneMuted,
|
||||
|
@ -471,7 +443,7 @@ function InRoomView({
|
|||
</LeftNav>
|
||||
<RightNav>
|
||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
||||
{!isGuest && <UserMenuContainer disableLogout />}
|
||||
<UserMenuContainer disableLogout />
|
||||
</RightNav>
|
||||
</Header>
|
||||
{items.length === 0 ? (
|
||||
|
|
|
@ -8,14 +8,8 @@ import { UserMenu } from "./UserMenu";
|
|||
export function UserMenuContainer({ disableLogout }) {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const {
|
||||
isAuthenticated,
|
||||
isGuest,
|
||||
isPasswordlessUser,
|
||||
logout,
|
||||
userName,
|
||||
client,
|
||||
} = useClient();
|
||||
const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
|
||||
useClient();
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
|
||||
|
@ -52,7 +46,6 @@ export function UserMenuContainer({ disableLogout }) {
|
|||
<ProfileModal
|
||||
client={client}
|
||||
isAuthenticated={isAuthenticated}
|
||||
isGuest={isGuest}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
{...modalProps}
|
||||
/>
|
||||
|
|
11
src/form/Form.jsx
Normal file
11
src/form/Form.jsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import classNames from "classnames";
|
||||
import React, { forwardRef } from "react";
|
||||
import styles from "./Form.module.css";
|
||||
|
||||
export const Form = forwardRef(({ children, className, ...rest }, ref) => {
|
||||
return (
|
||||
<form {...rest} className={classNames(styles.form, className)} ref={ref}>
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
});
|
4
src/form/Form.module.css
Normal file
4
src/form/Form.module.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
131
src/home/RegisteredView.jsx
Normal file
131
src/home/RegisteredView.jsx
Normal file
|
@ -0,0 +1,131 @@
|
|||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
useGroupCallRooms,
|
||||
usePublicRooms,
|
||||
} from "../ConferenceCallManagerHooks";
|
||||
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
||||
import styles from "../Home.module.css";
|
||||
import { FieldRow, InputField, ErrorMessage } from "../Input";
|
||||
import { Button } from "../button";
|
||||
import { CallList } from "../CallList";
|
||||
import classNames from "classnames";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
|
||||
export function RegisteredView({
|
||||
client,
|
||||
isPasswordlessUser,
|
||||
onCreateRoom,
|
||||
createRoomError,
|
||||
creatingRoom,
|
||||
onJoinRoom,
|
||||
}) {
|
||||
const publicRooms = usePublicRooms(
|
||||
client,
|
||||
import.meta.env.VITE_PUBLIC_SPACE_ROOM_ID
|
||||
);
|
||||
const recentRooms = useGroupCallRooms(client);
|
||||
|
||||
const hideCallList = publicRooms.length === 0 && recentRooms.length === 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.home, {
|
||||
[styles.fullWidth]: hideCallList,
|
||||
})}
|
||||
>
|
||||
<Header className={styles.header}>
|
||||
<LeftNav className={styles.leftNav}>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenuContainer />
|
||||
</RightNav>
|
||||
</Header>
|
||||
<div className={styles.splitContainer}>
|
||||
<div className={styles.left}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.centered}>
|
||||
<form onSubmit={onJoinRoom}>
|
||||
<h1>Join a call</h1>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<InputField
|
||||
id="roomId"
|
||||
name="roomId"
|
||||
label="Call ID"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Call ID"
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<Button className={styles.button} type="submit">
|
||||
Join call
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
<hr />
|
||||
<form onSubmit={onCreateRoom}>
|
||||
<h1>Create a call</h1>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<InputField
|
||||
id="roomName"
|
||||
name="roomName"
|
||||
label="Room Name"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Room Name"
|
||||
/>
|
||||
</FieldRow>
|
||||
{createRoomError && (
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<ErrorMessage>{createRoomError.message}</ErrorMessage>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
type="submit"
|
||||
disabled={creatingRoom}
|
||||
>
|
||||
{creatingRoom ? "Creating call..." : "Create call"}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
{isPasswordlessUser && (
|
||||
<div className={styles.authLinks}>
|
||||
<p>
|
||||
Not registered yet?{" "}
|
||||
<Link to="/register">Create an account</Link>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!hideCallList && (
|
||||
<div className={styles.right}>
|
||||
<div className={styles.content}>
|
||||
{publicRooms.length > 0 && (
|
||||
<CallList
|
||||
title="Public Calls"
|
||||
rooms={publicRooms}
|
||||
client={client}
|
||||
/>
|
||||
)}
|
||||
{recentRooms.length > 0 && (
|
||||
<CallList
|
||||
title="Recent Calls"
|
||||
rooms={recentRooms}
|
||||
client={client}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
154
src/home/UnauthenticatedView.jsx
Normal file
154
src/home/UnauthenticatedView.jsx
Normal file
|
@ -0,0 +1,154 @@
|
|||
import React, { useCallback, useState } from "react";
|
||||
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { FieldRow, InputField, ErrorMessage } from "../Input";
|
||||
import { Button } from "../button";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import {
|
||||
createRoom,
|
||||
useInteractiveRegistration,
|
||||
} from "../ConferenceCallManagerHooks";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { JoinExistingCallModal } from "../JoinExistingCallModal";
|
||||
import { useRecaptcha } from "../useRecaptcha";
|
||||
import { Body, Caption, Title, Link, Headline } from "../typography/Typography";
|
||||
import { Form } from "../form/Form";
|
||||
import styles from "./UnauthenticatedView.module.css";
|
||||
|
||||
export function UnauthenticatedView() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
const [{ privacyPolicyUrl, recaptchaKey }, register] =
|
||||
useInteractiveRegistration();
|
||||
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
||||
const onSubmit = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const roomName = data.get("roomName");
|
||||
const userName = data.get("userName");
|
||||
|
||||
async function submit() {
|
||||
setError(undefined);
|
||||
setLoading(true);
|
||||
const recaptchaResponse = await execute();
|
||||
const client = await register(
|
||||
userName,
|
||||
randomString(16),
|
||||
recaptchaResponse,
|
||||
true
|
||||
);
|
||||
|
||||
const roomIdOrAlias = await createRoom(client, roomName);
|
||||
|
||||
if (roomIdOrAlias) {
|
||||
history.push(`/room/${roomIdOrAlias}`);
|
||||
}
|
||||
}
|
||||
|
||||
submit().catch((error) => {
|
||||
if (error.errcode === "M_ROOM_IN_USE") {
|
||||
setExistingRoomId(roomAliasFromRoomName(roomName));
|
||||
setError(undefined);
|
||||
modalState.open();
|
||||
} else {
|
||||
console.error(error);
|
||||
setLoading(false);
|
||||
setError(error);
|
||||
reset();
|
||||
}
|
||||
});
|
||||
},
|
||||
[register]
|
||||
);
|
||||
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
const [existingRoomId, setExistingRoomId] = useState();
|
||||
const history = useHistory();
|
||||
const onJoinExistingRoom = useCallback(() => {
|
||||
history.push(`/${existingRoomId}`);
|
||||
}, [history, existingRoomId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav hideMobile>
|
||||
<UserMenuContainer />
|
||||
</RightNav>
|
||||
</Header>
|
||||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
<HeaderLogo className={styles.logo} />
|
||||
<Headline className={styles.headline}>Enter a call name</Headline>
|
||||
<Form className={styles.form} onSubmit={onSubmit}>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="callName"
|
||||
name="callName"
|
||||
label="Call name"
|
||||
placeholder="Call name"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="userName"
|
||||
name="userName"
|
||||
label="Your name"
|
||||
placeholder="Your name"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FieldRow>
|
||||
<Caption>
|
||||
This site is protected by ReCAPTCHA and the Google{" "}
|
||||
<Link href="https://www.google.com/policies/privacy/">
|
||||
Privacy Policy
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="https://policies.google.com/terms">
|
||||
Terms of Service
|
||||
</Link>{" "}
|
||||
apply.
|
||||
<br />
|
||||
By clicking "Go", you agree to our{" "}
|
||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||
</Caption>
|
||||
{error && (
|
||||
<FieldRow>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
</FieldRow>
|
||||
)}
|
||||
<Button type="submit" size="lg" disabled={loading}>
|
||||
{loading ? "Loading..." : "Go"}
|
||||
</Button>
|
||||
<div id={recaptchaId} />
|
||||
</Form>
|
||||
</main>
|
||||
<footer className={styles.footer}>
|
||||
<Body className={styles.mobileLoginLink}>
|
||||
<Link color="primary" to="/l;ogin">
|
||||
Login to your account
|
||||
</Link>
|
||||
</Body>
|
||||
<Body>
|
||||
Not registered yet?{" "}
|
||||
<Link color="primary" to="/register">
|
||||
Create an account
|
||||
</Link>
|
||||
</Body>
|
||||
</footer>
|
||||
</div>
|
||||
{modalState.isOpen && (
|
||||
<JoinExistingCallModal onJoin={onJoinExistingRoom} {...modalProps} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
63
src/home/UnauthenticatedView.module.css
Normal file
63
src/home/UnauthenticatedView.module.css
Normal file
|
@ -0,0 +1,63 @@
|
|||
.container {
|
||||
display: flex;
|
||||
min-height: calc(100% - 64px);
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.footer .mobileLoginLink {
|
||||
display: flex;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.headline {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.form {
|
||||
padding: 0 24px;
|
||||
justify-content: center;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.form > * + * {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobileLoginLink {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: calc(100% - 76px);
|
||||
}
|
||||
}
|
|
@ -20,6 +20,8 @@ limitations under the License.
|
|||
Therefore we define a unicode-range to load which excludes the glyphs
|
||||
(to avoid having to maintain a fork of Inter). */
|
||||
|
||||
@import "normalize.css/normalize.css";
|
||||
|
||||
:root {
|
||||
--inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f,
|
||||
U+25c2-2664, U+2666-2763, U+2765-2b05, U+2b07-2b1b, U+2b1d-10FFFF;
|
||||
|
@ -35,6 +37,7 @@ limitations under the License.
|
|||
--textColor4: #a9b2bc;
|
||||
--inputBorderColor: #394049;
|
||||
--inputBorderColorFocused: #0086e6;
|
||||
--linkColor: #0086e6;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
@ -139,6 +142,44 @@ body,
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
a {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Headline Semi Bold */
|
||||
h1 {
|
||||
font-weight: 600;
|
||||
font-size: 32px;
|
||||
line-height: 39px;
|
||||
}
|
||||
|
||||
/* Title */
|
||||
h2 {
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
line-height: 29px;
|
||||
}
|
||||
|
||||
/* Subtitle */
|
||||
h3 {
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
/* Body */
|
||||
p {
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primaryColor);
|
||||
text-decoration: none;
|
||||
|
@ -179,3 +220,7 @@ details[open] > summary {
|
|||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.grecaptcha-badge {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
|
123
src/typography/Typography.jsx
Normal file
123
src/typography/Typography.jsx
Normal file
|
@ -0,0 +1,123 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import styles from "./Typography.module.css";
|
||||
|
||||
export const Headline = forwardRef(
|
||||
({ as: Component = "h1", children, className, fontWeight, ...rest }, ref) => {
|
||||
return (
|
||||
<Component
|
||||
{...rest}
|
||||
className={classNames(styles[fontWeight], className)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const Title = forwardRef(
|
||||
({ as: Component = "h2", children, className, fontWeight, ...rest }, ref) => {
|
||||
return (
|
||||
<Component
|
||||
{...rest}
|
||||
className={classNames(styles[fontWeight], className)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const Subtitle = forwardRef(
|
||||
({ as: Component = "h3", children, className, fontWeight, ...rest }, ref) => {
|
||||
return (
|
||||
<Component
|
||||
{...rest}
|
||||
className={classNames(styles[fontWeight], className)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const Body = forwardRef(
|
||||
({ as: Component = "p", children, className, fontWeight, ...rest }, ref) => {
|
||||
return (
|
||||
<Component
|
||||
{...rest}
|
||||
className={classNames(styles[fontWeight], className)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const Caption = forwardRef(
|
||||
({ as: Component = "p", children, className, fontWeight, ...rest }, ref) => {
|
||||
return (
|
||||
<Component
|
||||
{...rest}
|
||||
className={classNames(styles.caption, styles[fontWeight], className)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const Micro = forwardRef(
|
||||
({ as: Component = "p", children, className, fontWeight, ...rest }, ref) => {
|
||||
return (
|
||||
<Component
|
||||
{...rest}
|
||||
className={classNames(styles.micro, styles[fontWeight], className)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const Link = forwardRef(
|
||||
(
|
||||
{
|
||||
as: Component = RouterLink,
|
||||
children,
|
||||
className,
|
||||
color = "link",
|
||||
href,
|
||||
fontWeight,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
let externalLinkProps;
|
||||
|
||||
if (href) {
|
||||
externalLinkProps = {
|
||||
target: "_blank",
|
||||
rel: "noreferrer noopener",
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...externalLinkProps}
|
||||
{...rest}
|
||||
className={classNames(styles[color], styles[fontWeight], className)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
);
|
29
src/typography/Typography.module.css
Normal file
29
src/typography/Typography.module.css
Normal file
|
@ -0,0 +1,29 @@
|
|||
.caption {
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.micro {
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.regular {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.semiBold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--linkColor);
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: var(--primaryColor);
|
||||
}
|
25
src/typography/Typography.stories.jsx
Normal file
25
src/typography/Typography.stories.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from "react";
|
||||
import { Headline, Title, Subtitle, Body, Caption, Micro } from "./Typography";
|
||||
|
||||
export default {
|
||||
title: "Typography",
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
};
|
||||
|
||||
export const Typography = () => (
|
||||
<>
|
||||
<Headline>Headline Semi Bold</Headline>
|
||||
<Title>Title</Title>
|
||||
<Subtitle>Subtitle</Subtitle>
|
||||
<Subtitle fontWeight="semiBold">Subtitle Semi Bold</Subtitle>
|
||||
<Body>Body</Body>
|
||||
<Body fontWeight="semiBold">Body Semi Bold</Body>
|
||||
<Caption>Caption</Caption>
|
||||
<Caption fontWeight="semiBold">Caption Semi Bold</Caption>
|
||||
<Caption fontWeight="bold">Caption Bold</Caption>
|
||||
<Micro>Micro</Micro>
|
||||
<Micro fontWeight="bold">Micro bold</Micro>
|
||||
</>
|
||||
);
|
69
src/useRecaptcha.js
Normal file
69
src/useRecaptcha.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { useEffect, useCallback, useRef, useState } from "react";
|
||||
|
||||
const RECAPTCHA_SCRIPT_URL =
|
||||
"https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit";
|
||||
|
||||
export function useRecaptcha(sitekey) {
|
||||
const [recaptchaId] = useState(() => randomString(16));
|
||||
const resolvePromiseRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
if (!sitekey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onRecaptchaLoaded = () => {
|
||||
if (!document.getElementById(recaptchaId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.grecaptcha.render(recaptchaId, {
|
||||
sitekey,
|
||||
size: "invisible",
|
||||
callback: (response) => {
|
||||
if (resolvePromiseRef.current) {
|
||||
resolvePromiseRef.current(response);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (
|
||||
typeof window.grecaptcha !== "undefined" &&
|
||||
typeof window.grecaptcha.render === "function"
|
||||
) {
|
||||
onRecaptchaLoaded();
|
||||
} else {
|
||||
window.mxOnRecaptchaLoaded = onRecaptchaLoaded;
|
||||
|
||||
if (!document.querySelector(`script[src="${RECAPTCHA_SCRIPT_URL}"]`)) {
|
||||
const scriptTag = document.createElement("script");
|
||||
scriptTag.src = RECAPTCHA_SCRIPT_URL;
|
||||
scriptTag.async = true;
|
||||
document.body.appendChild(scriptTag);
|
||||
}
|
||||
}
|
||||
}, [recaptchaId, sitekey]);
|
||||
|
||||
const execute = useCallback(() => {
|
||||
if (!window.grecaptcha) {
|
||||
return Promise.reject(new Error("Recaptcha not loaded"));
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolvePromiseRef.current = resolve;
|
||||
window.grecaptcha.execute();
|
||||
});
|
||||
}, [recaptchaId]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
if (window.grecaptcha) {
|
||||
window.grecaptcha.reset();
|
||||
}
|
||||
}, [recaptchaId]);
|
||||
|
||||
console.log(recaptchaId);
|
||||
|
||||
return { execute, reset, recaptchaId };
|
||||
}
|
|
@ -9155,6 +9155,11 @@ normalize-range@^0.1.2:
|
|||
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
|
||||
integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
|
||||
|
||||
normalize.css@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
|
||||
integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
|
||||
|
||||
npm-run-path@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
||||
|
|
Loading…
Reference in a new issue