Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a18ba0110 | ||
|
|
0a49ddb31e | ||
|
|
25385edf12 | ||
|
|
721cccf152 | ||
|
|
3b017eb92b | ||
|
|
641b82dc45 | ||
|
|
42e2041d6f | ||
|
|
2c3ebd4c03 | ||
|
|
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;
|
||||||
|
}
|
||||||
@@ -38,14 +38,25 @@ export const InputField = forwardRef(
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{prefix && <span>{prefix}</span>}
|
{prefix && <span>{prefix}</span>}
|
||||||
<input
|
{type === "textarea" ? (
|
||||||
id={id}
|
<textarea
|
||||||
{...rest}
|
id={id}
|
||||||
ref={ref}
|
{...rest}
|
||||||
type={type}
|
ref={ref}
|
||||||
checked={checked}
|
type={type}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
{...rest}
|
||||||
|
ref={ref}
|
||||||
|
type={type}
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<label htmlFor={id}>
|
<label htmlFor={id}>
|
||||||
{type === "checkbox" && (
|
{type === "checkbox" && (
|
||||||
<div className={styles.checkbox}>
|
<div className={styles.checkbox}>
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
border: 1px solid var(--inputBorderColor);
|
border: 1px solid var(--inputBorderColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input {
|
.inputField input,
|
||||||
|
.inputField textarea {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.inputField.disabled input,
|
.inputField.disabled input,
|
||||||
|
.inputField.disabled textarea,
|
||||||
.inputField.disabled span {
|
.inputField.disabled span {
|
||||||
color: var(--textColor2);
|
color: var(--textColor2);
|
||||||
}
|
}
|
||||||
@@ -54,12 +56,14 @@
|
|||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input::placeholder {
|
.inputField input::placeholder,
|
||||||
|
.inputField textarea::placeholder {
|
||||||
transition: color 0.25s ease-in 0s;
|
transition: color 0.25s ease-in 0s;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input:placeholder-shown:focus::placeholder {
|
.inputField input:placeholder-shown:focus::placeholder,
|
||||||
|
.inputField textarea:placeholder-shown:focus::placeholder {
|
||||||
transition: color 0.25s ease-in 0.1s;
|
transition: color 0.25s ease-in 0.1s;
|
||||||
color: var(--textColor2);
|
color: var(--textColor2);
|
||||||
}
|
}
|
||||||
@@ -86,13 +90,17 @@
|
|||||||
border-color: var(--inputBorderColorFocused);
|
border-color: var(--inputBorderColorFocused);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input:focus {
|
.inputField input:focus,
|
||||||
|
.inputField textarea:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input:focus + label,
|
.inputField input:focus + label,
|
||||||
.inputField input:not(:placeholder-shown) + label,
|
.inputField input:not(:placeholder-shown) + label,
|
||||||
.inputField.prefix input + label {
|
.inputField.prefix input + label,
|
||||||
|
.inputField textarea:focus + label,
|
||||||
|
.inputField textarea:not(:placeholder-shown) + label,
|
||||||
|
.inputField.prefix textarea + label {
|
||||||
background-color: var(--bgColor2);
|
background-color: var(--bgColor2);
|
||||||
transition: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s,
|
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;
|
top 0.25s ease-out 0s, background-color 0.25s ease-out 0s;
|
||||||
@@ -102,7 +110,8 @@
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input:focus + label {
|
.inputField input:focus + label,
|
||||||
|
.inputField textarea:focus + label {
|
||||||
color: var(--inputBorderColorFocused);
|
color: var(--inputBorderColorFocused);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function useProfile(client) {
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
displayName: user?.displayName,
|
displayName: user?.rawDisplayName,
|
||||||
avatarUrl: user && client && getAvatarUrl(client, user.avatarUrl),
|
avatarUrl: user && client && getAvatarUrl(client, user.avatarUrl),
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function FeedbackModal({ inCall, roomId, ...rest }) {
|
|||||||
description,
|
description,
|
||||||
sendLogs,
|
sendLogs,
|
||||||
rageshakeRequestId,
|
rageshakeRequestId,
|
||||||
|
roomId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (inCall && sendLogs) {
|
if (inCall && sendLogs) {
|
||||||
@@ -47,7 +48,7 @@ export function FeedbackModal({ inCall, roomId, ...rest }) {
|
|||||||
id="description"
|
id="description"
|
||||||
name="description"
|
name="description"
|
||||||
label="Description (optional)"
|
label="Description (optional)"
|
||||||
type="text"
|
type="textarea"
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
|
|||||||
@@ -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,9 +2,11 @@ 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, reload } = useLoadGroupCall(
|
||||||
client,
|
client,
|
||||||
roomId,
|
roomId,
|
||||||
viaServers
|
viaServers
|
||||||
@@ -20,6 +22,18 @@ 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} onReload={reload} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorView error={error} />;
|
return <ErrorView error={error} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ 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";
|
||||||
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
|
|
||||||
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 +49,10 @@ export function InCallView({
|
|||||||
showInspector,
|
showInspector,
|
||||||
roomId,
|
roomId,
|
||||||
}) {
|
}) {
|
||||||
const [layout, setLayout] = useVideoGridLayout();
|
usePreventScroll();
|
||||||
|
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
|
||||||
|
|
||||||
|
const { audioOutput } = useMediaHandler();
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const participants = [];
|
const participants = [];
|
||||||
@@ -57,7 +62,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 +85,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 +161,9 @@ export function InCallView({
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
getAvatar={renderAvatar}
|
getAvatar={renderAvatar}
|
||||||
|
showName={items.length > 2 || item.focused}
|
||||||
|
audioOutputDevice={audioOutput}
|
||||||
|
disableSpeakingIndicator={items.length < 3}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -186,7 +194,10 @@ export function InCallView({
|
|||||||
show={showInspector}
|
show={showInspector}
|
||||||
/>
|
/>
|
||||||
{rageshakeRequestModalState.isOpen && (
|
{rageshakeRequestModalState.isOpen && (
|
||||||
<RageshakeRequestModal {...rageshakeRequestModalProps} />
|
<RageshakeRequestModal
|
||||||
|
{...rageshakeRequestModalProps}
|
||||||
|
roomId={roomId}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ 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";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
|
|
||||||
export function LobbyView({
|
export function LobbyView({
|
||||||
client,
|
client,
|
||||||
@@ -32,7 +32,8 @@ export function LobbyView({
|
|||||||
roomId,
|
roomId,
|
||||||
}) {
|
}) {
|
||||||
const { stream } = useCallFeed(localCallFeed);
|
const { stream } = useCallFeed(localCallFeed);
|
||||||
const videoRef = useMediaStream(stream, true);
|
const { audioOutput } = useMediaHandler();
|
||||||
|
const videoRef = useMediaStream(stream, audioOutput, true);
|
||||||
const { displayName, avatarUrl } = useProfile(client);
|
const { displayName, avatarUrl } = useProfile(client);
|
||||||
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
const avatarSize = (previewBounds.height - 66) / 2;
|
const avatarSize = (previewBounds.height - 66) / 2;
|
||||||
@@ -86,7 +87,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>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export function OverflowMenu({
|
|||||||
roomId,
|
roomId,
|
||||||
setShowInspector,
|
setShowInspector,
|
||||||
showInspector,
|
showInspector,
|
||||||
client,
|
|
||||||
inCall,
|
inCall,
|
||||||
groupCall,
|
groupCall,
|
||||||
}) {
|
}) {
|
||||||
@@ -75,7 +74,6 @@ export function OverflowMenu({
|
|||||||
{...settingsModalProps}
|
{...settingsModalProps}
|
||||||
setShowInspector={setShowInspector}
|
setShowInspector={setShowInspector}
|
||||||
showInspector={showInspector}
|
showInspector={showInspector}
|
||||||
client={client}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{inviteModalState.isOpen && (
|
{inviteModalState.isOpen && (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { FieldRow, ErrorMessage } from "../input/Input";
|
|||||||
import { useSubmitRageshake } from "../settings/rageshake";
|
import { useSubmitRageshake } from "../settings/rageshake";
|
||||||
import { Body } from "../typography/Typography";
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
export function RageshakeRequestModal({ rageshakeRequestId, ...rest }) {
|
export function RageshakeRequestModal({ rageshakeRequestId, roomId, ...rest }) {
|
||||||
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -27,6 +27,7 @@ export function RageshakeRequestModal({ rageshakeRequestId, ...rest }) {
|
|||||||
submitRageshake({
|
submitRageshake({
|
||||||
sendLogs: true,
|
sendLogs: true,
|
||||||
rageshakeRequestId,
|
rageshakeRequestId,
|
||||||
|
roomId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={sending}
|
disabled={sending}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
74
src/room/RoomNotFoundView.jsx
Normal file
74
src/room/RoomNotFoundView.jsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
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, onReload }) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
await createRoom(client, roomName);
|
||||||
|
|
||||||
|
onReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
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%;
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import { ErrorView, LoadingView } from "../FullScreenView";
|
|||||||
import { RoomAuthView } from "./RoomAuthView";
|
import { RoomAuthView } from "./RoomAuthView";
|
||||||
import { GroupCallLoader } from "./GroupCallLoader";
|
import { GroupCallLoader } from "./GroupCallLoader";
|
||||||
import { GroupCallView } from "./GroupCallView";
|
import { GroupCallView } from "./GroupCallView";
|
||||||
|
import { MediaHandlerProvider } from "../settings/useMediaHandler";
|
||||||
|
|
||||||
export function RoomPage() {
|
export function RoomPage() {
|
||||||
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||||
@@ -32,7 +33,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 />;
|
||||||
@@ -47,16 +48,18 @@ export function RoomPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GroupCallLoader client={client} roomId={roomId} viaServers={viaServers}>
|
<MediaHandlerProvider client={client}>
|
||||||
{(groupCall) => (
|
<GroupCallLoader client={client} roomId={roomId} viaServers={viaServers}>
|
||||||
<GroupCallView
|
{(groupCall) => (
|
||||||
client={client}
|
<GroupCallView
|
||||||
roomId={roomId}
|
client={client}
|
||||||
groupCall={groupCall}
|
roomId={roomId}
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
groupCall={groupCall}
|
||||||
simpleGrid={simpleGrid}
|
isPasswordlessUser={isPasswordlessUser}
|
||||||
/>
|
simpleGrid={simpleGrid}
|
||||||
)}
|
/>
|
||||||
</GroupCallLoader>
|
)}
|
||||||
|
</GroupCallLoader>
|
||||||
|
</MediaHandlerProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("!")) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
async function fetchGroupCall(
|
async function fetchGroupCall(
|
||||||
client,
|
client,
|
||||||
@@ -41,14 +41,23 @@ export function useLoadGroupCall(client, roomId, viaServers) {
|
|||||||
loading: true,
|
loading: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
groupCall: undefined,
|
groupCall: undefined,
|
||||||
|
reloadId: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setState({ loading: true });
|
setState({ loading: true });
|
||||||
fetchGroupCall(client, roomId, viaServers, 30000)
|
fetchGroupCall(client, roomId, viaServers, 30000)
|
||||||
.then((groupCall) => setState({ loading: false, groupCall }))
|
.then((groupCall) =>
|
||||||
.catch((error) => setState({ loading: false, error }));
|
setState((prevState) => ({ ...prevState, loading: false, groupCall }))
|
||||||
}, [client, roomId]);
|
)
|
||||||
|
.catch((error) =>
|
||||||
|
setState((prevState) => ({ ...prevState, loading: false, error }))
|
||||||
|
);
|
||||||
|
}, [client, roomId, state.reloadId]);
|
||||||
|
|
||||||
return state;
|
const reload = useCallback(() => {
|
||||||
|
setState((prevState) => ({ ...prevState, reloadId: prevState.reloadId++ }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { ...state, reload };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,9 @@ 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({ setShowInspector, showInspector, ...rest }) {
|
||||||
client,
|
|
||||||
setShowInspector,
|
|
||||||
showInspector,
|
|
||||||
...rest
|
|
||||||
}) {
|
|
||||||
const {
|
const {
|
||||||
audioInput,
|
audioInput,
|
||||||
audioInputs,
|
audioInputs,
|
||||||
@@ -25,7 +21,10 @@ export function SettingsModal({
|
|||||||
videoInput,
|
videoInput,
|
||||||
videoInputs,
|
videoInputs,
|
||||||
setVideoInput,
|
setVideoInput,
|
||||||
} = useMediaHandler(client);
|
audioOutput,
|
||||||
|
audioOutputs,
|
||||||
|
setAudioOutput,
|
||||||
|
} = useMediaHandler();
|
||||||
|
|
||||||
const downloadDebugLog = useDownloadDebugLog();
|
const downloadDebugLog = useDownloadDebugLog();
|
||||||
|
|
||||||
@@ -55,6 +54,17 @@ export function SettingsModal({
|
|||||||
<Item key={deviceId}>{label}</Item>
|
<Item key={deviceId}>{label}</Item>
|
||||||
))}
|
))}
|
||||||
</SelectInput>
|
</SelectInput>
|
||||||
|
{audioOutputs.length > 0 && (
|
||||||
|
<SelectInput
|
||||||
|
label="Speaker"
|
||||||
|
selectedKey={audioOutput}
|
||||||
|
onSelectionChange={setAudioOutput}
|
||||||
|
>
|
||||||
|
{audioOutputs.map(({ deviceId, label }) => (
|
||||||
|
<Item key={deviceId}>{label}</Item>
|
||||||
|
))}
|
||||||
|
</SelectInput>
|
||||||
|
)}
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem
|
<TabItem
|
||||||
title={
|
title={
|
||||||
@@ -82,6 +92,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,15 +41,22 @@ 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);
|
||||||
|
|
||||||
if (client) {
|
if (client) {
|
||||||
|
const userId = client.getUserId();
|
||||||
|
const user = client.getUser(userId);
|
||||||
|
body.append("display_name", user?.displayName);
|
||||||
body.append("user_id", client.credentials.userId);
|
body.append("user_id", client.credentials.userId);
|
||||||
body.append("device_id", client.deviceId);
|
body.append("device_id", client.deviceId);
|
||||||
|
|
||||||
|
if (opts.roomId) {
|
||||||
|
body.append("room_id", opts.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
if (client.isCryptoEnabled()) {
|
if (client.isCryptoEnabled()) {
|
||||||
const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
|
const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
|
||||||
if (client.getDeviceCurve25519Key) {
|
if (client.getDeviceCurve25519Key) {
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
|
||||||
|
|
||||||
export function useMediaHandler(client) {
|
|
||||||
const [{ audioInput, videoInput, audioInputs, videoInputs }, setState] =
|
|
||||||
useState(() => {
|
|
||||||
const mediaHandler = client.getMediaHandler();
|
|
||||||
|
|
||||||
return {
|
|
||||||
audioInput: mediaHandler.audioInput,
|
|
||||||
videoInput: mediaHandler.videoInput,
|
|
||||||
audioInputs: [],
|
|
||||||
videoInputs: [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const mediaHandler = client.getMediaHandler();
|
|
||||||
|
|
||||||
function updateDevices() {
|
|
||||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
|
||||||
const audioInputs = devices.filter(
|
|
||||||
(device) => device.kind === "audioinput"
|
|
||||||
);
|
|
||||||
const videoInputs = devices.filter(
|
|
||||||
(device) => device.kind === "videoinput"
|
|
||||||
);
|
|
||||||
|
|
||||||
setState(() => ({
|
|
||||||
audioInput: mediaHandler.audioInput,
|
|
||||||
videoInput: mediaHandler.videoInput,
|
|
||||||
audioInputs,
|
|
||||||
videoInputs,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDevices();
|
|
||||||
|
|
||||||
mediaHandler.on("local_streams_changed", updateDevices);
|
|
||||||
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mediaHandler.removeListener("local_streams_changed", updateDevices);
|
|
||||||
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setAudioInput = useCallback(
|
|
||||||
(deviceId) => {
|
|
||||||
setState((prevState) => ({ ...prevState, audioInput: deviceId }));
|
|
||||||
client.getMediaHandler().setAudioInput(deviceId);
|
|
||||||
},
|
|
||||||
[client]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setVideoInput = useCallback(
|
|
||||||
(deviceId) => {
|
|
||||||
setState((prevState) => ({ ...prevState, videoInput: deviceId }));
|
|
||||||
client.getMediaHandler().setVideoInput(deviceId);
|
|
||||||
},
|
|
||||||
[client]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
audioInput,
|
|
||||||
audioInputs,
|
|
||||||
setAudioInput,
|
|
||||||
videoInput,
|
|
||||||
videoInputs,
|
|
||||||
setVideoInput,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
203
src/settings/useMediaHandler.jsx
Normal file
203
src/settings/useMediaHandler.jsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import React, {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useContext,
|
||||||
|
createContext,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
const MediaHandlerContext = createContext();
|
||||||
|
|
||||||
|
function getMediaPreferences() {
|
||||||
|
const mediaPreferences = localStorage.getItem("matrix-media-preferences");
|
||||||
|
|
||||||
|
if (mediaPreferences) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(mediaPreferences);
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMediaPreferences(newPreferences) {
|
||||||
|
const oldPreferences = getMediaPreferences(newPreferences);
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
"matrix-media-preferences",
|
||||||
|
JSON.stringify({
|
||||||
|
...oldPreferences,
|
||||||
|
...newPreferences,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MediaHandlerProvider({ client, children }) {
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
audioInput,
|
||||||
|
videoInput,
|
||||||
|
audioInputs,
|
||||||
|
videoInputs,
|
||||||
|
audioOutput,
|
||||||
|
audioOutputs,
|
||||||
|
},
|
||||||
|
setState,
|
||||||
|
] = useState(() => {
|
||||||
|
const mediaPreferences = getMediaPreferences();
|
||||||
|
const mediaHandler = client.getMediaHandler();
|
||||||
|
|
||||||
|
mediaHandler.restoreMediaSettings(
|
||||||
|
mediaPreferences?.audioInput,
|
||||||
|
mediaPreferences?.videoInput
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
audioInput: mediaHandler.audioInput,
|
||||||
|
videoInput: mediaHandler.videoInput,
|
||||||
|
audioOutput: undefined,
|
||||||
|
audioInputs: [],
|
||||||
|
videoInputs: [],
|
||||||
|
audioOutputs: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaHandler = client.getMediaHandler();
|
||||||
|
|
||||||
|
function updateDevices() {
|
||||||
|
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||||
|
const mediaPreferences = getMediaPreferences();
|
||||||
|
|
||||||
|
const audioInputs = devices.filter(
|
||||||
|
(device) => device.kind === "audioinput"
|
||||||
|
);
|
||||||
|
const audioConnected = audioInputs.some(
|
||||||
|
(device) => device.deviceId === mediaHandler.audioInput
|
||||||
|
);
|
||||||
|
|
||||||
|
let audioInput = mediaHandler.audioInput;
|
||||||
|
|
||||||
|
if (!audioConnected && audioInputs.length > 0) {
|
||||||
|
audioInput = audioInputs[0].deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoInputs = devices.filter(
|
||||||
|
(device) => device.kind === "videoinput"
|
||||||
|
);
|
||||||
|
const videoConnected = videoInputs.some(
|
||||||
|
(device) => device.deviceId === mediaHandler.videoInput
|
||||||
|
);
|
||||||
|
|
||||||
|
let videoInput = mediaHandler.videoInput;
|
||||||
|
|
||||||
|
if (!videoConnected && videoInputs.length > 0) {
|
||||||
|
videoInput = videoInputs[0].deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioOutputs = devices.filter(
|
||||||
|
(device) => device.kind === "audiooutput"
|
||||||
|
);
|
||||||
|
let audioOutput = undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
mediaPreferences &&
|
||||||
|
audioOutputs.some(
|
||||||
|
(device) => device.deviceId === mediaPreferences.audioOutput
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
audioOutput = mediaPreferences.audioOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
mediaHandler.videoInput !== videoInput ||
|
||||||
|
mediaHandler.audioInput !== audioInput
|
||||||
|
) {
|
||||||
|
mediaHandler.setMediaInputs(audioInput, videoInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMediaPreferences({ audioInput, videoInput, audioOutput });
|
||||||
|
|
||||||
|
setState({
|
||||||
|
audioInput,
|
||||||
|
videoInput,
|
||||||
|
audioOutput,
|
||||||
|
audioInputs,
|
||||||
|
videoInputs,
|
||||||
|
audioOutputs,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateDevices();
|
||||||
|
|
||||||
|
mediaHandler.on("local_streams_changed", updateDevices);
|
||||||
|
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaHandler.removeListener("local_streams_changed", updateDevices);
|
||||||
|
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
|
||||||
|
mediaHandler.stopAllStreams();
|
||||||
|
};
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
|
const setAudioInput = useCallback(
|
||||||
|
(deviceId) => {
|
||||||
|
updateMediaPreferences({ audioInput: deviceId });
|
||||||
|
setState((prevState) => ({ ...prevState, audioInput: deviceId }));
|
||||||
|
client.getMediaHandler().setAudioInput(deviceId);
|
||||||
|
},
|
||||||
|
[client]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setVideoInput = useCallback(
|
||||||
|
(deviceId) => {
|
||||||
|
updateMediaPreferences({ videoInput: deviceId });
|
||||||
|
setState((prevState) => ({ ...prevState, videoInput: deviceId }));
|
||||||
|
client.getMediaHandler().setVideoInput(deviceId);
|
||||||
|
},
|
||||||
|
[client]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setAudioOutput = useCallback((deviceId) => {
|
||||||
|
updateMediaPreferences({ audioOutput: deviceId });
|
||||||
|
setState((prevState) => ({ ...prevState, audioOutput: deviceId }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const context = useMemo(
|
||||||
|
() => ({
|
||||||
|
audioInput,
|
||||||
|
audioInputs,
|
||||||
|
setAudioInput,
|
||||||
|
videoInput,
|
||||||
|
videoInputs,
|
||||||
|
setVideoInput,
|
||||||
|
audioOutput,
|
||||||
|
audioOutputs,
|
||||||
|
setAudioOutput,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
audioInput,
|
||||||
|
audioInputs,
|
||||||
|
setAudioInput,
|
||||||
|
videoInput,
|
||||||
|
videoInputs,
|
||||||
|
setVideoInput,
|
||||||
|
audioOutput,
|
||||||
|
audioOutputs,
|
||||||
|
setAudioOutput,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MediaHandlerContext.Provider value={context}>
|
||||||
|
{children}
|
||||||
|
</MediaHandlerContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMediaHandler() {
|
||||||
|
return useContext(MediaHandlerContext);
|
||||||
|
}
|
||||||
@@ -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,13 @@ 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}
|
||||||
|
disableSpeakingIndicator={items.length < 3}
|
||||||
|
{...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