Compare commits

..

1 Commits

Author SHA1 Message Date
Robert Long
34f8023a4e Always authenticate user by registering temp guest accounts 2021-12-14 18:25:52 -08:00
34 changed files with 512 additions and 1148 deletions

View File

@@ -1 +0,0 @@
node_modules

3
.env
View File

@@ -4,9 +4,6 @@
# https://vitejs.dev/guide/env-and-mode.html#env-files
####
# Used for determining the homeserver to use for short urls etc.
# VITE_DEFAULT_HOMESERVER=http://localhost:8008
# The room id for the space to use for listing public group call rooms
# VITE_PUBLIC_SPACE_ROOM_ID=!hjdfshkdskjdsk:myhomeserver.com

View File

@@ -1,41 +0,0 @@
name: Build & publish images to the package registry for tags
on:
release:
types: [published]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
name: Build & publish
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Check it out
uses: actions/checkout@v2
- name: Log in to container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,18 +0,0 @@
FROM node:16-buster as builder
WORKDIR /src
COPY . /src/matrix-video-chat
RUN matrix-video-chat/scripts/dockerbuild.sh
# App
FROM nginxinc/nginx-unprivileged:alpine
COPY --from=builder /src/matrix-video-chat/dist /app
COPY scripts/default.conf /etc/nginx/conf.d/
USER root
RUN rm -rf /usr/share/nginx/html
USER 101

View File

@@ -13,12 +13,10 @@
"@react-aria/overlays": "^3.7.3",
"@react-aria/select": "^3.6.0",
"@react-aria/tabs": "^3.1.0",
"@react-aria/tooltip": "^3.1.3",
"@react-aria/utils": "^3.10.0",
"@react-stately/collections": "^3.3.4",
"@react-stately/overlays": "^3.1.3",
"@react-stately/select": "^3.1.3",
"@react-stately/tooltip": "^3.0.5",
"@react-stately/tree": "^3.2.0",
"@sentry/react": "^6.13.3",
"@sentry/tracing": "^6.13.3",

View File

@@ -1,10 +0,0 @@
server {
listen 8080;
server_name localhost;
location / {
root /app;
try_files $uri /$uri /index.html;
}
}

View File

@@ -1,29 +0,0 @@
#!/bin/sh
set -ex
export VITE_DEFAULT_HOMESERVER=https://call.ems.host
export VITE_SENTRY_DSN=https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41
git clone https://github.com/matrix-org/matrix-js-sdk.git
cd matrix-js-sdk
git checkout robertlong/group-call
yarn install
yarn run build
yarn link
cd ..
git clone https://github.com/matrix-org/matrix-react-sdk.git
cd matrix-react-sdk
git checkout robertlong/group-call
yarn link matrix-js-sdk
yarn install
yarn run build
yarn link
cd ..
cd matrix-video-chat
yarn link matrix-js-sdk
yarn link matrix-react-sdk
yarn install
yarn run build

View File

@@ -28,17 +28,18 @@ import { Home } from "./Home";
import { LoginPage } from "./LoginPage";
import { RegisterPage } from "./RegisterPage";
import { Room } from "./Room";
import {
ClientProvider,
defaultHomeserverHost,
} from "./ConferenceCallManagerHooks";
import { ClientProvider } from "./ConferenceCallManagerHooks";
import { useFocusVisible } from "@react-aria/interactions";
import styles from "./App.module.css";
import { LoadingView } from "./FullScreenView";
import { ErrorView, LoadingView } from "./FullScreenView";
const SentryRoute = Sentry.withSentryRouting(Route);
export default function App({ history }) {
const { protocol, host } = window.location;
// Assume homeserver is hosted on same domain (proxied in development by vite)
const homeserverUrl = `${protocol}//${host}`;
export default function App() {
const { isFocusVisible } = useFocusVisible();
useEffect(() => {
@@ -57,8 +58,8 @@ export default function App({ history }) {
}, [isFocusVisible]);
return (
<Router history={history}>
<ClientProvider>
<Router>
<ClientProvider homeserverUrl={homeserverUrl}>
<OverlayProvider>
<Switch>
<SentryRoute exact path="/">
@@ -86,20 +87,50 @@ export default function App({ history }) {
function RoomRedirect() {
const { pathname } = useLocation();
const history = useHistory();
const [error, setError] = useState();
useEffect(() => {
let roomId = pathname;
async function redirect() {
let roomId = pathname;
if (pathname.startsWith("/")) {
roomId = roomId.substr(1, roomId.length);
if (pathname.startsWith("/")) {
roomId = roomId.substr(1, roomId.length);
}
if (!roomId.startsWith("#") && !roomId.startsWith("!")) {
let loginHomeserverUrl = homeserverUrl.trim();
if (!loginHomeserverUrl.includes("://")) {
loginHomeserverUrl = "https://" + loginHomeserverUrl;
}
try {
const wellKnownUrl = new URL(
"/.well-known/matrix/client",
window.location
);
const response = await fetch(wellKnownUrl);
const config = await response.json();
if (config["m.homeserver"]) {
loginHomeserverUrl = config["m.homeserver"];
}
} catch (error) {}
const { host } = new URL(loginHomeserverUrl);
roomId = `#${roomId}:${host}`;
}
history.replace(`/room/${roomId}`);
}
if (!roomId.startsWith("#") && !roomId.startsWith("!")) {
roomId = `#${roomId}:${defaultHomeserverHost}`;
}
redirect().catch(setError);
}, [history, pathname]);
history.replace(`/room/${roomId}`);
}, [pathname, history]);
if (error) {
return <ErrorView error={error} />;
}
return <LoadingView />;
}

View File

@@ -6,8 +6,6 @@
justify-content: center;
pointer-events: none;
font-weight: 600;
overflow: hidden;
flex-shrink: 0;
}
.avatar img {
@@ -20,24 +18,13 @@
fill: #ffffff;
}
.avatar span {
padding-top: 1px;
}
.xs {
.sm {
width: 22px;
height: 22px;
border-radius: 22px;
font-size: 14px;
}
.sm {
width: 32px;
height: 32px;
border-radius: 32px;
font-size: 15px;
}
.md {
width: 36px;
height: 36px;

View File

@@ -7,7 +7,7 @@ import { ReactComponent as VideoIcon } from "./icons/Video.svg";
import styles from "./CallList.module.css";
import { getRoomUrl } from "./ConferenceCallManagerHooks";
export function CallList({ title, rooms, client }) {
export function CallList({ title, rooms }) {
return (
<>
<h3>{title}</h3>
@@ -15,7 +15,6 @@ export function CallList({ title, rooms, client }) {
{rooms.map(({ roomId, roomName, avatarUrl, participants }) => (
<CallTile
key={roomId}
client={client}
name={roomName}
avatarUrl={avatarUrl}
roomId={roomId}
@@ -27,7 +26,7 @@ export function CallList({ title, rooms, client }) {
);
}
function CallTile({ name, avatarUrl, roomId, participants, client }) {
function CallTile({ name, avatarUrl, roomId, participants }) {
return (
<div className={styles.callTile}>
<Link to={`/room/${roomId}`} className={styles.callTileLink}>
@@ -41,9 +40,7 @@ function CallTile({ name, avatarUrl, roomId, participants, client }) {
<div className={styles.callInfo}>
<h5>{name}</h5>
<p>{getRoomUrl(roomId)}</p>
{participants && (
<Facepile client={client} participants={participants} />
)}
{participants && <Facepile participants={participants} />}
</div>
<div className={styles.copyButtonSpacer} />
</Link>

View File

@@ -28,12 +28,7 @@ import {
GroupCallType,
} from "matrix-js-sdk/src/browser-index";
import { useHistory } from "react-router-dom";
export const defaultHomeserver =
import.meta.env.VITE_DEFAULT_HOMESERVER ||
`${window.location.protocol}//${window.location.host}`;
export const defaultHomeserverHost = new URL(defaultHomeserver).host;
import { randomString } from "matrix-js-sdk/src/randomstring";
const ClientContext = createContext();
@@ -70,6 +65,32 @@ async function initClient(clientOptions, guest) {
return client;
}
async function registerGuest(homeserverUrl) {
const registrationClient = matrix.createClient(homeserverUrl);
const { user_id, device_id, access_token } =
await registrationClient.registerGuest({});
const client = await initClient(
{
baseUrl: homeserverUrl,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
},
true
);
await client.setDisplayName(`Guest ${client.getUserIdLocalpart()}`);
localStorage.setItem(
"matrix-auth-store",
JSON.stringify({ user_id, device_id, access_token, guest: true })
);
return client;
}
export async function fetchGroupCall(
client,
roomIdOrAlias,
@@ -106,19 +127,17 @@ export async function fetchGroupCall(
});
}
export function ClientProvider({ children }) {
export function ClientProvider({ homeserverUrl, children }) {
const history = useHistory();
const [
{ loading, isAuthenticated, isPasswordlessUser, isGuest, client, userName },
setState,
] = useState({
loading: true,
isAuthenticated: false,
isPasswordlessUser: false,
isGuest: false,
client: undefined,
userName: null,
});
const [{ loading, isPasswordlessUser, isGuest, client, userName }, setState] =
useState({
loading: true,
isPasswordlessUser: false,
isGuest: false,
client: undefined,
userName: null,
displayName: null,
});
useEffect(() => {
async function restore() {
@@ -126,18 +145,12 @@ export function ClientProvider({ children }) {
const authStore = localStorage.getItem("matrix-auth-store");
if (authStore) {
const {
user_id,
device_id,
access_token,
guest,
passwordlessUser,
tempPassword,
} = JSON.parse(authStore);
const { user_id, device_id, access_token, guest, passwordlessUser } =
JSON.parse(authStore);
const client = await initClient(
{
baseUrl: defaultHomeserver,
baseUrl: homeserverUrl,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
@@ -153,14 +166,18 @@ export function ClientProvider({ children }) {
access_token,
guest,
passwordlessUser,
tempPassword,
})
);
return { client, guest, passwordlessUser };
}
} catch (err) {
localStorage.removeItem("matrix-auth-store");
}
return { client: undefined, guest: false };
try {
const client = await registerGuest(homeserverUrl);
return { client, guest: true, passwordlessUser: false };
} catch (err) {
localStorage.removeItem("matrix-auth-store");
throw err;
@@ -172,17 +189,16 @@ export function ClientProvider({ children }) {
setState({
client,
loading: false,
isAuthenticated: !!client,
isPasswordlessUser: !!passwordlessUser,
isGuest: guest,
userName: client?.getUserIdLocalpart(),
});
})
.catch(() => {
.catch((error) => {
setState({
error,
client: undefined,
loading: false,
isAuthenticated: false,
isPasswordlessUser: false,
isGuest: false,
userName: null,
@@ -191,200 +207,84 @@ export function ClientProvider({ children }) {
}, []);
const login = useCallback(async (homeserver, username, password) => {
try {
let loginHomeserverUrl = homeserver.trim();
let loginHomeserverUrl = homeserver.trim();
if (!loginHomeserverUrl.includes("://")) {
loginHomeserverUrl = "https://" + loginHomeserverUrl;
if (!loginHomeserverUrl.includes("://")) {
loginHomeserverUrl = "https://" + loginHomeserverUrl;
}
try {
const wellKnownUrl = new URL(
"/.well-known/matrix/client",
window.location
);
const response = await fetch(wellKnownUrl);
const config = await response.json();
if (config["m.homeserver"]) {
loginHomeserverUrl = config["m.homeserver"];
}
} catch (error) {}
try {
const wellKnownUrl = new URL(
"/.well-known/matrix/client",
window.location
);
const response = await fetch(wellKnownUrl);
const config = await response.json();
const registrationClient = matrix.createClient(loginHomeserverUrl);
if (config["m.homeserver"]) {
loginHomeserverUrl = config["m.homeserver"];
}
} catch (error) {}
const { user_id, device_id, access_token } =
await registrationClient.loginWithPassword(username, password);
const registrationClient = matrix.createClient(loginHomeserverUrl);
const client = await initClient({
baseUrl: loginHomeserverUrl,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
});
const { user_id, device_id, access_token } =
await registrationClient.loginWithPassword(username, password);
localStorage.setItem(
"matrix-auth-store",
JSON.stringify({ user_id, device_id, access_token })
);
const client = await initClient({
baseUrl: loginHomeserverUrl,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
});
setState({
client,
loading: false,
isPasswordlessUser: false,
isGuest: false,
userName: client.getUserIdLocalpart(),
});
localStorage.setItem(
"matrix-auth-store",
JSON.stringify({ user_id, device_id, access_token })
);
setState({
client,
loading: false,
isAuthenticated: true,
isPasswordlessUser: false,
isGuest: false,
userName: client.getUserIdLocalpart(),
});
} catch (err) {
localStorage.removeItem("matrix-auth-store");
setState({
client: undefined,
loading: false,
isAuthenticated: false,
isPasswordlessUser: false,
isGuest: false,
userName: null,
});
throw err;
}
}, []);
const registerGuest = useCallback(async () => {
try {
const registrationClient = matrix.createClient(defaultHomeserver);
const { user_id, device_id, access_token } =
await registrationClient.registerGuest({});
const client = await initClient(
{
baseUrl: defaultHomeserver,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
},
true
);
await client.setProfileInfo("displayname", {
displayname: `Guest ${client.getUserIdLocalpart()}`,
});
localStorage.setItem(
"matrix-auth-store",
JSON.stringify({ user_id, device_id, access_token, guest: true })
);
setState({
client,
loading: false,
isAuthenticated: true,
isGuest: true,
isPasswordlessUser: false,
userName: client.getUserIdLocalpart(),
});
} catch (err) {
localStorage.removeItem("matrix-auth-store");
setState({
client: undefined,
loading: false,
isAuthenticated: false,
isPasswordlessUser: false,
isGuest: false,
userName: null,
});
throw err;
}
return client;
}, []);
const register = useCallback(async (username, password, passwordlessUser) => {
try {
const registrationClient = matrix.createClient(defaultHomeserver);
const registrationClient = matrix.createClient(homeserverUrl);
const { user_id, device_id, access_token } =
await registrationClient.register(username, password, null, {
type: "m.login.dummy",
});
const client = await initClient({
baseUrl: defaultHomeserver,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
const { user_id, device_id, access_token } =
await registrationClient.register(username, password, null, {
type: "m.login.dummy",
});
const session = { user_id, device_id, access_token, passwordlessUser };
const client = await initClient({
baseUrl: homeserverUrl,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
});
if (passwordlessUser) {
session.tempPassword = password;
}
localStorage.setItem(
"matrix-auth-store",
JSON.stringify({ user_id, device_id, access_token, passwordlessUser })
);
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
setState({
client,
loading: false,
isGuest: false,
isPasswordlessUser: passwordlessUser,
userName: client.getUserIdLocalpart(),
});
setState({
client,
loading: false,
isGuest: false,
isAuthenticated: true,
isPasswordlessUser: passwordlessUser,
userName: client.getUserIdLocalpart(),
});
return client;
} catch (err) {
localStorage.removeItem("matrix-auth-store");
setState({
client: undefined,
loading: false,
isGuest: false,
isAuthenticated: false,
isPasswordlessUser: false,
userName: null,
});
throw err;
}
return client;
}, []);
const changePassword = useCallback(
async (password) => {
const { tempPassword, passwordlessUser, ...existingSession } = JSON.parse(
localStorage.getItem("matrix-auth-store")
);
await client.setPassword(
{
type: "m.login.password",
identifier: {
type: "m.id.user",
user: existingSession.user_id,
},
user: existingSession.user_id,
password: tempPassword,
},
password
);
localStorage.setItem(
"matrix-auth-store",
JSON.stringify({
...existingSession,
passwordlessUser: false,
})
);
setState({
client,
loading: false,
isGuest: false,
isAuthenticated: true,
isPasswordlessUser: false,
userName: client.getUserIdLocalpart(),
});
},
[client]
);
const logout = useCallback(() => {
localStorage.removeItem("matrix-auth-store");
window.location = "/";
@@ -393,27 +293,21 @@ export function ClientProvider({ children }) {
const context = useMemo(
() => ({
loading,
isAuthenticated,
isPasswordlessUser,
isGuest,
client,
login,
registerGuest,
register,
changePassword,
logout,
userName,
}),
[
loading,
isAuthenticated,
isPasswordlessUser,
isGuest,
client,
login,
registerGuest,
register,
changePassword,
logout,
userName,
]
@@ -428,7 +322,7 @@ export function useClient() {
return useContext(ClientContext);
}
export function roomAliasFromRoomName(roomName) {
function roomAliasFromRoomName(roomName) {
return roomName
.trim()
.replace(/\s/g, "-")
@@ -481,6 +375,41 @@ export async function createRoom(client, name) {
return room_alias || room_id;
}
export function useCreateRoom() {
const { register, client } = useClient();
const [creatingRoom, setCreatingRoom] = useState(false);
const [createRoomError, setCreateRoomError] = useState();
const onCreateRoom = useCallback(
(roomName, userName) => {
async function onCreateRoom(roomName, userName) {
let _client = client;
if (!_client) {
_client = await register(userName, randomString(16), true);
}
return await createRoom(_client, roomName);
}
setCreateRoomError(undefined);
setCreatingRoom(true);
return onCreateRoom(roomName, userName).catch((error) => {
setCreateRoomError(error);
setCreatingRoom(false);
});
},
[register, client]
);
return {
creatingRoom,
createRoomError,
createRoom: onCreateRoom,
};
}
export function useLoadGroupCall(client, roomId, viaServers) {
const [state, setState] = useState({
loading: true,
@@ -614,7 +543,7 @@ export function getRoomUrl(roomId) {
if (roomId.startsWith("#")) {
const [localPart, host] = roomId.replace("#", "").split(":");
if (host !== defaultHomeserverHost) {
if (host !== window.location.host) {
return `${window.location.host}/room/${roomId}`;
} else {
return `${window.location.host}/${localPart}`;
@@ -624,35 +553,17 @@ export function getRoomUrl(roomId) {
}
}
export function getAvatarUrl(client, mxcUrl, avatarSize = 96) {
const width = Math.floor(avatarSize * window.devicePixelRatio);
const height = Math.floor(avatarSize * window.devicePixelRatio);
return mxcUrl && client.mxcUrlToHttp(mxcUrl, width, height, "crop");
}
export function useProfile(client) {
const [{ loading, displayName, avatarUrl, error, success }, setState] =
useState(() => {
const user = client?.getUser(client.getUserId());
return {
success: false,
loading: false,
displayName: user?.displayName,
avatarUrl: user && client && getAvatarUrl(client, user.avatarUrl),
error: null,
};
});
export function useDisplayName(client) {
const [{ loading, displayName, error, success }, setState] = useState(() => ({
success: false,
loading: false,
displayName: client?.getUser(client.getUserId())?.displayName,
error: null,
}));
useEffect(() => {
const onChangeUser = (_event, { displayName, avatarUrl }) => {
setState({
success: false,
loading: false,
displayName,
avatarUrl: getAvatarUrl(client, avatarUrl),
error: null,
});
const onChangeDisplayName = (_event, { displayName }) => {
setState({ success: false, loading: false, displayName, error: null });
};
let user;
@@ -660,20 +571,18 @@ export function useProfile(client) {
if (client) {
const userId = client.getUserId();
user = client.getUser(userId);
user.on("User.displayName", onChangeUser);
user.on("User.avatarUrl", onChangeUser);
user.on("User.displayName", onChangeDisplayName);
}
return () => {
if (user) {
user.removeListener("User.displayName", onChangeUser);
user.removeListener("User.avatarUrl", onChangeUser);
user.removeListener("User.displayName", onChangeDisplayName);
}
};
}, [client]);
const saveProfile = useCallback(
async ({ displayName, avatar }) => {
const setDisplayName = useCallback(
(displayName) => {
if (client) {
setState((prev) => ({
...prev,
@@ -682,39 +591,30 @@ export function useProfile(client) {
success: false,
}));
try {
await client.setDisplayName(displayName);
let mxcAvatarUrl;
if (avatar) {
mxcAvatarUrl = await client.uploadContent(avatar);
await client.setAvatarUrl(mxcAvatarUrl);
}
setState((prev) => ({
...prev,
displayName,
avatarUrl: mxcAvatarUrl
? getAvatarUrl(client, mxcAvatarUrl)
: prev.avatarUrl,
loading: false,
success: true,
}));
} catch (error) {
setState((prev) => ({
...prev,
loading: false,
error,
success: false,
}));
}
client
.setDisplayName(displayName)
.then(() => {
setState((prev) => ({
...prev,
displayName,
loading: false,
success: true,
}));
})
.catch((error) => {
setState((prev) => ({
...prev,
loading: false,
error,
success: false,
}));
});
} else {
console.error("Client not initialized before calling saveProfile");
console.error("Client not initialized before calling setDisplayName");
}
},
[client]
);
return { loading, error, displayName, avatarUrl, saveProfile, success };
return { loading, error, displayName, setDisplayName, success };
}

View File

@@ -2,32 +2,27 @@ import React from "react";
import styles from "./Facepile.module.css";
import classNames from "classnames";
import { Avatar } from "./Avatar";
import { getAvatarUrl } from "./ConferenceCallManagerHooks";
export function Facepile({ className, client, participants, ...rest }) {
export function Facepile({ className, participants, ...rest }) {
return (
<div
className={classNames(styles.facepile, className)}
title={participants.map((member) => member.name).join(", ")}
{...rest}
>
{participants.slice(0, 3).map((member, i) => {
const avatarUrl = member.user?.avatarUrl;
return (
<Avatar
key={member.userId}
size="xs"
src={avatarUrl && getAvatarUrl(client, avatarUrl, 22)}
fallback={member.name.slice(0, 1).toUpperCase()}
className={styles.avatar}
style={{ left: i * 22 }}
/>
);
})}
{participants.slice(0, 3).map((member, i) => (
<Avatar
key={member.userId}
size="sm"
fallback={member.name.slice(0, 1).toUpperCase()}
className={styles.avatar}
style={{ left: i * 22 }}
/>
))}
{participants.length > 3 && (
<Avatar
key="additional"
size="xs"
size="sm"
fallback={`+${participants.length - 3}`}
className={styles.avatar}
style={{ left: 3 * 22 }}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Button } from "./button";
import { ButtonTooltip, Button } from "./button";
import { PopoverMenuTrigger } from "./PopoverMenu";
import { ReactComponent as SpotlightIcon } from "./icons/Spotlight.svg";
import { ReactComponent as FreedomIcon } from "./icons/Freedom.svg";
@@ -7,21 +7,14 @@ import { ReactComponent as CheckIcon } from "./icons/Check.svg";
import styles from "./GridLayoutMenu.module.css";
import { Menu } from "./Menu";
import { Item } from "@react-stately/collections";
import { Tooltip, TooltipTrigger } from "./Tooltip";
export function GridLayoutMenu({ layout, setLayout }) {
return (
<PopoverMenuTrigger placement="bottom right">
<TooltipTrigger>
<Button variant="icon">
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</Button>
{(props) => (
<Tooltip position="bottom" {...props}>
Layout Type
</Tooltip>
)}
</TooltipTrigger>
<Button variant="icon">
<ButtonTooltip>Layout Type</ButtonTooltip>
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</Button>
{(props) => (
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
<Item key="freedom" textValue="Freedom">

View File

@@ -14,14 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useState } from "react";
import React, { useCallback } from "react";
import { useHistory, Link } from "react-router-dom";
import {
useClient,
useGroupCallRooms,
usePublicRooms,
createRoom,
roomAliasFromRoomName,
useCreateRoom,
} from "./ConferenceCallManagerHooks";
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
import styles from "./Home.module.css";
@@ -31,26 +30,12 @@ import { Button } from "./button";
import { CallList } from "./CallList";
import classNames from "classnames";
import { ErrorView, LoadingView } from "./FullScreenView";
import { useModalTriggerState } from "./Modal";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { JoinExistingCallModal } from "./JoinExistingCallModal";
export function Home() {
const {
isAuthenticated,
isGuest,
isPasswordlessUser,
loading,
error,
client,
register,
} = useClient();
const { isGuest, isPasswordlessUser, loading, error, client } = useClient();
const history = useHistory();
const [creatingRoom, setCreatingRoom] = useState(false);
const [createRoomError, setCreateRoomError] = useState();
const { modalState, modalProps } = useModalTriggerState();
const [existingRoomId, setExistingRoomId] = useState();
const { createRoomError, creatingRoom, createRoom } = useCreateRoom();
const onCreateRoom = useCallback(
(e) => {
@@ -59,36 +44,13 @@ export function Home() {
const roomName = data.get("roomName");
const userName = data.get("userName");
async function onCreateRoom() {
let _client = client;
if (!_client || isGuest) {
_client = await register(userName, randomString(16), true);
}
const roomIdOrAlias = await createRoom(_client, roomName);
createRoom(roomName, userName).then((roomIdOrAlias) => {
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]
[history]
);
const onJoinRoom = useCallback(
@@ -101,39 +63,30 @@ export function Home() {
[history]
);
const onJoinExistingRoom = useCallback(() => {
history.push(`/${existingRoomId}`);
}, [history, existingRoomId]);
if (loading) {
return <LoadingView />;
} else if (error) {
return <ErrorView error={error} />;
} else if (error || createRoomError) {
return <ErrorView error={error || createRoomError} />;
} else if (isGuest) {
return (
<UnregisteredView
onCreateRoom={onCreateRoom}
createRoomError={createRoomError}
creatingRoom={creatingRoom}
onJoinRoom={onJoinRoom}
/>
);
} else {
return (
<>
{!isAuthenticated || isGuest ? (
<UnregisteredView
onCreateRoom={onCreateRoom}
createRoomError={createRoomError}
creatingRoom={creatingRoom}
onJoinRoom={onJoinRoom}
/>
) : (
<RegisteredView
client={client}
isPasswordlessUser={isPasswordlessUser}
isGuest={isGuest}
onCreateRoom={onCreateRoom}
createRoomError={createRoomError}
creatingRoom={creatingRoom}
onJoinRoom={onJoinRoom}
/>
)}
{modalState.isOpen && (
<JoinExistingCallModal onJoin={onJoinExistingRoom} {...modalProps} />
)}
</>
<RegisteredView
client={client}
isPasswordlessUser={isPasswordlessUser}
isGuest={isGuest}
onCreateRoom={onCreateRoom}
createRoomError={createRoomError}
creatingRoom={creatingRoom}
onJoinRoom={onJoinRoom}
/>
);
}
}
@@ -330,18 +283,10 @@ function RegisteredView({
<div className={styles.right}>
<div className={styles.content}>
{publicRooms.length > 0 && (
<CallList
title="Public Calls"
rooms={publicRooms}
client={client}
/>
<CallList title="Public Calls" rooms={publicRooms} />
)}
{recentRooms.length > 0 && (
<CallList
title="Recent Calls"
rooms={recentRooms}
client={client}
/>
<CallList title="Recent Calls" rooms={recentRooms} />
)}
</div>
</div>

View File

@@ -42,6 +42,7 @@
.left .content {
display: flex;
flex-direction: column;
padding-top: 40px;
align-items: center;
}
@@ -66,7 +67,7 @@
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 92px;
padding: 92px;
}
.fieldRow {
@@ -89,7 +90,7 @@
}
.right .content {
padding: 0 40px 40px 40px;
padding: 40px;
}
.right .content h3:first-child {

View File

@@ -22,30 +22,15 @@ export function Field({ children, className, ...rest }) {
}
export const InputField = forwardRef(
(
{ id, label, className, type, checked, prefix, suffix, disabled, ...rest },
ref
) => {
({ id, label, className, type, checked, ...rest }, ref) => {
return (
<Field
className={classNames(
type === "checkbox" ? styles.checkboxField : styles.inputField,
{
[styles.prefix]: !!prefix,
[styles.disabled]: disabled,
},
className
)}
>
{prefix && <span>{prefix}</span>}
<input
id={id}
{...rest}
ref={ref}
type={type}
checked={checked}
disabled={disabled}
/>
<input id={id} {...rest} ref={ref} type={type} checked={checked} />
<label htmlFor={id}>
{type === "checkbox" && (
<div className={styles.checkbox}>
@@ -54,7 +39,6 @@ export const InputField = forwardRef(
)}
{label}
</label>
{suffix && <span>{suffix}</span>}
</Field>
);
}

View File

@@ -33,26 +33,13 @@
font-size: 15px;
border: none;
border-radius: 4px;
padding: 12px 9px 10px 9px;
padding: 11px 9px;
color: var(--textColor1);
background-color: var(--bgColor1);
flex: 1;
min-width: 0;
}
.inputField.disabled input,
.inputField.disabled span {
color: var(--textColor2);
}
.inputField span {
padding: 11px 9px;
}
.inputField span:first-child {
padding-right: 0;
}
.inputField input::placeholder {
transition: color 0.25s ease-in 0s;
color: transparent;
@@ -90,8 +77,7 @@
}
.inputField input:focus + label,
.inputField input:not(:placeholder-shown) + label,
.inputField.prefix input + label {
.inputField input:not(:placeholder-shown) + label {
background-color: var(--bgColor2);
transition: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s,
top 0.25s ease-out 0s, background-color 0.25s ease-out 0s;

View File

@@ -1,19 +0,0 @@
import React from "react";
import { Modal, ModalContent } from "./Modal";
import { Button } from "./button";
import { FieldRow } from "./Input";
import styles from "./JoinExistingCallModal.module.css";
export function JoinExistingCallModal({ onJoin, ...rest }) {
return (
<Modal title="Join existing call?" isDismissable {...rest}>
<ModalContent>
<p>This call already exists, would you like to join?</p>
<FieldRow rightAlign className={styles.buttons}>
<Button onPress={rest.onClose}>No</Button>
<Button onPress={onJoin}>Yes, join call</Button>
</FieldRow>
</ModalContent>
</Modal>
);
}

View File

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

View File

@@ -14,21 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useRef, useState, useMemo } from "react";
import React, { useCallback, useRef, useState } from "react";
import { useHistory, useLocation, Link } from "react-router-dom";
import { ReactComponent as Logo } from "./icons/LogoLarge.svg";
import { FieldRow, InputField, ErrorMessage } from "./Input";
import { Button } from "./button";
import {
useClient,
defaultHomeserver,
defaultHomeserverHost,
} from "./ConferenceCallManagerHooks";
import { useClient } from "./ConferenceCallManagerHooks";
import styles from "./LoginPage.module.css";
export function LoginPage() {
const { login } = useClient();
const [homeserver, setHomeServer] = useState(defaultHomeserver);
const [homeserver, setHomeServer] = useState(
`${window.location.protocol}//${window.location.host}`
);
const usernameRef = useRef();
const passwordRef = useRef();
const history = useHistory();
@@ -59,14 +57,6 @@ export function LoginPage() {
[login, location, history, homeserver]
);
const homeserverHost = useMemo(() => {
try {
return new URL(homeserver).host;
} catch (_error) {
return defaultHomeserverHost;
}
}, [homeserver]);
return (
<>
<div className={styles.container}>
@@ -77,6 +67,17 @@ export function LoginPage() {
<h2>Log In</h2>
<h4>To continue to Element</h4>
<form onSubmit={onSubmitLoginForm}>
<FieldRow>
<InputField
type="text"
value={homeserver}
onChange={(e) => setHomeServer(e.target.value)}
placeholder="Homeserver"
label="Homeserver"
autoCorrect="off"
autoCapitalize="none"
/>
</FieldRow>
<FieldRow>
<InputField
type="text"
@@ -85,8 +86,6 @@ export function LoginPage() {
label="Username"
autoCorrect="off"
autoCapitalize="none"
prefix="@"
suffix={`:${homeserverHost}`}
/>
</FieldRow>
<FieldRow>

View File

@@ -30,12 +30,10 @@
.menuItem.focused:first-child,
.menuItem:hover:first-child {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-radius: 8px 8px 0 0;
}
.menuItem.focused:last-child,
.menuItem:hover:last-child {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
border-radius: 0 0 8px 8px;
}

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from "react";
import { Button } from "./button";
import { ButtonTooltip, Button } from "./button";
import { Menu } from "./Menu";
import { PopoverMenuTrigger } from "./PopoverMenu";
import { Item } from "@react-stately/collections";
@@ -9,7 +9,6 @@ import { ReactComponent as OverflowIcon } from "./icons/Overflow.svg";
import { useModalTriggerState } from "./Modal";
import { SettingsModal } from "./SettingsModal";
import { InviteModal } from "./InviteModal";
import { Tooltip, TooltipTrigger } from "./Tooltip";
export function OverflowMenu({
roomId,
@@ -38,16 +37,10 @@ export function OverflowMenu({
return (
<>
<PopoverMenuTrigger disableOnState>
<TooltipTrigger>
<Button variant="toolbar">
<OverflowIcon />
</Button>
{(props) => (
<Tooltip position="top" {...props}>
More
</Tooltip>
)}
</TooltipTrigger>
<Button variant="toolbar">
<ButtonTooltip>More</ButtonTooltip>
<OverflowIcon />
</Button>
{(props) => (
<Menu {...props} label="More menu" onAction={onAction}>
<Item key="invite" textValue="Invite people">

View File

@@ -1,24 +1,18 @@
import React, { useCallback, useEffect, useState } from "react";
import { Button } from "./button";
import { useProfile } from "./ConferenceCallManagerHooks";
import { useDisplayName } from "./ConferenceCallManagerHooks";
import { FieldRow, InputField, ErrorMessage } from "./Input";
import { Modal, ModalContent } from "./Modal";
export function ProfileModal({
client,
isAuthenticated,
isPasswordlessUser,
isGuest,
...rest
}) {
export function ProfileModal({ client, ...rest }) {
const { onClose } = rest;
const {
success,
error,
loading,
displayName: initialDisplayName,
saveProfile,
} = useProfile(client);
setDisplayName: submitDisplayName,
} = useDisplayName(client);
const [displayName, setDisplayName] = useState(initialDisplayName || "");
const onChangeDisplayName = useCallback(
@@ -33,14 +27,10 @@ export function ProfileModal({
e.preventDefault();
const data = new FormData(e.target);
const displayName = data.get("displayName");
const avatar = data.get("avatar");
saveProfile({
displayName,
avatar,
});
console.log(displayName);
submitDisplayName(displayName);
},
[saveProfile]
[setDisplayName]
);
useEffect(() => {
@@ -66,16 +56,6 @@ export function ProfileModal({
onChange={onChangeDisplayName}
/>
</FieldRow>
{isAuthenticated && !isGuest && !isPasswordlessUser && (
<FieldRow>
<InputField
type="file"
id="avatar"
name="avatar"
label="Avatar"
/>
</FieldRow>
)}
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>

View File

@@ -14,99 +14,47 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useCallback, useRef, useState } from "react";
import { useHistory, useLocation, Link } from "react-router-dom";
import { FieldRow, InputField, ErrorMessage } from "./Input";
import { Button } from "./button";
import { useClient, defaultHomeserverHost } from "./ConferenceCallManagerHooks";
import { useClient } from "./ConferenceCallManagerHooks";
import styles from "./LoginPage.module.css";
import { ReactComponent as Logo } from "./icons/LogoLarge.svg";
import { LoadingView } from "./FullScreenView";
export function RegisterPage() {
const {
loading,
client,
register,
changePassword,
isAuthenticated,
isPasswordlessUser,
} = useClient();
const confirmPasswordRef = useRef();
// TODO: Handle hitting login page with authenticated client
const { register } = useClient();
const registerUsernameRef = useRef();
const registerPasswordRef = useRef();
const history = useHistory();
const location = useLocation();
const [registering, setRegistering] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const [password, setPassword] = useState("");
const [passwordConfirmation, setPasswordConfirmation] = useState("");
const onSubmitRegisterForm = useCallback(
(e) => {
e.preventDefault();
const data = new FormData(e.target);
const userName = data.get("userName");
const password = data.get("password");
const passwordConfirmation = data.get("passwordConfirmation");
if (password !== passwordConfirmation) {
return;
}
setRegistering(true);
if (isPasswordlessUser) {
changePassword(password)
.then(() => {
if (location.state && location.state.from) {
history.push(location.state.from);
} else {
history.push("/");
}
})
.catch((error) => {
setError(error);
setRegistering(false);
});
} else {
register(userName, password)
.then(() => {
if (location.state && location.state.from) {
history.push(location.state.from);
} else {
history.push("/");
}
})
.catch((error) => {
setError(error);
setRegistering(false);
});
}
setLoading(true);
register(
registerUsernameRef.current.value,
registerPasswordRef.current.value
)
.then(() => {
if (location.state && location.state.from) {
history.push(location.state.from);
} else {
history.push("/");
}
})
.catch((error) => {
setError(error);
setLoading(false);
});
},
[register, changePassword, location, history, isPasswordlessUser]
[register, location, history]
);
useEffect(() => {
if (!confirmPasswordRef.current) {
return;
}
if (password && passwordConfirmation && password !== passwordConfirmation) {
confirmPasswordRef.current.setCustomValidity("Passwords must match");
} else {
confirmPasswordRef.current.setCustomValidity("");
}
}, [password, passwordConfirmation]);
useEffect(() => {
if (!loading && isAuthenticated && !isPasswordlessUser) {
history.push("/");
}
}, [history, isAuthenticated, isPasswordlessUser]);
if (loading) {
return <LoadingView />;
}
return (
<>
<div className={styles.container}>
@@ -118,52 +66,29 @@ export function RegisterPage() {
<FieldRow>
<InputField
type="text"
name="userName"
ref={registerUsernameRef}
placeholder="Username"
label="Username"
autoCorrect="off"
autoCapitalize="none"
prefix="@"
suffix={`:${defaultHomeserverHost}`}
value={
isAuthenticated && isPasswordlessUser
? client.getUserIdLocalpart()
: undefined
}
disabled={isAuthenticated && isPasswordlessUser}
/>
</FieldRow>
<FieldRow>
<InputField
required
name="password"
type="password"
onChange={(e) => setPassword(e.target.value)}
value={password}
ref={registerPasswordRef}
placeholder="Password"
label="Password"
/>
</FieldRow>
<FieldRow>
<InputField
required
type="password"
name="passwordConfirmation"
onChange={(e) => setPasswordConfirmation(e.target.value)}
value={passwordConfirmation}
placeholder="Confirm Password"
label="Confirm Password"
ref={confirmPasswordRef}
/>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
</FieldRow>
)}
<FieldRow>
<Button type="submit" disabled={registering}>
{registering ? "Registering..." : "Register"}
<Button type="submit" disabled={loading}>
{loading ? "Registering..." : "Register"}
</Button>
</FieldRow>
</form>

View File

@@ -26,7 +26,13 @@ import {
ScreenshareButton,
LinkButton,
} from "./button";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "./Header";
import {
Header,
LeftNav,
RightNav,
RoomHeaderInfo,
RoomSetupHeaderInfo,
} from "./Header";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import VideoGrid, {
useVideoGridLayout,
@@ -36,13 +42,7 @@ import "matrix-react-sdk/res/css/views/voip/GroupCallView/_VideoGrid.scss";
import { useGroupCall } from "matrix-react-sdk/src/hooks/useGroupCall";
import { useCallFeed } from "matrix-react-sdk/src/hooks/useCallFeed";
import { useMediaStream } from "matrix-react-sdk/src/hooks/useMediaStream";
import {
getAvatarUrl,
getRoomUrl,
useClient,
useLoadGroupCall,
useProfile,
} from "./ConferenceCallManagerHooks";
import { useClient, useLoadGroupCall } from "./ConferenceCallManagerHooks";
import { ErrorView, LoadingView, FullScreenView } from "./FullScreenView";
import { GroupCallInspector } from "./GroupCallInspector";
import * as Sentry from "@sentry/react";
@@ -50,7 +50,6 @@ import { OverflowMenu } from "./OverflowMenu";
import { GridLayoutMenu } from "./GridLayoutMenu";
import { UserMenu } from "./UserMenu";
import classNames from "classnames";
import { Avatar } from "./Avatar";
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -59,51 +58,20 @@ 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, error, client, isGuest } = useClient();
useEffect(() => {
if (!loading && !isAuthenticated) {
setRegisteringGuest(true);
registerGuest()
.then(() => {
setRegisteringGuest(false);
})
.catch((error) => {
setRegistrationError(error);
setRegisteringGuest(false);
});
}
}, [loading, isAuthenticated]);
if (loading || registeringGuest) {
if (loading) {
return <LoadingView />;
}
if (registrationError || error) {
return <ErrorView error={registrationError || error} />;
if (error) {
return <ErrorView error={error} />;
}
return (
<GroupCall
client={client}
isGuest={isGuest}
isPasswordlessUser={isPasswordlessUser}
/>
);
return <GroupCall client={client} isGuest={isGuest} />;
}
export function GroupCall({ client, isGuest, isPasswordlessUser }) {
export function GroupCall({ client, isGuest }) {
const { roomId: maybeRoomId } = useParams();
const { hash, search } = useLocation();
const [simpleGrid, viaServers] = useMemo(() => {
@@ -132,7 +100,6 @@ export function GroupCall({ client, isGuest, isPasswordlessUser }) {
return (
<GroupCallView
isGuest={isGuest}
isPasswordlessUser={isPasswordlessUser}
client={client}
roomId={roomId}
groupCall={groupCall}
@@ -144,7 +111,6 @@ export function GroupCall({ client, isGuest, isPasswordlessUser }) {
export function GroupCallView({
client,
isGuest,
isPasswordlessUser,
roomId,
groupCall,
simpleGrid,
@@ -200,7 +166,7 @@ export function GroupCallView({
const onLeave = useCallback(() => {
leave();
if (!isGuest && !isPasswordlessUser) {
if (!isGuest) {
history.push("/");
} else {
setLeft(true);
@@ -214,7 +180,6 @@ export function GroupCallView({
<InRoomView
groupCall={groupCall}
client={client}
isGuest={isGuest}
roomName={groupCall.room.name}
microphoneMuted={microphoneMuted}
localVideoMuted={localVideoMuted}
@@ -236,11 +201,7 @@ export function GroupCallView({
} else if (state === GroupCallState.Entering) {
return <EnteringRoomView />;
} else if (left) {
if (isPasswordlessUser) {
return <PasswordlessUserCallEndedScreen client={client} />;
} else {
return <GuestCallEndedScreen />;
}
return <CallEndedScreen />;
} else {
return (
<RoomSetupView
@@ -282,6 +243,7 @@ export function EnteringRoomView() {
function RoomSetupView({
client,
isGuest,
roomName,
state,
onInitLocalCallFeed,
@@ -297,6 +259,7 @@ function RoomSetupView({
}) {
const { stream } = useCallFeed(localCallFeed);
const videoRef = useMediaStream(stream, true);
const location = useLocation();
useEffect(() => {
onInitLocalCallFeed();
@@ -309,13 +272,19 @@ function RoomSetupView({
<RoomHeaderInfo roomName={roomName} />
</LeftNav>
<RightNav>
<UserMenu />
{isGuest ? (
<LinkButton to={{ pathname: "/login", state: { from: location } }}>
Log in
</LinkButton>
) : (
<UserMenu />
)}
</RightNav>
</Header>
<div className={styles.joinRoom}>
<div className={styles.joinRoomContent}>
<h1>{roomName}</h1>
<div className={styles.preview}>
<video ref={videoRef} muted playsInline disablePictureInPicture />
{state === GroupCallState.LocalCallFeedUninitialized && (
<p className={styles.webcamPermissions}>
Webcam/microphone permissions needed to join the call.
@@ -326,6 +295,7 @@ function RoomSetupView({
Accept webcam/microphone permissions to join the call.
</p>
)}
<video ref={videoRef} muted playsInline disablePictureInPicture />
{state === GroupCallState.LocalCallFeedInitialized && (
<>
<Button
@@ -356,7 +326,7 @@ function RoomSetupView({
</div>
<p>Or</p>
<CopyButton
value={getRoomUrl(roomId)}
value={window.location.href}
className={styles.copyButton}
copiedMessage="Call link copied"
>
@@ -375,7 +345,6 @@ function RoomSetupView({
function InRoomView({
client,
isGuest,
groupCall,
roomName,
microphoneMuted,
@@ -413,10 +382,7 @@ function InRoomView({
const participant = participants.find(
(p) => p.usermediaCallFeed.userId === callFeed.userId
);
if (participant) {
participant.screenshareCallFeed = callFeed;
}
participant.screenshareCallFeed = callFeed;
}
return participants;
@@ -447,29 +413,6 @@ function InRoomView({
[layout, setLayout]
);
const renderAvatar = useCallback(
(roomMember, width, height) => {
const avatarUrl = roomMember.user?.avatarUrl;
const size = Math.round(Math.min(width, height) / 2);
return (
<Avatar
key={roomMember.userId}
style={{
width: size,
height: size,
borderRadius: size,
fontSize: Math.round(size / 2),
}}
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)}
fallback={roomMember.name.slice(0, 1).toUpperCase()}
className={styles.avatar}
/>
);
},
[client]
);
return (
<div className={classNames(styles.room, styles.inRoom)}>
<Header>
@@ -478,7 +421,7 @@ function InRoomView({
</LeftNav>
<RightNav>
<GridLayoutMenu layout={layout} setLayout={setLayout} />
{!isGuest && <UserMenu disableLogout />}
<UserMenu />
</RightNav>
</Header>
{items.length === 0 ? (
@@ -491,7 +434,6 @@ function InRoomView({
<VideoGrid
items={items}
layout={layout}
getAvatar={renderAvatar}
onFocusTile={onFocusTile}
disableAnimations={isSafari}
/>
@@ -522,52 +464,31 @@ function InRoomView({
);
}
export function GuestCallEndedScreen() {
export function CallEndedScreen() {
return (
<FullScreenView className={styles.callEndedScreen}>
<h1>Your call is now ended</h1>
<div className={styles.callEndedContent}>
<p>Why not finish by creating an account?</p>
<p>You'll be able to:</p>
<ul>
<li>Easily access all your previous call links</li>
<li>Set a username and avatar</li>
</ul>
<LinkButton
className={styles.callEndedButton}
size="lg"
variant="default"
to="/register"
>
Create account
</LinkButton>
</div>
<Link to="/">Not now, return to home screen</Link>
</FullScreenView>
);
}
export function PasswordlessUserCallEndedScreen({ client }) {
const { displayName } = useProfile(client);
return (
<FullScreenView className={styles.callEndedScreen}>
<h1>{displayName}, your call is now ended</h1>
<div className={styles.callEndedContent}>
<p>Why not finish by setting up a password to keep your account?</p>
<p>
You'll be able to keep your name and set an avatar for use on future
calls
</p>
<LinkButton
className={styles.callEndedButton}
size="lg"
variant="default"
to="/register"
>
Create account
</LinkButton>
</div>
<hr />
<h2>Why not finish by creating an account?</h2>
<p>You'll be able to:</p>
<ul>
<li>Easily access all your previous call links</li>
<li>Set a username and avatar</li>
<li>
Get your own Matrix ID so you can log in to{" "}
<a href="https://element.io" target="_blank">
Element
</a>
</li>
</ul>
<LinkButton
className={styles.callEndedButton}
size="lg"
variant="default"
to="/login"
>
Create account
</LinkButton>
<Link to="/">Not now, return to home screen</Link>
</FullScreenView>
);

View File

@@ -63,8 +63,8 @@ limitations under the License.
.preview {
position: relative;
min-height: 280px;
height: 50vh;
max-width: 816px;
max-height: 75vh;
border-radius: 24px;
overflow: hidden;
background-color: var(--bgColor3);
@@ -165,7 +165,6 @@ limitations under the License.
}
.callEndedScreen h1 {
text-align: center;
margin-bottom: 60px;
}
@@ -182,26 +181,13 @@ limitations under the License.
.callEndedScreen ul {
padding: 0;
margin-bottom: 40px;
text-align: initial;
padding-left: 20px;
}
.callEndedButton {
width: 100%;
}
.callEndedContent {
text-align: center;
max-width: 360px;
}
.avatar {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
@media (min-width: 800px) {
.roomContainer {
flex-direction: row;

View File

@@ -1,56 +0,0 @@
import React, { forwardRef, useRef } from "react";
import { useTooltipTriggerState } from "@react-stately/tooltip";
import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip";
import { mergeProps } from "@react-aria/utils";
import styles from "./Tooltip.module.css";
import classNames from "classnames";
export function Tooltip({ position, state, ...props }) {
let { tooltipProps } = useTooltip(props, state);
return (
<div
className={classNames(styles.tooltip, styles[position || "bottom"])}
{...mergeProps(props, tooltipProps)}
>
{props.children}
</div>
);
}
export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => {
const tooltipState = useTooltipTriggerState(rest);
const fallbackRef = useRef();
const triggerRef = ref || fallbackRef;
const { triggerProps, tooltipProps } = useTooltipTrigger(
rest,
tooltipState,
triggerRef
);
if (
!Array.isArray(children) ||
children.length > 2 ||
typeof children[1] !== "function"
) {
throw new Error(
"TooltipTrigger must have two props. The first being a button and the second being a render prop."
);
}
const [tooltipTrigger, tooltip] = children;
return (
<div className={styles.tooltipContainer}>
<tooltipTrigger.type
{...mergeProps(triggerProps, tooltipTrigger.props, rest)}
ref={triggerRef}
/>
{tooltipState.isOpen && tooltip({ state: tooltipState, ...tooltipProps })}
</div>
);
});
TooltipTrigger.defaultProps = {
delay: 250,
};

View File

@@ -1,33 +0,0 @@
.tooltip {
background-color: var(--bgColor2);
position: absolute;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 8px 10px;
color: var(--textColor1);
border-radius: 8px;
max-width: 135px;
width: max-content;
z-index: 1;
left: 50%;
transform: translateX(-50%);
text-align: center;
}
.tooltip.top {
bottom: calc(100% + 6px);
}
.tooltip.bottom {
top: calc(100% + 6px);
}
.tooltip.bottomLeft {
top: calc(100% + 6px);
left: -25%;
}
.tooltipContainer {
position: relative;
}

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useMemo } from "react";
import { Button, LinkButton } from "./button";
import { ButtonTooltip, Button } from "./button";
import { PopoverMenuTrigger } from "./PopoverMenu";
import { ReactComponent as UserIcon } from "./icons/User.svg";
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
@@ -8,24 +8,15 @@ import styles from "./UserMenu.module.css";
import { Item } from "@react-stately/collections";
import { Menu } from "./Menu";
import { useHistory, useLocation } from "react-router-dom";
import { useClient, useProfile } from "./ConferenceCallManagerHooks";
import { useClient, useDisplayName } from "./ConferenceCallManagerHooks";
import { useModalTriggerState } from "./Modal";
import { ProfileModal } from "./ProfileModal";
import { Tooltip, TooltipTrigger } from "./Tooltip";
import { Avatar } from "./Avatar";
export function UserMenu({ disableLogout }) {
export function UserMenu() {
const location = useLocation();
const history = useHistory();
const {
isAuthenticated,
isGuest,
isPasswordlessUser,
logout,
userName,
client,
} = useClient();
const { displayName, avatarUrl } = useProfile(client);
const { isGuest, isPasswordlessUser, logout, userName, client } = useClient();
const { displayName } = useDisplayName(client);
const { modalState, modalProps } = useModalTriggerState();
const onAction = useCallback(
@@ -40,70 +31,57 @@ export function UserMenu({ disableLogout }) {
case "login":
history.push("/login", { state: { from: location } });
break;
case "register":
history.push("/register", { state: { from: location } });
break;
}
},
[history, location, logout, modalState]
);
const items = useMemo(() => {
const arr = [];
if (isAuthenticated && !isGuest) {
arr.push({
const arr = [
{
key: "user",
icon: UserIcon,
label: displayName || userName,
},
];
if (isGuest || isPasswordlessUser) {
arr.push({
key: "login",
label: "Sign In",
icon: LoginIcon,
});
}
if (isPasswordlessUser) {
arr.push({
key: "login",
label: "Sign In",
icon: LoginIcon,
});
}
if (isGuest) {
arr.push({
key: "register",
label: "Register",
icon: LoginIcon,
});
}
if (!isPasswordlessUser && !disableLogout) {
arr.push({
key: "logout",
label: "Sign Out",
icon: LogoutIcon,
});
}
if (!isGuest) {
arr.push({
key: "logout",
label: "Sign Out",
icon: LogoutIcon,
});
}
return arr;
}, [isAuthenticated, isGuest, userName, displayName]);
if (isGuest || !isAuthenticated) {
return (
<LinkButton to={{ pathname: "/login", state: { from: location } }}>
Log in
</LinkButton>
);
}
}, [isGuest, isPasswordlessUser, userName, displayName]);
return (
<>
<PopoverMenuTrigger placement="bottom right">
<TooltipTrigger>
<Button variant="icon" className={styles.userButton}>
{isAuthenticated && !isGuest && !isPasswordlessUser ? (
<Avatar
size="sm"
src={avatarUrl}
fallback={(displayName || userName).slice(0, 1).toUpperCase()}
/>
) : (
<UserIcon />
)}
</Button>
{(props) => (
<Tooltip position="bottomLeft" {...props}>
Profile
</Tooltip>
)}
</TooltipTrigger>
<Button variant="icon" className={styles.userButton}>
<ButtonTooltip>Profile</ButtonTooltip>
<UserIcon />
</Button>
{(props) => (
<Menu {...props} label="User menu" onAction={onAction}>
{items.map(({ key, icon: Icon, label }) => (
@@ -115,15 +93,7 @@ export function UserMenu({ disableLogout }) {
</Menu>
)}
</PopoverMenuTrigger>
{modalState.isOpen && (
<ProfileModal
client={client}
isAuthenticated={isAuthenticated}
isGuest={isGuest}
isPasswordlessUser={isPasswordlessUser}
{...modalProps}
/>
)}
{modalState.isOpen && <ProfileModal client={client} {...modalProps} />}
</>
);
}

View File

@@ -8,8 +8,7 @@ import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg";
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
import { useButton } from "@react-aria/button";
import { mergeProps, useObjectRef } from "@react-aria/utils";
import { Tooltip, TooltipTrigger } from "../Tooltip";
import { useObjectRef } from "@react-aria/utils";
export const variantToClassName = {
default: [styles.button],
@@ -81,64 +80,46 @@ export function ButtonTooltip({ className, children }) {
export function MicButton({ muted, ...rest }) {
return (
<TooltipTrigger>
<Button variant="toolbar" {...rest} off={muted}>
{muted ? <MuteMicIcon /> : <MicIcon />}
</Button>
{(props) => (
<Tooltip position="top" {...props}>
{muted ? "Unmute microphone" : "Mute microphone"}
</Tooltip>
)}
</TooltipTrigger>
<Button variant="toolbar" {...rest} off={muted}>
<ButtonTooltip>
{muted ? "Unmute microphone" : "Mute microphone"}
</ButtonTooltip>
{muted ? <MuteMicIcon /> : <MicIcon />}
</Button>
);
}
export function VideoButton({ muted, ...rest }) {
return (
<TooltipTrigger>
<Button variant="toolbar" {...rest} off={muted}>
{muted ? <DisableVideoIcon /> : <VideoIcon />}
</Button>
{(props) => (
<Tooltip position="top" {...props}>
{muted ? "Turn on camera" : "Turn off camera"}
</Tooltip>
)}
</TooltipTrigger>
<Button variant="toolbar" {...rest} off={muted}>
<ButtonTooltip>
{muted ? "Turn on camera" : "Turn off camera"}
</ButtonTooltip>
{muted ? <DisableVideoIcon /> : <VideoIcon />}
</Button>
);
}
export function ScreenshareButton({ enabled, className, ...rest }) {
return (
<TooltipTrigger>
<Button variant="toolbar" {...rest} on={enabled}>
<ScreenshareIcon />
</Button>
{(props) => (
<Tooltip position="top" {...props}>
{enabled ? "Stop sharing screen" : "Share screen"}
</Tooltip>
)}
</TooltipTrigger>
<Button variant="toolbar" {...rest} on={enabled}>
<ButtonTooltip>
{enabled ? "Stop sharing screen" : "Share screen"}
</ButtonTooltip>
<ScreenshareIcon />
</Button>
);
}
export function HangupButton({ className, ...rest }) {
return (
<TooltipTrigger>
<Button
variant="toolbar"
className={classNames(styles.hangupButton, className)}
{...rest}
>
<HangupIcon />
</Button>
{(props) => (
<Tooltip position="top" {...props}>
Leave
</Tooltip>
)}
</TooltipTrigger>
<Button
variant="toolbar"
className={classNames(styles.hangupButton, className)}
{...rest}
>
<ButtonTooltip>Leave</ButtonTooltip>
<HangupIcon />
</Button>
);
}

View File

@@ -7,7 +7,7 @@ import { Button } from "./Button";
export function CopyButton({
value,
children,
className,
onClassName,
variant,
copiedMessage,
...rest
@@ -19,7 +19,7 @@ export function CopyButton({
{...rest}
variant={variant === "icon" ? "iconCopy" : "copy"}
on={isCopied}
className={className}
onClassName={onClassName}
onPress={setCopied}
iconStyle={isCopied ? "stroke" : "fill"}
>

42
src/randomstring.js Normal file
View File

@@ -0,0 +1,42 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The 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.
*/
const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const DIGITS = "0123456789";
export function randomString(len) {
return randomStringFrom(len, UPPERCASE + LOWERCASE + DIGITS);
}
export function randomLowercaseString(len) {
return randomStringFrom(len, LOWERCASE);
}
export function randomUppercaseString(len) {
return randomStringFrom(len, UPPERCASE);
}
function randomStringFrom(len, chars) {
let ret = "";
for (let i = 0; i < len; ++i) {
ret += chars.charAt(Math.floor(Math.random() * chars.length));
}
return ret;
}

View File

@@ -14,26 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { defineConfig, loadEnv } from "vite";
import { defineConfig } from "vite";
import svgrPlugin from "vite-plugin-svgr";
import path from "path";
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd());
return {
plugins: [svgrPlugin()],
server: {
proxy: {
"/_matrix": env.VITE_DEFAULT_HOMESERVER || "http://localhost:8008",
},
export default defineConfig({
plugins: [svgrPlugin()],
server: {
proxy: {
"/_matrix": "http://localhost:8008",
},
resolve: {
alias: {
"$(res)": path.resolve(__dirname, "node_modules/matrix-react-sdk/res"),
},
dedupe: ["react", "react-dom", "matrix-js-sdk"],
},
resolve: {
alias: {
"$(res)": path.resolve(__dirname, "node_modules/matrix-react-sdk/res"),
},
};
dedupe: ["react", "react-dom", "matrix-js-sdk"],
},
});

View File

@@ -451,19 +451,6 @@
"@react-types/shared" "^3.10.0"
"@react-types/tabs" "^3.0.1"
"@react-aria/tooltip@^3.1.3":
version "3.1.3"
resolved "https://registry.yarnpkg.com/@react-aria/tooltip/-/tooltip-3.1.3.tgz#cf967d9306170ed2ec0ed589fe1cb4cc081ddbe6"
integrity sha512-l2/BS1XBKrLpg+dovI3xy6NdCgJ5n82TS4p8vQJa7GcynI1I64R0IjOUFv0lc6ZZsr1G8Wg71SNYfmlgTrPr2w==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/focus" "^3.4.1"
"@react-aria/interactions" "^3.5.1"
"@react-aria/utils" "^3.8.2"
"@react-stately/tooltip" "^3.0.5"
"@react-types/shared" "^3.8.0"
"@react-types/tooltip" "^3.1.2"
"@react-aria/utils@^3.10.0", "@react-aria/utils@^3.8.2", "@react-aria/utils@^3.9.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.10.0.tgz#2f6f0b0ccede17241fca1cbd76978e1bf8f5a2b0"
@@ -613,16 +600,6 @@
"@react-types/checkbox" "^3.2.3"
"@react-types/shared" "^3.8.0"
"@react-stately/tooltip@^3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@react-stately/tooltip/-/tooltip-3.0.5.tgz#0cb716791ef3242acd810794162d651b1c47a328"
integrity sha512-rHqPSfkxbx0T0B/j+WDl4G2CfLjFeBfyaifGiIUJWHO/0Kwvh5am88VeHtuTVzC2DPEGTdtXqYns21EuJOrDlQ==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-stately/overlays" "^3.1.3"
"@react-stately/utils" "^3.2.2"
"@react-types/tooltip" "^3.1.2"
"@react-stately/tree@^3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@react-stately/tree/-/tree-3.2.0.tgz#151c90f161c5c8339b6876f59a4f0502be08670b"
@@ -711,14 +688,6 @@
dependencies:
"@react-types/shared" "^3.8.0"
"@react-types/tooltip@^3.1.2":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@react-types/tooltip/-/tooltip-3.1.2.tgz#a80d1ab5a37337156881a032cdaa136a9e028e32"
integrity sha512-puyiRi3IaEeKH25AErZzQKthnxk1McU+7S+Qo2kFLy3F3PyXV0cmSqvKKOhH6kU5Cw4ZnuAlNjCI0tV8PYdlYA==
dependencies:
"@react-types/overlays" "^3.5.1"
"@react-types/shared" "^3.8.0"
"@sentry/browser@6.13.3":
version "6.13.3"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.13.3.tgz#d4511791b1e484ad48785eba3bce291fdf115c1e"