minty/src/app/molecules/room-aliases/RoomAliases.jsx
Ajay Bura 20db27fa7e
feat: URL navigation in auth (#1603)
* bump to react 18 and install react-router-dom

* Upgrade to react 18 root

* update vite

* add cs api's

* convert state/auth to ts

* add client config context

* add auto discovery context

* add spec version context

* add auth flow context

* add background dot pattern css

* add promise utils

* init url based routing

* update auth route server path as effect

* add auth server hook

* always use server from discovery info in context

* login - WIP

* upgrade jotai to v2

* add atom with localStorage util

* add multi account sessions atom

* add default IGNORE res to auto discovery

* add error type in async callback hook

* handle password login error

* fix async callback hook

* allow password login

* Show custom server not allowed error in mxId login

* add sso login component

* add token login

* fix hardcoded m.login.password in login func

* update server input on url change

* Improve sso login labels

* update folds

* fix async callback batching state update in safari

* wrap async callback set state in queueMicrotask

* wip

* wip - register

* arrange auth file structure

* add error codes

* extract filed error component form password login

* add register util function

* handle register flow - WIP

* update unsupported auth flow method reasons

* improve password input styles

* Improve UIA flow next stage calculation
complete stages can have any order so we will look for first stage which is not in completed

* process register UIA flow stages

* Extract register UIA stages component

* improve register error messages

* add focus trap & step count in UIA stages

* add reset password path and path utils

* add path with origin hook

* fix sso redirect url

* rename register token query param to token

* restyle auth screen header

* add reset password component - WIP

* add reset password form

* add netlify rewrites

* fix netlify file indentation

* test netlify redirect

* fix vite to include netlify toml

* add more netlify redirects

* add splat to public and assets path

* fix vite base name

* add option to use hash router in config and remove appVersion

* add splash screen component

* add client config loading and error screen

* fix server picker bug

* fix reset password email input type

* make auth page small screen responsive

* fix typo in reset password screen
2024-01-21 18:20:56 +05:30

358 lines
12 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './RoomAliases.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { Debounce } from '../../../util/common';
import { isRoomAliasAvailable } from '../../../util/matrixUtil';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Checkbox from '../../atoms/button/Checkbox';
import Toggle from '../../atoms/button/Toggle';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import SettingTile from '../setting-tile/SettingTile';
import { useStore } from '../../hooks/useStore';
function useValidate(hsString) {
const [debounce] = useState(new Debounce());
const [validate, setValidate] = useState({ alias: null, status: cons.status.PRE_FLIGHT });
const setValidateToDefault = () => {
setValidate({
alias: null,
status: cons.status.PRE_FLIGHT,
});
};
const checkValueOK = (value) => {
if (value.trim() === '') {
setValidateToDefault();
return false;
}
if (!value.match(/^[a-zA-Z0-9_-]+$/)) {
setValidate({
alias: null,
status: cons.status.ERROR,
msg: 'Invalid character: only letter, numbers and _- are allowed.',
});
return false;
}
return true;
};
const handleAliasChange = (e) => {
const input = e.target;
if (validate.status !== cons.status.PRE_FLIGHT) {
setValidateToDefault();
}
if (checkValueOK(input.value) === false) return;
debounce._(async () => {
const { value } = input;
const alias = `#${value}:${hsString}`;
if (checkValueOK(value) === false) return;
setValidate({
alias,
status: cons.status.IN_FLIGHT,
msg: `validating ${alias}...`,
});
const isValid = await isRoomAliasAvailable(alias);
setValidate(() => {
if (e.target.value !== value) {
return { alias: null, status: cons.status.PRE_FLIGHT };
}
return {
alias,
status: isValid ? cons.status.SUCCESS : cons.status.ERROR,
msg: isValid ? `${alias} is available.` : `${alias} is already in use.`,
};
});
}, 600)();
};
return [validate, setValidateToDefault, handleAliasChange];
}
function getAliases(roomId) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
const main = room.getCanonicalAlias();
const published = room.getAltAliases();
if (main && !published.includes(main)) published.splice(0, 0, main);
return {
main,
published: [...new Set(published)],
local: [],
};
}
function RoomAliases({ roomId }) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
const userId = mx.getUserId();
const hsString = userId.slice(userId.indexOf(':') + 1);
const isMountedStore = useStore();
const [isPublic, setIsPublic] = useState(false);
const [isLocalVisible, setIsLocalVisible] = useState(false);
const [aliases, setAliases] = useState(getAliases(roomId));
const [selectedAlias, setSelectedAlias] = useState(null);
const [deleteAlias, setDeleteAlias] = useState(null);
const [validate, setValidateToDefault, handleAliasChange] = useValidate(hsString);
const canPublishAlias = room.currentState.maySendStateEvent('m.room.canonical_alias', userId);
useEffect(() => {
isMountedStore.setItem(true)
}, []);
useEffect(() => {
let isUnmounted = false;
const loadLocalAliases = async () => {
let local = [];
try {
const result = await mx.getLocalAliases(roomId);
local = result.aliases.filter((alias) => !aliases.published.includes(alias));
} catch {
local = [];
}
aliases.local = [...new Set(local.reverse())];
if (isUnmounted) return;
setAliases({ ...aliases });
};
const loadVisibility = async () => {
const result = await mx.getRoomDirectoryVisibility(roomId);
if (isUnmounted) return;
setIsPublic(result.visibility === 'public');
};
loadLocalAliases();
loadVisibility();
return () => {
isUnmounted = true;
};
}, [roomId]);
const toggleDirectoryVisibility = () => {
mx.setRoomDirectoryVisibility(roomId, isPublic ? 'private' : 'public');
setIsPublic(!isPublic);
};
const handleAliasSubmit = async (e) => {
e.preventDefault();
if (validate.status === cons.status.ERROR) return;
if (!validate.alias) return;
const { alias } = validate;
const aliasInput = e.target.elements['alias-input'];
aliasInput.value = '';
setValidateToDefault();
try {
aliases.local.push(alias);
setAliases({ ...aliases });
await mx.createAlias(alias, roomId);
} catch {
if (isMountedStore.getItem()) {
const lIndex = alias.local.indexOf(alias);
if (lIndex === -1) return;
aliases.local.splice(lIndex, 1);
setAliases({ ...aliases });
}
}
};
const handleAliasSelect = (alias) => {
setSelectedAlias(alias === selectedAlias ? null : alias);
};
const handlePublishAlias = (alias) => {
const { main, published } = aliases;
let { local } = aliases;
if (!published.includes(aliases)) {
published.push(alias);
local = local.filter((al) => al !== alias);
mx.sendStateEvent(roomId, 'm.room.canonical_alias', {
alias: main,
alt_aliases: published.filter((al) => al !== main),
});
setAliases({ main, published, local });
setSelectedAlias(null);
}
};
const handleUnPublishAlias = (alias) => {
let { main, published } = aliases;
const { local } = aliases;
if (published.includes(alias) || main === alias) {
if (main === alias) main = null;
published = published.filter((al) => al !== alias);
local.push(alias);
mx.sendStateEvent(roomId, 'm.room.canonical_alias', {
alias: main,
alt_aliases: published.filter((al) => al !== main),
});
setAliases({ main, published, local });
setSelectedAlias(null);
}
};
const handleSetMainAlias = (alias) => {
let { main, local } = aliases;
const { published } = aliases;
if (main !== alias) {
main = alias;
if (!published.includes(alias)) published.splice(0, 0, alias);
local = local.filter((al) => al !== alias);
mx.sendStateEvent(roomId, 'm.room.canonical_alias', {
alias: main,
alt_aliases: published.filter((al) => al !== main),
});
setAliases({ main, published, local });
setSelectedAlias(null);
}
};
const handleDeleteAlias = async (alias) => {
try {
setDeleteAlias({ alias, status: cons.status.IN_FLIGHT, msg: 'deleting...' });
await mx.deleteAlias(alias);
let { main, published, local } = aliases;
if (published.includes(alias)) {
handleUnPublishAlias(alias);
if (main === alias) main = null;
published = published.filter((al) => al !== alias);
}
local = local.filter((al) => al !== alias);
setAliases({ main, published, local });
setDeleteAlias(null);
setSelectedAlias(null);
} catch (err) {
setDeleteAlias({ alias, status: cons.status.ERROR, msg: err.message });
}
};
const renderAliasBtns = (alias) => {
const isPublished = aliases.published.includes(alias);
const isMain = aliases.main === alias;
if (deleteAlias?.alias === alias) {
const isError = deleteAlias.status === cons.status.ERROR;
return (
<div className="room-aliases__item-btns">
<Text variant="b2">
<span style={{ color: isError ? 'var(--tc-danger-high' : 'inherit' }}>{deleteAlias.msg}</span>
</Text>
</div>
);
}
return (
<div className="room-aliases__item-btns">
{canPublishAlias && !isMain && <Button onClick={() => handleSetMainAlias(alias)} variant="primary">Set as Main</Button>}
{!isPublished && canPublishAlias && <Button onClick={() => handlePublishAlias(alias)} variant="positive">Publish</Button>}
{isPublished && canPublishAlias && <Button onClick={() => handleUnPublishAlias(alias)} variant="caution">Un-Publish</Button>}
<Button onClick={() => handleDeleteAlias(alias)} variant="danger">Delete</Button>
</div>
);
};
const renderAlias = (alias) => {
const isActive = selectedAlias === alias;
const disabled = !canPublishAlias && aliases.published.includes(alias);
const isMain = aliases.main === alias;
return (
<React.Fragment key={`${alias}-wrapper`}>
<div className="room-aliases__alias-item" key={alias}>
<Checkbox variant="positive" disabled={disabled} isActive={isActive} onToggle={() => handleAliasSelect(alias)} />
<Text>
{alias}
{isMain && <span>Main</span>}
</Text>
</div>
{isActive && renderAliasBtns(alias)}
</React.Fragment>
);
};
let inputState = 'normal';
if (validate.status === cons.status.ERROR) inputState = 'error';
if (validate.status === cons.status.SUCCESS) inputState = 'success';
return (
<div className="room-aliases">
<SettingTile
title="Publish to room directory"
content={<Text variant="b3">{`Publish this ${room.isSpaceRoom() ? 'space' : 'room'} to the ${hsString}'s public room directory?`}</Text>}
options={(
<Toggle
isActive={isPublic}
onToggle={toggleDirectoryVisibility}
disabled={!canPublishAlias}
/>
)}
/>
<div className="room-aliases__content">
<MenuHeader>Published addresses</MenuHeader>
{(aliases.published.length === 0) && <Text className="room-aliases__message">No published addresses</Text>}
{(aliases.published.length > 0 && !aliases.main) && <Text className="room-aliases__message">No Main address (select one from below)</Text>}
{aliases.published.map(renderAlias)}
<Text className="room-aliases__message" variant="b3">
{`Published addresses can be used by anyone on any server to join your ${room.isSpaceRoom() ? 'space' : 'room'}. To publish an address, it needs to be set as a local address first.`}
</Text>
</div>
{ isLocalVisible && (
<div className="room-aliases__content">
<MenuHeader>Local addresses</MenuHeader>
{(aliases.local.length === 0) && <Text className="room-aliases__message">No local addresses</Text>}
{aliases.local.map(renderAlias)}
<Text className="room-aliases__message" variant="b3">
{`Set local addresses for this ${room.isSpaceRoom() ? 'space' : 'room'} so users can find this ${room.isSpaceRoom() ? 'space' : 'room'} through your homeserver.`}
</Text>
<Text className="room-aliases__form-label" variant="b2">Add local address</Text>
<form className="room-aliases__form" onSubmit={handleAliasSubmit}>
<div className="room-aliases__input-wrapper">
<Input
name="alias-input"
state={inputState}
onChange={handleAliasChange}
placeholder={`my_${room.isSpaceRoom() ? 'space' : 'room'}_address`}
required
/>
</div>
<Button variant="primary" type="submit">Add</Button>
</form>
<div className="room-aliases__input-status">
{validate.status === cons.status.SUCCESS && <Text className="room-aliases__valid" variant="b2">{validate.msg}</Text>}
{validate.status === cons.status.ERROR && <Text className="room-aliases__invalid" variant="b2">{validate.msg}</Text>}
</div>
</div>
)}
<div className="room-aliases__content">
<Button onClick={() => setIsLocalVisible(!isLocalVisible)}>
{`${isLocalVisible ? 'Hide' : 'Add / View'} local address`}
</Button>
</div>
</div>
);
}
RoomAliases.propTypes = {
roomId: PropTypes.string.isRequired,
};
export default RoomAliases;