Merge branch 'develop' into travis/deactivate-2
This commit is contained in:
commit
616a5ab55d
19 changed files with 814 additions and 456 deletions
|
@ -27,7 +27,6 @@ import sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import createRoom from '../../../createRoom';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import Unread from '../../../Unread';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -40,6 +39,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
|
|||
import E2EIcon from "../rooms/E2EIcon";
|
||||
import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {textualPowerLevel} from '../../../Roles';
|
||||
|
||||
const _disambiguateDevices = (devices) => {
|
||||
const names = Object.create(null);
|
||||
|
@ -63,10 +63,92 @@ const _getE2EStatus = (devices) => {
|
|||
return hasUnverifiedDevice ? "warning" : "verified";
|
||||
};
|
||||
|
||||
const DevicesSection = ({devices, userId, loading}) => {
|
||||
const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
|
||||
async function unverifyUser(matrixClient, userId) {
|
||||
const devices = await matrixClient.getStoredDevicesForUser(userId);
|
||||
for (const device of devices) {
|
||||
if (device.isVerified()) {
|
||||
matrixClient.setDeviceVerified(
|
||||
userId, device.deviceId, false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openDMForUser(matrixClient, userId) {
|
||||
const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||
const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
|
||||
const room = matrixClient.getRoom(roomId);
|
||||
if (!room || room.getMyMembership() === "leave") {
|
||||
return lastActiveRoom;
|
||||
}
|
||||
if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) {
|
||||
return room;
|
||||
}
|
||||
return lastActiveRoom;
|
||||
}, null);
|
||||
|
||||
if (lastActiveRoom) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: lastActiveRoom.roomId,
|
||||
});
|
||||
} else {
|
||||
createRoom({dmUserId: userId});
|
||||
}
|
||||
}
|
||||
|
||||
function useIsEncrypted(cli, room) {
|
||||
const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId));
|
||||
|
||||
const update = useCallback((event) => {
|
||||
if (event.getType() === "m.room.encryption") {
|
||||
setIsEncrypted(cli.isRoomEncrypted(room.roomId));
|
||||
}
|
||||
}, [cli, room]);
|
||||
useEventEmitter(room.currentState, "RoomState.events", update);
|
||||
return isEncrypted;
|
||||
}
|
||||
|
||||
function verifyDevice(userId, device) {
|
||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
||||
userId: userId,
|
||||
device: device,
|
||||
});
|
||||
}
|
||||
|
||||
function DeviceItem({userId, device}) {
|
||||
const classes = classNames("mx_UserInfo_device", {
|
||||
mx_UserInfo_device_verified: device.isVerified(),
|
||||
mx_UserInfo_device_unverified: !device.isVerified(),
|
||||
});
|
||||
const iconClasses = classNames("mx_E2EIcon", {
|
||||
mx_E2EIcon_verified: device.isVerified(),
|
||||
mx_E2EIcon_warning: !device.isVerified(),
|
||||
});
|
||||
|
||||
const onDeviceClick = () => {
|
||||
if (!device.isVerified()) {
|
||||
verifyDevice(userId, device);
|
||||
}
|
||||
};
|
||||
|
||||
const deviceName = device.ambiguous ?
|
||||
(device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" :
|
||||
device.getDisplayName();
|
||||
const trustedLabel = device.isVerified() ? _t("Trusted") : _t("Not trusted");
|
||||
return (<AccessibleButton className={classes} onClick={onDeviceClick}>
|
||||
<div className={iconClasses} />
|
||||
<div className="mx_UserInfo_device_name">{deviceName}</div>
|
||||
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
|
||||
</AccessibleButton>);
|
||||
}
|
||||
|
||||
function DevicesSection({devices, userId, loading}) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
const [isExpanded, setExpanded] = useState(false);
|
||||
|
||||
if (loading) {
|
||||
// still loading
|
||||
return <Spinner />;
|
||||
|
@ -74,123 +156,50 @@ const DevicesSection = ({devices, userId, loading}) => {
|
|||
if (devices === null) {
|
||||
return _t("Unable to load device list");
|
||||
}
|
||||
if (devices.length === 0) {
|
||||
return _t("No devices with registered encryption keys");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Trust & Devices") }</h3>
|
||||
<div className="mx_UserInfo_devices">
|
||||
{ devices.map((device, i) => <MemberDeviceInfo key={i} userId={userId} device={device} />) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const unverifiedDevices = devices.filter(d => !d.isVerified());
|
||||
const verifiedDevices = devices.filter(d => d.isVerified());
|
||||
|
||||
const onRoomTileClick = (roomId) => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
};
|
||||
|
||||
const DirectChatsSection = withLegacyMatrixClient(({matrixClient: cli, userId, startUpdating, stopUpdating}) => {
|
||||
const onNewDMClick = async () => {
|
||||
startUpdating();
|
||||
await createRoom({dmUserId: userId});
|
||||
stopUpdating();
|
||||
};
|
||||
|
||||
// TODO: Immutable DMs replaces a lot of this
|
||||
// dmRooms will not include dmRooms that we have been invited into but did not join.
|
||||
// Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room.
|
||||
// XXX: we potentially want DMs we have been invited to, to also show up here :L
|
||||
// especially as logic below concerns specially if we haven't joined but have been invited
|
||||
const [dmRooms, setDmRooms] = useState(new DMRoomMap(cli).getDMRoomsForUserId(userId));
|
||||
|
||||
// TODO bind the below
|
||||
// cli.on("Room", this.onRoom);
|
||||
// cli.on("Room.name", this.onRoomName);
|
||||
// cli.on("deleteRoom", this.onDeleteRoom);
|
||||
|
||||
const accountDataHandler = useCallback((ev) => {
|
||||
if (ev.getType() === "m.direct") {
|
||||
const dmRoomMap = new DMRoomMap(cli);
|
||||
setDmRooms(dmRoomMap.getDMRoomsForUserId(userId));
|
||||
}
|
||||
}, [cli, userId]);
|
||||
useEventEmitter(cli, "accountData", accountDataHandler);
|
||||
|
||||
const RoomTile = sdk.getComponent("rooms.RoomTile");
|
||||
|
||||
const tiles = [];
|
||||
for (const roomId of dmRooms) {
|
||||
const room = cli.getRoom(roomId);
|
||||
if (room) {
|
||||
const myMembership = room.getMyMembership();
|
||||
// not a DM room if we have are not joined
|
||||
if (myMembership !== 'join') continue;
|
||||
|
||||
const them = room.getMember(userId);
|
||||
// not a DM room if they are not joined
|
||||
if (!them || !them.membership || them.membership !== 'join') continue;
|
||||
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
|
||||
tiles.push(
|
||||
<RoomTile key={room.roomId}
|
||||
room={room}
|
||||
transparent={true}
|
||||
collapsed={false}
|
||||
selected={false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
isInvite={false}
|
||||
onClick={onRoomTileClick}
|
||||
/>,
|
||||
);
|
||||
let expandButton;
|
||||
if (verifiedDevices.length) {
|
||||
if (isExpanded) {
|
||||
expandButton = (<AccessibleButton className="mx_UserInfo_expand" onClick={() => setExpanded(false)}>
|
||||
<div>{_t("Hide verified Sign-In's")}</div>
|
||||
</AccessibleButton>);
|
||||
} else {
|
||||
expandButton = (<AccessibleButton className="mx_UserInfo_expand" onClick={() => setExpanded(true)}>
|
||||
<div className="mx_E2EIcon mx_E2EIcon_verified" />
|
||||
<div>{_t("%(count)s verified Sign-In's", {count: verifiedDevices.length})}</div>
|
||||
</AccessibleButton>);
|
||||
}
|
||||
}
|
||||
|
||||
const labelClasses = classNames({
|
||||
mx_UserInfo_createRoom_label: true,
|
||||
mx_RoomTile_name: true,
|
||||
let deviceList = unverifiedDevices.map((device, i) => {
|
||||
return (<DeviceItem key={i} userId={userId} device={device} />);
|
||||
});
|
||||
|
||||
let body = tiles;
|
||||
if (!body) {
|
||||
body = (
|
||||
<AccessibleButton className="mx_UserInfo_createRoom" onClick={onNewDMClick}>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
<img src={require("../../../../res/img/create-big.svg")} width="26" height="26" alt={_t("Start a chat")} />
|
||||
</div>
|
||||
<div className={labelClasses}><i>{ _t("Start a chat") }</i></div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
if (isExpanded) {
|
||||
const keyStart = unverifiedDevices.length;
|
||||
deviceList = deviceList.concat(verifiedDevices.map((device, i) => {
|
||||
return (<DeviceItem key={i + keyStart} userId={userId} device={device} />);
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_UserInfo_container">
|
||||
<div className="mx_UserInfo_container_header">
|
||||
<h3>{ _t("Direct messages") }</h3>
|
||||
<AccessibleButton
|
||||
className="mx_UserInfo_container_header_right mx_UserInfo_newDmButton"
|
||||
onClick={onNewDMClick}
|
||||
title={_t("Start a chat")}
|
||||
/>
|
||||
</div>
|
||||
{ body }
|
||||
<div className="mx_UserInfo_devices">
|
||||
<div>{deviceList}</div>
|
||||
<div>{expandButton}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite}) => {
|
||||
const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => {
|
||||
let ignoreButton = null;
|
||||
let insertPillButton = null;
|
||||
let inviteUserButton = null;
|
||||
let readReceiptButton = null;
|
||||
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
|
||||
const onShareUserClick = () => {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room member dialog', '', ShareDialog, {
|
||||
|
@ -200,7 +209,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
|
|||
|
||||
// Only allow the user to ignore the user if its not ourselves
|
||||
// same goes for jumping to read receipt
|
||||
if (member.userId !== cli.getUserId()) {
|
||||
if (!isMe) {
|
||||
const onIgnoreToggle = () => {
|
||||
const ignoredUsers = cli.getIgnoredUsers();
|
||||
if (isIgnored) {
|
||||
|
@ -214,7 +223,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
|
|||
};
|
||||
|
||||
ignoreButton = (
|
||||
<AccessibleButton onClick={onIgnoreToggle} className="mx_UserInfo_field">
|
||||
<AccessibleButton onClick={onIgnoreToggle} className={classNames("mx_UserInfo_field", {mx_UserInfo_destructive: !isIgnored})}>
|
||||
{ isIgnored ? _t("Unignore") : _t("Ignore") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -285,15 +294,34 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
|
|||
</AccessibleButton>
|
||||
);
|
||||
|
||||
let directMessageButton;
|
||||
if (!isMe) {
|
||||
directMessageButton = (
|
||||
<AccessibleButton onClick={() => openDMForUser(cli, member.userId)} className="mx_UserInfo_field">
|
||||
{ _t('Direct message') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
let unverifyButton;
|
||||
if (devices && devices.some(device => device.isVerified())) {
|
||||
unverifyButton = (
|
||||
<AccessibleButton onClick={() => unverifyUser(cli, member.userId)} className="mx_UserInfo_field mx_UserInfo_destructive">
|
||||
{ _t('Unverify user') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{ _t("User Options") }</h3>
|
||||
<div className="mx_UserInfo_buttons">
|
||||
<h3>{ _t("Options") }</h3>
|
||||
<div>
|
||||
{ directMessageButton }
|
||||
{ readReceiptButton }
|
||||
{ shareUserButton }
|
||||
{ insertPillButton }
|
||||
{ ignoreButton }
|
||||
{ inviteUserButton }
|
||||
{ ignoreButton }
|
||||
{ unverifyButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -337,10 +365,13 @@ const _isMuted = (member, powerLevelContent) => {
|
|||
return member.powerLevel < levelToSend;
|
||||
};
|
||||
|
||||
const useRoomPowerLevels = (room) => {
|
||||
const useRoomPowerLevels = (cli, room) => {
|
||||
const [powerLevels, setPowerLevels] = useState({});
|
||||
|
||||
const update = useCallback(() => {
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
const event = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (event) {
|
||||
setPowerLevels(event.getContent());
|
||||
|
@ -352,7 +383,7 @@ const useRoomPowerLevels = (room) => {
|
|||
};
|
||||
}, [room]);
|
||||
|
||||
useEventEmitter(room, "RoomState.events", update);
|
||||
useEventEmitter(cli, "RoomState.members", update);
|
||||
useEffect(() => {
|
||||
update();
|
||||
return () => {
|
||||
|
@ -399,7 +430,7 @@ const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, start
|
|||
};
|
||||
|
||||
const kickLabel = member.membership === "invite" ? _t("Disinvite") : _t("Kick");
|
||||
return <AccessibleButton className="mx_UserInfo_field" onClick={onKick}>
|
||||
return <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onKick}>
|
||||
{ kickLabel }
|
||||
</AccessibleButton>;
|
||||
});
|
||||
|
@ -472,7 +503,7 @@ const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member}
|
|||
}
|
||||
};
|
||||
|
||||
return <AccessibleButton className="mx_UserInfo_field" onClick={onRedactAllMessages}>
|
||||
return <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onRedactAllMessages}>
|
||||
{ _t("Remove recent messages") }
|
||||
</AccessibleButton>;
|
||||
});
|
||||
|
@ -524,7 +555,11 @@ const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, star
|
|||
label = _t("Unban");
|
||||
}
|
||||
|
||||
return <AccessibleButton className="mx_UserInfo_field" onClick={onBanOrUnban}>
|
||||
const classes = classNames("mx_UserInfo_field", {
|
||||
mx_UserInfo_destructive: member.membership !== 'ban',
|
||||
});
|
||||
|
||||
return <AccessibleButton className={classes} onClick={onBanOrUnban}>
|
||||
{ label }
|
||||
</AccessibleButton>;
|
||||
});
|
||||
|
@ -581,21 +616,24 @@ const MuteToggleButton = withLegacyMatrixClient(
|
|||
}
|
||||
};
|
||||
|
||||
const classes = classNames("mx_UserInfo_field", {
|
||||
mx_UserInfo_destructive: !isMuted,
|
||||
});
|
||||
|
||||
const muteLabel = isMuted ? _t("Unmute") : _t("Mute");
|
||||
return <AccessibleButton className="mx_UserInfo_field" onClick={onMuteToggle}>
|
||||
return <AccessibleButton className={classes} onClick={onMuteToggle}>
|
||||
{ muteLabel }
|
||||
</AccessibleButton>;
|
||||
},
|
||||
);
|
||||
|
||||
const RoomAdminToolsContainer = withLegacyMatrixClient(
|
||||
({matrixClient: cli, room, children, member, startUpdating, stopUpdating}) => {
|
||||
({matrixClient: cli, room, children, member, startUpdating, stopUpdating, powerLevels}) => {
|
||||
let kickButton;
|
||||
let banButton;
|
||||
let muteButton;
|
||||
let redactButton;
|
||||
|
||||
const powerLevels = useRoomPowerLevels(room);
|
||||
const editPowerLevel = (
|
||||
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
||||
powerLevels.state_default
|
||||
|
@ -705,7 +743,7 @@ const GroupAdminToolsSection = withLegacyMatrixClient(
|
|||
};
|
||||
|
||||
const kickButton = (
|
||||
<AccessibleButton className="mx_UserInfo_field" onClick={_onKick}>
|
||||
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={_onKick}>
|
||||
{ isInvited ? _t('Disinvite') : _t('Remove from community') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -744,47 +782,17 @@ const useIsSynapseAdmin = (cli) => {
|
|||
return isAdmin;
|
||||
};
|
||||
|
||||
// cli is injected by withLegacyMatrixClient
|
||||
const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => {
|
||||
// Load room if we are given a room id and memoize it
|
||||
const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]);
|
||||
|
||||
// only display the devices list if our client supports E2E
|
||||
const _enableDevices = cli.isCryptoEnabled();
|
||||
|
||||
// Load whether or not we are a Synapse Admin
|
||||
const isSynapseAdmin = useIsSynapseAdmin(cli);
|
||||
|
||||
// Check whether the user is ignored
|
||||
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId));
|
||||
// Recheck if the user or client changes
|
||||
useEffect(() => {
|
||||
setIsIgnored(cli.isUserIgnored(user.userId));
|
||||
}, [cli, user.userId]);
|
||||
// Recheck also if we receive new accountData m.ignored_user_list
|
||||
const accountDataHandler = useCallback((ev) => {
|
||||
if (ev.getType() === "m.ignored_user_list") {
|
||||
setIsIgnored(cli.isUserIgnored(user.userId));
|
||||
}
|
||||
}, [cli, user.userId]);
|
||||
useEventEmitter(cli, "accountData", accountDataHandler);
|
||||
|
||||
// Count of how many operations are currently in progress, if > 0 then show a Spinner
|
||||
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
|
||||
const startUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount + 1);
|
||||
}, [pendingUpdateCount]);
|
||||
const stopUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount - 1);
|
||||
}, [pendingUpdateCount]);
|
||||
|
||||
function useRoomPermissions(cli, room, user) {
|
||||
const [roomPermissions, setRoomPermissions] = useState({
|
||||
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
|
||||
modifyLevelMax: -1,
|
||||
canEdit: false,
|
||||
canInvite: false,
|
||||
});
|
||||
const updateRoomPermissions = useCallback(async () => {
|
||||
if (!room) return;
|
||||
const updateRoomPermissions = useCallback(() => {
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
@ -811,20 +819,197 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
|
||||
setRoomPermissions({
|
||||
canInvite: me.powerLevel >= powerLevels.invite,
|
||||
canEdit: modifyLevelMax >= 0,
|
||||
modifyLevelMax,
|
||||
});
|
||||
}, [cli, user, room]);
|
||||
useEventEmitter(cli, "RoomState.events", updateRoomPermissions);
|
||||
useEventEmitter(cli, "RoomState.members", updateRoomPermissions);
|
||||
useEffect(() => {
|
||||
updateRoomPermissions();
|
||||
return () => {
|
||||
setRoomPermissions({
|
||||
maximalPowerLevel: -1,
|
||||
canEdit: false,
|
||||
canInvite: false,
|
||||
});
|
||||
};
|
||||
}, [updateRoomPermissions]);
|
||||
|
||||
return roomPermissions;
|
||||
}
|
||||
|
||||
const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => {
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
if (room && user.roomId) { // is in room
|
||||
if (isEditing) {
|
||||
return (<PowerLevelEditor
|
||||
user={user} room={room} roomPermissions={roomPermissions}
|
||||
onFinished={() => setEditing(false)} />);
|
||||
} else {
|
||||
const IconButton = sdk.getComponent('elements.IconButton');
|
||||
const powerLevelUsersDefault = powerLevels.users_default || 0;
|
||||
const powerLevel = parseInt(user.powerLevel, 10);
|
||||
const modifyButton = roomPermissions.canEdit ?
|
||||
(<IconButton icon="edit" onClick={() => setEditing(true)} />) : null;
|
||||
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
|
||||
const label = _t("<strong>%(role)s</strong> in %(roomName)s",
|
||||
{role, roomName: room.name},
|
||||
{strong: label => <strong>{label}</strong>},
|
||||
);
|
||||
return (
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<div className="mx_UserInfo_roleDescription">{label}{modifyButton}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, onFinished}) => {
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10));
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const onPowerChange = useCallback((powerLevel) => {
|
||||
setIsDirty(true);
|
||||
setSelectedPowerLevel(parseInt(powerLevel, 10));
|
||||
}, [setSelectedPowerLevel, setIsDirty]);
|
||||
|
||||
const changePowerLevel = useCallback(async () => {
|
||||
const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
||||
return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Power change success");
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to change power level " + err);
|
||||
Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Failed to change power level"),
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
if (!isDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdating(true);
|
||||
|
||||
const powerLevel = selectedPowerLevel;
|
||||
|
||||
const roomId = user.roomId;
|
||||
const target = user.userId;
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
||||
if (!powerLevelEvent.getContent().users) {
|
||||
_applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
const myUserId = cli.getUserId();
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
if (myUserId === target) {
|
||||
try {
|
||||
if (!(await _warnSelfDemote())) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
}
|
||||
await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
const myPower = powerLevelEvent.getContent().users[myUserId];
|
||||
if (parseInt(myPower) === parseInt(powerLevel)) {
|
||||
const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are promoting the user " +
|
||||
"to have the same power level as yourself.") }<br />
|
||||
{ _t("Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
});
|
||||
|
||||
const [confirmed] = await finished;
|
||||
if (confirmed) return;
|
||||
}
|
||||
await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
} finally {
|
||||
onFinished();
|
||||
}
|
||||
}, [user.roomId, user.userId, cli, selectedPowerLevel, isDirty, setIsUpdating, onFinished, room]);
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
|
||||
const IconButton = sdk.getComponent('elements.IconButton');
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const buttonOrSpinner = isUpdating ? <Spinner w={16} h={16} /> :
|
||||
<IconButton icon="check" onClick={changePowerLevel} />;
|
||||
|
||||
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
return (
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<PowerSelector
|
||||
label={null}
|
||||
value={selectedPowerLevel}
|
||||
maxValue={roomPermissions.modifyLevelMax}
|
||||
usersDefault={powerLevelUsersDefault}
|
||||
onChange={onPowerChange}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
{buttonOrSpinner}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// cli is injected by withLegacyMatrixClient
|
||||
const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => {
|
||||
// Load room if we are given a room id and memoize it
|
||||
const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]);
|
||||
|
||||
// only display the devices list if our client supports E2E
|
||||
const _enableDevices = cli.isCryptoEnabled();
|
||||
|
||||
const powerLevels = useRoomPowerLevels(cli, room);
|
||||
// Load whether or not we are a Synapse Admin
|
||||
const isSynapseAdmin = useIsSynapseAdmin(cli);
|
||||
|
||||
// Check whether the user is ignored
|
||||
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId));
|
||||
// Recheck if the user or client changes
|
||||
useEffect(() => {
|
||||
setIsIgnored(cli.isUserIgnored(user.userId));
|
||||
}, [cli, user.userId]);
|
||||
// Recheck also if we receive new accountData m.ignored_user_list
|
||||
const accountDataHandler = useCallback((ev) => {
|
||||
if (ev.getType() === "m.ignored_user_list") {
|
||||
setIsIgnored(cli.isUserIgnored(user.userId));
|
||||
}
|
||||
}, [cli, user.userId]);
|
||||
useEventEmitter(cli, "accountData", accountDataHandler);
|
||||
|
||||
// Count of how many operations are currently in progress, if > 0 then show a Spinner
|
||||
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
|
||||
const startUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount + 1);
|
||||
}, [pendingUpdateCount]);
|
||||
const stopUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount - 1);
|
||||
}, [pendingUpdateCount]);
|
||||
|
||||
const roomPermissions = useRoomPermissions(cli, room, user);
|
||||
|
||||
const onSynapseDeactivate = useCallback(async () => {
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
|
||||
|
@ -855,70 +1040,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
}
|
||||
}, [cli, user.userId]);
|
||||
|
||||
const onPowerChange = useCallback(async (powerLevel) => {
|
||||
const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
||||
startUpdating();
|
||||
cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Power change success");
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to change power level " + err);
|
||||
Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Failed to change power level"),
|
||||
});
|
||||
},
|
||||
).finally(() => {
|
||||
stopUpdating();
|
||||
}).done();
|
||||
};
|
||||
|
||||
const roomId = user.roomId;
|
||||
const target = user.userId;
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
||||
if (!powerLevelEvent.getContent().users) {
|
||||
_applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
return;
|
||||
const onMemberAvatarKey = e => {
|
||||
if (e.key === "Enter") {
|
||||
onMemberAvatarClick();
|
||||
}
|
||||
|
||||
const myUserId = cli.getUserId();
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
if (myUserId === target) {
|
||||
try {
|
||||
if (!(await _warnSelfDemote())) return;
|
||||
_applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const myPower = powerLevelEvent.getContent().users[myUserId];
|
||||
if (parseInt(myPower) === parseInt(powerLevel)) {
|
||||
const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are promoting the user " +
|
||||
"to have the same power level as yourself.") }<br />
|
||||
{ _t("Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
});
|
||||
|
||||
const [confirmed] = await finished;
|
||||
if (confirmed) return;
|
||||
}
|
||||
_applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
}, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line
|
||||
};
|
||||
|
||||
const onMemberAvatarClick = useCallback(() => {
|
||||
const member = user;
|
||||
|
@ -938,17 +1065,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
let synapseDeactivateButton;
|
||||
let spinner;
|
||||
|
||||
let directChatsSection;
|
||||
if (user.userId !== cli.getUserId()) {
|
||||
directChatsSection = <DirectChatsSection userId={user.userId} />;
|
||||
}
|
||||
|
||||
// We don't need a perfect check here, just something to pass as "probably not our homeserver". If
|
||||
// someone does figure out how to bypass this check the worst that happens is an error.
|
||||
// FIXME this should be using cli instead of MatrixClientPeg.matrixClient
|
||||
if (isSynapseAdmin && user.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) {
|
||||
synapseDeactivateButton = (
|
||||
<AccessibleButton onClick={onSynapseDeactivate} className="mx_UserInfo_field">
|
||||
<AccessibleButton onClick={onSynapseDeactivate} className="mx_UserInfo_field mx_UserInfo_destructive">
|
||||
{_t("Deactivate user")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -958,6 +1080,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
if (room && user.roomId) {
|
||||
adminToolsContainer = (
|
||||
<RoomAdminToolsContainer
|
||||
powerLevels={powerLevels}
|
||||
member={user}
|
||||
room={room}
|
||||
startUpdating={startUpdating}
|
||||
|
@ -995,7 +1118,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
let presenceCurrentlyActive;
|
||||
let statusMessage;
|
||||
|
||||
if (user instanceof RoomMember) {
|
||||
if (user instanceof RoomMember && user.user) {
|
||||
presenceState = user.user.presence;
|
||||
presenceLastActiveAgo = user.user.lastActiveAgo;
|
||||
presenceCurrentlyActive = user.user.currentlyActive;
|
||||
|
@ -1024,32 +1147,19 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
statusLabel = <span className="mx_UserInfo_statusMessage">{ statusMessage }</span>;
|
||||
}
|
||||
|
||||
let memberDetails = null;
|
||||
|
||||
if (room && user.roomId) { // is in room
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
|
||||
|
||||
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
memberDetails = <div>
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<PowerSelector
|
||||
value={parseInt(user.powerLevel)}
|
||||
maxValue={roomPermissions.modifyLevelMax}
|
||||
disabled={roomPermissions.modifyLevelMax < 0}
|
||||
usersDefault={powerLevelUsersDefault}
|
||||
onChange={onPowerChange} />
|
||||
</div>
|
||||
|
||||
</div>;
|
||||
}
|
||||
|
||||
const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl;
|
||||
let avatarElement;
|
||||
if (avatarUrl) {
|
||||
const httpUrl = cli.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||
avatarElement = <div className="mx_UserInfo_avatar" onClick={onMemberAvatarClick}>
|
||||
<img src={httpUrl} alt={_t("Profile picture")} />
|
||||
avatarElement = <div
|
||||
className="mx_UserInfo_avatar"
|
||||
onClick={onMemberAvatarClick}
|
||||
onKeyDown={onMemberAvatarKey}
|
||||
tabIndex="0"
|
||||
role="img"
|
||||
aria-label={_t("Profile picture")}
|
||||
>
|
||||
<div><div style={{backgroundImage: `url(${httpUrl})`}} /></div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -1061,6 +1171,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
title={_t('Close')} />;
|
||||
}
|
||||
|
||||
const memberDetails = <PowerLevelSection
|
||||
powerLevels={powerLevels}
|
||||
user={user} room={room} roomPermissions={roomPermissions}
|
||||
/>;
|
||||
|
||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
|
||||
const [devices, setDevices] = useState(undefined);
|
||||
// Download device lists
|
||||
|
@ -1085,14 +1201,15 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
setDevices(null);
|
||||
}
|
||||
}
|
||||
|
||||
_downloadDeviceList();
|
||||
if (isRoomEncrypted) {
|
||||
_downloadDeviceList();
|
||||
}
|
||||
|
||||
// Handle being unmounted
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [cli, user.userId]);
|
||||
}, [cli, user.userId, isRoomEncrypted]);
|
||||
|
||||
// Listen to changes
|
||||
useEffect(() => {
|
||||
|
@ -1109,21 +1226,20 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
}
|
||||
};
|
||||
|
||||
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
if (isRoomEncrypted) {
|
||||
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
}
|
||||
// Handle being unmounted
|
||||
return () => {
|
||||
cancel = true;
|
||||
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
if (isRoomEncrypted) {
|
||||
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
}
|
||||
};
|
||||
}, [cli, user.userId]);
|
||||
|
||||
let devicesSection;
|
||||
const isRoomEncrypted = _enableDevices && room && cli.isRoomEncrypted(room.roomId);
|
||||
if (isRoomEncrypted) {
|
||||
devicesSection = <DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />;
|
||||
} else {
|
||||
let text;
|
||||
}, [cli, user.userId, isRoomEncrypted]);
|
||||
|
||||
let text;
|
||||
if (!isRoomEncrypted) {
|
||||
if (!_enableDevices) {
|
||||
text = _t("This client does not support end-to-end encryption.");
|
||||
} else if (room) {
|
||||
|
@ -1131,22 +1247,24 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
} else {
|
||||
// TODO what to render for GroupMember
|
||||
}
|
||||
|
||||
if (text) {
|
||||
devicesSection = (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Trust & Devices") }</h3>
|
||||
<div className="mx_UserInfo_devices">
|
||||
{ text }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
text = _t("Messages in this room are end-to-end encrypted.");
|
||||
}
|
||||
|
||||
const devicesSection = isRoomEncrypted ?
|
||||
(<DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />) : null;
|
||||
const securitySection = (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Security") }</h3>
|
||||
<p>{ text }</p>
|
||||
<AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyDevice(user.userId, null)}>{_t("Verify")}</AccessibleButton>
|
||||
{ devicesSection }
|
||||
</div>
|
||||
);
|
||||
|
||||
let e2eIcon;
|
||||
if (isRoomEncrypted && devices) {
|
||||
e2eIcon = <E2EIcon status={_getE2EStatus(devices)} isUser={true} />;
|
||||
e2eIcon = <E2EIcon size={18} status={_getE2EStatus(devices)} isUser={true} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1156,16 +1274,14 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
|
||||
<div className="mx_UserInfo_container">
|
||||
<div className="mx_UserInfo_profile">
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<h2>
|
||||
<div >
|
||||
<h2 aria-label={displayName}>
|
||||
{ e2eIcon }
|
||||
{ displayName }
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mx_UserInfo_profileField">
|
||||
{ user.userId }
|
||||
</div>
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<div>{ user.userId }</div>
|
||||
<div className="mx_UserInfo_profileStatus">
|
||||
{presenceLabel}
|
||||
{statusLabel}
|
||||
</div>
|
||||
|
@ -1179,11 +1295,9 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
</div> }
|
||||
|
||||
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
|
||||
{ devicesSection }
|
||||
|
||||
{ directChatsSection }
|
||||
|
||||
{ securitySection }
|
||||
<UserOptionsSection
|
||||
devices={devices}
|
||||
canInvite={roomPermissions.canInvite}
|
||||
isIgnored={isIgnored}
|
||||
member={user} />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue