From 9706114bb571a903fd607838229717fe767fe465 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 16:54:31 +0000 Subject: [PATCH 1/6] move E2E_STATE to E2EIcon to simplify imports --- src/components/views/rooms/E2EIcon.js | 18 +++++++++++++----- src/components/views/rooms/EventTile.js | 8 +------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js index 545d1fd7ed..6ee20023ff 100644 --- a/src/components/views/rooms/E2EIcon.js +++ b/src/components/views/rooms/E2EIcon.js @@ -14,22 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; import SettingsStore from '../../../settings/SettingsStore'; +export const E2E_STATE = { + VERIFIED: "verified", + WARNING: "warning", + UNKNOWN: "unknown", + NORMAL: "normal", +}; + export default function(props) { - const { isUser } = props; - const isNormal = props.status === "normal"; - const isWarning = props.status === "warning"; - const isVerified = props.status === "verified"; + const { isUser, status, className } = props; + const isNormal = status === E2E_STATE.NORMAL; + const isWarning = status === E2E_STATE.WARNING; + const isVerified = status === E2E_STATE.VERIFIED; const e2eIconClasses = classNames({ mx_E2EIcon: true, mx_E2EIcon_warning: isWarning, mx_E2EIcon_normal: isNormal, mx_E2EIcon_verified: isVerified, - }, props.className); + }, className); let e2eTitle; const crossSigning = SettingsStore.isFeatureEnabled("feature_cross_signing"); 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'; From b7d1c17ad1453c237e99e3bd87d1fdb722bb01d7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 16:56:27 +0000 Subject: [PATCH 2/6] simple optimization to bail out of check on first failure --- src/components/structures/RoomView.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9b02f6d503..23d3002faa 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -812,10 +812,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", }); From 78e1d1674f15d99a182158fcb73974284512e8c3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 23 Jan 2020 13:00:17 +0000 Subject: [PATCH 3/6] reactor E2EIcon for reusability --- src/components/views/rooms/E2EIcon.js | 100 +++++++++++++------------- 1 file changed, 48 insertions(+), 52 deletions(-) diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js index 6ee20023ff..36f230f472 100644 --- a/src/components/views/rooms/E2EIcon.js +++ b/src/components/views/rooms/E2EIcon.js @@ -15,8 +15,10 @@ limitations under the License. */ import React from "react"; +import PropTypes from "prop-types"; import classNames from 'classnames'; -import { _t } from '../../../languageHandler'; + +import {_t, _td} from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; import SettingsStore from '../../../settings/SettingsStore'; @@ -27,71 +29,65 @@ export const E2E_STATE = { NORMAL: "normal", }; -export default function(props) { - const { isUser, status, className } = props; - const isNormal = status === E2E_STATE.NORMAL; - const isWarning = status === E2E_STATE.WARNING; - const isVerified = status === E2E_STATE.VERIFIED; +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("Some users in this encrypted room are not verified by you or they have not verified " + + "their own devices."), + [E2E_STATE.VERIFIED]: _td("All users in this encrypted room are verified by you and they have verified their " + + "own devices."), +}; +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 e2eIconClasses = classNames({ mx_E2EIcon: true, - mx_E2EIcon_warning: isWarning, - mx_E2EIcon_normal: isNormal, - mx_E2EIcon_verified: isVerified, + mx_E2EIcon_warning: status === E2E_STATE.WARNING, + mx_E2EIcon_normal: status === E2E_STATE.NORMAL, + mx_E2EIcon_verified: status === E2E_STATE.VERIFIED, }, className); - let e2eTitle; + let e2eTitle; const crossSigning = SettingsStore.isFeatureEnabled("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`}; + if (size) { + style = {width: `${size}px`, height: `${size}px`}; } - const icon = (
); - if (props.onClick) { - return ({ icon }); + const icon = (
); + if (onClick) { + return ({ icon }); } else { return icon; } -} +}; + +E2EIcon.propTypes = { + isUser: PropTypes.bool, + status: PropTypes.oneOf(Object.values(E2E_STATE)), + className: PropTypes.string, + size: PropTypes.number, + onClick: PropTypes.func, +}; + +export default E2EIcon; From 74b08ea4895a581ebb16a83a470e170213f7b24f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 23 Jan 2020 14:38:17 +0000 Subject: [PATCH 4/6] Clean up E2EIcon for better maintainability --- src/components/views/rooms/E2EIcon.js | 13 +++---- src/hooks/useSettings.js | 52 +++++++++++++++++++++++++++ src/i18n/strings/en_EN.json | 5 +-- 3 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 src/hooks/useSettings.js diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js index 36f230f472..7ac3b5af2d 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. @@ -20,7 +21,7 @@ import classNames from 'classnames'; import {_t, _td} from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; -import SettingsStore from '../../../settings/SettingsStore'; +import {useFeatureEnabled} from "../../../hooks/useSettings"; export const E2E_STATE = { VERIFIED: "verified", @@ -35,11 +36,11 @@ const crossSigningUserTitles = { [E2E_STATE.VERIFIED]: _td("You have verified this user. This user has verified all of their devices."), }; const crossSigningRoomTitles = { - [E2E_STATE.WARNING]: _td("Some users in this encrypted room are not verified by you or they have not verified " + - "their own devices."), - [E2E_STATE.VERIFIED]: _td("All users in this encrypted room are verified by you and they have verified their " + - "own devices."), + [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"), @@ -58,7 +59,7 @@ const E2EIcon = ({isUser, status, className, size, onClick}) => { }, className); let e2eTitle; - const crossSigning = SettingsStore.isFeatureEnabled("feature_cross_signing"); + const crossSigning = useFeatureEnabled("feature_cross_signing"); if (crossSigning && isUser) { e2eTitle = crossSigningUserTitles[status]; } else if (crossSigning && !isUser) { 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 d19cbb9bfd..fb2898ae3d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -884,8 +884,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", From 662b34c8dbc2196b4061f08c8555c2a1e02cf5a2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 23 Jan 2020 14:38:39 +0000 Subject: [PATCH 5/6] Update MessageComposer placeholder and e2e icon size --- res/css/views/rooms/_MessageComposer.scss | 2 + src/components/views/rooms/MessageComposer.js | 46 ++++++++++--------- src/i18n/strings/en_EN.json | 4 +- 3 files changed, 29 insertions(+), 23 deletions(-) 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/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/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fb2898ae3d..20707a4f29 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -962,8 +962,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.", From b72ab57e1b33d443b40a56e73645044233a736f5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 24 Jan 2020 10:13:03 +0000 Subject: [PATCH 6/6] add to --- src/components/views/rooms/E2EIcon.js | 40 +++++++++++++++++++++------ 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js index 7ac3b5af2d..df5fe204d4 100644 --- a/src/components/views/rooms/E2EIcon.js +++ b/src/components/views/rooms/E2EIcon.js @@ -15,13 +15,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, {useState} from "react"; import PropTypes from "prop-types"; import classNames from 'classnames'; import {_t, _td} from '../../../languageHandler'; -import AccessibleButton from '../elements/AccessibleButton'; import {useFeatureEnabled} from "../../../hooks/useSettings"; +import AccessibleButton from "../elements/AccessibleButton"; +import Tooltip from "../elements/Tooltip"; export const E2E_STATE = { VERIFIED: "verified", @@ -51,7 +52,9 @@ const legacyRoomTitles = { }; const E2EIcon = ({isUser, status, className, size, onClick}) => { - const e2eIconClasses = classNames({ + const [hover, setHover] = useState(false); + + const classes = classNames({ mx_E2EIcon: true, mx_E2EIcon_warning: status === E2E_STATE.WARNING, mx_E2EIcon_normal: status === E2E_STATE.NORMAL, @@ -70,17 +73,36 @@ const E2EIcon = ({isUser, status, className, size, onClick}) => { e2eTitle = legacyRoomTitles[status]; } - let style = null; + let style; if (size) { style = {width: `${size}px`, height: `${size}px`}; } - const icon = (
); - if (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 = {