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 (
{deleteAlias.msg}
); } return (
{canPublishAlias && !isMain && } {!isPublished && canPublishAlias && } {isPublished && canPublishAlias && }
); }; const renderAlias = (alias) => { const isActive = selectedAlias === alias; const disabled = !canPublishAlias && aliases.published.includes(alias); const isMain = aliases.main === alias; return (
handleAliasSelect(alias)} /> {alias} {isMain && Main}
{isActive && renderAliasBtns(alias)}
); }; let inputState = 'normal'; if (validate.status === cons.status.ERROR) inputState = 'error'; if (validate.status === cons.status.SUCCESS) inputState = 'success'; return (
{`Publish this ${room.isSpaceRoom() ? 'space' : 'room'} to the ${hsString}'s public room directory?`}} options={( )} />
Published addresses {(aliases.published.length === 0) && No published addresses} {(aliases.published.length > 0 && !aliases.main) && No Main address (select one from below)} {aliases.published.map(renderAlias)} {`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.`}
{ isLocalVisible && (
Local addresses {(aliases.local.length === 0) && No local addresses} {aliases.local.map(renderAlias)} {`Set local addresses for this ${room.isSpaceRoom() ? 'space' : 'room'} so users can find this ${room.isSpaceRoom() ? 'space' : 'room'} through your homeserver.`} Add local address
{validate.status === cons.status.SUCCESS && {validate.msg}} {validate.status === cons.status.ERROR && {validate.msg}}
)}
); } RoomAliases.propTypes = { roomId: PropTypes.string.isRequired, }; export default RoomAliases;