diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index 5efca51844..fae9d0dfe3 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -76,6 +76,8 @@ limitations under the License.
left: 60px;
margin-right: 0; // Counteract the E2EIcon class
margin-left: 3px; // Counteract the E2EIcon class
+ width: 12px;
+ height: 12px;
}
.mx_MessageComposer_noperm_error {
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 6e20b2c8f2..5c243f04bc 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -813,10 +813,10 @@ export default createReactClass({
/* Check all verified user devices. */
for (const userId of verified) {
const devices = await cli.getStoredDevicesForUser(userId);
- const allDevicesVerified = devices.every(({deviceId}) => {
- return cli.checkDeviceTrust(userId, deviceId).isVerified();
+ const anyDeviceNotVerified = devices.some(({deviceId}) => {
+ return !cli.checkDeviceTrust(userId, deviceId).isVerified();
});
- if (!allDevicesVerified) {
+ if (anyDeviceNotVerified) {
this.setState({
e2eStatus: "warning",
});
diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js
index 545d1fd7ed..df5fe204d4 100644
--- a/src/components/views/rooms/E2EIcon.js
+++ b/src/components/views/rooms/E2EIcon.js
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
+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.
@@ -14,76 +15,102 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import React, {useState} from "react";
+import PropTypes from "prop-types";
import classNames from 'classnames';
-import { _t } from '../../../languageHandler';
-import AccessibleButton from '../elements/AccessibleButton';
-import SettingsStore from '../../../settings/SettingsStore';
-export default function(props) {
- const { isUser } = props;
- const isNormal = props.status === "normal";
- const isWarning = props.status === "warning";
- const isVerified = props.status === "verified";
- const e2eIconClasses = classNames({
+import {_t, _td} from '../../../languageHandler';
+import {useFeatureEnabled} from "../../../hooks/useSettings";
+import AccessibleButton from "../elements/AccessibleButton";
+import Tooltip from "../elements/Tooltip";
+
+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_warning: isWarning,
- mx_E2EIcon_normal: isNormal,
- mx_E2EIcon_verified: isVerified,
- }, props.className);
+ mx_E2EIcon_warning: status === E2E_STATE.WARNING,
+ mx_E2EIcon_normal: status === E2E_STATE.NORMAL,
+ mx_E2EIcon_verified: status === E2E_STATE.VERIFIED,
+ }, className);
+
let e2eTitle;
-
- const crossSigning = SettingsStore.isFeatureEnabled("feature_cross_signing");
+ const crossSigning = useFeatureEnabled("feature_cross_signing");
if (crossSigning && isUser) {
- if (isWarning) {
- 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.",
- );
- }
+ e2eTitle = crossSigningUserTitles[status];
} else if (crossSigning && !isUser) {
- if (isWarning) {
- 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.",
- );
- }
+ e2eTitle = crossSigningRoomTitles[status];
} else if (!crossSigning && isUser) {
- if (isWarning) {
- e2eTitle = _t("Some devices for this user are not trusted");
- } else if (isVerified) {
- e2eTitle = _t("All devices for this user are trusted");
- }
+ e2eTitle = legacyUserTitles[status];
} else if (!crossSigning && !isUser) {
- if (isWarning) {
- e2eTitle = _t("Some devices in this encrypted room are not trusted");
- } else if (isVerified) {
- e2eTitle = _t("All devices in this encrypted room are trusted");
- }
+ e2eTitle = legacyRoomTitles[status];
}
- let style = null;
- if (props.size) {
- style = {width: `${props.size}px`, height: `${props.size}px`};
+ let style;
+ if (size) {
+ style = {width: `${size}px`, height: `${size}px`};
}
- const icon = (
);
- if (props.onClick) {
- return ({ icon });
- } else {
- return icon;
+ const onMouseOver = () => setHover(true);
+ const onMouseOut = () => setHover(false);
+
+ let tip;
+ if (hover) {
+ tip = ;
}
-}
+
+ if (onClick) {
+ return (
+
+ { tip }
+
+ );
+ }
+
+ return
+ { tip }
+
;
+};
+
+E2EIcon.propTypes = {
+ isUser: PropTypes.bool,
+ status: PropTypes.oneOf(Object.values(E2E_STATE)),
+ className: PropTypes.string,
+ size: PropTypes.number,
+ onClick: PropTypes.func,
+};
+
+export default E2EIcon;
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 634b77c9e1..940515f02e 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -33,6 +33,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
import * as ObjectUtils from "../../../ObjectUtils";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {E2E_STATE} from "./E2EIcon";
const eventTileTypes = {
'm.room.message': 'messages.MessageEvent',
@@ -66,13 +67,6 @@ const stateEventTileTypes = {
'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
for (const evType of ALL_RULE_TYPES) {
stateEventTileTypes[evType] = 'messages.TextualEvent';
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 8d36f02d02..53e10fa750 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -26,6 +26,7 @@ import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
import ContentMessages from '../../../ContentMessages';
import E2EIcon from './E2EIcon';
+import SettingsStore from "../../../settings/SettingsStore";
function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@@ -168,7 +169,6 @@ export default class MessageComposer extends React.Component {
constructor(props) {
super(props);
this.onInputStateChanged = this.onInputStateChanged.bind(this);
- this.onEvent = this.onEvent.bind(this);
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this._onTombstoneClick = this._onTombstoneClick.bind(this);
@@ -182,11 +182,6 @@ export default class MessageComposer extends React.Component {
}
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);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._waitForOwnMember();
@@ -210,7 +205,6 @@ export default class MessageComposer extends React.Component {
componentWillUnmount() {
if (MatrixClientPeg.get()) {
- MatrixClientPeg.get().removeListener("event", this.onEvent);
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
}
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) {
if (ev.getRoomId() !== this.props.room.roomId) return;
@@ -282,18 +269,33 @@ export default class MessageComposer extends React.Component {
}
renderPlaceholderText() {
- const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
- if (this.state.isQuoting) {
- if (roomIsEncrypted) {
- return _t('Send an encrypted reply…');
+ if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
+ if (this.state.isQuoting) {
+ if (this.props.e2eStatus) {
+ return _t('Send an encrypted reply…');
+ } else {
+ return _t('Send a reply…');
+ }
} else {
- return _t('Send a reply (unencrypted)…');
+ if (this.props.e2eStatus) {
+ return _t('Send an encrypted message…');
+ } else {
+ return _t('Send a message…');
+ }
}
} else {
- if (roomIsEncrypted) {
- return _t('Send an encrypted message…');
+ if (this.state.isQuoting) {
+ if (this.props.e2eStatus) {
+ return _t('Send an encrypted reply…');
+ } else {
+ return _t('Send a reply (unencrypted)…');
+ }
} else {
- return _t('Send a message (unencrypted)…');
+ if (this.props.e2eStatus) {
+ return _t('Send an encrypted message…');
+ } else {
+ return _t('Send a message (unencrypted)…');
+ }
}
}
}
diff --git a/src/hooks/useSettings.js b/src/hooks/useSettings.js
new file mode 100644
index 0000000000..151a6369de
--- /dev/null
+++ b/src/hooks/useSettings.js
@@ -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;
+};
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 099b64dd49..504ed7c876 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -887,8 +887,9 @@
"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 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.",
- "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.",
+ "Someone is using an unknown device": "Someone is using an unknown device",
+ "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",
"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",
@@ -964,8 +965,10 @@
"Hangup": "Hangup",
"Upload file": "Upload file",
"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 a message…": "Send a message…",
+ "Send a reply (unencrypted)…": "Send a reply (unencrypted)…",
"Send a message (unencrypted)…": "Send a message (unencrypted)…",
"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.",