Files
cinny/src/app/organisms/profile-viewer/ProfileViewer.jsx
T

430 lines
14 KiB
React
Raw Normal View History

2021-10-18 17:25:52 +02:00
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import './ProfileViewer.scss';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
2024-07-08 16:57:10 +05:30
import { openReusableContextMenu } from '../../../client/action/navigation';
2021-10-18 17:25:52 +02:00
import * as roomActions from '../../../client/action/room';
2022-02-05 19:25:59 +05:30
import {
2024-07-08 16:57:10 +05:30
getUsername,
getUsernameOfRoomMember,
getPowerLabel,
hasDevices,
2022-02-05 19:25:59 +05:30
} from '../../../util/matrixUtil';
import { getEventCords } from '../../../util/common';
2021-10-18 17:25:52 +02:00
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import Chip from '../../atoms/chip/Chip';
import IconButton from '../../atoms/button/IconButton';
2022-01-12 18:26:52 +05:30
import Input from '../../atoms/input/Input';
2021-10-18 17:25:52 +02:00
import Avatar from '../../atoms/avatar/Avatar';
import Button from '../../atoms/button/Button';
2022-01-13 09:42:23 +05:30
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
import PowerLevelSelector from '../../molecules/power-level-selector/PowerLevelSelector';
2021-10-18 17:25:52 +02:00
import Dialog from '../../molecules/dialog/Dialog';
import ShieldEmptyIC from '../../../../public/res/ic/outlined/shield-empty.svg';
2022-01-13 09:42:23 +05:30
import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg';
2021-10-18 17:25:52 +02:00
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';
2024-07-08 16:57:10 +05:30
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { getDMRoomFor } from '../../utils/matrix';
2024-07-22 16:17:19 +05:30
import { useMatrixClient } from '../../hooks/useMatrixClient';
2024-07-08 16:57:10 +05:30
function ModerationTools({ roomId, userId }) {
2024-07-22 16:17:19 +05:30
const mx = useMatrixClient();
2022-01-12 18:26:52 +05:30
const room = mx.getRoom(roomId);
const roomMember = room.getMember(userId);
2022-01-16 18:17:20 +05:30
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
2022-01-12 18:26:52 +05:30
const powerLevel = roomMember?.powerLevel || 0;
2024-07-08 16:57:10 +05:30
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;
2022-01-12 18:26:52 +05:30
const handleKick = (e) => {
e.preventDefault();
const kickReason = e.target.elements['kick-reason']?.value.trim();
2024-07-22 16:17:19 +05:30
mx.kick(roomId, userId, kickReason !== '' ? kickReason : undefined);
2022-01-12 18:26:52 +05:30
};
2022-01-12 18:50:54 +05:30
const handleBan = (e) => {
e.preventDefault();
const banReason = e.target.elements['ban-reason']?.value.trim();
2024-07-22 16:17:19 +05:30
mx.ban(roomId, userId, banReason !== '' ? banReason : undefined);
2022-01-12 18:50:54 +05:30
};
2022-01-12 18:26:52 +05:30
return (
<div className="moderation-tools">
{canIKick && (
2022-01-12 18:50:54 +05:30
<form onSubmit={handleKick}>
<Input label="Kick reason" name="kick-reason" />
<Button type="submit">Kick</Button>
</form>
)}
{canIBan && (
<form onSubmit={handleBan}>
<Input label="Ban reason" name="ban-reason" />
<Button type="submit">Ban</Button>
</form>
2022-01-12 18:26:52 +05:30
)}
</div>
);
}
ModerationTools.propTypes = {
roomId: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
};
2021-10-18 17:25:52 +02:00
function SessionInfo({ userId }) {
const [devices, setDevices] = useState(null);
2022-01-13 09:42:23 +05:30
const [isVisible, setIsVisible] = useState(false);
2024-07-22 16:17:19 +05:30
const mx = useMatrixClient();
2021-10-18 17:25:52 +02:00
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;
};
2024-07-22 16:17:19 +05:30
}, [mx, userId]);
2021-10-18 17:25:52 +02:00
function renderSessionChips() {
2022-01-13 09:42:23 +05:30
if (!isVisible) return null;
2021-10-18 17:25:52 +02:00
return (
<div className="session-info__chips">
2022-01-13 09:42:23 +05:30
{devices === null && <Text variant="b2">Loading sessions...</Text>}
{devices?.length === 0 && <Text variant="b2">No session found.</Text>}
2024-07-08 16:57:10 +05:30
{devices !== null &&
devices.map((device) => (
<Chip
key={device.deviceId}
iconSrc={ShieldEmptyIC}
text={device.getDisplayName() || device.deviceId}
/>
))}
2021-10-18 17:25:52 +02:00
</div>
);
}
return (
<div className="session-info">
2022-01-13 09:42:23 +05:30
<MenuItem
onClick={() => setIsVisible(!isVisible)}
iconSrc={isVisible ? ChevronBottomIC : ChevronRightIC}
>
2024-07-08 16:57:10 +05:30
<Text variant="b2">{`View ${
devices?.length > 0
2024-07-22 16:17:19 +05:30
? `${devices.length} ${devices.length === 1 ? 'session' : 'sessions'}`
2024-07-08 16:57:10 +05:30
: 'sessions'
}`}</Text>
2022-01-13 09:42:23 +05:30
</MenuItem>
{renderSessionChips()}
2021-10-18 17:25:52 +02:00
</div>
);
}
SessionInfo.propTypes = {
userId: PropTypes.string.isRequired,
};
function ProfileFooter({ roomId, userId, onRequestClose }) {
2021-10-18 17:25:52 +02:00
const [isCreatingDM, setIsCreatingDM] = useState(false);
const [isIgnoring, setIsIgnoring] = useState(false);
2024-07-22 16:17:19 +05:30
const mx = useMatrixClient();
const [isUserIgnored, setIsUserIgnored] = useState(mx.isUserIgnored(userId));
2021-10-18 17:25:52 +02:00
const isMountedRef = useRef(true);
2024-07-08 16:57:10 +05:30
const { navigateRoom } = useRoomNavigate();
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');
2021-10-18 17:25:52 +02:00
2022-01-16 18:17:20 +05:30
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
const userPL = room.getMember(userId)?.powerLevel || 0;
2024-07-08 16:57:10 +05:30
const canIKick =
room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
2021-10-29 18:11:02 +05:30
2022-01-13 10:28:33 +05:30
const isBanned = member?.membership === 'ban';
2021-11-23 16:24:12 +05:30
const onCreated = (dmRoomId) => {
if (isMountedRef.current === false) return;
setIsCreatingDM(false);
2024-07-08 16:57:10 +05:30
navigateRoom(dmRoomId);
2021-11-23 16:24:12 +05:30
onRequestClose();
};
2021-10-18 17:25:52 +02:00
useEffect(() => {
2024-07-22 16:17:19 +05:30
setIsUserIgnored(mx.isUserIgnored(userId));
setIsIgnoring(false);
setIsInviting(false);
2024-07-22 16:17:19 +05:30
}, [mx, userId]);
2021-10-18 17:25:52 +02:00
2022-01-13 10:28:33 +05:30
const openDM = async () => {
2021-10-18 17:25:52 +02:00
// Check and open if user already have a DM with userId.
2024-07-08 16:57:10 +05:30
const dmRoomId = getDMRoomFor(mx, userId)?.roomId;
2022-02-05 19:25:59 +05:30
if (dmRoomId) {
2024-07-08 16:57:10 +05:30
navigateRoom(dmRoomId);
2022-02-05 19:25:59 +05:30
onRequestClose();
return;
2021-10-18 17:25:52 +02:00
}
// Create new DM
try {
setIsCreatingDM(true);
2024-07-22 16:17:19 +05:30
const result = await roomActions.createDM(mx, userId, await hasDevices(mx, userId));
2024-07-08 16:57:10 +05:30
onCreated(result.room_id);
2021-10-18 17:25:52 +02:00
} catch {
2021-11-23 16:24:12 +05:30
if (isMountedRef.current === false) return;
2021-10-18 17:25:52 +02:00
setIsCreatingDM(false);
}
2022-01-13 10:28:33 +05:30
};
2021-10-18 17:25:52 +02:00
2022-01-13 10:28:33 +05:30
const toggleIgnore = async () => {
2022-09-03 21:46:40 +05:30
const isIgnored = mx.getIgnoredUsers().includes(userId);
2021-10-18 17:25:52 +02:00
try {
setIsIgnoring(true);
2022-09-03 21:46:40 +05:30
if (isIgnored) {
2024-07-22 16:17:19 +05:30
await roomActions.unignore(mx, [userId]);
2022-09-03 21:46:40 +05:30
} else {
2024-07-22 16:17:19 +05:30
await roomActions.ignore(mx, [userId]);
2022-09-03 21:46:40 +05:30
}
2021-10-18 17:25:52 +02:00
if (isMountedRef.current === false) return;
2022-09-03 21:46:40 +05:30
setIsUserIgnored(!isIgnored);
2021-10-18 17:25:52 +02:00
setIsIgnoring(false);
} catch {
setIsIgnoring(false);
}
2022-01-13 10:28:33 +05:30
};
2022-01-13 10:28:33 +05:30
const toggleInvite = async () => {
try {
setIsInviting(true);
let isInviteSent = false;
2024-07-22 16:17:19 +05:30
if (isInvited) await mx.kick(roomId, userId);
else {
2024-07-22 16:17:19 +05:30
await mx.invite(roomId, userId);
isInviteSent = true;
}
if (isMountedRef.current === false) return;
setIsInvited(isInviteSent);
setIsInviting(false);
} catch {
setIsInviting(false);
}
2022-01-13 10:28:33 +05:30
};
2021-10-18 17:25:52 +02:00
return (
<div className="profile-viewer__buttons">
2024-07-08 16:57:10 +05:30
<Button variant="primary" onClick={openDM} disabled={isCreatingDM}>
2021-10-18 17:25:52 +02:00
{isCreatingDM ? 'Creating room...' : 'Message'}
</Button>
2024-07-08 16:57:10 +05:30
{isBanned && canIKick && (
2024-07-22 16:17:19 +05:30
<Button variant="positive" onClick={() => mx.unban(roomId, userId)}>
2022-01-13 10:28:33 +05:30
Unban
</Button>
)}
2024-07-08 16:57:10 +05:30
{(isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
<Button onClick={toggleInvite} disabled={isInviting}>
{isInvited
? `${isInviting ? 'Disinviting...' : 'Disinvite'}`
: `${isInviting ? 'Inviting...' : 'Invite'}`}
</Button>
)}
2021-10-18 17:25:52 +02:00
<Button
variant={isUserIgnored ? 'positive' : 'danger'}
onClick={toggleIgnore}
disabled={isIgnoring}
>
2024-07-08 16:57:10 +05:30
{isUserIgnored
? `${isIgnoring ? 'Unignoring...' : 'Unignore'}`
: `${isIgnoring ? 'Ignoring...' : 'Ignore'}`}
2021-10-18 17:25:52 +02:00
</Button>
</div>
);
}
ProfileFooter.propTypes = {
roomId: PropTypes.string.isRequired,
2021-10-18 17:25:52 +02:00
userId: PropTypes.string.isRequired,
onRequestClose: PropTypes.func.isRequired,
};
2022-01-12 16:46:56 +05:30
function useToggleDialog() {
2021-10-18 17:25:52 +02:00
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);
};
2021-10-18 17:25:52 +02:00
navigation.on(cons.events.navigation.PROFILE_VIEWER_OPENED, loadProfile);
return () => {
navigation.removeListener(cons.events.navigation.PROFILE_VIEWER_OPENED, loadProfile);
};
}, []);
2022-01-12 16:46:56 +05:30
const closeDialog = () => setIsOpen(false);
const afterClose = () => {
setUserId(null);
setRoomId(null);
};
return [isOpen, roomId, userId, closeDialog, afterClose];
}
2022-01-12 18:26:52 +05:30
function useRerenderOnProfileChange(roomId, userId) {
2024-07-22 16:17:19 +05:30
const mx = useMatrixClient();
2022-01-12 16:46:56 +05:30
const [, forceUpdate] = useForceUpdate();
useEffect(() => {
2022-01-12 18:26:52 +05:30
const handleProfileChange = (mEvent, member) => {
if (
2024-07-08 16:57:10 +05:30
mEvent.getRoomId() === roomId &&
(member.userId === userId || member.userId === mx.getUserId())
2022-01-12 18:26:52 +05:30
) {
forceUpdate();
}
};
2022-01-12 18:26:52 +05:30
mx.on('RoomMember.powerLevel', handleProfileChange);
mx.on('RoomMember.membership', handleProfileChange);
return () => {
2022-01-12 18:26:52 +05:30
mx.removeListener('RoomMember.powerLevel', handleProfileChange);
mx.removeListener('RoomMember.membership', handleProfileChange);
};
2024-07-22 16:17:19 +05:30
}, [mx, roomId, userId]);
2022-01-12 16:46:56 +05:30
}
2022-01-12 16:46:56 +05:30
function ProfileViewer() {
const [isOpen, roomId, userId, closeDialog, handleAfterClose] = useToggleDialog();
2022-01-12 18:26:52 +05:30
useRerenderOnProfileChange(roomId, userId);
2022-01-12 16:46:56 +05:30
2024-07-22 16:17:19 +05:30
const mx = useMatrixClient();
2022-01-12 16:46:56 +05:30
const room = mx.getRoom(roomId);
2021-10-18 17:25:52 +02:00
2022-01-12 16:46:56 +05:30
const renderProfile = () => {
2022-01-13 09:42:23 +05:30
const roomMember = room.getMember(userId);
2024-07-22 16:17:19 +05:30
const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(mx, userId);
2022-01-13 10:33:04 +05:30
const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl;
2024-07-08 16:57:10 +05:30
const avatarUrl =
avatarMxc && avatarMxc !== 'null' ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null;
2022-01-12 16:46:56 +05:30
2022-01-16 18:17:20 +05:30
const powerLevel = roomMember?.powerLevel || 0;
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
2024-07-08 16:57:10 +05:30
const canChangeRole =
room.currentState.maySendEvent('m.room.power_levels', mx.getUserId()) &&
(powerLevel < myPowerLevel || userId === mx.getUserId());
const handleChangePowerLevel = async (newPowerLevel) => {
if (newPowerLevel === powerLevel) return;
2024-07-08 16:57:10 +05:30
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',
2024-07-08 16:57:10 +05:30
'caution'
);
if (!isConfirmed) return;
2024-07-22 16:17:19 +05:30
roomActions.setPowerLevel(mx, roomId, userId, newPowerLevel);
} else {
2024-07-22 16:17:19 +05:30
roomActions.setPowerLevel(mx, roomId, userId, newPowerLevel);
}
};
2022-01-12 16:46:56 +05:30
const handlePowerSelector = (e) => {
2024-07-08 16:57:10 +05:30
openReusableContextMenu('bottom', getEventCords(e, '.btn-surface'), (closeMenu) => (
<PowerLevelSelector
value={powerLevel}
max={myPowerLevel}
onSelect={(pl) => {
closeMenu();
handleChangePowerLevel(pl);
}}
/>
));
};
2021-10-18 17:25:52 +02:00
return (
<div className="profile-viewer">
<div className="profile-viewer__user">
2022-01-12 16:46:56 +05:30
<Avatar imageSrc={avatarUrl} text={username} bgColor={colorMXID(userId)} size="large" />
2021-10-18 17:25:52 +02:00
<div className="profile-viewer__user__info">
2024-07-08 16:57:10 +05:30
<Text variant="s1" weight="medium">
{username}
</Text>
<Text variant="b2">{userId}</Text>
2021-10-18 17:25:52 +02:00
</div>
<div className="profile-viewer__user__role">
<Text variant="b3">Role</Text>
<Button
onClick={canChangeRole ? handlePowerSelector : null}
iconSrc={canChangeRole ? ChevronBottomIC : null}
>
2022-01-10 20:34:54 +05:30
{`${getPowerLabel(powerLevel) || 'Member'} - ${powerLevel}`}
</Button>
2021-10-18 17:25:52 +02:00
</div>
</div>
2022-01-12 18:26:52 +05:30
<ModerationTools roomId={roomId} userId={userId} />
2021-10-18 17:25:52 +02:00
<SessionInfo userId={userId} />
2024-07-08 16:57:10 +05:30
{userId !== mx.getUserId() && (
2022-01-12 18:26:52 +05:30
<ProfileFooter roomId={roomId} userId={userId} onRequestClose={closeDialog} />
2021-10-18 17:25:52 +02:00
)}
</div>
);
2022-01-12 16:46:56 +05:30
};
2021-10-18 17:25:52 +02:00
return (
<Dialog
className="profile-viewer__dialog"
isOpen={isOpen}
2022-01-13 09:42:23 +05:30
title={room?.name ?? ''}
2021-12-14 17:26:32 +05:30
onAfterClose={handleAfterClose}
2022-01-12 16:46:56 +05:30
onRequestClose={closeDialog}
contentOptions={<IconButton src={CrossIC} onClick={closeDialog} tooltip="Close" />}
2021-10-18 17:25:52 +02:00
>
2021-12-14 17:26:32 +05:30
{roomId ? renderProfile() : <div />}
2021-10-18 17:25:52 +02:00
</Dialog>
);
}
export default ProfileViewer;