From 7367c73a37a795f1baf3e8be0f33da51ca97dbd7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Jan 2020 10:50:02 +0000 Subject: [PATCH 01/57] Searchbox Enter is to clear, tabbing to clear button doesn't work, remove it Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/SearchBox.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 9090152de8..6bf7c754f0 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -133,9 +133,11 @@ module.exports = createReactClass({ return null; } const clearButton = (!this.state.blurred || this.state.searchTerm) ? - ( {this._clearSearch("button"); } }> + ( {this._clearSearch("button"); } }> ) : undefined; // show a shorter placeholder when blurred, if requested From 5252cf4c454c8790ea7f5d12eb7e8ac426aa57a7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jan 2020 02:44:22 +0000 Subject: [PATCH 02/57] Implement roving tab index context based magic thing and demo on LeftPanel Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_RoomTile.scss | 5 +- src/components/structures/LeftPanel.js | 3 - src/components/structures/RoomSubList.js | 36 +++- .../views/groups/GroupInviteTile.js | 17 +- src/components/views/rooms/RoomList.js | 5 +- src/components/views/rooms/RoomTile.js | 8 +- src/contexts/RovingTabIndexContext.js | 193 ++++++++++++++++++ 7 files changed, 242 insertions(+), 25 deletions(-) create mode 100644 src/contexts/RovingTabIndexContext.js diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index cb1137bb2f..db2c09f6f1 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -142,10 +142,11 @@ limitations under the License. } } -// toggle menuButton and badge on hover/menu displayed +// toggle menuButton and badge on menu displayed .mx_RoomTile_menuDisplayed, // or on keyboard focus of room tile -.mx_RoomTile.focus-visible:focus-within, +.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:focus-within, +// or on pointer hover .mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:hover { .mx_RoomTile_menuButton { display: block; diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 796840a625..3444225d06 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -129,9 +129,6 @@ const LeftPanel = createReactClass({ if (!this.focusedElement) return; switch (ev.key) { - case Key.TAB: - this._onMoveFocus(ev, ev.shiftKey); - break; case Key.ARROW_UP: this._onMoveFocus(ev, true, true); break; diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 123ed7c4e1..915a952e79 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -31,6 +31,7 @@ import PropTypes from 'prop-types'; import RoomTile from "../views/rooms/RoomTile"; import LazyRenderList from "../views/elements/LazyRenderList"; import {_t} from "../../languageHandler"; +import {RovingTabIndex, RovingTabIndexGroup} from "../../contexts/RovingTabIndexContext"; // turn this on for drop & drag console debugging galore const debug = false; @@ -272,20 +273,32 @@ export default class RoomSubList extends React.PureComponent { // Wrap the contents in a div and apply styles to the child div so that the browser default outline works if (subListNotifCount > 0) { badge = ( - +
{ FormattingUtils.formatCount(subListNotifCount) }
-
+ ); } else if (this.props.isInvite && this.props.list.length) { // no notifications but highlight anyway because this is an invite badge badge = ( - +
{ this.props.list.length }
-
+ ); } } @@ -308,7 +321,9 @@ export default class RoomSubList extends React.PureComponent { let addRoomButton; if (this.props.onAddRoom) { addRoomButton = ( - ); } - return ( + return
- {this.props.label} { incomingCall } - + { badge } { addRoomButton }
- ); +
; } checkOverflow = () => { diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index c0d0d9eafe..e7ccbdf40b 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -26,6 +26,7 @@ import classNames from 'classnames'; import MatrixClientPeg from "../../../MatrixClientPeg"; import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {RovingTabIndex, RovingTabIndexGroup} from "../../../contexts/RovingTabIndexContext"; // XXX this class copies a lot from RoomTile.js export default createReactClass({ @@ -138,14 +139,16 @@ export default createReactClass({ const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!'; const badge = ( - { badgeContent } - + ); let tooltip; @@ -170,8 +173,10 @@ export default createReactClass({ ); } - return - + { tooltip } - + { contextMenu } - ; + ; }, }); diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 35a5ca9e66..277aedb65e 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -41,6 +41,7 @@ import ResizeHandle from '../elements/ResizeHandle'; import {Resizer} from '../../../resizer'; import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; +import {RovingTabIndexContextWrapper} from "../../../contexts/RovingTabIndexContext"; const HIDE_CONFERENCE_CHANS = true; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; const HOVER_MOVE_TIMEOUT = 1000; @@ -788,7 +789,9 @@ module.exports = createReactClass({ onMouseMove={this.onMouseMove} onMouseLeave={this.onMouseLeave} > - { subListComponents } + + { subListComponents } + ); }, diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 4dbcc7ca03..6358564042 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -32,6 +32,7 @@ import ActiveRoomObserver from '../../../ActiveRoomObserver'; import RoomViewStore from '../../../stores/RoomViewStore'; import SettingsStore from "../../../settings/SettingsStore"; import {_t} from "../../../languageHandler"; +import {RovingTabIndex} from "../../../contexts/RovingTabIndexContext"; module.exports = createReactClass({ displayName: 'RoomTile', @@ -432,8 +433,9 @@ module.exports = createReactClass({ } return - { /* { incomingCallBox } */ } { tooltip } - + { contextMenu } ; diff --git a/src/contexts/RovingTabIndexContext.js b/src/contexts/RovingTabIndexContext.js new file mode 100644 index 0000000000..a571bd2eae --- /dev/null +++ b/src/contexts/RovingTabIndexContext.js @@ -0,0 +1,193 @@ +/* + * + * Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + * + * 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 React, { + createContext, + useCallback, + useContext, + useLayoutEffect, + useMemo, + useRef, + useReducer, +} from "react"; +import PropTypes from "prop-types"; +import {Key} from "../Keyboard"; + +const DOCUMENT_POSITION_PRECEDING = 2; +const ANY = Symbol(); + +const RovingTabIndexContext = createContext({ + state: { + activeRef: null, + refs: [], + }, + dispatch: () => {}, +}); +RovingTabIndexContext.displayName = "RovingTabIndexContext"; + +// TODO use a TypeScript type here +const types = { + REGISTER: "REGISTER", + UNREGISTER: "UNREGISTER", + SET_FOCUS: "SET_FOCUS", +}; + +const reducer = (state, action) => { + switch (action.type) { + case types.REGISTER: { + if (state.refs.length === 0) { + return { + ...state, + activeRef: action.payload.ref, + refs: [action.payload.ref], + }; + } + + if (state.refs.includes(action.payload.ref)) { + return state; // already in refs, this should not happen + } + + let newIndex = state.refs.findIndex(ref => { + return ref.current.compareDocumentPosition(action.payload.ref.current) & DOCUMENT_POSITION_PRECEDING; + }); + + if (newIndex < 0) { + newIndex = state.refs.length; // append to the end + } + + return { + ...state, + refs: [ + ...state.refs.slice(0, newIndex), + action.payload.ref, + ...state.refs.slice(newIndex), + ], + }; + } + case types.UNREGISTER: { + const refs = state.refs.filter(r => r !== action.payload.ref); // keep all other refs + + if (refs.length === state.refs.length) { + return state; // already removed, this should not happen + } + + if (state.activeRef === action.payload.ref) { // we just removed the active ref, need to replace it + const oldIndex = state.refs.findIndex(r => r === action.payload.ref); + return { + ...state, + activeRef: oldIndex >= refs.length ? refs[refs.length - 1] : refs[oldIndex], + refs, + }; + } + + return { + ...state, + refs, + }; + } + case types.SET_FOCUS: { + return { + ...state, + activeRef: action.payload.ref, + }; + } + default: + return state; + } +}; + +export const RovingTabIndexContextWrapper = ({children}) => { + const [state, dispatch] = useReducer(reducer, { + activeRef: null, + refs: [], + }); + + const context = useMemo(() => ({state, dispatch}), [state]); + + return + {children} + ; +}; + +export const useRovingTabIndex = () => { + const ref = useRef(null); + const context = useContext(RovingTabIndexContext); + + // setup/teardown + // add ref to the context + useLayoutEffect(() => { + context.dispatch({ + type: types.REGISTER, + payload: {ref}, + }); + return () => { + context.dispatch({ + type: types.UNREGISTER, + payload: {ref}, + }); + }; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const onFocus = useCallback(() => { + context.dispatch({ + type: types.SET_FOCUS, + payload: {ref}, + }); + }, [ref, context]); + const isActive = context.state.activeRef === ref || context.state.activeRef === ANY; + return [onFocus, isActive, ref]; +}; + +export const RovingTabIndexGroup = ({children}) => { + const [onFocus, isActive, ref] = useRovingTabIndex(); + + // fake reducer dispatch to catch SET_FOCUS calls and pass them to parent as a focus of the group + const dispatch = useCallback(({type}) => { + if (type === types.SET_FOCUS) { + onFocus(); + } + }, [onFocus]); + + const context = useMemo(() => ({ + state: {activeRef: isActive ? ANY : undefined}, + dispatch, + }), [isActive, dispatch]); + + return
+ + {children} + +
; +}; + +// Wraps a given element to attach it to the roving context, props onFocus and tabIndex overridden +export const RovingTabIndex = ({component: E, useInputRef, ...props}) => { + const [onFocus, isActive, ref] = useRovingTabIndex(); + const refProps = {}; + if (useInputRef) { + refProps.inputRef = ref; + } else { + refProps.ref = ref; + } + return ; +}; +RovingTabIndex.propTypes = { + component: PropTypes.elementType.isRequired, + useInputRef: PropTypes.bool, // whether to pass inputRef instead of ref like for AccessibleButton +}; + From dedf1eab315347cd85ea45ed3d6a85d60f542104 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jan 2020 11:37:14 +0000 Subject: [PATCH 03/57] Iterate to get rid of the magic group and just provide a generic functional render wrapper Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomSubList.js | 149 +++++++++--------- .../views/groups/GroupInviteTile.js | 67 ++++---- src/components/views/rooms/RoomTile.js | 69 ++++---- src/contexts/RovingTabIndexContext.js | 81 +++++----- 4 files changed, 184 insertions(+), 182 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 915a952e79..98e69f6edb 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -31,7 +31,7 @@ import PropTypes from 'prop-types'; import RoomTile from "../views/rooms/RoomTile"; import LazyRenderList from "../views/elements/LazyRenderList"; import {_t} from "../../languageHandler"; -import {RovingTabIndex, RovingTabIndexGroup} from "../../contexts/RovingTabIndexContext"; +import {RovingTabIndexWrapper} from "../../contexts/RovingTabIndexContext"; // turn this on for drop & drag console debugging galore const debug = false; @@ -264,45 +264,6 @@ export default class RoomSubList extends React.PureComponent { const subListNotifCount = subListNotifications.count; const subListNotifHighlight = subListNotifications.highlight; - let badge; - if (!this.props.collapsed) { - const badgeClasses = classNames({ - 'mx_RoomSubList_badge': true, - 'mx_RoomSubList_badgeHighlight': subListNotifHighlight, - }); - // Wrap the contents in a div and apply styles to the child div so that the browser default outline works - if (subListNotifCount > 0) { - badge = ( - -
- { FormattingUtils.formatCount(subListNotifCount) } -
-
- ); - } else if (this.props.isInvite && this.props.list.length) { - // no notifications but highlight anyway because this is an invite badge - badge = ( - -
- { this.props.list.length } -
-
- ); - } - } - // When collapsed, allow a long hover on the header to show user // the full tag name and room count let title; @@ -318,19 +279,6 @@ export default class RoomSubList extends React.PureComponent { ; } - let addRoomButton; - if (this.props.onAddRoom) { - addRoomButton = ( - - ); - } - const len = this.props.list.length + this.props.extraTiles.length; let chevron; if (len) { @@ -342,26 +290,81 @@ export default class RoomSubList extends React.PureComponent { chevron = (
); } - return -
- - { chevron } - {this.props.label} - { incomingCall } - - { badge } - { addRoomButton } -
-
; + return + {({onFocus, isActive, ref}) => { + const tabIndex = isActive ? 0 : -1; + + let badge; + if (!this.props.collapsed) { + const badgeClasses = classNames({ + 'mx_RoomSubList_badge': true, + 'mx_RoomSubList_badgeHighlight': subListNotifHighlight, + }); + // Wrap the contents in a div and apply styles to the child div so that the browser default outline works + if (subListNotifCount > 0) { + badge = ( + +
+ { FormattingUtils.formatCount(subListNotifCount) } +
+
+ ); + } else if (this.props.isInvite && this.props.list.length) { + // no notifications but highlight anyway because this is an invite badge + badge = ( + +
+ { this.props.list.length } +
+
+ ); + } + } + + let addRoomButton; + if (this.props.onAddRoom) { + addRoomButton = ( + + ); + } + + return ( +
+ + { chevron } + {this.props.label} + { incomingCall } + + { badge } + { addRoomButton } +
+ ); + } } +
; } checkOverflow = () => { diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index e7ccbdf40b..70baeb1e78 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -26,7 +26,7 @@ import classNames from 'classnames'; import MatrixClientPeg from "../../../MatrixClientPeg"; import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {RovingTabIndex, RovingTabIndexGroup} from "../../../contexts/RovingTabIndexContext"; +import {RovingTabIndexWrapper} from "../../../contexts/RovingTabIndexContext"; // XXX this class copies a lot from RoomTile.js export default createReactClass({ @@ -138,18 +138,6 @@ export default createReactClass({ }); const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!'; - const badge = ( - - { badgeContent } - - ); let tooltip; if (this.props.collapsed && this.state.hover) { @@ -173,27 +161,40 @@ export default createReactClass({ ); } - return - -
- { av } -
-
- { label } - { badge } -
- { tooltip } -
+ return + + {({onFocus, isActive, ref}) => + +
+ { av } +
+
+ { label } + + { badgeContent } + +
+ { tooltip } +
+ } +
{ contextMenu } -
; + ; }, }); diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 6358564042..001baf0b96 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -32,7 +32,7 @@ import ActiveRoomObserver from '../../../ActiveRoomObserver'; import RoomViewStore from '../../../stores/RoomViewStore'; import SettingsStore from "../../../settings/SettingsStore"; import {_t} from "../../../languageHandler"; -import {RovingTabIndex} from "../../../contexts/RovingTabIndexContext"; +import {RovingTabIndexWrapper} from "../../../contexts/RovingTabIndexContext"; module.exports = createReactClass({ displayName: 'RoomTile', @@ -433,37 +433,42 @@ module.exports = createReactClass({ } return - -
-
- - { dmIndicator } -
-
- { privateIcon } -
-
- { label } - { subtextLabel } -
- { dmOnline } - { contextMenuButton } - { badge } -
- { /* { incomingCallBox } */ } - { tooltip } -
+ + {({onFocus, isActive, ref}) => + +
+
+ + { dmIndicator } +
+
+ { privateIcon } +
+
+ { label } + { subtextLabel } +
+ { dmOnline } + { contextMenuButton } + { badge } +
+ { /* { incomingCallBox } */ } + { tooltip } +
+ } +
{ contextMenu }
; diff --git a/src/contexts/RovingTabIndexContext.js b/src/contexts/RovingTabIndexContext.js index a571bd2eae..f5001d28cc 100644 --- a/src/contexts/RovingTabIndexContext.js +++ b/src/contexts/RovingTabIndexContext.js @@ -25,11 +25,9 @@ import React, { useRef, useReducer, } from "react"; -import PropTypes from "prop-types"; import {Key} from "../Keyboard"; const DOCUMENT_POSITION_PRECEDING = 2; -const ANY = Symbol(); const RovingTabIndexContext = createContext({ state: { @@ -119,15 +117,42 @@ export const RovingTabIndexContextWrapper = ({children}) => { const context = useMemo(() => ({state, dispatch}), [state]); - return - {children} - ; + const onKeyDown = useCallback((ev) => { + if (state.refs.length <= 0) return; + + let handled = true; + switch (ev.key) { + case Key.HOME: + setImmediate(() => state.refs[0].current.focus()); + break; + case Key.END: + state.refs[state.refs.length - 1].current.focus(); + break; + default: + handled = false; + } + + if (handled) { + ev.preventDefault(); + ev.stopPropagation(); + } + }, [state]); + + return
+ + {children} + +
; }; -export const useRovingTabIndex = () => { - const ref = useRef(null); +export const useRovingTabIndex = (inputRef) => { + let ref = useRef(null); const context = useContext(RovingTabIndexContext); + if (inputRef) { + ref = inputRef; + } + // setup/teardown // add ref to the context useLayoutEffect(() => { @@ -149,45 +174,13 @@ export const useRovingTabIndex = () => { payload: {ref}, }); }, [ref, context]); - const isActive = context.state.activeRef === ref || context.state.activeRef === ANY; + + const isActive = context.state.activeRef === ref; return [onFocus, isActive, ref]; }; -export const RovingTabIndexGroup = ({children}) => { - const [onFocus, isActive, ref] = useRovingTabIndex(); - - // fake reducer dispatch to catch SET_FOCUS calls and pass them to parent as a focus of the group - const dispatch = useCallback(({type}) => { - if (type === types.SET_FOCUS) { - onFocus(); - } - }, [onFocus]); - - const context = useMemo(() => ({ - state: {activeRef: isActive ? ANY : undefined}, - dispatch, - }), [isActive, dispatch]); - - return
- - {children} - -
; -}; - -// Wraps a given element to attach it to the roving context, props onFocus and tabIndex overridden -export const RovingTabIndex = ({component: E, useInputRef, ...props}) => { - const [onFocus, isActive, ref] = useRovingTabIndex(); - const refProps = {}; - if (useInputRef) { - refProps.inputRef = ref; - } else { - refProps.ref = ref; - } - return ; -}; -RovingTabIndex.propTypes = { - component: PropTypes.elementType.isRequired, - useInputRef: PropTypes.bool, // whether to pass inputRef instead of ref like for AccessibleButton +export const RovingTabIndexWrapper = ({children, inputRef}) => { + const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); + return children({onFocus, isActive, ref}); }; From 2b37fe76242fe1ae328d435abb8d82ca454ec13d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jan 2020 00:40:08 +0000 Subject: [PATCH 04/57] do some renaming Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomList.js | 6 +++--- src/contexts/RovingTabIndexContext.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 45ff940c22..4441b4d539 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -39,7 +39,7 @@ import * as sdk from "../../../index"; import * as Receipt from "../../../utils/Receipt"; import {Resizer} from '../../../resizer'; import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; -import {RovingTabIndexContextWrapper} from "../../../contexts/RovingTabIndexContext"; +import {RovingTabIndexContextProvider} from "../../../contexts/RovingTabIndexContext"; const HIDE_CONFERENCE_CHANS = true; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; @@ -788,9 +788,9 @@ export default createReactClass({ onMouseMove={this.onMouseMove} onMouseLeave={this.onMouseLeave} > - + { subListComponents } - +
); }, diff --git a/src/contexts/RovingTabIndexContext.js b/src/contexts/RovingTabIndexContext.js index f5001d28cc..182a7f5504 100644 --- a/src/contexts/RovingTabIndexContext.js +++ b/src/contexts/RovingTabIndexContext.js @@ -109,7 +109,7 @@ const reducer = (state, action) => { } }; -export const RovingTabIndexContextWrapper = ({children}) => { +export const RovingTabIndexContextProvider = ({children}) => { const [state, dispatch] = useReducer(reducer, { activeRef: null, refs: [], From 8c1fdf4cabfb0984f5d34d4656bc3e48d345d7d8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jan 2020 01:25:44 +0000 Subject: [PATCH 05/57] tidy up Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/contexts/RovingTabIndexContext.js | 78 ++++++++++++++++++--------- 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/src/contexts/RovingTabIndexContext.js b/src/contexts/RovingTabIndexContext.js index 182a7f5504..2e8439d2a4 100644 --- a/src/contexts/RovingTabIndexContext.js +++ b/src/contexts/RovingTabIndexContext.js @@ -1,20 +1,18 @@ /* - * - * Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> - * - * 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. - * / - */ +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 React, { createContext, @@ -27,12 +25,25 @@ import React, { } from "react"; import {Key} from "../Keyboard"; +/** + * Module to simplify implementing the Roving TabIndex accessibility technique + * + * Wrap the Widget in an RovingTabIndexContextProvider + * and then for all buttons make use of useRovingTabIndex or RovingTabIndexWrapper. + * The code will keep track of which tabIndex was most recently focused and expose that information as `isActive` which + * can then be used to only set the tabIndex to 0 as expected by the roving tabindex technique. + * When the active button gets unmounted the closest button will be chosen as expected. + * Initially the first button to mount will be given active state. + * + * https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#Technique_1_Roving_tabindex + */ + const DOCUMENT_POSITION_PRECEDING = 2; const RovingTabIndexContext = createContext({ state: { activeRef: null, - refs: [], + refs: [], // list of refs in DOM order }, dispatch: () => {}, }); @@ -49,6 +60,7 @@ const reducer = (state, action) => { switch (action.type) { case types.REGISTER: { if (state.refs.length === 0) { + // Our list of refs was empty, set activeRef to this first item return { ...state, activeRef: action.payload.ref, @@ -60,6 +72,7 @@ const reducer = (state, action) => { return state; // already in refs, this should not happen } + // find the index of the first ref which is not preceding this one in DOM order let newIndex = state.refs.findIndex(ref => { return ref.current.compareDocumentPosition(action.payload.ref.current) & DOCUMENT_POSITION_PRECEDING; }); @@ -68,6 +81,7 @@ const reducer = (state, action) => { newIndex = state.refs.length; // append to the end } + // update the refs list return { ...state, refs: [ @@ -78,13 +92,16 @@ const reducer = (state, action) => { }; } case types.UNREGISTER: { - const refs = state.refs.filter(r => r !== action.payload.ref); // keep all other refs + // filter out the ref which we are removing + const refs = state.refs.filter(r => r !== action.payload.ref); if (refs.length === state.refs.length) { return state; // already removed, this should not happen } - if (state.activeRef === action.payload.ref) { // we just removed the active ref, need to replace it + if (state.activeRef === action.payload.ref) { + // we just removed the active ref, need to replace it + // pick the ref which is now in the index the old ref was in const oldIndex = state.refs.findIndex(r => r === action.payload.ref); return { ...state, @@ -93,12 +110,14 @@ const reducer = (state, action) => { }; } + // update the refs list return { ...state, refs, }; } case types.SET_FOCUS: { + // update active ref return { ...state, activeRef: action.payload.ref, @@ -115,17 +134,18 @@ export const RovingTabIndexContextProvider = ({children}) => { refs: [], }); - const context = useMemo(() => ({state, dispatch}), [state]); - const onKeyDown = useCallback((ev) => { + // check if we actually have any items if (state.refs.length <= 0) return; let handled = true; switch (ev.key) { case Key.HOME: + // move focus to first item setImmediate(() => state.refs[0].current.focus()); break; case Key.END: + // move focus to last item state.refs[state.refs.length - 1].current.focus(); break; default: @@ -138,6 +158,9 @@ export const RovingTabIndexContextProvider = ({children}) => { } }, [state]); + const context = useMemo(() => ({state, dispatch}), [state]); + + // wrap in a div with key-down handling for HOME/END keys return
{children} @@ -145,21 +168,27 @@ export const RovingTabIndexContextProvider = ({children}) => {
; }; +// Hook to register a roving tab index +// inputRef parameter specifies the ref to use +// onFocus should be called when the index gained focus in any manner +// isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}` +// ref should be passed to a DOM node which will be used for DOM compareDocumentPosition export const useRovingTabIndex = (inputRef) => { - let ref = useRef(null); const context = useContext(RovingTabIndexContext); + let ref = useRef(null); if (inputRef) { + // if we are given a ref, use it instead of ours ref = inputRef; } - // setup/teardown - // add ref to the context + // setup (after refs) useLayoutEffect(() => { context.dispatch({ type: types.REGISTER, payload: {ref}, }); + // teardown return () => { context.dispatch({ type: types.UNREGISTER, @@ -179,6 +208,7 @@ export const useRovingTabIndex = (inputRef) => { return [onFocus, isActive, ref]; }; +// Wrapper to allow use of useRovingTabIndex outside of React Functional Components. export const RovingTabIndexWrapper = ({children, inputRef}) => { const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); return children({onFocus, isActive, ref}); From 781db63fa639e286622542bc7cba4e29c3c17e5f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jan 2020 01:35:42 +0000 Subject: [PATCH 06/57] split out home/end handling into a helper as not all roving-tab-index widgets want it Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomList.js | 6 ++++-- src/contexts/RovingTabIndexContext.js | 26 ++++++++++++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 4441b4d539..21cd1ed719 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -39,7 +39,7 @@ import * as sdk from "../../../index"; import * as Receipt from "../../../utils/Receipt"; import {Resizer} from '../../../resizer'; import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; -import {RovingTabIndexContextProvider} from "../../../contexts/RovingTabIndexContext"; +import {RovingTabIndexContextProvider, RovingTabIndexHomeEndHelper} from "../../../contexts/RovingTabIndexContext"; const HIDE_CONFERENCE_CHANS = true; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; @@ -789,7 +789,9 @@ export default createReactClass({ onMouseLeave={this.onMouseLeave} > - { subListComponents } + + { subListComponents } + ); diff --git a/src/contexts/RovingTabIndexContext.js b/src/contexts/RovingTabIndexContext.js index 2e8439d2a4..55ac19e40f 100644 --- a/src/contexts/RovingTabIndexContext.js +++ b/src/contexts/RovingTabIndexContext.js @@ -134,19 +134,30 @@ export const RovingTabIndexContextProvider = ({children}) => { refs: [], }); + const context = useMemo(() => ({state, dispatch}), [state]); + + return + {children} + ; +}; + +// Helper to handle Home/End to jump to first/last roving-tab-index for widgets such as treeview +export const RovingTabIndexHomeEndHelper = ({children}) => { + const context = useContext(RovingTabIndexContext); + const onKeyDown = useCallback((ev) => { // check if we actually have any items - if (state.refs.length <= 0) return; + if (context.state.refs.length <= 0) return; let handled = true; switch (ev.key) { case Key.HOME: // move focus to first item - setImmediate(() => state.refs[0].current.focus()); + setImmediate(() => context.state.refs[0].current.focus()); break; case Key.END: // move focus to last item - state.refs[state.refs.length - 1].current.focus(); + context.state.refs[context.state.refs.length - 1].current.focus(); break; default: handled = false; @@ -156,15 +167,10 @@ export const RovingTabIndexContextProvider = ({children}) => { ev.preventDefault(); ev.stopPropagation(); } - }, [state]); + }, [context.state]); - const context = useMemo(() => ({state, dispatch}), [state]); - - // wrap in a div with key-down handling for HOME/END keys return
- - {children} - + { children }
; }; From 2230b7732a703215c49a356697a7acd9cd969383 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jan 2020 01:45:16 +0000 Subject: [PATCH 07/57] rearrange Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../RovingTabIndex.js} | 2 +- src/components/structures/RoomSubList.js | 2 +- src/components/views/groups/GroupInviteTile.js | 2 +- src/components/views/rooms/RoomList.js | 6 +++--- src/components/views/rooms/RoomTile.js | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) rename src/{contexts/RovingTabIndexContext.js => accessibility/RovingTabIndex.js} (99%) diff --git a/src/contexts/RovingTabIndexContext.js b/src/accessibility/RovingTabIndex.js similarity index 99% rename from src/contexts/RovingTabIndexContext.js rename to src/accessibility/RovingTabIndex.js index 55ac19e40f..ad2051f1f1 100644 --- a/src/contexts/RovingTabIndexContext.js +++ b/src/accessibility/RovingTabIndex.js @@ -128,7 +128,7 @@ const reducer = (state, action) => { } }; -export const RovingTabIndexContextProvider = ({children}) => { +export const RovingTabIndexProvider = ({children}) => { const [state, dispatch] = useReducer(reducer, { activeRef: null, refs: [], diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 775b6b69ce..2d41abf902 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -31,7 +31,7 @@ import PropTypes from 'prop-types'; import RoomTile from "../views/rooms/RoomTile"; import LazyRenderList from "../views/elements/LazyRenderList"; import {_t} from "../../languageHandler"; -import {RovingTabIndexWrapper} from "../../contexts/RovingTabIndexContext"; +import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex"; // turn this on for drop & drag console debugging galore const debug = false; diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index 488c1e20cf..3b15c6ff41 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -26,7 +26,7 @@ import classNames from 'classnames'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {RovingTabIndexWrapper} from "../../../contexts/RovingTabIndexContext"; +import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex"; // XXX this class copies a lot from RoomTile.js export default createReactClass({ diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 21cd1ed719..a137a36c60 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -39,7 +39,7 @@ import * as sdk from "../../../index"; import * as Receipt from "../../../utils/Receipt"; import {Resizer} from '../../../resizer'; import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; -import {RovingTabIndexContextProvider, RovingTabIndexHomeEndHelper} from "../../../contexts/RovingTabIndexContext"; +import {RovingTabIndexProvider, RovingTabIndexHomeEndHelper} from "../../../accessibility/RovingTabIndex"; const HIDE_CONFERENCE_CHANS = true; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; @@ -788,11 +788,11 @@ export default createReactClass({ onMouseMove={this.onMouseMove} onMouseLeave={this.onMouseLeave} > - + { subListComponents } - + ); }, diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 8701c3d287..3b13001225 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -32,7 +32,7 @@ import ActiveRoomObserver from '../../../ActiveRoomObserver'; import RoomViewStore from '../../../stores/RoomViewStore'; import SettingsStore from "../../../settings/SettingsStore"; import {_t} from "../../../languageHandler"; -import {RovingTabIndexWrapper} from "../../../contexts/RovingTabIndexContext"; +import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex"; export default createReactClass({ displayName: 'RoomTile', From 4504d9b790c1fdbaa2ab2545cf9c7de2a601f6c8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jan 2020 03:15:52 +0000 Subject: [PATCH 08/57] add tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/accessibility/RovingTabIndex.js | 2 +- test/accessibility/RovingTabIndex-test.js | 117 ++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 test/accessibility/RovingTabIndex-test.js diff --git a/src/accessibility/RovingTabIndex.js b/src/accessibility/RovingTabIndex.js index ad2051f1f1..85aa133aa4 100644 --- a/src/accessibility/RovingTabIndex.js +++ b/src/accessibility/RovingTabIndex.js @@ -153,7 +153,7 @@ export const RovingTabIndexHomeEndHelper = ({children}) => { switch (ev.key) { case Key.HOME: // move focus to first item - setImmediate(() => context.state.refs[0].current.focus()); + context.state.refs[0].current.focus(); break; case Key.END: // move focus to last item diff --git a/test/accessibility/RovingTabIndex-test.js b/test/accessibility/RovingTabIndex-test.js new file mode 100644 index 0000000000..2b55d1420c --- /dev/null +++ b/test/accessibility/RovingTabIndex-test.js @@ -0,0 +1,117 @@ +/* +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 React from "react"; +import Adapter from "enzyme-adapter-react-16"; +import { configure, mount } from "enzyme"; + +import { + RovingTabIndexProvider, + RovingTabIndexWrapper, + useRovingTabIndex, +} from "../../src/accessibility/RovingTabIndex"; + +configure({ adapter: new Adapter() }); + +const Button = (props) => { + const [onFocus, isActive, ref] = useRovingTabIndex(); + return ; +const button2 = ; +const button3 = ; +const button4 = ; + +describe("RovingTabIndex", () => { + it("RovingTabIndexProvider renders children as expected", () => { + const wrapper = mount( +
Test
+
); + expect(wrapper.text()).toBe("Test"); + expect(wrapper.html()).toBe('
Test
'); + }); + + it("RovingTabIndexProvider works as expected with useRovingTabIndex", () => { + const wrapper = mount( + { button1 } + { button2 } + { button3 } + ); + + // should begin with 0th being active + checkTabIndexes(wrapper.find("button"), [0, -1, -1]); + + // focus on 2nd button and test it is the only active one + wrapper.find("button").at(2).simulate("focus"); + wrapper.update(); + checkTabIndexes(wrapper.find("button"), [-1, -1, 0]); + + // focus on 1st button and test it is the only active one + wrapper.find("button").at(1).simulate("focus"); + wrapper.update(); + checkTabIndexes(wrapper.find("button"), [-1, 0, -1]); + + // check that the active button does not change even on an explicit blur event + wrapper.find("button").at(1).simulate("blur"); + wrapper.update(); + checkTabIndexes(wrapper.find("button"), [-1, 0, -1]); + + // update the children, it should remain on the same button + wrapper.setProps({ + children: [button1, button4, button2, button3], + }); + wrapper.update(); + checkTabIndexes(wrapper.find("button"), [-1, -1, 0, -1]); + + // update the children, remove the active button, it should move to the next one + wrapper.setProps({ + children: [button1, button4, button3], + }); + wrapper.update(); + checkTabIndexes(wrapper.find("button"), [-1, -1, 0]); + }); + + it("RovingTabIndexProvider works as expected with RovingTabIndexWrapper", () => { + const wrapper = mount( + { button1 } + { button2 } + + {({onFocus, isActive, ref}) => + + } + + ); + + // should begin with 0th being active + checkTabIndexes(wrapper.find("button"), [0, -1, -1]); + + // focus on 2nd button and test it is the only active one + wrapper.find("button").at(2).simulate("focus"); + wrapper.update(); + checkTabIndexes(wrapper.find("button"), [-1, -1, 0]); + }); +}); + + From f77eb078498e1bdd761e176519fb5a5dc884529c Mon Sep 17 00:00:00 2001 From: Zoe Date: Mon, 20 Jan 2020 15:16:41 +0000 Subject: [PATCH 09/57] Verify individual messages via cross-signing Fixes #11880 --- res/css/views/rooms/_EventTile.scss | 21 ++++++++-- res/themes/light/css/_light.scss | 1 + src/components/views/rooms/EventTile.js | 51 +++++++++++++++++++++---- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index fbac1e932a..81ba547ff0 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -367,6 +367,11 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { opacity: 1; } +.mx_EventTile_e2eIcon_userVerified { + background-image: url('$(res)/img/e2e/normal.svg'); + opacity: 0.5; +} + .mx_EventTile_e2eIcon_unencrypted { background-image: url('$(res)/img/e2e/warning.svg'); opacity: 1; @@ -415,7 +420,8 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { } .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { +.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, +.mx_EventTile:hover.mx_EventTile_userVerified .mx_EventTile_line { padding-left: 60px; } @@ -427,8 +433,13 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { border-left: $e2e-unverified-color 5px solid; } +.mx_EventTile:hover.mx_EventTile_userVerified .mx_EventTile_line { + border-left: $e2e-userVerified-color 5px solid; +} + .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line { +.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, +.mx_EventTile:hover.mx_EventTile_userVerified.mx_EventTile_info .mx_EventTile_line { padding-left: 78px; } @@ -439,14 +450,16 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp { +.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, +.mx_EventTile:hover.mx_EventTile_userVerified .mx_EventTile_line > a > .mx_MessageTimestamp { left: 3px; width: auto; } // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon { +.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon, +.mx_EventTile:hover.mx_EventTile_userVerified .mx_EventTile_line > .mx_EventTile_e2eIcon { display: block; left: 41px; } diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 288fb3cadc..17b9a344ef 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -224,6 +224,7 @@ $copy-button-url: "$(res)/img/icon_copy_message.svg"; // e2e $e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color +$e2e-userVerified-color: #e8bf37; $e2e-unverified-color: #e8bf37; $e2e-warning-color: #ba6363; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index dce4dc8a93..4aefe6929b 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -235,6 +235,7 @@ export default createReactClass({ this._suppressReadReceiptAnimation = false; const client = this.context; client.on("deviceVerificationChanged", this.onDeviceVerificationChanged); + client.on("userTrustStatusChanged", this.onUserVerificationChanged); this.props.mxEvent.on("Event.decrypted", this._onDecrypted); if (this.props.showReactions) { this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated); @@ -260,6 +261,7 @@ export default createReactClass({ componentWillUnmount: function() { const client = this.context; client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); + client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged); this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted); if (this.props.showReactions) { this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated); @@ -282,18 +284,42 @@ export default createReactClass({ } }, + onUserVerificationChanged: function(userId, _trustStatus) { + if (userId === this.props.mxEvent.getSender()) { + this._verifyEvent(this.props.mxEvent); + } + }, + _verifyEvent: async function(mxEvent) { if (!mxEvent.isEncrypted()) { return; } + // If we directly trust the device, short-circuit here const verified = await this.context.isEventSenderVerified(mxEvent); + if (verified) { + this.setState({ + verified: "verified" + }, () => { + // Decryption may have caused a change in size + this.props.onHeightChanged(); + }); + return; + } + + const eventSenderTrust = await this.context.checkEventSenderTrust(mxEvent); + if (!eventSenderTrust) { + // We cannot find the device. Instead, we have to verify the user. + const userTrust = await this.context.checkUserTrust(mxEvent.getSender()); + this.setState({ + verified: userTrust.isVerified() ? "user-verified": "warning", + }, this.props.onHeightChanged); // Decryption may have cause a change in size + return; + } + this.setState({ - verified: verified, - }, () => { - // Decryption may have caused a change in size - this.props.onHeightChanged(); - }); + verified: eventSenderTrust.isVerified() ? "verified" : "warning", + }, this.props.onHeightChanged); // Decryption may have caused a change in size }, _propsEqual: function(objA, objB) { @@ -473,8 +499,10 @@ export default createReactClass({ // event is encrypted, display padlock corresponding to whether or not it is verified if (ev.isEncrypted()) { - if (this.state.verified) { + if (this.state.verified === "verified") { return; // no icon for verified + } else if (this.state.verified === "user-verified") { + return (); } else { return (); } @@ -604,8 +632,9 @@ export default createReactClass({ mx_EventTile_last: this.props.last, mx_EventTile_contextual: this.props.contextual, mx_EventTile_actionBarFocused: this.state.actionBarFocused, - mx_EventTile_verified: !isBubbleMessage && this.state.verified === true, - mx_EventTile_unverified: !isBubbleMessage && this.state.verified === false, + mx_EventTile_verified: !isBubbleMessage && this.state.verified === "verified", + mx_EventTile_unverified: !isBubbleMessage && this.state.verified === "warning", + mx_EventTile_userVerified: !isBubbleMessage && this.state.verified === "user-verified", mx_EventTile_bad: isEncryptionFailure, mx_EventTile_emote: msgtype === 'm.emote', mx_EventTile_redacted: isRedacted, @@ -901,6 +930,12 @@ function E2ePadlockUnencrypted(props) { ); } +function E2ePadlockUserVerified(props) { + return ( + + ); +} + class E2ePadlock extends React.Component { static propTypes = { icon: PropTypes.string.isRequired, From 51fb3b494f8a87f28e2b20f9eafe0e96275de924 Mon Sep 17 00:00:00 2001 From: Zoe Date: Mon, 20 Jan 2020 15:25:01 +0000 Subject: [PATCH 10/57] lint and i18n --- src/components/views/rooms/EventTile.js | 2 +- src/i18n/strings/en_EN.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 4aefe6929b..f2a77935bc 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -299,7 +299,7 @@ export default createReactClass({ const verified = await this.context.isEventSenderVerified(mxEvent); if (verified) { this.setState({ - verified: "verified" + verified: "verified", }, () => { // Decryption may have caused a change in size this.props.onHeightChanged(); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4af203177c..510c1be0c7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -906,6 +906,7 @@ "This message cannot be decrypted": "This message cannot be decrypted", "Encrypted by an unverified device": "Encrypted by an unverified device", "Unencrypted": "Unencrypted", + "Encrypted by a deleted device": "Encrypted by a deleted device", "Please select the destination room for this message": "Please select the destination room for this message", "Scroll to bottom of page": "Scroll to bottom of page", "Close preview": "Close preview", From 12c4e453870f4570aa2cb85631be8efdfc423e97 Mon Sep 17 00:00:00 2001 From: Zoe Date: Mon, 20 Jan 2020 17:14:31 +0000 Subject: [PATCH 11/57] User verified but device deleted isn't a useful state --- res/css/views/rooms/_EventTile.scss | 14 +++++++------- res/themes/light/css/_light.scss | 2 +- src/components/views/rooms/EventTile.js | 14 ++++++-------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 81ba547ff0..e54255c4c4 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -367,7 +367,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { opacity: 1; } -.mx_EventTile_e2eIcon_userVerified { +.mx_EventTile_e2eIcon_unknown { background-image: url('$(res)/img/e2e/normal.svg'); opacity: 0.5; } @@ -421,7 +421,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_userVerified .mx_EventTile_line { +.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { padding-left: 60px; } @@ -433,13 +433,13 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { border-left: $e2e-unverified-color 5px solid; } -.mx_EventTile:hover.mx_EventTile_userVerified .mx_EventTile_line { - border-left: $e2e-userVerified-color 5px solid; +.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { + border-left: $e2e-unknown-color 5px solid; } .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_userVerified.mx_EventTile_info .mx_EventTile_line { +.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { padding-left: 78px; } @@ -451,7 +451,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, -.mx_EventTile:hover.mx_EventTile_userVerified .mx_EventTile_line > a > .mx_MessageTimestamp { +.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { left: 3px; width: auto; } @@ -459,7 +459,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon, -.mx_EventTile:hover.mx_EventTile_userVerified .mx_EventTile_line > .mx_EventTile_e2eIcon { +.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon { display: block; left: 41px; } diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 17b9a344ef..c868c81549 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -224,7 +224,7 @@ $copy-button-url: "$(res)/img/icon_copy_message.svg"; // e2e $e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color -$e2e-userVerified-color: #e8bf37; +$e2e-unknown-color: #e8bf37; $e2e-unverified-color: #e8bf37; $e2e-warning-color: #ba6363; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index f2a77935bc..037b080aa3 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -309,10 +309,8 @@ export default createReactClass({ const eventSenderTrust = await this.context.checkEventSenderTrust(mxEvent); if (!eventSenderTrust) { - // We cannot find the device. Instead, we have to verify the user. - const userTrust = await this.context.checkUserTrust(mxEvent.getSender()); this.setState({ - verified: userTrust.isVerified() ? "user-verified": "warning", + verified: "unknown", }, this.props.onHeightChanged); // Decryption may have cause a change in size return; } @@ -501,8 +499,8 @@ export default createReactClass({ if (ev.isEncrypted()) { if (this.state.verified === "verified") { return; // no icon for verified - } else if (this.state.verified === "user-verified") { - return (); + } else if (this.state.verified === "unknown") { + return (); } else { return (); } @@ -634,7 +632,7 @@ export default createReactClass({ mx_EventTile_actionBarFocused: this.state.actionBarFocused, mx_EventTile_verified: !isBubbleMessage && this.state.verified === "verified", mx_EventTile_unverified: !isBubbleMessage && this.state.verified === "warning", - mx_EventTile_userVerified: !isBubbleMessage && this.state.verified === "user-verified", + mx_EventTile_unknown: !isBubbleMessage && this.state.verified === "unknown", mx_EventTile_bad: isEncryptionFailure, mx_EventTile_emote: msgtype === 'm.emote', mx_EventTile_redacted: isRedacted, @@ -930,9 +928,9 @@ function E2ePadlockUnencrypted(props) { ); } -function E2ePadlockUserVerified(props) { +function E2ePadlockUnknown(props) { return ( - + ); } From befd4e1f5a0f893722858b70280d278c5aa05b6d Mon Sep 17 00:00:00 2001 From: Zoe Date: Mon, 20 Jan 2020 17:25:08 +0000 Subject: [PATCH 12/57] shout more for unknown devices, but keep the tooltip --- res/css/views/rooms/_EventTile.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index e54255c4c4..d292c729dd 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -368,8 +368,8 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { } .mx_EventTile_e2eIcon_unknown { - background-image: url('$(res)/img/e2e/normal.svg'); - opacity: 0.5; + background-image: url('$(res)/img/e2e/warning.svg'); + opacity: 1; } .mx_EventTile_e2eIcon_unencrypted { From 0bcfe5819fd033040429aba0941d450b5477e0ee Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 20 Jan 2020 20:31:36 +0000 Subject: [PATCH 13/57] Integrate handleHomeEnd --- src/accessibility/RovingTabIndex.js | 18 +++++++++++++++--- src/components/views/rooms/RoomList.js | 8 +++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/accessibility/RovingTabIndex.js b/src/accessibility/RovingTabIndex.js index 85aa133aa4..2445a47e35 100644 --- a/src/accessibility/RovingTabIndex.js +++ b/src/accessibility/RovingTabIndex.js @@ -23,6 +23,7 @@ import React, { useRef, useReducer, } from "react"; +import PropTypes from "prop-types"; import {Key} from "../Keyboard"; /** @@ -128,7 +129,7 @@ const reducer = (state, action) => { } }; -export const RovingTabIndexProvider = ({children}) => { +export const RovingTabIndexProvider = ({children, handleHomeEnd}) => { const [state, dispatch] = useReducer(reducer, { activeRef: null, refs: [], @@ -136,13 +137,24 @@ export const RovingTabIndexProvider = ({children}) => { const context = useMemo(() => ({state, dispatch}), [state]); + if (handleHomeEnd) { + return + + { children } + + + } + return - {children} + { children } ; }; +RovingTabIndexProvider.propTypes = { + handleHomeEnd: PropTypes.bool, +}; // Helper to handle Home/End to jump to first/last roving-tab-index for widgets such as treeview -export const RovingTabIndexHomeEndHelper = ({children}) => { +export const HomeEndHelper = ({children}) => { const context = useContext(RovingTabIndexContext); const onKeyDown = useCallback((ev) => { diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index a137a36c60..bd563b2f28 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -39,7 +39,7 @@ import * as sdk from "../../../index"; import * as Receipt from "../../../utils/Receipt"; import {Resizer} from '../../../resizer'; import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; -import {RovingTabIndexProvider, RovingTabIndexHomeEndHelper} from "../../../accessibility/RovingTabIndex"; +import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex"; const HIDE_CONFERENCE_CHANS = true; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; @@ -788,10 +788,8 @@ export default createReactClass({ onMouseMove={this.onMouseMove} onMouseLeave={this.onMouseLeave} > - - - { subListComponents } - + + { subListComponents } ); From 5a67bd4b463b160821bd430bee63f9635446451d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 20 Jan 2020 20:35:21 +0000 Subject: [PATCH 14/57] Repair cross-signing panel with async status This repairs the cross-signing panel after recent changes that made the panel's status an async function. Regressed by https://github.com/matrix-org/matrix-react-sdk/pull/3864 Fixes https://github.com/vector-im/riot-web/issues/11952 --- .../views/settings/CrossSigningPanel.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index 49046cd051..f7d3d62b4f 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 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. @@ -29,7 +29,9 @@ export default class CrossSigningPanel extends React.PureComponent { this.state = { error: null, - ...this._getUpdatedStatus(), + crossSigningPublicKeysOnDevice: false, + crossSigningPrivateKeysInStorage: false, + secretStorageKeyInAccount: false, }; } @@ -38,6 +40,7 @@ export default class CrossSigningPanel extends React.PureComponent { cli.on("accountData", this.onAccountData); cli.on("userTrustStatusChanged", this.onStatusChanged); cli.on("crossSigning.keysChanged", this.onStatusChanged); + this._getUpdatedStatus(); } componentWillUnmount() { @@ -52,12 +55,12 @@ export default class CrossSigningPanel extends React.PureComponent { onAccountData = (event) => { const type = event.getType(); if (type.startsWith("m.cross_signing") || type.startsWith("m.secret_storage")) { - this.setState(this._getUpdatedStatus()); + this._getUpdatedStatus(); } }; onStatusChanged = () => { - this.setState(this._getUpdatedStatus()); + this._getUpdatedStatus(); }; async _getUpdatedStatus() { @@ -69,11 +72,11 @@ export default class CrossSigningPanel extends React.PureComponent { const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage); const secretStorageKeyInAccount = await secretStorage.hasKey(); - return { + this.setState({ crossSigningPublicKeysOnDevice, crossSigningPrivateKeysInStorage, secretStorageKeyInAccount, - }; + }); } /** @@ -93,7 +96,7 @@ export default class CrossSigningPanel extends React.PureComponent { console.error("Error bootstrapping secret storage", e); } if (this._unmounted) return; - this.setState(this._getUpdatedStatus()); + this._getUpdatedStatus(); } render() { From be6a3821215b699d683cd7a628e583bdaa68d792 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 20 Jan 2020 20:46:12 +0000 Subject: [PATCH 15/57] delint --- src/accessibility/RovingTabIndex.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessibility/RovingTabIndex.js b/src/accessibility/RovingTabIndex.js index 2445a47e35..8924815f23 100644 --- a/src/accessibility/RovingTabIndex.js +++ b/src/accessibility/RovingTabIndex.js @@ -142,7 +142,7 @@ export const RovingTabIndexProvider = ({children, handleHomeEnd}) => { { children } - + ; } return From 4e018905fc6ad87e6f75f4fd31fe30a8e96d1699 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 Jan 2020 15:17:47 -0700 Subject: [PATCH 16/57] Remove Chrome stuff (not needed for riot-web tests anymore) --- .buildkite/pipeline.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml index a8ce1273fb..85aff94069 100644 --- a/.buildkite/pipeline.yaml +++ b/.buildkite/pipeline.yaml @@ -83,12 +83,6 @@ steps: # webpack loves to gorge itself on resources. queue: "medium" command: - # Install chrome - - "echo '--- Installing Chrome'" - - "wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -" - - "sh -c 'echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list'" - - "apt-get update" - - "apt-get install -y google-chrome-stable" # TODO: Remove hacky chmod for BuildKite - "chmod +x ./scripts/ci/*.sh" - "chmod +x ./scripts/*" @@ -98,8 +92,6 @@ steps: - "yarn build" - "echo '+++ Running Tests'" - "./scripts/ci/riot-unit-tests.sh" - env: - CHROME_BIN: "/usr/bin/google-chrome-stable" plugins: - docker#v3.0.1: image: "node:10" From 62b1dd77a68fdf5cee58fd0f9041a05e9155d0d7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 Jan 2020 16:12:59 -0700 Subject: [PATCH 17/57] Fix layering of the riot-web tests pipeline --- .buildkite/pipeline.yaml | 5 +---- scripts/ci/layered-riot-web.sh | 33 +++++++++++++++++++++++++++++++++ scripts/ci/riot-unit-tests.sh | 7 ++----- scripts/fetchdep.sh | 2 +- 4 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 scripts/ci/layered-riot-web.sh diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml index 85aff94069..100d14e967 100644 --- a/.buildkite/pipeline.yaml +++ b/.buildkite/pipeline.yaml @@ -86,16 +86,13 @@ steps: # TODO: Remove hacky chmod for BuildKite - "chmod +x ./scripts/ci/*.sh" - "chmod +x ./scripts/*" - - "echo '--- Installing Dependencies'" - - "./scripts/ci/install-deps.sh" - - "echo '--- Running initial build steps'" - - "yarn build" - "echo '+++ Running Tests'" - "./scripts/ci/riot-unit-tests.sh" plugins: - docker#v3.0.1: image: "node:10" propagate-environment: true + workdir: "/workdir/matrix-react-sdk" - label: "🌐 i18n" command: diff --git a/scripts/ci/layered-riot-web.sh b/scripts/ci/layered-riot-web.sh new file mode 100644 index 0000000000..2b908be68f --- /dev/null +++ b/scripts/ci/layered-riot-web.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Creates an environment similar to one that riot-web would expect for +# development. This means going one directory up (and assuming we're in +# a directory like /workdir/matrix-react-sdk) and putting riot-web and +# the js-sdk there. + +cd ../ # Assume we're at something like /workdir/matrix-react-sdk + +# Set up the js-sdk first +matrix-react-sdk/scripts/fetchdep.sh matrix-org matrix-js-sdk +pushd matrix-js-sdk +yarn link +yarn install +#yarn build +popd + +# Now set up the react-sdk +pushd matrix-react-sdk +yarn link matrix-js-sdk +yarn link +yarn install +#yarn build +popd + +# Finally, set up riot-web +matrix-react-sdk/scripts/fetchdep.sh vector-im riot-web +pushd riot-web +yarn link matrix-js-sdk +yarn link matrix-react-sdk +yarn install +yarn build:res +popd diff --git a/scripts/ci/riot-unit-tests.sh b/scripts/ci/riot-unit-tests.sh index 215af13030..7a9ed77793 100755 --- a/scripts/ci/riot-unit-tests.sh +++ b/scripts/ci/riot-unit-tests.sh @@ -6,9 +6,6 @@ set -ev -RIOT_WEB_DIR=riot-web - -scripts/ci/build.sh -pushd "$RIOT_WEB_DIR" +scripts/ci/layered-riot-web.sh +cd ../riot-web yarn test -popd diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index f82752bfc5..f477fd08b8 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -17,7 +17,7 @@ clone() { if [ -n "$branch" ] then echo "Trying to use $org/$repo#$branch" - git clone git://github.com/$org/$repo.git $repo --branch "$branch" && exit 0 + git clone git://github.com/$org/$repo.git $repo --branch "$branch" --depth=1 && exit 0 fi } From 3eeeb9c6afbacf46da266675ff17772747a3a47f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 Jan 2020 16:20:02 -0700 Subject: [PATCH 18/57] Remove irrelevant build steps --- scripts/ci/layered-riot-web.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/ci/layered-riot-web.sh b/scripts/ci/layered-riot-web.sh index 2b908be68f..f58794b451 100644 --- a/scripts/ci/layered-riot-web.sh +++ b/scripts/ci/layered-riot-web.sh @@ -12,7 +12,6 @@ matrix-react-sdk/scripts/fetchdep.sh matrix-org matrix-js-sdk pushd matrix-js-sdk yarn link yarn install -#yarn build popd # Now set up the react-sdk @@ -20,7 +19,6 @@ pushd matrix-react-sdk yarn link matrix-js-sdk yarn link yarn install -#yarn build popd # Finally, set up riot-web From 27412ba0b2f342a42652ae6da69d41cfc2a17b03 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 Jan 2020 16:23:33 -0700 Subject: [PATCH 19/57] Fix end-to-end test layering too --- .buildkite/pipeline.yaml | 1 + scripts/ci/build.sh | 25 ------------------------- scripts/ci/end-to-end-tests.sh | 10 +++++----- 3 files changed, 6 insertions(+), 30 deletions(-) delete mode 100755 scripts/ci/build.sh diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml index 100d14e967..de61d4e5b9 100644 --- a/.buildkite/pipeline.yaml +++ b/.buildkite/pipeline.yaml @@ -76,6 +76,7 @@ steps: - docker#v3.0.1: image: "matrixdotorg/riotweb-ci-e2etests-env:latest" propagate-environment: true + workdir: "/workdir/matrix-react-sdk" - label: "🔧 Riot Tests" agents: diff --git a/scripts/ci/build.sh b/scripts/ci/build.sh deleted file mode 100755 index 0b1fa23093..0000000000 --- a/scripts/ci/build.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# -# script which is run by the CI build (after `yarn test`). -# -# clones riot-web develop and runs the tests against our version of react-sdk. - -set -ev - -RIOT_WEB_DIR=riot-web -REACT_SDK_DIR=`pwd` - -yarn link - -scripts/fetchdep.sh vector-im riot-web - -pushd "$RIOT_WEB_DIR" - -yarn link matrix-js-sdk -yarn link matrix-react-sdk - -yarn install - -yarn build - -popd diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index a592888292..9eb3c2bd87 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -21,15 +21,15 @@ handle_error() { trap 'handle_error' ERR -RIOT_WEB_DIR=riot-web -REACT_SDK_DIR=`pwd` - echo "--- Building Riot" -scripts/ci/build.sh +scripts/ci/layered-riot-web.sh +cd ../riot-web +yarn build +cd ../matrix-react-sdk # run end to end tests pushd test/end-to-end-tests -ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web +ln -s ../riot-web riot/riot-web # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh # CHROME_PATH=$(which google-chrome-stable) ./run.sh echo "--- Install synapse & other dependencies" From 19615d372175e6f7e8f7756e33f236ac41cb626e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 Jan 2020 16:33:13 -0700 Subject: [PATCH 20/57] Disable minification of Riot in end-to-end tests --- scripts/ci/end-to-end-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index 9eb3c2bd87..cc1e548af9 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -25,7 +25,7 @@ trap 'handle_error' ERR echo "--- Building Riot" scripts/ci/layered-riot-web.sh cd ../riot-web -yarn build +CI_PACKAGE=true yarn build cd ../matrix-react-sdk # run end to end tests pushd test/end-to-end-tests From 776b3af6bb46c7a55a121abfffe32dc26efcf772 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 Jan 2020 16:33:22 -0700 Subject: [PATCH 21/57] Fix relative pathing on riot-web link --- scripts/ci/end-to-end-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index cc1e548af9..bae268bbe3 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -29,7 +29,7 @@ CI_PACKAGE=true yarn build cd ../matrix-react-sdk # run end to end tests pushd test/end-to-end-tests -ln -s ../riot-web riot/riot-web +ln -s ../../../riot-web riot/riot-web # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh # CHROME_PATH=$(which google-chrome-stable) ./run.sh echo "--- Install synapse & other dependencies" From 3b2f96bc047bbe221efe216ced07dc927d016d09 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 Jan 2020 18:02:54 -0700 Subject: [PATCH 22/57] Try explicitly mapping the directory --- scripts/ci/end-to-end-tests.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index bae268bbe3..9bdb512940 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -25,11 +25,12 @@ trap 'handle_error' ERR echo "--- Building Riot" scripts/ci/layered-riot-web.sh cd ../riot-web +riot_web_dir=`pwd` CI_PACKAGE=true yarn build cd ../matrix-react-sdk # run end to end tests pushd test/end-to-end-tests -ln -s ../../../riot-web riot/riot-web +ln -s $riot_web_dir riot/riot-web # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh # CHROME_PATH=$(which google-chrome-stable) ./run.sh echo "--- Install synapse & other dependencies" From d34f1e52ad1e7d499018f96f6942501c7598b005 Mon Sep 17 00:00:00 2001 From: Zoe Date: Tue, 21 Jan 2020 10:08:53 +0000 Subject: [PATCH 23/57] constants for e2estates --- src/components/views/rooms/EventTile.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 037b080aa3..9c73daaa50 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -66,6 +66,12 @@ const stateEventTileTypes = { 'm.room.related_groups': 'messages.TextualEvent', }; +const E2ESTATE = { + VERIFIED: "verified", + WARNING: "warning", + UNKNOWN: "unknown", +}; + // Add all the Mjolnir stuff to the renderer for (const evType of ALL_RULE_TYPES) { stateEventTileTypes[evType] = 'messages.TextualEvent'; @@ -299,7 +305,7 @@ export default createReactClass({ const verified = await this.context.isEventSenderVerified(mxEvent); if (verified) { this.setState({ - verified: "verified", + verified: E2ESTATE.VERIFIED, }, () => { // Decryption may have caused a change in size this.props.onHeightChanged(); @@ -310,13 +316,13 @@ export default createReactClass({ const eventSenderTrust = await this.context.checkEventSenderTrust(mxEvent); if (!eventSenderTrust) { this.setState({ - verified: "unknown", + verified: E2ESTATE.UNKNOWN, }, this.props.onHeightChanged); // Decryption may have cause a change in size return; } this.setState({ - verified: eventSenderTrust.isVerified() ? "verified" : "warning", + verified: eventSenderTrust.isVerified() ? E2ESTATE.VERIFIED : E2ESTATE.WARNING, }, this.props.onHeightChanged); // Decryption may have caused a change in size }, @@ -497,9 +503,9 @@ export default createReactClass({ // event is encrypted, display padlock corresponding to whether or not it is verified if (ev.isEncrypted()) { - if (this.state.verified === "verified") { + if (this.state.verified === E2ESTATE.VERIFIED) { return; // no icon for verified - } else if (this.state.verified === "unknown") { + } else if (this.state.verified === E2ESTATE.UNKNOWN) { return (); } else { return (); @@ -630,9 +636,9 @@ export default createReactClass({ mx_EventTile_last: this.props.last, mx_EventTile_contextual: this.props.contextual, mx_EventTile_actionBarFocused: this.state.actionBarFocused, - mx_EventTile_verified: !isBubbleMessage && this.state.verified === "verified", - mx_EventTile_unverified: !isBubbleMessage && this.state.verified === "warning", - mx_EventTile_unknown: !isBubbleMessage && this.state.verified === "unknown", + mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2ESTATE.VERIFIED, + mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2ESTATE.WARNING, + mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2ESTATE.UNKNOWN, mx_EventTile_bad: isEncryptionFailure, mx_EventTile_emote: msgtype === 'm.emote', mx_EventTile_redacted: isRedacted, From 26bba4416b35d7219ce34fcffaf03cc3b28e0385 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 10:12:23 +0000 Subject: [PATCH 24/57] Fix emoticon space completion for upper case emoticons like :D xD --- src/components/views/rooms/BasicMessageComposer.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 94904242c3..73c3d961ee 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -107,8 +107,9 @@ export default class BasicMessageEditor extends React.Component { }); const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text); if (emoticonMatch) { - const query = emoticonMatch[1].toLowerCase().replace("-", ""); - const data = EMOTICON_TO_EMOJI.get(query); + const query = emoticonMatch[1].replace("-", ""); + // try both exact match and lower-case, this means that xd won't match xD but :P will match :p + const data = EMOTICON_TO_EMOJI.get(query) || EMOTICON_TO_EMOJI.get(query.toLowerCase()); if (data) { const {partCreator} = model; From 8a00ff7f1f931939746bc150435951fcb6350003 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 21 Jan 2020 11:00:38 +0000 Subject: [PATCH 25/57] Change all user info verification checks to cross-signing This fixes some user vs. device verification confusion in user info by changing all the verification tests to the cross-signing variant when the lab is enabled. Fixes https://github.com/vector-im/riot-web/issues/11886 --- src/components/views/right_panel/UserInfo.js | 25 +++++++++++++------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 5f7de42368..15387af8d5 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -129,18 +129,21 @@ function verifyUser(user) { function DeviceItem({userId, device}) { const cli = useContext(MatrixClientContext); const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId); + const isVerified = SettingsStore.isFeatureEnabled("feature_cross_signing") ? + deviceTrust.isCrossSigningVerified() : + deviceTrust.isVerified(); const classes = classNames("mx_UserInfo_device", { - mx_UserInfo_device_verified: deviceTrust.isVerified(), - mx_UserInfo_device_unverified: !deviceTrust.isVerified(), + mx_UserInfo_device_verified: isVerified, + mx_UserInfo_device_unverified: !isVerified, }); const iconClasses = classNames("mx_E2EIcon", { - mx_E2EIcon_verified: deviceTrust.isVerified(), - mx_E2EIcon_warning: !deviceTrust.isVerified(), + mx_E2EIcon_verified: isVerified, + mx_E2EIcon_warning: !isVerified, }); const onDeviceClick = () => { - if (!deviceTrust.isVerified()) { + if (!isVerified) { verifyDevice(userId, device); } }; @@ -148,7 +151,7 @@ function DeviceItem({userId, device}) { const deviceName = device.ambiguous ? (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" : device.getDisplayName(); - const trustedLabel = deviceTrust.isVerified() ? _t("Trusted") : _t("Not trusted"); + const trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted"); return (
{deviceName}
@@ -177,8 +180,11 @@ function DevicesSection({devices, userId, loading}) { for (let i = 0; i < devices.length; ++i) { const device = devices[i]; const deviceTrust = deviceTrusts[i]; + const isVerified = SettingsStore.isFeatureEnabled("feature_cross_signing") ? + deviceTrust.isCrossSigningVerified() : + deviceTrust.isVerified(); - if (deviceTrust.isVerified()) { + if (isVerified) { verifiedDevices.push(device); } else { unverifiedDevices.push(device); @@ -1277,7 +1283,10 @@ const UserInfo = ({user, groupId, roomId, onClose}) => { text = _t("Messages in this room are end-to-end encrypted."); } - const userVerified = cli.checkUserTrust(user.userId).isVerified(); + const userTrust = cli.checkUserTrust(user.userId); + const userVerified = SettingsStore.isFeatureEnabled("feature_cross_signing") ? + userTrust.isCrossSigningVerified() : + userTrust.isVerified(); const isMe = user.userId === cli.getUserId(); let verifyButton; if (!userVerified && !isMe) { From f56a9d246fa91df5b06e781221028243ebeee5d1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 11:13:08 +0000 Subject: [PATCH 26/57] Fix index for _insertMention --- src/components/views/rooms/SendMessageComposer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index c11d940331..e3b794b1d0 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -326,7 +326,8 @@ export default class SendMessageComposer extends React.Component { member.rawDisplayName : userId; const caret = this._editorRef.getCaret(); const position = model.positionForOffset(caret.offset, caret.atNodeEnd); - const insertIndex = position.index + 1; + // index is -1 if there are no parts but we only care for if this would be the part in position 0 + const insertIndex = position.index > 0 ? position.index : 0; const parts = partCreator.createMentionParts(insertIndex, displayName, userId); model.transform(() => { const addedLen = model.insert(parts, position); From a7231d73367828e70a2005080ae44c317a579cb0 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 21 Jan 2020 11:33:09 +0000 Subject: [PATCH 27/57] New session toast should check cross-signing verification To ensure all your sessions are cross-signing verified, we use the more specific test for only that kind of verification in the new session toast. --- src/DeviceListener.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DeviceListener.js b/src/DeviceListener.js index 9ae6a62ab1..a4c5785db4 100644 --- a/src/DeviceListener.js +++ b/src/DeviceListener.js @@ -75,7 +75,7 @@ export default class DeviceListener { if (device.deviceId == cli.deviceId) continue; const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId); - if (deviceTrust.isVerified() || this._dismissed.has(device.deviceId)) { + if (deviceTrust.isCrossSigningVerified() || this._dismissed.has(device.deviceId)) { ToastStore.sharedInstance().dismissToast(toastKey(device)); } else { ToastStore.sharedInstance().addOrReplaceToast({ From b3d56b378e3e5e959204fc0c1a880fb89b3203b7 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 21 Jan 2020 12:03:46 +0000 Subject: [PATCH 28/57] Use cross-signing verification only for own devices The device verification checks are slightly more nuanced: we want to use stricter cross-signing checks for your own devices to encourage everyone to trust their devices via cross-signing so that other users can in turn trust them. However, for other users, it's okay to use the looser verification check that also includes locally verified devices. --- src/components/views/right_panel/UserInfo.js | 25 +++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 15387af8d5..a0819be472 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -64,10 +64,17 @@ const _getE2EStatus = (cli, userId, devices) => { const hasUnverifiedDevice = devices.some((device) => device.isUnverified()); return hasUnverifiedDevice ? "warning" : "verified"; } + const isMe = userId === cli.getUserId(); const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified(); const allDevicesVerified = devices.every(device => { const { deviceId } = device; - return cli.checkDeviceTrust(userId, deviceId).isCrossSigningVerified(); + // For your own devices, we use the stricter check of cross-signing + // verification to encourage everyone to trust their own devices via + // cross-signing so that other users can then safely trust you. + // For other people's devices, the more general verified check that + // includes locally verified devices can be used. + const deviceTrust = cli.checkDeviceTrust(userId, deviceId); + return isMe ? deviceTrust.isCrossSigningVerified() : deviceTrust.isVerified(); }); if (allDevicesVerified) { return userVerified ? "verified" : "normal"; @@ -128,8 +135,14 @@ function verifyUser(user) { function DeviceItem({userId, device}) { const cli = useContext(MatrixClientContext); + const isMe = userId === cli.getUserId(); const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId); - const isVerified = SettingsStore.isFeatureEnabled("feature_cross_signing") ? + // For your own devices, we use the stricter check of cross-signing + // verification to encourage everyone to trust their own devices via + // cross-signing so that other users can then safely trust you. + // For other people's devices, the more general verified check that + // includes locally verified devices can be used. + const isVerified = (isMe && SettingsStore.isFeatureEnabled("feature_cross_signing")) ? deviceTrust.isCrossSigningVerified() : deviceTrust.isVerified(); @@ -172,6 +185,7 @@ function DevicesSection({devices, userId, loading}) { if (devices === null) { return _t("Unable to load device list"); } + const isMe = userId === cli.getUserId(); const deviceTrusts = devices.map(d => cli.checkDeviceTrust(userId, d.deviceId)); const unverifiedDevices = []; @@ -180,7 +194,12 @@ function DevicesSection({devices, userId, loading}) { for (let i = 0; i < devices.length; ++i) { const device = devices[i]; const deviceTrust = deviceTrusts[i]; - const isVerified = SettingsStore.isFeatureEnabled("feature_cross_signing") ? + // For your own devices, we use the stricter check of cross-signing + // verification to encourage everyone to trust their own devices via + // cross-signing so that other users can then safely trust you. + // For other people's devices, the more general verified check that + // includes locally verified devices can be used. + const isVerified = (isMe && SettingsStore.isFeatureEnabled("feature_cross_signing")) ? deviceTrust.isCrossSigningVerified() : deviceTrust.isVerified(); From c8a2f6a5a0133a55f324adfcb11fc25af53e6cab Mon Sep 17 00:00:00 2001 From: Zoe Date: Tue, 21 Jan 2020 13:33:16 +0000 Subject: [PATCH 29/57] Move room header shields over the avatar for the room Currently this is calibrated like the lil' DM icon is --- res/css/views/rooms/_RoomHeader.scss | 6 +++++- src/components/views/rooms/RoomHeader.js | 3 +-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 45b9733faa..0d92247735 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -19,7 +19,10 @@ limitations under the License. border-bottom: 1px solid $primary-hairline-color; .mx_E2EIcon { - margin: 0 5px; + margin: 0; + position: absolute; + bottom: 0; + right: -5px; } } @@ -171,6 +174,7 @@ limitations under the License. width: 28px; height: 28px; margin: 0 7px; + position: relative; } .mx_RoomHeader_avatar .mx_BaseAvatar_image { diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 09f3fd489f..15f0daa200 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -310,8 +310,7 @@ export default createReactClass({ return (
-
{ roomAvatar }
- { e2eIcon } +
{ roomAvatar }{ e2eIcon }
{ privateIcon } { name } { topicElement } From 9c0cf326c1992d4d641c46509cd4534504e656d3 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 21 Jan 2020 15:12:59 +0000 Subject: [PATCH 30/57] Only show devices and verify actions in E2EE rooms This changes logic to only show the devices list and verify button in E2EE rooms, matching the design. Fixes https://github.com/vector-im/riot-web/issues/11839 --- src/components/views/right_panel/UserInfo.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index a0819be472..b08f07ace4 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1308,15 +1308,18 @@ const UserInfo = ({user, groupId, roomId, onClose}) => { userTrust.isVerified(); const isMe = user.userId === cli.getUserId(); let verifyButton; - if (!userVerified && !isMe) { + if (isRoomEncrypted && !userVerified && !isMe) { verifyButton = verifyUser(user)}> {_t("Verify")} ; } - const devicesSection = ; + let devicesSection; + if (isRoomEncrypted) { + devicesSection = ; + } const securitySection = (
From 790d2c147203ca390804dbfa8ed14120416ab969 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 21 Jan 2020 15:30:01 +0000 Subject: [PATCH 31/57] Fix toast icon to prevent clipping This fixes the bottom and right edges of the toast icon, which were getting clipped away. Fixes https://github.com/vector-im/riot-web/issues/11915 --- res/css/structures/_ToastContainer.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 5634a97c53..5b5c49f357 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -51,8 +51,8 @@ limitations under the License. &.mx_Toast_hasIcon { &::after { content: ""; - width: 21px; - height: 20px; + width: 22px; + height: 22px; grid-column: 1; grid-row: 1; mask-size: 100%; From b5e902e1f2f8f3a00fea93adeb7985db3ba13b23 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 15:55:21 +0000 Subject: [PATCH 32/57] Fix escaping commands using double-slash //, e.g //plain sends `/plain` --- src/components/views/rooms/SendMessageComposer.js | 9 +++++++-- src/editor/serialize.js | 12 ++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index c11d940331..c4ae2929af 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -24,6 +24,8 @@ import { containsEmote, stripEmoteCommand, unescapeMessage, + startsWith, + stripPrefix, } from '../../../editor/serialize'; import {CommandPartCreator} from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; @@ -61,6 +63,9 @@ function createMessageContent(model, permalinkCreator) { if (isEmote) { model = stripEmoteCommand(model); } + if (startsWith(model, "//")) { + model = stripPrefix(model, "/"); + } model = unescapeMessage(model); const repliedToEvent = RoomViewStore.getQuotingEvent(); @@ -175,13 +180,13 @@ export default class SendMessageComposer extends React.Component { const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { - if (firstPart.type === "command") { + if (firstPart.type === "command" && !firstPart.text.startsWith("//")) { return true; } // be extra resilient when somehow the AutocompleteWrapperModel or // CommandPartCreator fails to insert a command part, so we don't send // a command as a message - if (firstPart.text.startsWith("/") && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { + if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { return true; } } diff --git a/src/editor/serialize.js b/src/editor/serialize.js index a55eed97da..ba380f2809 100644 --- a/src/editor/serialize.js +++ b/src/editor/serialize.js @@ -61,18 +61,26 @@ export function textSerialize(model) { } export function containsEmote(model) { + return startsWith(model, "/me "); +} + +export function startsWith(model, prefix) { const firstPart = model.parts[0]; // part type will be "plain" while editing, // and "command" while composing a message. return firstPart && (firstPart.type === "plain" || firstPart.type === "command") && - firstPart.text.startsWith("/me "); + firstPart.text.startsWith(prefix); } export function stripEmoteCommand(model) { // trim "/me " + return stripPrefix(model, "/me "); +} + +export function stripPrefix(model, prefix) { model = model.clone(); - model.removeText({index: 0, offset: 0}, 4); + model.removeText({index: 0, offset: 0}, prefix.length); return model; } From 060938379a7183f7959aa4af436eef775ce6c9d3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 15:58:51 +0000 Subject: [PATCH 33/57] Fix changes after typing / at pos=0 allowing to cancel command --- src/components/views/rooms/SendMessageComposer.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index c4ae2929af..8de105d84d 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -180,13 +180,14 @@ export default class SendMessageComposer extends React.Component { const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { - if (firstPart.type === "command" && !firstPart.text.startsWith("//")) { + if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { return true; } // be extra resilient when somehow the AutocompleteWrapperModel or // CommandPartCreator fails to insert a command part, so we don't send // a command as a message - if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { + if (firstPart.text.startsWith("/") && firstPart.text.startsWith("//") && !firstPart.text.startsWith("//") + && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { return true; } } From b34fe45518fbfff23f92a860a7af653fc383180b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 16:50:04 +0000 Subject: [PATCH 34/57] First attempt. Has a lag issue due to the async-clear :( --- src/SlashCommands.js | 13 +++---- .../views/rooms/SendMessageComposer.js | 39 +++++++++++++++++-- src/i18n/strings/en_EN.json | 4 +- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 20b8ba76da..414dd60121 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -81,6 +81,8 @@ class Command { } run(roomId, args) { + // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` + if (!this.runFn) return; return this.runFn.bind(this)(roomId, args); } @@ -918,12 +920,12 @@ export function processCommandInput(roomId, input) { input = input.replace(/\s+$/, ''); if (input[0] !== '/') return null; // not a command - const bits = input.match(/^(\S+?)( +((.|\n)*))?$/); + const bits = input.match(/^(\S+?)(?: +((.|\n)*))?$/); let cmd; let args; if (bits) { cmd = bits[1].substring(1).toLowerCase(); - args = bits[3]; + args = bits[2]; } else { cmd = input; } @@ -932,11 +934,8 @@ export function processCommandInput(roomId, input) { cmd = aliases[cmd]; } if (CommandMap[cmd]) { - // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` - if (!CommandMap[cmd].runFn) return null; - return CommandMap[cmd].run(roomId, args); - } else { - return reject(_t('Unrecognised command:') + ' ' + input); } + return null; + // return reject(_t('Unrecognised command:') + ' ' + input); } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 8de105d84d..9f3a407402 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -43,6 +43,9 @@ import ContentMessages from '../../../ContentMessages'; import {Key} from "../../../Keyboard"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +const SEND_ANYWAY = Symbol("send-anyway"); +const UNKNOWN_CMD = Symbol("unknown-cmd"); + function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); Object.assign(content, replyContent); @@ -194,7 +197,12 @@ export default class SendMessageComposer extends React.Component { return false; } - async _runSlashCommand() { + /** + * Parses and executes current input as a Slash Command + * @returns {Promise} UNKNOWN_CMD if the command is not known, + * SEND_ANYWAY if the input should be sent as message instead + */ + async _tryRunSlashCommand() { const commandText = this.model.parts.reduce((text, part) => { // use mxid to textify user pills in a command if (part.type === "user-pill") { @@ -236,16 +244,38 @@ export default class SendMessageComposer extends React.Component { } else { console.log("Command success."); } + } else { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + // unknown command, ask the user if they meant to send it as a message + const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { + title: _t("Unknown Command"), + description: _t("Unrecognised command: ") + commandText, + button: _t('Send as message'), + danger: true, + }); + const [sendAnyway] = await finished; + return sendAnyway ? SEND_ANYWAY : UNKNOWN_CMD; } } - _sendMessage() { + async _sendMessage() { if (this.model.isEmpty) { return; } + + let shouldSend = true; + if (!containsEmote(this.model) && this._isSlashCommand()) { - this._runSlashCommand(); - } else { + const resp = await this._tryRunSlashCommand(); + if (resp === UNKNOWN_CMD) { + // unknown command, bail to let the user modify it + return; + } + + shouldSend = resp === SEND_ANYWAY; + } + + if (shouldSend) { const isReply = !!RoomViewStore.getQuotingEvent(); const {roomId} = this.props.room; const content = createMessageContent(this.model, this.props.permalinkCreator); @@ -259,6 +289,7 @@ export default class SendMessageComposer extends React.Component { }); } } + this.sendHistoryManager.save(this.model); // clear composer this.model.reset([]); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f0eab6b12d..314731a910 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -200,7 +200,6 @@ "Sends the given message coloured as a rainbow": "Sends the given message coloured as a rainbow", "Sends the given emote coloured as a rainbow": "Sends the given emote coloured as a rainbow", "Displays list of commands with usages and descriptions": "Displays list of commands with usages and descriptions", - "Unrecognised command:": "Unrecognised command:", "Reason": "Reason", "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.", "%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.", @@ -1077,6 +1076,9 @@ "Server error": "Server error", "Command error": "Command error", "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", + "Unknown Command": "Unknown Command", + "Unrecognised command: ": "Unrecognised command: ", + "Send as message": "Send as message", "Failed to connect to integration manager": "Failed to connect to integration manager", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", From 9f7df33bc30acaceaa7d1f9d100fbf2de153d8d3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 16:57:07 +0000 Subject: [PATCH 35/57] re-arrange to split the async task into two and only wait on the user-blocking one --- src/SlashCommands.js | 10 +- .../views/rooms/SendMessageComposer.js | 100 +++++++++--------- 2 files changed, 52 insertions(+), 58 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 414dd60121..2eb34576ac 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -907,14 +907,14 @@ const aliases = { /** - * Process the given text for /commands and perform them. + * Process the given text for /commands and return a bound method to perform them. * @param {string} roomId The room in which the command was performed. * @param {string} input The raw text input by the user. - * @return {Object|null} An object with the property 'error' if there was an error + * @return {null|function(): Object} Function returning an object with the property 'error' if there was an error * processing the command, or 'promise' if a request was sent out. * Returns null if the input didn't match a command. */ -export function processCommandInput(roomId, input) { +export function getCommand(roomId, input) { // trim any trailing whitespace, as it can confuse the parser for // IRC-style commands input = input.replace(/\s+$/, ''); @@ -934,8 +934,6 @@ export function processCommandInput(roomId, input) { cmd = aliases[cmd]; } if (CommandMap[cmd]) { - return CommandMap[cmd].run(roomId, args); + return () => CommandMap[cmd].run(roomId, args); } - return null; - // return reject(_t('Unrecognised command:') + ' ' + input); } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 9f3a407402..994c28f531 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -35,7 +35,7 @@ import ReplyThread from "../elements/ReplyThread"; import {parseEvent} from '../../../editor/deserialize'; import {findEditableEvent} from '../../../utils/EventUtils'; import SendHistoryManager from "../../../SendHistoryManager"; -import {processCommandInput} from '../../../SlashCommands'; +import {getCommand} from '../../../SlashCommands'; import * as sdk from '../../../index'; import Modal from '../../../Modal'; import {_t, _td} from '../../../languageHandler'; @@ -197,12 +197,7 @@ export default class SendMessageComposer extends React.Component { return false; } - /** - * Parses and executes current input as a Slash Command - * @returns {Promise} UNKNOWN_CMD if the command is not known, - * SEND_ANYWAY if the input should be sent as message instead - */ - async _tryRunSlashCommand() { + _getSlashCommand() { const commandText = this.model.parts.reduce((text, part) => { // use mxid to textify user pills in a command if (part.type === "user-pill") { @@ -210,51 +205,41 @@ export default class SendMessageComposer extends React.Component { } return text + part.text; }, ""); - const cmd = processCommandInput(this.props.room.roomId, commandText); + return [getCommand(this.props.room.roomId, commandText), commandText]; + } - if (cmd) { - let error = cmd.error; - if (cmd.promise) { - try { - await cmd.promise; - } catch (err) { - error = err; - } + async _runSlashCommand(fn) { + const cmd = fn(); + let error = cmd.error; + if (cmd.promise) { + try { + await cmd.promise; + } catch (err) { + error = err; } - if (error) { - console.error("Command failure: %s", error); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - // assume the error is a server error when the command is async - const isServerError = !!cmd.promise; - const title = isServerError ? _td("Server error") : _td("Command error"); + } + if (error) { + console.error("Command failure: %s", error); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + // assume the error is a server error when the command is async + const isServerError = !!cmd.promise; + const title = isServerError ? _td("Server error") : _td("Command error"); - let errText; - if (typeof error === 'string') { - errText = error; - } else if (error.message) { - errText = error.message; - } else { - errText = _t("Server unavailable, overloaded, or something else went wrong."); - } - - Modal.createTrackedDialog(title, '', ErrorDialog, { - title: _t(title), - description: errText, - }); + let errText; + if (typeof error === 'string') { + errText = error; + } else if (error.message) { + errText = error.message; } else { - console.log("Command success."); + errText = _t("Server unavailable, overloaded, or something else went wrong."); } - } else { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - // unknown command, ask the user if they meant to send it as a message - const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { - title: _t("Unknown Command"), - description: _t("Unrecognised command: ") + commandText, - button: _t('Send as message'), - danger: true, + + Modal.createTrackedDialog(title, '', ErrorDialog, { + title: _t(title), + description: errText, }); - const [sendAnyway] = await finished; - return sendAnyway ? SEND_ANYWAY : UNKNOWN_CMD; + } else { + console.log("Command success."); } } @@ -266,13 +251,24 @@ export default class SendMessageComposer extends React.Component { let shouldSend = true; if (!containsEmote(this.model) && this._isSlashCommand()) { - const resp = await this._tryRunSlashCommand(); - if (resp === UNKNOWN_CMD) { - // unknown command, bail to let the user modify it - return; + const [cmd, commandText] = this._getSlashCommand(); + if (cmd) { + shouldSend = false; + this._runSlashCommand(cmd); + } else { + // ask the user if their unknown command should be sent as a message instead + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + // unknown command, ask the user if they meant to send it as a message + const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { + title: _t("Unknown Command"), + description: _t("Unrecognised command: ") + commandText, + button: _t('Send as message'), + danger: true, + }); + const [sendAnyway] = await finished; + // if !sendAnyway bail to let the user edit the composer and try again + if (!sendAnyway) return; } - - shouldSend = resp === SEND_ANYWAY; } if (shouldSend) { From 2480f709b31f9f05270430bf73235c904f029b2c Mon Sep 17 00:00:00 2001 From: Zoe Date: Tue, 21 Jan 2020 17:19:10 +0000 Subject: [PATCH 36/57] E2ESTATE -> E2E_STATE --- src/components/views/rooms/EventTile.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 9c73daaa50..bcd32d2c9c 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -66,7 +66,7 @@ const stateEventTileTypes = { 'm.room.related_groups': 'messages.TextualEvent', }; -const E2ESTATE = { +const E2E_STATE = { VERIFIED: "verified", WARNING: "warning", UNKNOWN: "unknown", @@ -305,7 +305,7 @@ export default createReactClass({ const verified = await this.context.isEventSenderVerified(mxEvent); if (verified) { this.setState({ - verified: E2ESTATE.VERIFIED, + verified: E2E_STATE.VERIFIED, }, () => { // Decryption may have caused a change in size this.props.onHeightChanged(); @@ -316,13 +316,13 @@ export default createReactClass({ const eventSenderTrust = await this.context.checkEventSenderTrust(mxEvent); if (!eventSenderTrust) { this.setState({ - verified: E2ESTATE.UNKNOWN, + verified: E2E_STATE.UNKNOWN, }, this.props.onHeightChanged); // Decryption may have cause a change in size return; } this.setState({ - verified: eventSenderTrust.isVerified() ? E2ESTATE.VERIFIED : E2ESTATE.WARNING, + verified: eventSenderTrust.isVerified() ? E2E_STATE.VERIFIED : E2E_STATE.WARNING, }, this.props.onHeightChanged); // Decryption may have caused a change in size }, @@ -503,9 +503,9 @@ export default createReactClass({ // event is encrypted, display padlock corresponding to whether or not it is verified if (ev.isEncrypted()) { - if (this.state.verified === E2ESTATE.VERIFIED) { + if (this.state.verified === E2E_STATE.VERIFIED) { return; // no icon for verified - } else if (this.state.verified === E2ESTATE.UNKNOWN) { + } else if (this.state.verified === E2E_STATE.UNKNOWN) { return (); } else { return (); @@ -636,9 +636,9 @@ export default createReactClass({ mx_EventTile_last: this.props.last, mx_EventTile_contextual: this.props.contextual, mx_EventTile_actionBarFocused: this.state.actionBarFocused, - mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2ESTATE.VERIFIED, - mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2ESTATE.WARNING, - mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2ESTATE.UNKNOWN, + mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED, + mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2E_STATE.WARNING, + mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN, mx_EventTile_bad: isEncryptionFailure, mx_EventTile_emote: msgtype === 'm.emote', mx_EventTile_redacted: isRedacted, From 33220c2d7230c999b0f65158a66affdd87e756a1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 21 Jan 2020 10:53:17 -0700 Subject: [PATCH 37/57] Ensure generated files are present for riot-web tests --- scripts/ci/riot-unit-tests.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ci/riot-unit-tests.sh b/scripts/ci/riot-unit-tests.sh index 7a9ed77793..337c0fe6c3 100755 --- a/scripts/ci/riot-unit-tests.sh +++ b/scripts/ci/riot-unit-tests.sh @@ -8,4 +8,5 @@ set -ev scripts/ci/layered-riot-web.sh cd ../riot-web +yarn build:genfiles # so the tests can run. Faster version of `build` yarn test From a8df058ea6f7e55e2b8f1bbddc171a46777febe8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 17:54:27 +0000 Subject: [PATCH 38/57] tidy up, improve wording on modal --- .../views/rooms/SendMessageComposer.js | 23 +++++++++++++------ src/i18n/strings/en_EN.json | 4 +++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 994c28f531..7870699fec 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -43,9 +43,6 @@ import ContentMessages from '../../../ContentMessages'; import {Key} from "../../../Keyboard"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -const SEND_ANYWAY = Symbol("send-anyway"); -const UNKNOWN_CMD = Symbol("unknown-cmd"); - function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); Object.assign(content, replyContent); @@ -256,14 +253,26 @@ export default class SendMessageComposer extends React.Component { shouldSend = false; this._runSlashCommand(cmd); } else { - // ask the user if their unknown command should be sent as a message instead + // ask the user if their unknown command should be sent as a message const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - // unknown command, ask the user if they meant to send it as a message const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { title: _t("Unknown Command"), - description: _t("Unrecognised command: ") + commandText, + description:
+

+ { _t("Unrecognised command: %(commandText)s", {commandText}) } +

+

+ { _t("You can use /help to list available commands. Did you mean to send this as a message?", {}, { + code: t => { t }, + }) } +

+

+ { _t("Protip: Begin your message with // to start it with a slash.", {}, { + code: t => { t }, + }) } +

+
, button: _t('Send as message'), - danger: true, }); const [sendAnyway] = await finished; // if !sendAnyway bail to let the user edit the composer and try again diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 314731a910..da4111aec8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1077,7 +1077,9 @@ "Command error": "Command error", "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", "Unknown Command": "Unknown Command", - "Unrecognised command: ": "Unrecognised command: ", + "Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s", + "You can use /help to list available commands. Did you mean to send this as a message?": "You can use /help to list available commands. Did you mean to send this as a message?", + "Protip: Begin your message with // to start it with a slash.": "Protip: Begin your message with // to start it with a slash.", "Send as message": "Send as message", "Failed to connect to integration manager": "Failed to connect to integration manager", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", From e455aa474d652c1f66228ea4e2e2f8b69f2a796b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 17:58:53 +0000 Subject: [PATCH 39/57] improve copy further --- src/components/views/rooms/SendMessageComposer.js | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 7870699fec..4402a034f6 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -267,7 +267,7 @@ export default class SendMessageComposer extends React.Component { }) }

- { _t("Protip: Begin your message with // to start it with a slash.", {}, { + { _t("Hint: Begin your message with // to start it with a slash.", {}, { code: t => { t }, }) }

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index da4111aec8..a3c56e5973 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1079,7 +1079,7 @@ "Unknown Command": "Unknown Command", "Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s", "You can use /help to list available commands. Did you mean to send this as a message?": "You can use /help to list available commands. Did you mean to send this as a message?", - "Protip: Begin your message with // to start it with a slash.": "Protip: Begin your message with // to start it with a slash.", + "Hint: Begin your message with // to start it with a slash.": "Hint: Begin your message with // to start it with a slash.", "Send as message": "Send as message", "Failed to connect to integration manager": "Failed to connect to integration manager", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", From 708f62784fbf7c46e2105e255faa28f6d1fd5879 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 21 Jan 2020 10:59:33 -0700 Subject: [PATCH 40/57] Consistency Co-Authored-By: J. Ryan Stinnett --- scripts/fetchdep.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index f477fd08b8..0142305797 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -17,7 +17,7 @@ clone() { if [ -n "$branch" ] then echo "Trying to use $org/$repo#$branch" - git clone git://github.com/$org/$repo.git $repo --branch "$branch" --depth=1 && exit 0 + git clone git://github.com/$org/$repo.git $repo --branch "$branch" --depth 1 && exit 0 fi } From 7b26067397e5bc1870c725968778bde83c6b0b34 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 18:03:01 +0000 Subject: [PATCH 41/57] delint --- src/components/views/rooms/SendMessageComposer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 4402a034f6..c4970c4570 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -262,7 +262,8 @@ export default class SendMessageComposer extends React.Component { { _t("Unrecognised command: %(commandText)s", {commandText}) }

- { _t("You can use /help to list available commands. Did you mean to send this as a message?", {}, { + { _t("You can use /help to list available commands. " + + "Did you mean to send this as a message?", {}, { code: t => { t }, }) }

From 2c6fe780123e2cdffdb65961f282c7afb8e06156 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 10:36:20 +0000 Subject: [PATCH 42/57] Fix roving room list for resizer and ff tabstop a11y --- src/accessibility/RovingTabIndex.js | 70 ++++++++----------- src/components/structures/RoomSubList.js | 4 -- .../views/groups/GroupInviteTile.js | 3 +- src/components/views/rooms/RoomList.js | 27 +++---- src/components/views/rooms/RoomTile.js | 3 +- 5 files changed, 48 insertions(+), 59 deletions(-) diff --git a/src/accessibility/RovingTabIndex.js b/src/accessibility/RovingTabIndex.js index 8924815f23..38f2594baf 100644 --- a/src/accessibility/RovingTabIndex.js +++ b/src/accessibility/RovingTabIndex.js @@ -129,7 +129,7 @@ const reducer = (state, action) => { } }; -export const RovingTabIndexProvider = ({children, handleHomeEnd}) => { +export const RovingTabIndexProvider = ({children, handleHomeEnd, onKeyDown}) => { const [state, dispatch] = useReducer(reducer, { activeRef: null, refs: [], @@ -137,53 +137,43 @@ export const RovingTabIndexProvider = ({children, handleHomeEnd}) => { const context = useMemo(() => ({state, dispatch}), [state]); - if (handleHomeEnd) { - return - - { children } - - ; - } - - return - { children } - ; -}; -RovingTabIndexProvider.propTypes = { - handleHomeEnd: PropTypes.bool, -}; - -// Helper to handle Home/End to jump to first/last roving-tab-index for widgets such as treeview -export const HomeEndHelper = ({children}) => { - const context = useContext(RovingTabIndexContext); - - const onKeyDown = useCallback((ev) => { - // check if we actually have any items - if (context.state.refs.length <= 0) return; - - let handled = true; - switch (ev.key) { - case Key.HOME: - // move focus to first item - context.state.refs[0].current.focus(); - break; - case Key.END: - // move focus to last item - context.state.refs[context.state.refs.length - 1].current.focus(); - break; - default: - handled = false; + const onKeyDownHandler = useCallback((ev) => { + let handled = false; + if (handleHomeEnd) { + // check if we actually have any items + switch (ev.key) { + case Key.HOME: + handled = true; + // move focus to first item + if (context.state.refs.length > 0) { + context.state.refs[0].current.focus(); + } + break; + case Key.END: + handled = true; + // move focus to last item + if (context.state.refs.length > 0) { + context.state.refs[context.state.refs.length - 1].current.focus(); + } + break; + } } if (handled) { ev.preventDefault(); ev.stopPropagation(); + } else if (onKeyDown) { + return onKeyDown(ev); } }, [context.state]); - return
- { children } -
; + return + { children({onKeyDownHandler}) } + ; +}; +RovingTabIndexProvider.propTypes = { + handleHomeEnd: PropTypes.bool, + onKeyDown: PropTypes.func, }; // Hook to register a roving tab index diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 2d41abf902..600b418fe0 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -142,10 +142,6 @@ export default class RoomSubList extends React.PureComponent { onHeaderKeyDown = (ev) => { switch (ev.key) { - case Key.TAB: - // Prevent LeftPanel handling Tab if focus is on the sublist header itself - ev.stopPropagation(); - break; case Key.ARROW_LEFT: // On ARROW_LEFT collapse the room sublist if (!this.state.hidden && !this.props.forceExpand) { diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index 3b15c6ff41..91c930525d 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -128,7 +128,8 @@ export default createReactClass({ 'mx_RoomTile_badgeShown': this.state.badgeHover || isMenuDisplayed, }); - const label =
+ // XXX: this is a workaround for Firefox giving this div a tabstop :( [tabIndex] + const label =
{ groupName }
; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index bd563b2f28..ee3100b535 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -777,21 +777,22 @@ export default createReactClass({ const subListComponents = this._mapSubListProps(subLists); - const {resizeNotifier, collapsed, searchFilter, ConferenceHandler, ...props} = this.props; // eslint-disable-line + const {resizeNotifier, collapsed, searchFilter, ConferenceHandler, onKeyDown, ...props} = this.props; // eslint-disable-line return ( -
- + + {({onKeyDownHandler}) =>
{ subListComponents } - -
+
} + ); }, }); diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 3b13001225..f4f5fa10fc 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -353,7 +353,8 @@ export default createReactClass({ }); subtextLabel = subtext ? { subtext } : null; - label =
{ name }
; + // XXX: this is a workaround for Firefox giving this div a tabstop :( [tabIndex] + label =
{ name }
; } else if (this.state.hover) { const Tooltip = sdk.getComponent("elements.Tooltip"); tooltip = ; From 37fb500e22aeb4762cef90ca8ca4f28eead57fa6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 10:41:10 +0000 Subject: [PATCH 43/57] fix useCallback dependencies, delint --- src/accessibility/RovingTabIndex.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessibility/RovingTabIndex.js b/src/accessibility/RovingTabIndex.js index 38f2594baf..b481f08fe2 100644 --- a/src/accessibility/RovingTabIndex.js +++ b/src/accessibility/RovingTabIndex.js @@ -165,7 +165,7 @@ export const RovingTabIndexProvider = ({children, handleHomeEnd, onKeyDown}) => } else if (onKeyDown) { return onKeyDown(ev); } - }, [context.state]); + }, [context.state, onKeyDown, handleHomeEnd]); return { children({onKeyDownHandler}) } From 176605c302698d7146380624bd82696ababde64f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 10:49:58 +0000 Subject: [PATCH 44/57] update tests to match new rendering method --- test/accessibility/RovingTabIndex-test.js | 26 +++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/test/accessibility/RovingTabIndex-test.js b/test/accessibility/RovingTabIndex-test.js index 2b55d1420c..e7d7a0977d 100644 --- a/test/accessibility/RovingTabIndex-test.js +++ b/test/accessibility/RovingTabIndex-test.js @@ -47,7 +47,7 @@ const button4 = ; describe("RovingTabIndex", () => { it("RovingTabIndexProvider renders children as expected", () => { const wrapper = mount( -
Test
+ {() =>
Test
}
); expect(wrapper.text()).toBe("Test"); expect(wrapper.html()).toBe('
Test
'); @@ -55,9 +55,11 @@ describe("RovingTabIndex", () => { it("RovingTabIndexProvider works as expected with useRovingTabIndex", () => { const wrapper = mount( - { button1 } - { button2 } - { button3 } + {() => + { button1 } + { button2 } + { button3 } + } ); // should begin with 0th being active @@ -95,13 +97,15 @@ describe("RovingTabIndex", () => { it("RovingTabIndexProvider works as expected with RovingTabIndexWrapper", () => { const wrapper = mount( - { button1 } - { button2 } - - {({onFocus, isActive, ref}) => - - } - + {() => + { button1 } + { button2 } + + {({onFocus, isActive, ref}) => + + } + + } ); // should begin with 0th being active From fc724cfe709ec046db13283e2fd8bf91646bb017 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 11:05:25 +0000 Subject: [PATCH 45/57] fix tests some moar --- test/accessibility/RovingTabIndex-test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/accessibility/RovingTabIndex-test.js b/test/accessibility/RovingTabIndex-test.js index e7d7a0977d..8be4a2976c 100644 --- a/test/accessibility/RovingTabIndex-test.js +++ b/test/accessibility/RovingTabIndex-test.js @@ -47,7 +47,7 @@ const button4 = ; describe("RovingTabIndex", () => { it("RovingTabIndexProvider renders children as expected", () => { const wrapper = mount( - {() =>
Test
} + {() =>
Test
}
); expect(wrapper.text()).toBe("Test"); expect(wrapper.html()).toBe('
Test
'); @@ -82,14 +82,14 @@ describe("RovingTabIndex", () => { // update the children, it should remain on the same button wrapper.setProps({ - children: [button1, button4, button2, button3], + children: () => [button1, button4, button2, button3], }); wrapper.update(); checkTabIndexes(wrapper.find("button"), [-1, -1, 0, -1]); // update the children, remove the active button, it should move to the next one wrapper.setProps({ - children: [button1, button4, button3], + children: () => [button1, button4, button3], }); wrapper.update(); checkTabIndexes(wrapper.find("button"), [-1, -1, 0]); From 85ee6bd51f06be795c1e596f353de422a9f21083 Mon Sep 17 00:00:00 2001 From: Zoe Date: Wed, 22 Jan 2020 11:17:54 +0000 Subject: [PATCH 46/57] Don't warn on unverified users; ensured behavior stays the same with flags off --- src/components/views/rooms/EventTile.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index bcd32d2c9c..634b77c9e1 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -70,6 +70,7 @@ const E2E_STATE = { VERIFIED: "verified", WARNING: "warning", UNKNOWN: "unknown", + NORMAL: "normal", }; // Add all the Mjolnir stuff to the renderer @@ -313,6 +314,22 @@ export default createReactClass({ return; } + // If cross-signing is off, the old behaviour is to scream at the user + // as if they've done something wrong, which they haven't + if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) { + this.setState({ + verified: E2E_STATE.WARNING, + }, this.props.onHeightChanged); + return; + } + + if (!this.context.checkUserTrust(mxEvent.getSender()).isCrossSigningVerified()) { + this.setState({ + verified: E2E_STATE.NORMAL, + }, this.props.onHeightChanged); + return; + } + const eventSenderTrust = await this.context.checkEventSenderTrust(mxEvent); if (!eventSenderTrust) { this.setState({ @@ -503,7 +520,9 @@ export default createReactClass({ // event is encrypted, display padlock corresponding to whether or not it is verified if (ev.isEncrypted()) { - if (this.state.verified === E2E_STATE.VERIFIED) { + if (this.state.verified === E2E_STATE.NORMAL) { + return; // no icon if we've not even cross-signed the user + } else if (this.state.verified === E2E_STATE.VERIFIED) { return; // no icon for verified } else if (this.state.verified === E2E_STATE.UNKNOWN) { return (); From e1e53f567f93ab044f24bc195d40fa4046acd2fb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 11:56:27 +0000 Subject: [PATCH 47/57] add more tests --- .../views/rooms/SendMessageComposer.js | 3 +- .../views/rooms/SendMessageComposer-test.js | 83 +++++++++++++++++++ test/editor/mock.js | 10 +++ test/editor/model-test.js | 12 +-- 4 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 test/components/views/rooms/SendMessageComposer-test.js diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index c4970c4570..a857e40f55 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -58,7 +58,8 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { } } -function createMessageContent(model, permalinkCreator) { +// exported for tests +export function createMessageContent(model, permalinkCreator) { const isEmote = containsEmote(model); if (isEmote) { model = stripEmoteCommand(model); diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js new file mode 100644 index 0000000000..d5a143a1fb --- /dev/null +++ b/test/components/views/rooms/SendMessageComposer-test.js @@ -0,0 +1,83 @@ +/* +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 RoomViewStore from "../../../../src/stores/RoomViewStore"; +import {createMessageContent} from "../../../../src/components/views/rooms/SendMessageComposer"; +import EditorModel from "../../../../src/editor/model"; +import {createPartCreator, createRenderer} from "../../../editor/mock"; + +jest.mock("../../../../src/stores/RoomViewStore"); + +describe('', () => { + describe("createMessageContent", () => { + RoomViewStore.getQuotingEvent.mockReturnValue(false); + const permalinkCreator = jest.fn(); + + it("sends plaintext messages correctly", () => { + const model = new EditorModel([], createPartCreator(), createRenderer()); + model.update("hello world", "insertText", {offset: 11, atNodeEnd: true}); + + const content = createMessageContent(model, permalinkCreator); + + expect(content).toEqual({ + body: "hello world", + msgtype: "m.text", + }); + }); + + it("sends markdown messages correctly", () => { + const model = new EditorModel([], createPartCreator(), createRenderer()); + model.update("hello *world*", "insertText", {offset: 13, atNodeEnd: true}); + + const content = createMessageContent(model, permalinkCreator); + + expect(content).toEqual({ + body: "hello *world*", + msgtype: "m.text", + format: "org.matrix.custom.html", + formatted_body: "hello world", + }); + }); + + it("strips /me from messages and marks them as m.emote accordingly", () => { + const model = new EditorModel([], createPartCreator(), createRenderer()); + model.update("/me blinks __quickly__", "insertText", {offset: 22, atNodeEnd: true}); + + const content = createMessageContent(model, permalinkCreator); + + expect(content).toEqual({ + body: "blinks __quickly__", + msgtype: "m.emote", + format: "org.matrix.custom.html", + formatted_body: "blinks quickly", + }); + }); + + it("allows sending double-slash escaped slash commands correctly", () => { + const model = new EditorModel([], createPartCreator(), createRenderer()); + model.update("//dev/null is my favourite place", "insertText", {offset: 32, atNodeEnd: true}); + + const content = createMessageContent(model, permalinkCreator); + + expect(content).toEqual({ + body: "/dev/null is my favourite place", + msgtype: "m.text", + }); + }); + }); +}); + + diff --git a/test/editor/mock.js b/test/editor/mock.js index bb1a51d14b..6de65cf23d 100644 --- a/test/editor/mock.js +++ b/test/editor/mock.js @@ -67,3 +67,13 @@ export function createPartCreator(completions = []) { }; return new PartCreator(new MockRoom(), new MockClient(), autoCompleteCreator); } + +export function createRenderer() { + const render = (c) => { + render.caret = c; + render.count += 1; + }; + render.count = 0; + render.caret = null; + return render; +} diff --git a/test/editor/model-test.js b/test/editor/model-test.js index 826dde3d68..2a3584d508 100644 --- a/test/editor/model-test.js +++ b/test/editor/model-test.js @@ -15,17 +15,7 @@ limitations under the License. */ import EditorModel from "../../src/editor/model"; -import {createPartCreator} from "./mock"; - -function createRenderer() { - const render = (c) => { - render.caret = c; - render.count += 1; - }; - render.count = 0; - render.caret = null; - return render; -} +import {createPartCreator, createRenderer} from "./mock"; describe('editor/model', function() { describe('plain text manipulation', function() { From fbb65f068a6bb8d153ef65310ec52b886cfdc5ee Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 22 Jan 2020 14:07:16 +0000 Subject: [PATCH 48/57] Support admin configurable message when reporting content This adds support for an admin-configured message in config.json to be shown in the report content dialog to allow linking to community rules, etc. Fixes https://github.com/vector-im/riot-web/issues/11992 --- src/components/views/dialogs/ReportEventDialog.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ReportEventDialog.js b/src/components/views/dialogs/ReportEventDialog.js index e77bb0693b..2442bc2f95 100644 --- a/src/components/views/dialogs/ReportEventDialog.js +++ b/src/components/views/dialogs/ReportEventDialog.js @@ -20,6 +20,8 @@ import { _t } from '../../../languageHandler'; import PropTypes from "prop-types"; import {MatrixEvent} from "matrix-js-sdk"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import SdkConfig from '../../../SdkConfig'; +import Markdown from '../../../Markdown'; /* * A dialog for reporting an event. @@ -95,6 +97,15 @@ export default class ReportEventDialog extends PureComponent { ); } + const adminMessageMD = + SdkConfig.get().reportEvent && + SdkConfig.get().reportEvent.adminMessageMD; + let adminMessage; + if (adminMessageMD) { + const html = new Markdown(adminMessageMD).toHTML(); + adminMessage =

; + } + return ( - + {adminMessage} Date: Wed, 22 Jan 2020 14:15:17 +0000 Subject: [PATCH 49/57] Change prepublish script to prepare prepublish is deprecated (prepare also runs for git checkouts, and lib will need to be built in this case). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aa2cf8bf8b..8f49eed4b5 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "typings": "./lib/index.d.ts", "matrix_src_main": "./src/index.js", "scripts": { - "prepublish": "yarn build", + "prepare": "yarn build", "i18n": "matrix-gen-i18n", "prunei18n": "matrix-prune-i18n", "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", From 516dd25797f4ad5c8fb53a6471ef65cf212879a7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 14:24:10 +0000 Subject: [PATCH 50/57] fix typo in fallback codepath --- src/components/views/rooms/SendMessageComposer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index a857e40f55..6a60037036 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -187,7 +187,7 @@ export default class SendMessageComposer extends React.Component { // be extra resilient when somehow the AutocompleteWrapperModel or // CommandPartCreator fails to insert a command part, so we don't send // a command as a message - if (firstPart.text.startsWith("/") && firstPart.text.startsWith("//") && !firstPart.text.startsWith("//") + if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { return true; } From 088b1ea6285b544108cb4e122aced3449ab14082 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 15:03:48 +0000 Subject: [PATCH 51/57] Retry end-to-end tests automatically once if they fail, flakey flake --- .buildkite/pipeline.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml index de61d4e5b9..747625ae6e 100644 --- a/.buildkite/pipeline.yaml +++ b/.buildkite/pipeline.yaml @@ -77,6 +77,10 @@ steps: image: "matrixdotorg/riotweb-ci-e2etests-env:latest" propagate-environment: true workdir: "/workdir/matrix-react-sdk" + retry: + automatic: + - exit_status: 1 # retry end-to-end tests once as Puppeteer sometimes fails + - limit: 1 - label: "🔧 Riot Tests" agents: From e3a28e3e449a451a85710de272d32d26b47fab56 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Jan 2020 15:05:25 +0000 Subject: [PATCH 52/57] Remove the react-sdk version I'm not sure if there was ever a point where this did work and we had 'dist' and 'gitHead' properties in our package.json but I can't find any trace of them now and I'm sick of this just being there syaing '' all the time. --- .../views/settings/tabs/user/HelpUserSettingsTab.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index ab71de86b9..99b94d47f2 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -26,10 +26,6 @@ import Modal from "../../../../../Modal"; import * as sdk from "../../../../../"; import PlatformPeg from "../../../../../PlatformPeg"; -// if this looks like a release, use the 'version' from package.json; else use -// the git sha. Prepend version with v, to look like riot-web version -const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || ''; - // Simple method to help prettify GH Release Tags and Commit Hashes. const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i; const ghVersionLabel = function(repo, token='') { @@ -188,9 +184,6 @@ export default class HelpUserSettingsTab extends React.Component { ); } - const reactSdkVersion = REACT_SDK_VERSION !== '' - ? ghVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION) - : REACT_SDK_VERSION; const vectorVersion = this.state.vectorVersion ? ghVersionLabel('vector-im/riot-web', this.state.vectorVersion) : 'unknown'; @@ -243,7 +236,6 @@ export default class HelpUserSettingsTab extends React.Component {

{_t("Versions")}
- {_t("matrix-react-sdk version:")} {reactSdkVersion}
{_t("riot-web version:")} {vectorVersion}
{_t("olm version:")} {olmVersion}
{updateButton} From d04ba40efe49ef31f757e823a99f44fc9650e9ff Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 15:05:40 +0000 Subject: [PATCH 53/57] fix syntax --- .buildkite/pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml index 747625ae6e..f5f63b647a 100644 --- a/.buildkite/pipeline.yaml +++ b/.buildkite/pipeline.yaml @@ -80,7 +80,7 @@ steps: retry: automatic: - exit_status: 1 # retry end-to-end tests once as Puppeteer sometimes fails - - limit: 1 + limit: 1 - label: "🔧 Riot Tests" agents: From c04872dd9b6974e334dca1286a69c422bc8ccaf9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Jan 2020 15:12:38 +0000 Subject: [PATCH 54/57] i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4daf7cd29e..d19cbb9bfd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -687,7 +687,6 @@ "Clear cache and reload": "Clear cache and reload", "FAQ": "FAQ", "Versions": "Versions", - "matrix-react-sdk version:": "matrix-react-sdk version:", "riot-web version:": "riot-web version:", "olm version:": "olm version:", "Homeserver is": "Homeserver is", From 7e52eb9f65374ffcb0bc9cba99ea8c3d16ecd51b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Jan 2020 15:16:41 +0000 Subject: [PATCH 55/57] Unused import --- src/components/views/settings/tabs/user/HelpUserSettingsTab.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index 99b94d47f2..a245c7c7b9 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -21,7 +21,6 @@ import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import AccessibleButton from "../../../elements/AccessibleButton"; import SdkConfig from "../../../../../SdkConfig"; import createRoom from "../../../../../createRoom"; -import packageJson from "../../../../../../package.json"; import Modal from "../../../../../Modal"; import * as sdk from "../../../../../"; import PlatformPeg from "../../../../../PlatformPeg"; From 33b5d42c0687185ca02c1661e22e4d49dfcdb867 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 15:34:17 +0000 Subject: [PATCH 56/57] Be consistent about our settings svg, free the other one --- res/css/structures/_GroupView.scss | 2 +- res/img/icons-settings-room.svg | 6 ------ src/components/views/context_menus/RoomTileContextMenu.js | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 res/img/icons-settings-room.svg diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss index 4ec53a3c9a..517b8b1922 100644 --- a/res/css/structures/_GroupView.scss +++ b/res/css/structures/_GroupView.scss @@ -63,7 +63,7 @@ limitations under the License. } .mx_GroupHeader_editButton::before { - mask-image: url('$(res)/img/icons-settings-room.svg'); + mask-image: url('$(res)/img/feather-customised/settings.svg'); } .mx_GroupHeader_shareButton::before { diff --git a/res/img/icons-settings-room.svg b/res/img/icons-settings-room.svg deleted file mode 100644 index 421eefdefa..0000000000 --- a/res/img/icons-settings-room.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 6e2bd8ebf5..2d8dec29c7 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -306,7 +306,7 @@ export default createReactClass({ return (
- + { _t('Settings') }
From a504faa2f6f80fa879d46e10009c45e4474aa257 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 22 Jan 2020 22:08:34 +0000 Subject: [PATCH 57/57] Treat links as external in report content admin message This marks all the links in the report content admin message (in Markdown format) as external so they open in a new tab. --- src/Markdown.js | 20 ++++++++++++++++++- .../views/dialogs/ReportEventDialog.js | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index acfea52100..437ceec88b 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -91,7 +91,7 @@ export default class Markdown { return true; } - toHTML() { + toHTML({ externalLinks = false } = {}) { const renderer = new commonmark.HtmlRenderer({ safe: false, @@ -125,6 +125,24 @@ export default class Markdown { } }; + renderer.link = function(node, entering) { + const attrs = this.attrs(node); + if (entering) { + attrs.push(['href', this.esc(node.destination)]); + if (node.title) { + attrs.push(['title', this.esc(node.title)]); + } + // Modified link behaviour to treat them all as external and + // thus opening in a new tab. + if (externalLinks) { + attrs.push(['target', '_blank']); + attrs.push(['rel', 'noopener']); + } + this.tag('a', attrs); + } else { + this.tag('/a'); + } + }; renderer.html_inline = html_if_tag_allowed; diff --git a/src/components/views/dialogs/ReportEventDialog.js b/src/components/views/dialogs/ReportEventDialog.js index 2442bc2f95..99853582dd 100644 --- a/src/components/views/dialogs/ReportEventDialog.js +++ b/src/components/views/dialogs/ReportEventDialog.js @@ -102,7 +102,7 @@ export default class ReportEventDialog extends PureComponent { SdkConfig.get().reportEvent.adminMessageMD; let adminMessage; if (adminMessageMD) { - const html = new Markdown(adminMessageMD).toHTML(); + const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true }); adminMessage =

; }