Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81a763f17f | ||
|
|
1ab7d27ba9 | ||
|
|
e76a805c8f | ||
|
|
9fc4af2bd7 | ||
|
|
0f3a7f9fd9 | ||
|
|
1cc634509b | ||
|
|
cb07ce32cb | ||
|
|
6866d662f7 | ||
|
|
51a2027d64 | ||
|
|
0f6b8f9bb1 | ||
|
|
63229ce2d7 | ||
|
|
1d620910c5 | ||
|
|
47357b3fc6 | ||
|
|
3ed35f9477 | ||
|
|
a369444b62 | ||
|
|
742d658021 | ||
|
|
681c24a0ca | ||
|
|
fc057bf988 |
@@ -41,7 +41,8 @@
|
|||||||
"react-router": "6",
|
"react-router": "6",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-use-clipboard": "^1.0.7",
|
"react-use-clipboard": "^1.0.7",
|
||||||
"react-use-measure": "^2.1.1"
|
"react-use-measure": "^2.1.1",
|
||||||
|
"unique-names-generator": "^4.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.5",
|
"@babel/core": "^7.16.5",
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ yarn link
|
|||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
cd matrix-video-chat
|
cd matrix-video-chat
|
||||||
|
|
||||||
|
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
|
||||||
|
|
||||||
yarn link matrix-js-sdk
|
yarn link matrix-js-sdk
|
||||||
yarn link matrix-react-sdk
|
yarn link matrix-react-sdk
|
||||||
yarn install
|
yarn install
|
||||||
|
|||||||
@@ -56,4 +56,5 @@
|
|||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
border-radius: 90px;
|
border-radius: 90px;
|
||||||
|
font-size: 48px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import React, {
|
|||||||
useContext,
|
useContext,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { ErrorView } from "./FullScreenView";
|
||||||
import { initClient, defaultHomeserver } from "./matrix-utils";
|
import { initClient, defaultHomeserver } from "./matrix-utils";
|
||||||
|
|
||||||
const ClientContext = createContext();
|
const ClientContext = createContext();
|
||||||
@@ -30,7 +31,7 @@ const ClientContext = createContext();
|
|||||||
export function ClientProvider({ children }) {
|
export function ClientProvider({ children }) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [
|
const [
|
||||||
{ loading, isAuthenticated, isPasswordlessUser, client, userName },
|
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
|
||||||
setState,
|
setState,
|
||||||
] = useState({
|
] = useState({
|
||||||
loading: true,
|
loading: true,
|
||||||
@@ -38,6 +39,7 @@ export function ClientProvider({ children }) {
|
|||||||
isPasswordlessUser: false,
|
isPasswordlessUser: false,
|
||||||
client: undefined,
|
client: undefined,
|
||||||
userName: null,
|
userName: null,
|
||||||
|
error: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -143,35 +145,85 @@ export function ClientProvider({ children }) {
|
|||||||
[client]
|
[client]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setClient = useCallback((client, session) => {
|
const setClient = useCallback(
|
||||||
if (client) {
|
(newClient, session) => {
|
||||||
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
|
if (client && client !== newClient) {
|
||||||
|
client.stopClient();
|
||||||
|
}
|
||||||
|
|
||||||
setState({
|
if (newClient) {
|
||||||
client,
|
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
|
||||||
loading: false,
|
|
||||||
isAuthenticated: true,
|
|
||||||
isPasswordlessUser: !!session.passwordlessUser,
|
|
||||||
userName: client.getUserIdLocalpart(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem("matrix-auth-store");
|
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
client: undefined,
|
client: newClient,
|
||||||
loading: false,
|
loading: false,
|
||||||
isAuthenticated: false,
|
isAuthenticated: true,
|
||||||
isPasswordlessUser: false,
|
isPasswordlessUser: !!session.passwordlessUser,
|
||||||
userName: null,
|
userName: newClient.getUserIdLocalpart(),
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
}, []);
|
localStorage.removeItem("matrix-auth-store");
|
||||||
|
|
||||||
|
setState({
|
||||||
|
client: undefined,
|
||||||
|
loading: false,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isPasswordlessUser: false,
|
||||||
|
userName: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client]
|
||||||
|
);
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
const logout = useCallback(() => {
|
||||||
localStorage.removeItem("matrix-auth-store");
|
localStorage.removeItem("matrix-auth-store");
|
||||||
window.location = "/";
|
window.location = "/";
|
||||||
}, [history]);
|
}, [history]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (client) {
|
||||||
|
const loadTime = Date.now();
|
||||||
|
|
||||||
|
const onToDeviceEvent = (event) => {
|
||||||
|
if (event.getType() !== "org.matrix.call_duplicate_session") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = event.getContent();
|
||||||
|
|
||||||
|
if (content.session_id === client.getSessionId()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.timestamp > loadTime) {
|
||||||
|
if (client) {
|
||||||
|
client.stopClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
error: new Error(
|
||||||
|
"This application has been opened in another tab."
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
client.on("toDeviceEvent", onToDeviceEvent);
|
||||||
|
|
||||||
|
client.sendToDevice("org.matrix.call_duplicate_session", {
|
||||||
|
[client.getUserId()]: {
|
||||||
|
"*": { session_id: client.getSessionId(), timestamp: loadTime },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
client.removeListener("toDeviceEvent", onToDeviceEvent);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
const context = useMemo(
|
const context = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
loading,
|
loading,
|
||||||
@@ -195,6 +247,14 @@ export function ClientProvider({ children }) {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.matrixclient = client;
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorView error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
|
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,14 +43,7 @@ export function UserMenuContainer({ preventNavigation }) {
|
|||||||
displayName || (userName ? userName.replace("@", "") : undefined)
|
displayName || (userName ? userName.replace("@", "") : undefined)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{modalState.isOpen && (
|
{modalState.isOpen && <ProfileModal client={client} {...modalProps} />}
|
||||||
<ProfileModal
|
|
||||||
client={client}
|
|
||||||
isAuthenticated={isAuthenticated}
|
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
|
||||||
{...modalProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,13 +31,7 @@ import { usePageTitle } from "../usePageTitle";
|
|||||||
export function RegisterPage() {
|
export function RegisterPage() {
|
||||||
usePageTitle("Register");
|
usePageTitle("Register");
|
||||||
|
|
||||||
const {
|
const { loading, isAuthenticated, isPasswordlessUser, client } = useClient();
|
||||||
loading,
|
|
||||||
client,
|
|
||||||
changePassword,
|
|
||||||
isAuthenticated,
|
|
||||||
isPasswordlessUser,
|
|
||||||
} = useClient();
|
|
||||||
const confirmPasswordRef = useRef();
|
const confirmPasswordRef = useRef();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -64,11 +58,31 @@ export function RegisterPage() {
|
|||||||
async function submit() {
|
async function submit() {
|
||||||
setRegistering(true);
|
setRegistering(true);
|
||||||
|
|
||||||
if (isPasswordlessUser) {
|
let roomIds;
|
||||||
await changePassword(password);
|
|
||||||
} else {
|
if (client && isPasswordlessUser) {
|
||||||
const recaptchaResponse = await execute();
|
const groupCalls = client.groupCallEventHandler.groupCalls.values();
|
||||||
await register(userName, password, recaptchaResponse);
|
roomIds = Array.from(groupCalls).map(
|
||||||
|
(groupCall) => groupCall.room.roomId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recaptchaResponse = await execute();
|
||||||
|
const newClient = await register(
|
||||||
|
userName,
|
||||||
|
password,
|
||||||
|
userName,
|
||||||
|
recaptchaResponse
|
||||||
|
);
|
||||||
|
|
||||||
|
if (roomIds) {
|
||||||
|
for (const roomId of roomIds) {
|
||||||
|
try {
|
||||||
|
await newClient.joinRoom(roomId);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Couldn't join room ${roomId}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,15 +100,7 @@ export function RegisterPage() {
|
|||||||
reset();
|
reset();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[
|
[register, location, history, isPasswordlessUser, reset, execute, client]
|
||||||
register,
|
|
||||||
changePassword,
|
|
||||||
location,
|
|
||||||
history,
|
|
||||||
isPasswordlessUser,
|
|
||||||
reset,
|
|
||||||
execute,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -110,10 +116,10 @@ export function RegisterPage() {
|
|||||||
}, [password, passwordConfirmation]);
|
}, [password, passwordConfirmation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && isAuthenticated && !isPasswordlessUser) {
|
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
}
|
}
|
||||||
}, [history, isAuthenticated, isPasswordlessUser]);
|
}, [history, isAuthenticated, isPasswordlessUser, registering]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
@@ -137,12 +143,6 @@ export function RegisterPage() {
|
|||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
prefix="@"
|
prefix="@"
|
||||||
suffix={`:${defaultHomeserverHost}`}
|
suffix={`:${defaultHomeserverHost}`}
|
||||||
value={
|
|
||||||
isAuthenticated && isPasswordlessUser
|
|
||||||
? client.getUserIdLocalpart()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
disabled={isAuthenticated && isPasswordlessUser}
|
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
@@ -168,22 +168,20 @@ export function RegisterPage() {
|
|||||||
ref={confirmPasswordRef}
|
ref={confirmPasswordRef}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
{!isPasswordlessUser && (
|
<Caption>
|
||||||
<Caption>
|
This site is protected by ReCAPTCHA and the Google{" "}
|
||||||
This site is protected by ReCAPTCHA and the Google{" "}
|
<Link href="https://www.google.com/policies/privacy/">
|
||||||
<Link href="https://www.google.com/policies/privacy/">
|
Privacy Policy
|
||||||
Privacy Policy
|
</Link>{" "}
|
||||||
</Link>{" "}
|
and{" "}
|
||||||
and{" "}
|
<Link href="https://policies.google.com/terms">
|
||||||
<Link href="https://policies.google.com/terms">
|
Terms of Service
|
||||||
Terms of Service
|
</Link>{" "}
|
||||||
</Link>{" "}
|
apply.
|
||||||
apply.
|
<br />
|
||||||
<br />
|
By clicking "Register", you agree to our{" "}
|
||||||
By clicking "Log in", you agree to our{" "}
|
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
</Caption>
|
||||||
</Caption>
|
|
||||||
)}
|
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage>{error.message}</ErrorMessage>
|
||||||
|
|||||||
137
src/auth/generateRandomName.js
Normal file
137
src/auth/generateRandomName.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import {
|
||||||
|
uniqueNamesGenerator,
|
||||||
|
adjectives,
|
||||||
|
colors,
|
||||||
|
animals,
|
||||||
|
} from "unique-names-generator";
|
||||||
|
|
||||||
|
const elements = [
|
||||||
|
"hydrogen",
|
||||||
|
"helium",
|
||||||
|
"lithium",
|
||||||
|
"beryllium",
|
||||||
|
"boron",
|
||||||
|
"carbon",
|
||||||
|
"nitrogen",
|
||||||
|
"oxygen",
|
||||||
|
"fluorine",
|
||||||
|
"neon",
|
||||||
|
"sodium",
|
||||||
|
"magnesium",
|
||||||
|
"aluminum",
|
||||||
|
"silicon",
|
||||||
|
"phosphorus",
|
||||||
|
"sulfur",
|
||||||
|
"chlorine",
|
||||||
|
"argon",
|
||||||
|
"potassium",
|
||||||
|
"calcium",
|
||||||
|
"scandium",
|
||||||
|
"titanium",
|
||||||
|
"vanadium",
|
||||||
|
"chromium",
|
||||||
|
"manganese",
|
||||||
|
"iron",
|
||||||
|
"cobalt",
|
||||||
|
"nickel",
|
||||||
|
"copper",
|
||||||
|
"zinc",
|
||||||
|
"gallium",
|
||||||
|
"germanium",
|
||||||
|
"arsenic",
|
||||||
|
"selenium",
|
||||||
|
"bromine",
|
||||||
|
"krypton",
|
||||||
|
"rubidium",
|
||||||
|
"strontium",
|
||||||
|
"yttrium",
|
||||||
|
"zirconium",
|
||||||
|
"niobium",
|
||||||
|
"molybdenum",
|
||||||
|
"technetium",
|
||||||
|
"ruthenium",
|
||||||
|
"rhodium",
|
||||||
|
"palladium",
|
||||||
|
"silver",
|
||||||
|
"cadmium",
|
||||||
|
"indium",
|
||||||
|
"tin",
|
||||||
|
"antimony",
|
||||||
|
"tellurium",
|
||||||
|
"iodine",
|
||||||
|
"xenon",
|
||||||
|
"cesium",
|
||||||
|
"barium",
|
||||||
|
"lanthanum",
|
||||||
|
"cerium",
|
||||||
|
"praseodymium",
|
||||||
|
"neodymium",
|
||||||
|
"promethium",
|
||||||
|
"samarium",
|
||||||
|
"europium",
|
||||||
|
"gadolinium",
|
||||||
|
"terbium",
|
||||||
|
"dysprosium",
|
||||||
|
"holmium",
|
||||||
|
"erbium",
|
||||||
|
"thulium",
|
||||||
|
"ytterbium",
|
||||||
|
"lutetium",
|
||||||
|
"hafnium",
|
||||||
|
"tantalum",
|
||||||
|
"wolfram",
|
||||||
|
"rhenium",
|
||||||
|
"osmium",
|
||||||
|
"iridium",
|
||||||
|
"platinum",
|
||||||
|
"gold",
|
||||||
|
"mercury",
|
||||||
|
"thallium",
|
||||||
|
"lead",
|
||||||
|
"bismuth",
|
||||||
|
"polonium",
|
||||||
|
"astatine",
|
||||||
|
"radon",
|
||||||
|
"francium",
|
||||||
|
"radium",
|
||||||
|
"actinium",
|
||||||
|
"thorium",
|
||||||
|
"protactinium",
|
||||||
|
"uranium",
|
||||||
|
"neptunium",
|
||||||
|
"plutonium",
|
||||||
|
"americium",
|
||||||
|
"curium",
|
||||||
|
"berkelium",
|
||||||
|
"californium",
|
||||||
|
"einsteinium",
|
||||||
|
"fermium",
|
||||||
|
"mendelevium",
|
||||||
|
"nobelium",
|
||||||
|
"lawrencium",
|
||||||
|
"rutherfordium",
|
||||||
|
"dubnium",
|
||||||
|
"seaborgium",
|
||||||
|
"bohrium",
|
||||||
|
"hassium",
|
||||||
|
"meitnerium",
|
||||||
|
"darmstadtium",
|
||||||
|
"roentgenium",
|
||||||
|
"copernicium",
|
||||||
|
"nihonium",
|
||||||
|
"flerovium",
|
||||||
|
"moscovium",
|
||||||
|
"livermorium",
|
||||||
|
"tennessine",
|
||||||
|
"oganesson",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function generateRandomName(config) {
|
||||||
|
return uniqueNamesGenerator({
|
||||||
|
dictionaries: [colors, adjectives, animals, elements],
|
||||||
|
style: "lowerCase",
|
||||||
|
length: 3,
|
||||||
|
separator: "-",
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -25,7 +25,13 @@ export function useInteractiveRegistration() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const register = useCallback(
|
const register = useCallback(
|
||||||
async (username, password, recaptchaResponse, passwordlessUser) => {
|
async (
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
displayName,
|
||||||
|
recaptchaResponse,
|
||||||
|
passwordlessUser
|
||||||
|
) => {
|
||||||
const interactiveAuth = new InteractiveAuth({
|
const interactiveAuth = new InteractiveAuth({
|
||||||
matrixClient: authClientRef.current,
|
matrixClient: authClientRef.current,
|
||||||
busyChanged(loading) {
|
busyChanged(loading) {
|
||||||
@@ -66,7 +72,7 @@ export function useInteractiveRegistration() {
|
|||||||
deviceId: device_id,
|
deviceId: device_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.setDisplayName(username);
|
await client.setDisplayName(displayName);
|
||||||
|
|
||||||
const session = { user_id, device_id, access_token, passwordlessUser };
|
const session = { user_id, device_id, access_token, passwordlessUser };
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { TooltipTrigger } from "../Tooltip";
|
|||||||
export const variantToClassName = {
|
export const variantToClassName = {
|
||||||
default: [styles.button],
|
default: [styles.button],
|
||||||
toolbar: [styles.toolbarButton],
|
toolbar: [styles.toolbarButton],
|
||||||
|
toolbarSecondary: [styles.toolbarButtonSecondary],
|
||||||
icon: [styles.iconButton],
|
icon: [styles.iconButton],
|
||||||
secondary: [styles.secondary],
|
secondary: [styles.secondary],
|
||||||
copy: [styles.copyButton],
|
copy: [styles.copyButton],
|
||||||
@@ -103,7 +104,7 @@ export function VideoButton({ muted, ...rest }) {
|
|||||||
export function ScreenshareButton({ enabled, className, ...rest }) {
|
export function ScreenshareButton({ enabled, className, ...rest }) {
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Button variant="toolbar" {...rest} on={enabled}>
|
<Button variant="toolbarSecondary" {...rest} on={enabled}>
|
||||||
<ScreenshareIcon />
|
<ScreenshareIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{() => (enabled ? "Stop sharing screen" : "Share screen")}
|
{() => (enabled ? "Stop sharing screen" : "Share screen")}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
.button,
|
.button,
|
||||||
.toolbarButton,
|
.toolbarButton,
|
||||||
|
.toolbarButtonSecondary,
|
||||||
.iconButton,
|
.iconButton,
|
||||||
.iconCopyButton,
|
.iconCopyButton,
|
||||||
.secondary,
|
.secondary,
|
||||||
@@ -48,6 +49,7 @@ limitations under the License.
|
|||||||
|
|
||||||
.button:focus,
|
.button:focus,
|
||||||
.toolbarButton:focus,
|
.toolbarButton:focus,
|
||||||
|
.toolbarButtonSecondary:focus,
|
||||||
.iconButton:focus,
|
.iconButton:focus,
|
||||||
.iconCopyButton:focus,
|
.iconCopyButton:focus,
|
||||||
.secondary:focus,
|
.secondary:focus,
|
||||||
@@ -55,14 +57,16 @@ limitations under the License.
|
|||||||
outline: auto;
|
outline: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButton {
|
.toolbarButton,
|
||||||
|
.toolbarButtonSecondary {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
background-color: var(--bgColor2);
|
background-color: var(--bgColor2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButton:hover {
|
.toolbarButton:hover,
|
||||||
|
.toolbarButtonSecondary:hover {
|
||||||
background-color: var(--bgColor4);
|
background-color: var(--bgColor4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +75,10 @@ limitations under the License.
|
|||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbarButtonSecondary.on {
|
||||||
|
background-color: #0dbd8b;
|
||||||
|
}
|
||||||
|
|
||||||
.iconButton:not(.stroke) svg * {
|
.iconButton:not(.stroke) svg * {
|
||||||
fill: #ffffff;
|
fill: #ffffff;
|
||||||
}
|
}
|
||||||
@@ -100,6 +108,10 @@ limitations under the License.
|
|||||||
fill: #21262c;
|
fill: #21262c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbarButtonSecondary.on svg * {
|
||||||
|
fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.secondary,
|
.secondary,
|
||||||
.copyButton {
|
.copyButton {
|
||||||
color: #0dbd8b;
|
color: #0dbd8b;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Link } from "react-router-dom";
|
|||||||
import { CopyButton } from "../button";
|
import { CopyButton } from "../button";
|
||||||
import { Facepile } from "../Facepile";
|
import { Facepile } from "../Facepile";
|
||||||
import { Avatar } from "../Avatar";
|
import { Avatar } from "../Avatar";
|
||||||
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
|
||||||
import styles from "./CallList.module.css";
|
import styles from "./CallList.module.css";
|
||||||
import { getRoomUrl } from "../matrix-utils";
|
import { getRoomUrl } from "../matrix-utils";
|
||||||
import { Body, Caption } from "../typography/Typography";
|
import { Body, Caption } from "../typography/Typography";
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar,
|
.avatar,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { Form } from "../form/Form";
|
|||||||
export function RegisteredView({ client }) {
|
export function RegisteredView({ client }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
|
const history = useHistory();
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -55,7 +56,6 @@ export function RegisteredView({ client }) {
|
|||||||
|
|
||||||
const { modalState, modalProps } = useModalTriggerState();
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
const [existingRoomId, setExistingRoomId] = useState();
|
const [existingRoomId, setExistingRoomId] = useState();
|
||||||
const history = useHistory();
|
|
||||||
const onJoinExistingRoom = useCallback(() => {
|
const onJoinExistingRoom = useCallback(() => {
|
||||||
history.push(`/${existingRoomId}`);
|
history.push(`/${existingRoomId}`);
|
||||||
}, [history, existingRoomId]);
|
}, [history, existingRoomId]);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Body, Caption, Link, Headline } from "../typography/Typography";
|
|||||||
import { Form } from "../form/Form";
|
import { Form } from "../form/Form";
|
||||||
import styles from "./UnauthenticatedView.module.css";
|
import styles from "./UnauthenticatedView.module.css";
|
||||||
import commonStyles from "./common.module.css";
|
import commonStyles from "./common.module.css";
|
||||||
|
import { generateRandomName } from "../auth/generateRandomName";
|
||||||
|
|
||||||
export function UnauthenticatedView() {
|
export function UnauthenticatedView() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -26,19 +27,20 @@ export function UnauthenticatedView() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = new FormData(e.target);
|
const data = new FormData(e.target);
|
||||||
const roomName = data.get("callName");
|
const roomName = data.get("callName");
|
||||||
const userName = data.get("userName");
|
const displayName = data.get("displayName");
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const recaptchaResponse = await execute();
|
const recaptchaResponse = await execute();
|
||||||
|
const userName = generateRandomName();
|
||||||
const client = await register(
|
const client = await register(
|
||||||
userName,
|
userName,
|
||||||
randomString(16),
|
randomString(16),
|
||||||
|
displayName,
|
||||||
recaptchaResponse,
|
recaptchaResponse,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
const roomIdOrAlias = await createRoom(client, roomName);
|
const roomIdOrAlias = await createRoom(client, roomName);
|
||||||
|
|
||||||
if (roomIdOrAlias) {
|
if (roomIdOrAlias) {
|
||||||
@@ -100,10 +102,10 @@ export function UnauthenticatedView() {
|
|||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="userName"
|
id="displayName"
|
||||||
name="userName"
|
name="displayName"
|
||||||
label="Username"
|
label="Display Name"
|
||||||
placeholder="Username"
|
placeholder="Display Name"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
|||||||
4
src/icons/Edit.svg
Normal file
4
src/icons/Edit.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.64856 7.35501C2.65473 7.31601 2.67231 7.27972 2.69908 7.25069L8.40377 1.06442C8.47865 0.983217 8.60518 0.978093 8.68638 1.05297L9.8626 2.13763C9.9438 2.21251 9.94893 2.33904 9.87405 2.42024L4.16936 8.60651C4.1426 8.63554 4.10783 8.656 4.06946 8.6653L2.66781 9.00511C2.52911 9.03873 2.40084 8.92044 2.42315 8.77948L2.64856 7.35501Z" fill="white"/>
|
||||||
|
<path d="M1.75 9.44346C1.33579 9.44346 1 9.77925 1 10.1935C1 10.6077 1.33579 10.9435 1.75 10.9435L10.75 10.9435C11.1642 10.9435 11.5 10.6077 11.5 10.1935C11.5 9.77925 11.1642 9.44346 10.75 9.44346L1.75 9.44346Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 689 B |
78
src/input/AvatarInputField.jsx
Normal file
78
src/input/AvatarInputField.jsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { useObjectRef } from "@react-aria/utils";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
import { Avatar } from "../Avatar";
|
||||||
|
import { Button } from "../button";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { ReactComponent as EditIcon } from "../icons/Edit.svg";
|
||||||
|
import styles from "./AvatarInputField.module.css";
|
||||||
|
|
||||||
|
export const AvatarInputField = forwardRef(
|
||||||
|
(
|
||||||
|
{ id, label, className, avatarUrl, displayName, onRemoveAvatar, ...rest },
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [removed, setRemoved] = useState(false);
|
||||||
|
const [objUrl, setObjUrl] = useState(null);
|
||||||
|
|
||||||
|
const fileInputRef = useObjectRef(ref);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onChange = (e) => {
|
||||||
|
if (e.target.files.length > 0) {
|
||||||
|
setObjUrl(URL.createObjectURL(e.target.files[0]));
|
||||||
|
setRemoved(false);
|
||||||
|
} else {
|
||||||
|
setObjUrl(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fileInputRef.current.addEventListener("change", onChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.removeEventListener("change", onChange);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const onPressRemoveAvatar = useCallback(() => {
|
||||||
|
setRemoved(true);
|
||||||
|
onRemoveAvatar();
|
||||||
|
}, [onRemoveAvatar]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.avatarInputField, className)}>
|
||||||
|
<div className={styles.avatarContainer}>
|
||||||
|
<Avatar
|
||||||
|
size="xl"
|
||||||
|
src={removed ? null : objUrl || avatarUrl}
|
||||||
|
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
accept="image/png, image/jpeg"
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
className={styles.fileInput}
|
||||||
|
role="button"
|
||||||
|
aria-label={label}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
<label htmlFor={id} className={styles.fileInputButton}>
|
||||||
|
<EditIcon />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className={styles.removeButton}
|
||||||
|
variant="icon"
|
||||||
|
onPress={onPressRemoveAvatar}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
41
src/input/AvatarInputField.module.css
Normal file
41
src/input/AvatarInputField.module.css
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
.avatarInputField {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarContainer {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileInput {
|
||||||
|
width: 0.1px;
|
||||||
|
height: 0.1px;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileInput:focus + .fileInputButton {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileInputButton {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 11px;
|
||||||
|
right: -4px;
|
||||||
|
background-color: var(--bgColor4);
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeButton {
|
||||||
|
color: #0dbd8b;
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@ import { InspectorContextProvider } from "./room/GroupCallInspector";
|
|||||||
|
|
||||||
rageshake.init();
|
rageshake.init();
|
||||||
|
|
||||||
|
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
|
||||||
|
|
||||||
if (import.meta.env.VITE_CUSTOM_THEME) {
|
if (import.meta.env.VITE_CUSTOM_THEME) {
|
||||||
const style = document.documentElement.style;
|
const style = document.documentElement.style;
|
||||||
style.setProperty("--primaryColor", import.meta.env.VITE_PRIMARY_COLOR);
|
style.setProperty("--primaryColor", import.meta.env.VITE_PRIMARY_COLOR);
|
||||||
|
|||||||
@@ -50,6 +50,31 @@ export function roomAliasFromRoomName(roomName) {
|
|||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function roomNameFromRoomId(roomId) {
|
||||||
|
return roomId
|
||||||
|
.match(/([^:]+):.*$/)[1]
|
||||||
|
.substring(1)
|
||||||
|
.split("-")
|
||||||
|
.map((part) =>
|
||||||
|
part.length > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part
|
||||||
|
)
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLocalRoomId(roomId) {
|
||||||
|
if (!roomId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = roomId.match(/[^:]+:(.*)$/);
|
||||||
|
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts[1] === defaultHomeserverHost;
|
||||||
|
}
|
||||||
|
|
||||||
export async function createRoom(client, name) {
|
export async function createRoom(client, name) {
|
||||||
const { room_id, room_alias } = await client.createRoom({
|
const { room_id, room_alias } = await client.createRoom({
|
||||||
visibility: "private",
|
visibility: "private",
|
||||||
@@ -95,12 +120,12 @@ export function getRoomUrl(roomId) {
|
|||||||
const [localPart, host] = roomId.replace("#", "").split(":");
|
const [localPart, host] = roomId.replace("#", "").split(":");
|
||||||
|
|
||||||
if (host !== defaultHomeserverHost) {
|
if (host !== defaultHomeserverHost) {
|
||||||
return `${window.location.host}/room/${roomId}`;
|
return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
|
||||||
} else {
|
} else {
|
||||||
return `${window.location.host}/${localPart}`;
|
return `${window.location.protocol}//${window.location.host}/${localPart}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return `${window.location.host}/room/${roomId}`;
|
return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,25 @@ import { Button } from "../button";
|
|||||||
import { useProfile } from "./useProfile";
|
import { useProfile } from "./useProfile";
|
||||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||||
import { Modal, ModalContent } from "../Modal";
|
import { Modal, ModalContent } from "../Modal";
|
||||||
|
import { AvatarInputField } from "../input/AvatarInputField";
|
||||||
|
import styles from "./ProfileModal.module.css";
|
||||||
|
|
||||||
export function ProfileModal({
|
export function ProfileModal({ client, ...rest }) {
|
||||||
client,
|
|
||||||
isAuthenticated,
|
|
||||||
isPasswordlessUser,
|
|
||||||
...rest
|
|
||||||
}) {
|
|
||||||
const { onClose } = rest;
|
const { onClose } = rest;
|
||||||
const {
|
const {
|
||||||
success,
|
success,
|
||||||
error,
|
error,
|
||||||
loading,
|
loading,
|
||||||
displayName: initialDisplayName,
|
displayName: initialDisplayName,
|
||||||
|
avatarUrl,
|
||||||
saveProfile,
|
saveProfile,
|
||||||
} = useProfile(client);
|
} = useProfile(client);
|
||||||
const [displayName, setDisplayName] = useState(initialDisplayName || "");
|
const [displayName, setDisplayName] = useState(initialDisplayName || "");
|
||||||
|
const [removeAvatar, setRemoveAvatar] = useState(false);
|
||||||
|
|
||||||
|
const onRemoveAvatar = useCallback(() => {
|
||||||
|
setRemoveAvatar(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onChangeDisplayName = useCallback(
|
const onChangeDisplayName = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
@@ -37,9 +40,10 @@ export function ProfileModal({
|
|||||||
saveProfile({
|
saveProfile({
|
||||||
displayName,
|
displayName,
|
||||||
avatar: avatar && avatar.size > 0 ? avatar : undefined,
|
avatar: avatar && avatar.size > 0 ? avatar : undefined,
|
||||||
|
removeAvatar: removeAvatar && (!avatar || avatar.size === 0),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[saveProfile]
|
[saveProfile, removeAvatar]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -52,6 +56,16 @@ export function ProfileModal({
|
|||||||
<Modal title="Profile" isDismissable {...rest}>
|
<Modal title="Profile" isDismissable {...rest}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
|
<FieldRow className={styles.avatarFieldRow}>
|
||||||
|
<AvatarInputField
|
||||||
|
id="avatar"
|
||||||
|
name="avatar"
|
||||||
|
label="Avatar"
|
||||||
|
avatarUrl={avatarUrl}
|
||||||
|
displayName={displayName}
|
||||||
|
onRemoveAvatar={onRemoveAvatar}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="userId"
|
id="userId"
|
||||||
@@ -75,16 +89,6 @@ export function ProfileModal({
|
|||||||
onChange={onChangeDisplayName}
|
onChange={onChangeDisplayName}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
{isAuthenticated && (
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
type="file"
|
|
||||||
id="avatar"
|
|
||||||
name="avatar"
|
|
||||||
label="Avatar"
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
)}
|
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage>{error.message}</ErrorMessage>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.avatarFieldRow {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function useProfile(client) {
|
|||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
||||||
const saveProfile = useCallback(
|
const saveProfile = useCallback(
|
||||||
async ({ displayName, avatar }) => {
|
async ({ displayName, avatar, removeAvatar }) => {
|
||||||
if (client) {
|
if (client) {
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -58,7 +58,9 @@ export function useProfile(client) {
|
|||||||
|
|
||||||
let mxcAvatarUrl;
|
let mxcAvatarUrl;
|
||||||
|
|
||||||
if (avatar) {
|
if (removeAvatar) {
|
||||||
|
await client.setAvatarUrl("");
|
||||||
|
} else if (avatar) {
|
||||||
mxcAvatarUrl = await client.uploadContent(avatar);
|
mxcAvatarUrl = await client.uploadContent(avatar);
|
||||||
await client.setAvatarUrl(mxcAvatarUrl);
|
await client.setAvatarUrl(mxcAvatarUrl);
|
||||||
}
|
}
|
||||||
@@ -66,7 +68,9 @@ export function useProfile(client) {
|
|||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
displayName,
|
displayName,
|
||||||
avatarUrl: mxcAvatarUrl
|
avatarUrl: removeAvatar
|
||||||
|
? null
|
||||||
|
: mxcAvatarUrl
|
||||||
? getAvatarUrl(client, mxcAvatarUrl)
|
? getAvatarUrl(client, mxcAvatarUrl)
|
||||||
: prev.avatarUrl,
|
: prev.avatarUrl,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ function getUserName(userId) {
|
|||||||
const match = userId.match(/@([^\:]+):/);
|
const match = userId.match(/@([^\:]+):/);
|
||||||
|
|
||||||
return match && match.length > 0
|
return match && match.length > 0
|
||||||
? match[1].replace("-", " ").replace("W", "")
|
? match[1].replace("-", " ").replace(/\W/g, "")
|
||||||
: userId.replace("W", "");
|
: userId.replace(/\W/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatContent(type, content) {
|
function formatContent(type, content) {
|
||||||
@@ -231,7 +231,7 @@ function reducer(state, action) {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "receive_to_device_event": {
|
case "received_voip_event": {
|
||||||
const event = action.event;
|
const event = action.event;
|
||||||
const eventsByUserId = { ...state.eventsByUserId };
|
const eventsByUserId = { ...state.eventsByUserId };
|
||||||
const fromId = event.getSender();
|
const fromId = event.getSender();
|
||||||
@@ -338,8 +338,8 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
|||||||
// dispatch({ type: "call_hangup", call });
|
// dispatch({ type: "call_hangup", call });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
function onToDeviceEvent(event) {
|
function onReceivedVoipEvent(event) {
|
||||||
dispatch({ type: "receive_to_device_event", event });
|
dispatch({ type: "received_voip_event", event });
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSendVoipEvent(event) {
|
function onSendVoipEvent(event) {
|
||||||
@@ -351,7 +351,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
|||||||
groupCall.on("send_voip_event", onSendVoipEvent);
|
groupCall.on("send_voip_event", onSendVoipEvent);
|
||||||
//client.on("state", onCallsChanged);
|
//client.on("state", onCallsChanged);
|
||||||
//client.on("hangup", onCallHangup);
|
//client.on("hangup", onCallHangup);
|
||||||
client.on("toDeviceEvent", onToDeviceEvent);
|
client.on("received_voip_event", onReceivedVoipEvent);
|
||||||
|
|
||||||
onUpdateRoomState();
|
onUpdateRoomState();
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
|||||||
groupCall.removeListener("send_voip_event", onSendVoipEvent);
|
groupCall.removeListener("send_voip_event", onSendVoipEvent);
|
||||||
//client.removeListener("state", onCallsChanged);
|
//client.removeListener("state", onCallsChanged);
|
||||||
//client.removeListener("hangup", onCallHangup);
|
//client.removeListener("hangup", onCallHangup);
|
||||||
client.removeListener("toDeviceEvent", onToDeviceEvent);
|
client.removeListener("received_voip_event", onReceivedVoipEvent);
|
||||||
};
|
};
|
||||||
}, [client, groupCall]);
|
}, [client, groupCall]);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import React from "react";
|
|||||||
import { useLoadGroupCall } from "./useLoadGroupCall";
|
import { useLoadGroupCall } from "./useLoadGroupCall";
|
||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||||
import { usePageTitle } from "../usePageTitle";
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
import { isLocalRoomId } from "../matrix-utils";
|
||||||
|
import { RoomNotFoundView } from "./RoomNotFoundView";
|
||||||
|
|
||||||
export function GroupCallLoader({ client, roomId, viaServers, children }) {
|
export function GroupCallLoader({ client, roomId, viaServers, children }) {
|
||||||
const { loading, error, groupCall } = useLoadGroupCall(
|
const { loading, error, groupCall } = useLoadGroupCall(
|
||||||
@@ -20,6 +22,16 @@ export function GroupCallLoader({ client, roomId, viaServers, children }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
error &&
|
||||||
|
(error.errcode === "M_NOT_FOUND" ||
|
||||||
|
(error.message &&
|
||||||
|
error.message.indexOf("Failed to fetch alias") !== -1)) &&
|
||||||
|
isLocalRoomId(roomId)
|
||||||
|
) {
|
||||||
|
return <RoomNotFoundView client={client} roomId={roomId} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorView error={error} />;
|
return <ErrorView error={error} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { Avatar } from "../Avatar";
|
|||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
import { useRageshakeRequestModal } from "../settings/rageshake";
|
import { useRageshakeRequestModal } from "../settings/rageshake";
|
||||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||||
|
import { usePreventScroll } from "@react-aria/overlays";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||||
@@ -47,7 +48,8 @@ export function InCallView({
|
|||||||
showInspector,
|
showInspector,
|
||||||
roomId,
|
roomId,
|
||||||
}) {
|
}) {
|
||||||
const [layout, setLayout] = useVideoGridLayout();
|
usePreventScroll();
|
||||||
|
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const participants = [];
|
const participants = [];
|
||||||
@@ -57,7 +59,7 @@ export function InCallView({
|
|||||||
id: callFeed.stream.id,
|
id: callFeed.stream.id,
|
||||||
callFeed,
|
callFeed,
|
||||||
focused:
|
focused:
|
||||||
screenshareFeeds.length === 0
|
screenshareFeeds.length === 0 && layout === "spotlight"
|
||||||
? callFeed.userId === activeSpeaker
|
? callFeed.userId === activeSpeaker
|
||||||
: false,
|
: false,
|
||||||
});
|
});
|
||||||
@@ -80,14 +82,14 @@ export function InCallView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return participants;
|
return participants;
|
||||||
}, [userMediaFeeds, activeSpeaker, screenshareFeeds]);
|
}, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]);
|
||||||
|
|
||||||
const onFocusTile = useCallback(
|
const onFocusTile = useCallback(
|
||||||
(tiles, focusedTile) => {
|
(tiles, focusedTile) => {
|
||||||
if (layout === "freedom") {
|
if (layout === "freedom") {
|
||||||
return tiles.map((tile) => {
|
return tiles.map((tile) => {
|
||||||
if (tile === focusedTile) {
|
if (tile === focusedTile) {
|
||||||
return { ...tile, presenter: !tile.presenter };
|
return { ...tile, focused: !tile.focused };
|
||||||
}
|
}
|
||||||
|
|
||||||
return tile;
|
return tile;
|
||||||
@@ -156,6 +158,7 @@ export function InCallView({
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
getAvatar={renderAvatar}
|
getAvatar={renderAvatar}
|
||||||
|
showName={items.length > 2 || item.focused}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { OverflowMenu } from "./OverflowMenu";
|
|||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
import { Body, Link } from "../typography/Typography";
|
import { Body, Link } from "../typography/Typography";
|
||||||
import { Avatar } from "../Avatar";
|
import { Avatar } from "../Avatar";
|
||||||
import { getAvatarUrl } from "../matrix-utils";
|
|
||||||
import { useProfile } from "../profile/useProfile";
|
import { useProfile } from "../profile/useProfile";
|
||||||
import useMeasure from "react-use-measure";
|
import useMeasure from "react-use-measure";
|
||||||
import { ResizeObserver } from "@juggle/resize-observer";
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
@@ -86,7 +85,7 @@ export function LobbyView({
|
|||||||
borderRadius: avatarSize,
|
borderRadius: avatarSize,
|
||||||
fontSize: Math.round(avatarSize / 2),
|
fontSize: Math.round(avatarSize / 2),
|
||||||
}}
|
}}
|
||||||
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)}
|
src={avatarUrl}
|
||||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { randomString } from "matrix-js-sdk/src/randomstring";
|
|||||||
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
|
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
|
||||||
import { Form } from "../form/Form";
|
import { Form } from "../form/Form";
|
||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
|
import { generateRandomName } from "../auth/generateRandomName";
|
||||||
|
|
||||||
export function RoomAuthView() {
|
export function RoomAuthView() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -21,13 +22,20 @@ export function RoomAuthView() {
|
|||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = new FormData(e.target);
|
const data = new FormData(e.target);
|
||||||
const userName = data.get("userName");
|
const displayName = data.get("displayName");
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const recaptchaResponse = await execute();
|
const recaptchaResponse = await execute();
|
||||||
await register(userName, randomString(16), recaptchaResponse, true);
|
const userName = generateRandomName();
|
||||||
|
await register(
|
||||||
|
userName,
|
||||||
|
randomString(16),
|
||||||
|
displayName,
|
||||||
|
recaptchaResponse,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
submit().catch((error) => {
|
submit().catch((error) => {
|
||||||
@@ -58,10 +66,10 @@ export function RoomAuthView() {
|
|||||||
<Form className={styles.form} onSubmit={onSubmit}>
|
<Form className={styles.form} onSubmit={onSubmit}>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="userName"
|
id="displayName"
|
||||||
name="userName"
|
name="displayName"
|
||||||
label="Pick a user name"
|
label="Display Name"
|
||||||
placeholder="Pick a user name"
|
placeholder="Display Name"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
|||||||
76
src/room/RoomNotFoundView.jsx
Normal file
76
src/room/RoomNotFoundView.jsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import React, { useState, useCallback } from "react";
|
||||||
|
import { FullScreenView } from "../FullScreenView";
|
||||||
|
import { Headline, Subtitle } from "../typography/Typography";
|
||||||
|
import { createRoom, roomNameFromRoomId } from "../matrix-utils";
|
||||||
|
import { FieldRow, ErrorMessage, InputField } from "../input/Input";
|
||||||
|
import { Button } from "../button";
|
||||||
|
import { Form } from "../form/Form";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import styles from "./RoomNotFoundView.module.css";
|
||||||
|
|
||||||
|
export function RoomNotFoundView({ client, roomId }) {
|
||||||
|
const history = useHistory();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState();
|
||||||
|
const roomName = roomNameFromRoomId(roomId);
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
setError(undefined);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const roomIdOrAlias = await createRoom(client, roomName);
|
||||||
|
|
||||||
|
if (roomIdOrAlias) {
|
||||||
|
history.push(`/room/${roomIdOrAlias}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
setLoading(false);
|
||||||
|
setError(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[client, roomName]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FullScreenView>
|
||||||
|
<Headline>Call Not Found</Headline>
|
||||||
|
<Subtitle>Would you like to create this call?</Subtitle>
|
||||||
|
<Form onSubmit={onSubmit} className={styles.form}>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="callName"
|
||||||
|
name="callName"
|
||||||
|
label="Call name"
|
||||||
|
placeholder="Call name"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
autoComplete="off"
|
||||||
|
value={roomName}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
<FieldRow>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="lg"
|
||||||
|
disabled={loading}
|
||||||
|
className={styles.button}
|
||||||
|
>
|
||||||
|
{loading ? "Loading..." : "Create Room"}
|
||||||
|
</Button>
|
||||||
|
</FieldRow>
|
||||||
|
{error && (
|
||||||
|
<FieldRow>
|
||||||
|
<ErrorMessage>{error.message}</ErrorMessage>
|
||||||
|
</FieldRow>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</FullScreenView>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/room/RoomNotFoundView.module.css
Normal file
11
src/room/RoomNotFoundView.module.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.form {
|
||||||
|
padding: 0 24px;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 409px;
|
||||||
|
width: calc(100% - 48px);
|
||||||
|
margin-bottom: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ export function RoomPage() {
|
|||||||
const params = new URLSearchParams(search);
|
const params = new URLSearchParams(search);
|
||||||
return [params.has("simple"), params.getAll("via")];
|
return [params.has("simple"), params.getAll("via")];
|
||||||
}, [search]);
|
}, [search]);
|
||||||
const roomId = maybeRoomId || hash;
|
const roomId = (maybeRoomId || hash || "").toLowerCase();
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function RoomRedirect() {
|
|||||||
let roomId = pathname;
|
let roomId = pathname;
|
||||||
|
|
||||||
if (pathname.startsWith("/")) {
|
if (pathname.startsWith("/")) {
|
||||||
roomId = roomId.substr(1, roomId.length);
|
roomId = roomId.substring(1, roomId.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!roomId.startsWith("#") && !roomId.startsWith("!")) {
|
if (!roomId.startsWith("#") && !roomId.startsWith("!")) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useMediaHandler } from "./useMediaHandler";
|
|||||||
import { FieldRow, InputField } from "../input/Input";
|
import { FieldRow, InputField } from "../input/Input";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { useDownloadDebugLog } from "./rageshake";
|
import { useDownloadDebugLog } from "./rageshake";
|
||||||
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
export function SettingsModal({
|
export function SettingsModal({
|
||||||
client,
|
client,
|
||||||
@@ -82,6 +83,11 @@ export function SettingsModal({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<FieldRow>
|
||||||
|
<Body className={styles.fieldRowText}>
|
||||||
|
Version: {import.meta.env.VITE_APP_VERSION || "dev"}
|
||||||
|
</Body>
|
||||||
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="showInspector"
|
id="showInspector"
|
||||||
|
|||||||
@@ -6,3 +6,7 @@
|
|||||||
.tabContainer {
|
.tabContainer {
|
||||||
margin: 27px 16px;
|
margin: 27px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fieldRowText {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function useSubmitRageshake() {
|
|||||||
opts.description || "User did not supply any additional text."
|
opts.description || "User did not supply any additional text."
|
||||||
);
|
);
|
||||||
body.append("app", "matrix-video-chat");
|
body.append("app", "matrix-video-chat");
|
||||||
body.append("version", "dev");
|
body.append("version", import.meta.env.VITE_APP_VERSION || "dev");
|
||||||
body.append("user_agent", userAgent);
|
body.append("user_agent", userAgent);
|
||||||
body.append("installed_pwa", false);
|
body.append("installed_pwa", false);
|
||||||
body.append("touch_input", touchInput);
|
body.append("touch_input", touchInput);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ParticipantsTest = () => {
|
export const ParticipantsTest = () => {
|
||||||
const [layout, setLayout] = useVideoGridLayout();
|
const [layout, setLayout] = useVideoGridLayout(false);
|
||||||
const [participantCount, setParticipantCount] = useState(1);
|
const [participantCount, setParticipantCount] = useState(1);
|
||||||
|
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
@@ -60,7 +60,12 @@ export const ParticipantsTest = () => {
|
|||||||
>
|
>
|
||||||
<VideoGrid layout={layout} items={items}>
|
<VideoGrid layout={layout} items={items}>
|
||||||
{({ item, ...rest }) => (
|
{({ item, ...rest }) => (
|
||||||
<VideoTile key={item.id} name={`User ${item.id}`} {...rest} />
|
<VideoTile
|
||||||
|
key={item.id}
|
||||||
|
name={`User ${item.id}`}
|
||||||
|
showName={items.length > 2 || item.focused}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</VideoGrid>
|
</VideoGrid>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12039,6 +12039,11 @@ unique-filename@^1.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
unique-slug "^2.0.0"
|
unique-slug "^2.0.0"
|
||||||
|
|
||||||
|
unique-names-generator@^4.6.0:
|
||||||
|
version "4.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/unique-names-generator/-/unique-names-generator-4.6.0.tgz#852c1db8149815d6cf665a601820fe80ec2fbc37"
|
||||||
|
integrity sha512-m0fke1emBeT96UYn2psPQYwljooDWRTKt9oUZ5vlt88ZFMBGxqwPyLHXwCfkbgdm8jzioCp7oIpo6KdM+fnUlQ==
|
||||||
|
|
||||||
unique-slug@^2.0.0:
|
unique-slug@^2.0.0:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
|
resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
|
||||||
|
|||||||
Reference in New Issue
Block a user