diff --git a/src/app/atoms/context-menu/ContextMenu.jsx b/src/app/atoms/context-menu/ContextMenu.jsx index 99dd9e6..7d1acd4 100644 --- a/src/app/atoms/context-menu/ContextMenu.jsx +++ b/src/app/atoms/context-menu/ContextMenu.jsx @@ -91,16 +91,17 @@ function MenuItem({ MenuItem.defaultProps = { variant: 'surface', - iconSrc: 'none', + iconSrc: null, type: 'button', disabled: false, + onClick: null, }; MenuItem.propTypes = { variant: PropTypes.oneOf(['surface', 'positive', 'caution', 'danger']), iconSrc: PropTypes.string, type: PropTypes.oneOf(['button', 'submit']), - onClick: PropTypes.func.isRequired, + onClick: PropTypes.func, children: PropTypes.node.isRequired, disabled: PropTypes.bool, }; diff --git a/src/app/molecules/room-aliases/RoomAliases.jsx b/src/app/molecules/room-aliases/RoomAliases.jsx new file mode 100644 index 0000000..f8896eb --- /dev/null +++ b/src/app/molecules/room-aliases/RoomAliases.jsx @@ -0,0 +1,352 @@ +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 { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu'; +import SettingTile from '../../molecules/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.unstableGetLocalAliases(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 ( +