Merge branch 'develop' into travis/deactivate-2

This commit is contained in:
Travis Ralston 2019-11-15 10:24:20 -07:00
commit 616a5ab55d
19 changed files with 814 additions and 456 deletions

View file

@ -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} />