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 } 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 Avatar from '../../atoms/avatar/Avatar'; import Button from '../../atoms/button/Button'; import PowerLevelSelector from '../../molecules/power-level-selector/PowerLevelSelector'; import Dialog from '../../molecules/dialog/Dialog'; import SettingTile from '../../molecules/setting-tile/SettingTile'; import ShieldEmptyIC from '../../../../public/res/ic/outlined/shield-empty.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'; function SessionInfo({ userId }) { const [devices, setDevices] = useState(null); 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() { return (
{devices === null && Loading sessions...} {devices?.length === 0 && No session found.} {devices !== null && (devices.map((device) => ( )))}
); } return (
); } 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; const userPL = room.getMember(userId)?.powerLevel || 0; const canIKick = room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel; 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]); async function openDM() { const directIds = [...initMatrix.roomList.directs]; // Check and open if user already have a DM with userId. for (let i = 0; i < directIds.length; i += 1) { const dRoom = mx.getRoom(directIds[i]); const roomMembers = dRoom.getMembers(); if (roomMembers.length <= 2 && dRoom.getMember(userId)) { selectRoom(directIds[i]); onRequestClose(); return; } } // Create new DM try { setIsCreatingDM(true); await roomActions.create({ isEncrypted: true, isDirect: true, invite: [userId], }); } catch { if (isMountedRef.current === false) return; setIsCreatingDM(false); } } async function toggleIgnore() { 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); } } async function toggleInvite() { 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 (
{ member?.membership === 'join' && } { (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 useRerenderOnRoleChange(roomId, userId) { const mx = initMatrix.matrixClient; const [, forceUpdate] = useForceUpdate(); useEffect(() => { const handlePowerLevelChange = (mEvent, member) => { if (mEvent.getRoomId() === roomId && member.userId === userId) { forceUpdate(); } }; mx.on('RoomMember.powerLevel', handlePowerLevelChange); return () => { mx.removeListener('RoomMember.powerLevel', handlePowerLevelChange); }; }, [roomId, userId]); } function ProfileViewer() { const [isOpen, roomId, userId, closeDialog, handleAfterClose] = useToggleDialog(); useRerenderOnRoleChange(roomId, userId); const mx = initMatrix.matrixClient; const room = mx.getRoom(roomId); let [roomMember, username] = [null, null]; if (roomId) { roomMember = room.getMember(userId); username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(userId); } const renderProfile = () => { const avatarMxc = roomMember.getMxcAvatarUrl?.() || mx.getMember(userId).avatarUrl; const avatarUrl = avatarMxc ? 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 = (newPowerLevel) => { if (newPowerLevel === powerLevel) return; if (newPowerLevel === myPowerLevel ? confirm('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?') : true ) { 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;