/* eslint-disable react/prop-types */ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import './SpaceManage.scss'; import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; import colorMXID from '../../../util/colorMXID'; import { selectRoom, selectTab } from '../../../client/action/navigation'; import RoomsHierarchy from '../../../client/state/RoomsHierarchy'; import { joinRuleToIconSrc } from '../../../util/matrixUtil'; import { join } from '../../../client/action/room'; import { Debounce } from '../../../util/common'; import Text from '../../atoms/text/Text'; import RawIcon from '../../atoms/system-icons/RawIcon'; import Button from '../../atoms/button/Button'; import IconButton from '../../atoms/button/IconButton'; import Checkbox from '../../atoms/button/Checkbox'; import Avatar from '../../atoms/avatar/Avatar'; import Spinner from '../../atoms/spinner/Spinner'; import ScrollView from '../../atoms/scroll/ScrollView'; import PopupWindow from '../../molecules/popup-window/PopupWindow'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg'; import InfoIC from '../../../../public/res/ic/outlined/info.svg'; import { useForceUpdate } from '../../hooks/useForceUpdate'; import { useStore } from '../../hooks/useStore'; function SpaceManageBreadcrumb({ path, onSelect }) { return (
{ path.map((item, index) => ( {index > 0 && } )) }
); } SpaceManageBreadcrumb.propTypes = { path: PropTypes.arrayOf(PropTypes.exact({ roomId: PropTypes.string, name: PropTypes.string, })).isRequired, onSelect: PropTypes.func.isRequired, }; function SpaceManageItem({ parentId, roomInfo, onSpaceClick, requestClose, isSelected, onSelect, roomHierarchy, }) { const [isExpand, setIsExpand] = useState(false); const [isJoining, setIsJoining] = useState(false); const { directs } = initMatrix.roomList; const mx = initMatrix.matrixClient; const parentRoom = mx.getRoom(parentId); const isSpace = roomInfo.room_type === 'm.space'; const roomId = roomInfo.room_id; const canManage = parentRoom?.currentState.maySendStateEvent('m.space.child', mx.getUserId()) || false; const isSuggested = parentRoom?.currentState.getStateEvents('m.space.child', roomId)?.getContent().suggested === true; const room = mx.getRoom(roomId); const isJoined = !!(room?.getMyMembership() === 'join' || null); const name = room?.name || roomInfo.name || roomInfo.canonical_alias || roomId; let imageSrc = mx.mxcUrlToHttp(roomInfo.avatar_url, 24, 24, 'crop') || null; if (!imageSrc && room) { imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; } const isDM = directs.has(roomId); const handleOpen = () => { if (isSpace) selectTab(roomId); else selectRoom(roomId); requestClose(); }; const handleJoin = () => { const viaSet = roomHierarchy.viaMap.get(roomId); const via = viaSet ? [...viaSet] : undefined; join(roomId, false, via); setIsJoining(true); }; const roomAvatarJSX = ( ); const roomNameJSX = ( {twemojify(name)} {` • ${roomInfo.num_joined_members} members`} ); const expandBtnJsx = ( setIsExpand(!isExpand)} /> ); return (
{canManage && onSelect(roomId)} variant="positive" />} {roomInfo.topic && expandBtnJsx} { isJoined ? : }
{isExpand && roomInfo.topic && {twemojify(roomInfo.topic, undefined, true)}}
); } SpaceManageItem.propTypes = { parentId: PropTypes.string.isRequired, roomHierarchy: PropTypes.shape({}).isRequired, roomInfo: PropTypes.shape({}).isRequired, onSpaceClick: PropTypes.func.isRequired, requestClose: PropTypes.func.isRequired, isSelected: PropTypes.bool.isRequired, onSelect: PropTypes.func.isRequired, }; function SpaceManageFooter({ parentId, selected }) { const [process, setProcess] = useState(null); const mx = initMatrix.matrixClient; const room = mx.getRoom(parentId); const { currentState } = room; const allSuggested = selected.every((roomId) => { const sEvent = currentState.getStateEvents('m.space.child', roomId); return !!sEvent?.getContent()?.suggested; }); const handleRemove = () => { setProcess(`Removing ${selected.length} items`); selected.forEach((roomId) => { mx.sendStateEvent(parentId, 'm.space.child', {}, roomId); }); }; const handleToggleSuggested = (isMark) => { if (isMark) setProcess(`Marking as suggested ${selected.length} items`); else setProcess(`Marking as not suggested ${selected.length} items`); selected.forEach((roomId) => { const sEvent = room.currentState.getStateEvents('m.space.child', roomId); if (!sEvent) return; const content = { ...sEvent.getContent() }; if (isMark && content.suggested) return; if (!isMark && !content.suggested) return; content.suggested = isMark; mx.sendStateEvent(parentId, 'm.space.child', content, roomId); }); }; return (
{process && } {process || `${selected.length} item selected`} { !process && ( <> )}
); } SpaceManageFooter.propTypes = { parentId: PropTypes.string.isRequired, selected: PropTypes.arrayOf(PropTypes.string).isRequired, }; function useSpacePath(roomId) { const mx = initMatrix.matrixClient; const room = mx.getRoom(roomId); const [spacePath, setSpacePath] = useState([{ roomId, name: room.name }]); const addPathItem = (rId, name) => { const newPath = [...spacePath]; const itemIndex = newPath.findIndex((item) => item.roomId === rId); if (itemIndex < 0) { newPath.push({ roomId: rId, name }); setSpacePath(newPath); return; } newPath.splice(itemIndex + 1); setSpacePath(newPath); }; return [spacePath, addPathItem]; } function useUpdateOnJoin(roomId) { const [, forceUpdate] = useForceUpdate(); const { roomList } = initMatrix; useEffect(() => { const handleRoomList = () => forceUpdate(); roomList.on(cons.events.roomList.ROOM_JOINED, handleRoomList); roomList.on(cons.events.roomList.ROOM_LEAVED, handleRoomList); return () => { roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleRoomList); roomList.removeListener(cons.events.roomList.ROOM_LEAVED, handleRoomList); }; }, [roomId]); } function useChildUpdate(roomId, roomsHierarchy) { const [, forceUpdate] = useForceUpdate(); const [debounce] = useState(new Debounce()); const mx = initMatrix.matrixClient; useEffect(() => { let isMounted = true; const handleStateEvent = (event) => { if (event.getRoomId() !== roomId) return; if (event.getType() !== 'm.space.child') return; debounce._(() => { if (!isMounted) return; roomsHierarchy.removeHierarchy(roomId); forceUpdate(); }, 500)(); }; mx.on('RoomState.events', handleStateEvent); return () => { isMounted = false; mx.removeListener('RoomState.events', handleStateEvent); }; }, [roomId, roomsHierarchy]); } function SpaceManageContent({ roomId, requestClose }) { const mx = initMatrix.matrixClient; useUpdateOnJoin(roomId); const [, forceUpdate] = useForceUpdate(); const [roomsHierarchy] = useState(new RoomsHierarchy(mx, 30)); const [spacePath, addPathItem] = useSpacePath(roomId); const [isLoading, setIsLoading] = useState(true); const [selected, setSelected] = useState([]); const mountStore = useStore(); const currentPath = spacePath[spacePath.length - 1]; useChildUpdate(currentPath.roomId, roomsHierarchy); const currentHierarchy = roomsHierarchy.getHierarchy(currentPath.roomId); useEffect(() => { mountStore.setItem(true); return () => { mountStore.setItem(false); }; }, [roomId]); useEffect(() => setSelected([]), [spacePath]); const handleSelected = (selectedRoomId) => { const newSelected = [...selected]; const selectedIndex = newSelected.indexOf(selectedRoomId); if (selectedIndex > -1) { newSelected.splice(selectedIndex, 1); setSelected(newSelected); return; } newSelected.push(selectedRoomId); setSelected(newSelected); }; const loadRoomHierarchy = async () => { if (!roomsHierarchy.canLoadMore(currentPath.roomId)) return; if (!roomsHierarchy.getHierarchy(currentPath.roomId)) setSelected([]); setIsLoading(true); try { await roomsHierarchy.load(currentPath.roomId); if (!mountStore.getItem()) return; setIsLoading(false); forceUpdate(); } catch { if (!mountStore.getItem()) return; setIsLoading(false); forceUpdate(); } }; if (!currentHierarchy) loadRoomHierarchy(); return (
{spacePath.length > 1 && ( )} Rooms and spaces
{!isLoading && currentHierarchy?.rooms?.length === 1 && ( Either the space contains private rooms or you need to join space to view it's rooms. )} {currentHierarchy && (currentHierarchy.rooms?.map((roomInfo) => ( roomInfo.room_id === currentPath.roomId ? null : ( ) )))} {!currentHierarchy && loading...}
{currentHierarchy?.canLoadMore && !isLoading && ( )} {isLoading && (
Loading rooms...
)} {selected.length > 0 && ( )}
); } SpaceManageContent.propTypes = { roomId: PropTypes.string.isRequired, requestClose: PropTypes.func.isRequired, }; function useWindowToggle() { const [roomId, setRoomId] = useState(null); useEffect(() => { const openSpaceManage = (rId) => { setRoomId(rId); }; navigation.on(cons.events.navigation.SPACE_MANAGE_OPENED, openSpaceManage); return () => { navigation.removeListener(cons.events.navigation.SPACE_MANAGE_OPENED, openSpaceManage); }; }, []); const requestClose = () => setRoomId(null); return [roomId, requestClose]; } function SpaceManage() { const mx = initMatrix.matrixClient; const [roomId, requestClose] = useWindowToggle(); const room = mx.getRoom(roomId); return ( {roomId && twemojify(room.name)} — manage rooms )} contentOptions={} onRequestClose={requestClose} > { roomId ? :
} ); } export default SpaceManage;