import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import './ProfileViewer.scss'; import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; import { selectRoom, openReusableContextMenu } from '../../../client/action/navigation'; import * as roomActions from '../../../client/action/room'; import { getUsername, getUsernameOfRoomMember, getPowerLabel, hasDMWith } from '../../../util/matrixUtil'; import { getEventCords } from '../../../util/common'; import colorMXID from '../../../util/colorMXID'; import Text from '../../atoms/text/Text'; import Chip from '../../atoms/chip/Chip'; import IconButton from '../../atoms/button/IconButton'; import Input from '../../atoms/input/Input'; import Avatar from '../../atoms/avatar/Avatar'; import Button from '../../atoms/button/Button'; import { MenuItem } from '../../atoms/context-menu/ContextMenu'; import PowerLevelSelector from '../../molecules/power-level-selector/PowerLevelSelector'; import Dialog from '../../molecules/dialog/Dialog'; import ShieldEmptyIC from '../../../../public/res/ic/outlined/shield-empty.svg'; import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg'; import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import { useForceUpdate } from '../../hooks/useForceUpdate'; import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; function ModerationTools({ roomId, userId, }) { const mx = initMatrix.matrixClient; const room = mx.getRoom(roomId); const roomMember = room.getMember(userId); const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0; const powerLevel = roomMember?.powerLevel || 0; const canIKick = ( roomMember?.membership === 'join' && room.currentState.hasSufficientPowerLevelFor('kick', myPowerLevel) && powerLevel < myPowerLevel ); const canIBan = ( ['join', 'leave'].includes(roomMember?.membership) && room.currentState.hasSufficientPowerLevelFor('ban', myPowerLevel) && powerLevel < myPowerLevel ); const handleKick = (e) => { e.preventDefault(); const kickReason = e.target.elements['kick-reason']?.value.trim(); roomActions.kick(roomId, userId, kickReason !== '' ? kickReason : undefined); }; const handleBan = (e) => { e.preventDefault(); const banReason = e.target.elements['ban-reason']?.value.trim(); roomActions.ban(roomId, userId, banReason !== '' ? banReason : undefined); }; return (
{canIKick && (
)} {canIBan && (
)}
); } ModerationTools.propTypes = { roomId: PropTypes.string.isRequired, userId: PropTypes.string.isRequired, }; function SessionInfo({ userId }) { const [devices, setDevices] = useState(null); const [isVisible, setIsVisible] = useState(false); const mx = initMatrix.matrixClient; useEffect(() => { let isUnmounted = false; async function loadDevices() { try { await mx.downloadKeys([userId], true); const myDevices = mx.getStoredDevicesForUser(userId); if (isUnmounted) return; setDevices(myDevices); } catch { setDevices([]); } } loadDevices(); return () => { isUnmounted = true; }; }, [userId]); function renderSessionChips() { if (!isVisible) return null; return (
{devices === null && Loading sessions...} {devices?.length === 0 && No session found.} {devices !== null && (devices.map((device) => ( )))}
); } return (
setIsVisible(!isVisible)} iconSrc={isVisible ? ChevronBottomIC : ChevronRightIC} > {`View ${devices?.length > 0 ? `${devices.length} ` : ''}sessions`} {renderSessionChips()}
); } SessionInfo.propTypes = { userId: PropTypes.string.isRequired, }; function ProfileFooter({ roomId, userId, onRequestClose }) { const [isCreatingDM, setIsCreatingDM] = useState(false); const [isIgnoring, setIsIgnoring] = useState(false); const [isUserIgnored, setIsUserIgnored] = useState(initMatrix.matrixClient.isUserIgnored(userId)); const isMountedRef = useRef(true); const mx = initMatrix.matrixClient; const room = mx.getRoom(roomId); const member = room.getMember(userId); const isInvitable = member?.membership !== 'join' && member?.membership !== 'ban'; const [isInviting, setIsInviting] = useState(false); const [isInvited, setIsInvited] = useState(member?.membership === 'invite'); const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0; const userPL = room.getMember(userId)?.powerLevel || 0; const canIKick = room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel; const isBanned = member?.membership === 'ban'; const onCreated = (dmRoomId) => { if (isMountedRef.current === false) return; setIsCreatingDM(false); selectRoom(dmRoomId); onRequestClose(); }; useEffect(() => { const { roomList } = initMatrix; roomList.on(cons.events.roomList.ROOM_CREATED, onCreated); return () => { isMountedRef.current = false; roomList.removeListener(cons.events.roomList.ROOM_CREATED, onCreated); }; }, []); useEffect(() => { setIsUserIgnored(initMatrix.matrixClient.isUserIgnored(userId)); setIsIgnoring(false); setIsInviting(false); }, [userId]); const openDM = async () => { // Check and open if user already have a DM with userId. const dmRoomId = hasDMWith(userId); if (dmRoomId) { selectRoom(dmRoomId); onRequestClose(); return; } // Create new DM try { setIsCreatingDM(true); await roomActions.createDM(userId); } catch { if (isMountedRef.current === false) return; setIsCreatingDM(false); } }; const toggleIgnore = async () => { const ignoredUsers = mx.getIgnoredUsers(); const uIndex = ignoredUsers.indexOf(userId); if (uIndex >= 0) { if (uIndex === -1) return; ignoredUsers.splice(uIndex, 1); } else ignoredUsers.push(userId); try { setIsIgnoring(true); await mx.setIgnoredUsers(ignoredUsers); if (isMountedRef.current === false) return; setIsUserIgnored(uIndex < 0); setIsIgnoring(false); } catch { setIsIgnoring(false); } }; const toggleInvite = async () => { try { setIsInviting(true); let isInviteSent = false; if (isInvited) await roomActions.kick(roomId, userId); else { await roomActions.invite(roomId, userId); isInviteSent = true; } if (isMountedRef.current === false) return; setIsInvited(isInviteSent); setIsInviting(false); } catch { setIsInviting(false); } }; return (
{ isBanned && canIKick && ( )} { (isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && ( )}
); } ProfileFooter.propTypes = { roomId: PropTypes.string.isRequired, userId: PropTypes.string.isRequired, onRequestClose: PropTypes.func.isRequired, }; function useToggleDialog() { const [isOpen, setIsOpen] = useState(false); const [roomId, setRoomId] = useState(null); const [userId, setUserId] = useState(null); useEffect(() => { const loadProfile = (uId, rId) => { setIsOpen(true); setUserId(uId); setRoomId(rId); }; navigation.on(cons.events.navigation.PROFILE_VIEWER_OPENED, loadProfile); return () => { navigation.removeListener(cons.events.navigation.PROFILE_VIEWER_OPENED, loadProfile); }; }, []); const closeDialog = () => setIsOpen(false); const afterClose = () => { setUserId(null); setRoomId(null); }; return [isOpen, roomId, userId, closeDialog, afterClose]; } function useRerenderOnProfileChange(roomId, userId) { const mx = initMatrix.matrixClient; const [, forceUpdate] = useForceUpdate(); useEffect(() => { const handleProfileChange = (mEvent, member) => { if ( mEvent.getRoomId() === roomId && (member.userId === userId || member.userId === mx.getUserId()) ) { forceUpdate(); } }; mx.on('RoomMember.powerLevel', handleProfileChange); mx.on('RoomMember.membership', handleProfileChange); return () => { mx.removeListener('RoomMember.powerLevel', handleProfileChange); mx.removeListener('RoomMember.membership', handleProfileChange); }; }, [roomId, userId]); } function ProfileViewer() { const [isOpen, roomId, userId, closeDialog, handleAfterClose] = useToggleDialog(); useRerenderOnProfileChange(roomId, userId); const mx = initMatrix.matrixClient; const room = mx.getRoom(roomId); const renderProfile = () => { const roomMember = room.getMember(userId); const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(userId); const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl; const avatarUrl = (avatarMxc && avatarMxc !== 'null') ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null; const powerLevel = roomMember?.powerLevel || 0; const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0; const canChangeRole = ( room.currentState.maySendEvent('m.room.power_levels', mx.getUserId()) && (powerLevel < myPowerLevel || userId === mx.getUserId()) ); const handleChangePowerLevel = async (newPowerLevel) => { if (newPowerLevel === powerLevel) return; const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?'; const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?'; const isSharedPower = newPowerLevel === myPowerLevel; const isDemotingMyself = userId === mx.getUserId(); if (isSharedPower || isDemotingMyself) { const isConfirmed = await confirmDialog( 'Change power level', isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG, 'Change', 'caution', ); if (!isConfirmed) return; roomActions.setPowerLevel(roomId, userId, newPowerLevel); } else { roomActions.setPowerLevel(roomId, userId, newPowerLevel); } }; const handlePowerSelector = (e) => { openReusableContextMenu( 'bottom', getEventCords(e, '.btn-surface'), (closeMenu) => ( { closeMenu(); handleChangePowerLevel(pl); }} /> ), ); }; return (
{twemojify(username)} {twemojify(userId)}
Role
{ userId !== mx.getUserId() && ( )}
); }; return ( } > {roomId ? renderProfile() :
}
); } export default ProfileViewer;