Merge pull request #3910 from matrix-org/t3chguy/cross-signing-composer

Cross Signing redesign for composer
This commit is contained in:
Michael Telatynski 2020-01-24 13:18:09 +00:00 committed by GitHub
commit 2c40b73ff6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 175 additions and 95 deletions

View file

@ -76,6 +76,8 @@ limitations under the License.
left: 60px; left: 60px;
margin-right: 0; // Counteract the E2EIcon class margin-right: 0; // Counteract the E2EIcon class
margin-left: 3px; // Counteract the E2EIcon class margin-left: 3px; // Counteract the E2EIcon class
width: 12px;
height: 12px;
} }
.mx_MessageComposer_noperm_error { .mx_MessageComposer_noperm_error {

View file

@ -813,10 +813,10 @@ export default createReactClass({
/* Check all verified user devices. */ /* Check all verified user devices. */
for (const userId of verified) { for (const userId of verified) {
const devices = await cli.getStoredDevicesForUser(userId); const devices = await cli.getStoredDevicesForUser(userId);
const allDevicesVerified = devices.every(({deviceId}) => { const anyDeviceNotVerified = devices.some(({deviceId}) => {
return cli.checkDeviceTrust(userId, deviceId).isVerified(); return !cli.checkDeviceTrust(userId, deviceId).isVerified();
}); });
if (!allDevicesVerified) { if (anyDeviceNotVerified) {
this.setState({ this.setState({
e2eStatus: "warning", e2eStatus: "warning",
}); });

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,76 +15,102 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useState} from "react";
import PropTypes from "prop-types";
import classNames from 'classnames'; import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import SettingsStore from '../../../settings/SettingsStore';
export default function(props) { import {_t, _td} from '../../../languageHandler';
const { isUser } = props; import {useFeatureEnabled} from "../../../hooks/useSettings";
const isNormal = props.status === "normal"; import AccessibleButton from "../elements/AccessibleButton";
const isWarning = props.status === "warning"; import Tooltip from "../elements/Tooltip";
const isVerified = props.status === "verified";
const e2eIconClasses = classNames({ export const E2E_STATE = {
VERIFIED: "verified",
WARNING: "warning",
UNKNOWN: "unknown",
NORMAL: "normal",
};
const crossSigningUserTitles = {
[E2E_STATE.WARNING]: _td("This user has not verified all of their devices."),
[E2E_STATE.NORMAL]: _td("You have not verified this user. This user has verified all of their devices."),
[E2E_STATE.VERIFIED]: _td("You have verified this user. This user has verified all of their devices."),
};
const crossSigningRoomTitles = {
[E2E_STATE.WARNING]: _td("Someone is using an unknown device"),
[E2E_STATE.NORMAL]: _td("This room is end-to-end encrypted"),
[E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"),
};
const legacyUserTitles = {
[E2E_STATE.WARNING]: _td("Some devices for this user are not trusted"),
[E2E_STATE.VERIFIED]: _td("All devices for this user are trusted"),
};
const legacyRoomTitles = {
[E2E_STATE.WARNING]: _td("Some devices in this encrypted room are not trusted"),
[E2E_STATE.VERIFIED]: _td("All devices in this encrypted room are trusted"),
};
const E2EIcon = ({isUser, status, className, size, onClick}) => {
const [hover, setHover] = useState(false);
const classes = classNames({
mx_E2EIcon: true, mx_E2EIcon: true,
mx_E2EIcon_warning: isWarning, mx_E2EIcon_warning: status === E2E_STATE.WARNING,
mx_E2EIcon_normal: isNormal, mx_E2EIcon_normal: status === E2E_STATE.NORMAL,
mx_E2EIcon_verified: isVerified, mx_E2EIcon_verified: status === E2E_STATE.VERIFIED,
}, props.className); }, className);
let e2eTitle; let e2eTitle;
const crossSigning = useFeatureEnabled("feature_cross_signing");
const crossSigning = SettingsStore.isFeatureEnabled("feature_cross_signing");
if (crossSigning && isUser) { if (crossSigning && isUser) {
if (isWarning) { e2eTitle = crossSigningUserTitles[status];
e2eTitle = _t(
"This user has not verified all of their devices.",
);
} else if (isNormal) {
e2eTitle = _t(
"You have not verified this user. " +
"This user has verified all of their devices.",
);
} else if (isVerified) {
e2eTitle = _t(
"You have verified this user. " +
"This user has verified all of their devices.",
);
}
} else if (crossSigning && !isUser) { } else if (crossSigning && !isUser) {
if (isWarning) { e2eTitle = crossSigningRoomTitles[status];
e2eTitle = _t(
"Some users in this encrypted room are not verified by you or " +
"they have not verified their own devices.",
);
} else if (isVerified) {
e2eTitle = _t(
"All users in this encrypted room are verified by you and " +
"they have verified their own devices.",
);
}
} else if (!crossSigning && isUser) { } else if (!crossSigning && isUser) {
if (isWarning) { e2eTitle = legacyUserTitles[status];
e2eTitle = _t("Some devices for this user are not trusted");
} else if (isVerified) {
e2eTitle = _t("All devices for this user are trusted");
}
} else if (!crossSigning && !isUser) { } else if (!crossSigning && !isUser) {
if (isWarning) { e2eTitle = legacyRoomTitles[status];
e2eTitle = _t("Some devices in this encrypted room are not trusted");
} else if (isVerified) {
e2eTitle = _t("All devices in this encrypted room are trusted");
}
} }
let style = null; let style;
if (props.size) { if (size) {
style = {width: `${props.size}px`, height: `${props.size}px`}; style = {width: `${size}px`, height: `${size}px`};
} }
const icon = (<div className={e2eIconClasses} style={style} title={e2eTitle} />); const onMouseOver = () => setHover(true);
if (props.onClick) { const onMouseOut = () => setHover(false);
return (<AccessibleButton onClick={props.onClick}>{ icon }</AccessibleButton>);
} else { let tip;
return icon; if (hover) {
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} />;
} }
if (onClick) {
return (
<AccessibleButton
onClick={onClick}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
className={classes}
style={style}
>
{ tip }
</AccessibleButton>
);
} }
return <div onMouseOver={onMouseOver} onMouseOut={onMouseOut} className={classes} style={style}>
{ tip }
</div>;
};
E2EIcon.propTypes = {
isUser: PropTypes.bool,
status: PropTypes.oneOf(Object.values(E2E_STATE)),
className: PropTypes.string,
size: PropTypes.number,
onClick: PropTypes.func,
};
export default E2EIcon;

View file

@ -33,6 +33,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList"; import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
import * as ObjectUtils from "../../../ObjectUtils"; import * as ObjectUtils from "../../../ObjectUtils";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {E2E_STATE} from "./E2EIcon";
const eventTileTypes = { const eventTileTypes = {
'm.room.message': 'messages.MessageEvent', 'm.room.message': 'messages.MessageEvent',
@ -66,13 +67,6 @@ const stateEventTileTypes = {
'm.room.related_groups': 'messages.TextualEvent', 'm.room.related_groups': 'messages.TextualEvent',
}; };
const E2E_STATE = {
VERIFIED: "verified",
WARNING: "warning",
UNKNOWN: "unknown",
NORMAL: "normal",
};
// Add all the Mjolnir stuff to the renderer // Add all the Mjolnir stuff to the renderer
for (const evType of ALL_RULE_TYPES) { for (const evType of ALL_RULE_TYPES) {
stateEventTileTypes[evType] = 'messages.TextualEvent'; stateEventTileTypes[evType] = 'messages.TextualEvent';

View file

@ -26,6 +26,7 @@ import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks'; import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
import ContentMessages from '../../../ContentMessages'; import ContentMessages from '../../../ContentMessages';
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
import SettingsStore from "../../../settings/SettingsStore";
function ComposerAvatar(props) { function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@ -168,7 +169,6 @@ export default class MessageComposer extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.onInputStateChanged = this.onInputStateChanged.bind(this); this.onInputStateChanged = this.onInputStateChanged.bind(this);
this.onEvent = this.onEvent.bind(this);
this._onRoomStateEvents = this._onRoomStateEvents.bind(this); this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this); this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this._onTombstoneClick = this._onTombstoneClick.bind(this); this._onTombstoneClick = this._onTombstoneClick.bind(this);
@ -182,11 +182,6 @@ export default class MessageComposer extends React.Component {
} }
componentDidMount() { componentDidMount() {
// N.B. using 'event' rather than 'RoomEvents' otherwise the crypto handler
// for 'event' fires *after* 'RoomEvent', and our room won't have yet been
// marked as encrypted.
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
MatrixClientPeg.get().on("event", this.onEvent);
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents); MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._waitForOwnMember(); this._waitForOwnMember();
@ -210,7 +205,6 @@ export default class MessageComposer extends React.Component {
componentWillUnmount() { componentWillUnmount() {
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("event", this.onEvent);
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents); MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
} }
if (this._roomStoreToken) { if (this._roomStoreToken) {
@ -218,13 +212,6 @@ export default class MessageComposer extends React.Component {
} }
} }
onEvent(event) {
if (event.getType() !== 'm.room.encryption') return;
if (event.getRoomId() !== this.props.room.roomId) return;
// TODO: put (encryption state??) in state
this.forceUpdate();
}
_onRoomStateEvents(ev, state) { _onRoomStateEvents(ev, state) {
if (ev.getRoomId() !== this.props.room.roomId) return; if (ev.getRoomId() !== this.props.room.roomId) return;
@ -282,21 +269,36 @@ export default class MessageComposer extends React.Component {
} }
renderPlaceholderText() { renderPlaceholderText() {
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (this.state.isQuoting) { if (this.state.isQuoting) {
if (roomIsEncrypted) { if (this.props.e2eStatus) {
return _t('Send an encrypted reply…');
} else {
return _t('Send a reply…');
}
} else {
if (this.props.e2eStatus) {
return _t('Send an encrypted message…');
} else {
return _t('Send a message…');
}
}
} else {
if (this.state.isQuoting) {
if (this.props.e2eStatus) {
return _t('Send an encrypted reply…'); return _t('Send an encrypted reply…');
} else { } else {
return _t('Send a reply (unencrypted)…'); return _t('Send a reply (unencrypted)…');
} }
} else { } else {
if (roomIsEncrypted) { if (this.props.e2eStatus) {
return _t('Send an encrypted message…'); return _t('Send an encrypted message…');
} else { } else {
return _t('Send a message (unencrypted)…'); return _t('Send a message (unencrypted)…');
} }
} }
} }
}
render() { render() {
const controls = [ const controls = [

52
src/hooks/useSettings.js Normal file
View file

@ -0,0 +1,52 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {useEffect, useState} from "react";
import SettingsStore from '../settings/SettingsStore';
// Hook to fetch the value of a setting and dynamically update when it changes
export const useSettingValue = (settingName, roomId = null, excludeDefault = false) => {
const [value, setValue] = useState(SettingsStore.getValue(settingName, roomId, excludeDefault));
useEffect(() => {
const ref = SettingsStore.watchSetting(settingName, roomId, () => {
setValue(SettingsStore.getValue(settingName, roomId, excludeDefault));
});
// clean-up
return () => {
SettingsStore.unwatchSetting(ref);
};
}, [settingName, roomId, excludeDefault]);
return value;
};
// Hook to fetch whether a feature is enabled and dynamically update when that changes
export const useFeatureEnabled = (featureName, roomId = null) => {
const [enabled, setEnabled] = useState(SettingsStore.isFeatureEnabled(featureName, roomId));
useEffect(() => {
const ref = SettingsStore.watchSetting(featureName, roomId, () => {
setEnabled(SettingsStore.isFeatureEnabled(featureName, roomId));
});
// clean-up
return () => {
SettingsStore.unwatchSetting(ref);
};
}, [featureName, roomId]);
return enabled;
};

View file

@ -887,8 +887,9 @@
"This user has not verified all of their devices.": "This user has not verified all of their devices.", "This user has not verified all of their devices.": "This user has not verified all of their devices.",
"You have not verified this user. This user has verified all of their devices.": "You have not verified this user. This user has verified all of their devices.", "You have not verified this user. This user has verified all of their devices.": "You have not verified this user. This user has verified all of their devices.",
"You have verified this user. This user has verified all of their devices.": "You have verified this user. This user has verified all of their devices.", "You have verified this user. This user has verified all of their devices.": "You have verified this user. This user has verified all of their devices.",
"Some users in this encrypted room are not verified by you or they have not verified their own devices.": "Some users in this encrypted room are not verified by you or they have not verified their own devices.", "Someone is using an unknown device": "Someone is using an unknown device",
"All users in this encrypted room are verified by you and they have verified their own devices.": "All users in this encrypted room are verified by you and they have verified their own devices.", "This room is end-to-end encrypted": "This room is end-to-end encrypted",
"Everyone in this room is verified": "Everyone in this room is verified",
"Some devices for this user are not trusted": "Some devices for this user are not trusted", "Some devices for this user are not trusted": "Some devices for this user are not trusted",
"All devices for this user are trusted": "All devices for this user are trusted", "All devices for this user are trusted": "All devices for this user are trusted",
"Some devices in this encrypted room are not trusted": "Some devices in this encrypted room are not trusted", "Some devices in this encrypted room are not trusted": "Some devices in this encrypted room are not trusted",
@ -964,8 +965,10 @@
"Hangup": "Hangup", "Hangup": "Hangup",
"Upload file": "Upload file", "Upload file": "Upload file",
"Send an encrypted reply…": "Send an encrypted reply…", "Send an encrypted reply…": "Send an encrypted reply…",
"Send a reply (unencrypted)…": "Send a reply (unencrypted)…", "Send a reply…": "Send a reply…",
"Send an encrypted message…": "Send an encrypted message…", "Send an encrypted message…": "Send an encrypted message…",
"Send a message…": "Send a message…",
"Send a reply (unencrypted)…": "Send a reply (unencrypted)…",
"Send a message (unencrypted)…": "Send a message (unencrypted)…", "Send a message (unencrypted)…": "Send a message (unencrypted)…",
"The conversation continues here.": "The conversation continues here.", "The conversation continues here.": "The conversation continues here.",
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.", "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",