From b5598df3f793b942f91137e0d98c118e9792317a Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 21 Jun 2019 11:06:27 +0100 Subject: [PATCH 01/70] Remove redundant extra chevrons from ContextualMenu This removes a seemingly redundant layer of extra chevrons from `ContextualMenu`. Since both chevrons are the same color, there should be no visual change. --- res/css/structures/_ContextualMenu.scss | 49 +------------------------ 1 file changed, 1 insertion(+), 48 deletions(-) diff --git a/res/css/structures/_ContextualMenu.scss b/res/css/structures/_ContextualMenu.scss index e818bb092e..fa2d87029d 100644 --- a/res/css/structures/_ContextualMenu.scss +++ b/res/css/structures/_ContextualMenu.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 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. @@ -58,18 +59,6 @@ limitations under the License. border-bottom: 8px solid transparent; } -.mx_ContextualMenu_chevron_right::after { - content: ''; - width: 0; - height: 0; - border-top: 7px solid transparent; - border-left: 7px solid $menu-bg-color; - border-bottom: 7px solid transparent; - position: absolute; - top: -7px; - right: 1px; -} - .mx_ContextualMenu_left { left: 0; } @@ -89,18 +78,6 @@ limitations under the License. border-bottom: 8px solid transparent; } -.mx_ContextualMenu_chevron_left::after { - content: ''; - width: 0; - height: 0; - border-top: 7px solid transparent; - border-right: 7px solid $menu-bg-color; - border-bottom: 7px solid transparent; - position: absolute; - top: -7px; - left: 1px; -} - .mx_ContextualMenu_top { top: 0; } @@ -120,18 +97,6 @@ limitations under the License. border-right: 8px solid transparent; } -.mx_ContextualMenu_chevron_top::after { - content: ''; - width: 0; - height: 0; - border-left: 7px solid transparent; - border-bottom: 7px solid $menu-bg-color; - border-right: 7px solid transparent; - position: absolute; - left: -7px; - top: 1px; -} - .mx_ContextualMenu_bottom { bottom: 0; } @@ -151,18 +116,6 @@ limitations under the License. border-right: 8px solid transparent; } -.mx_ContextualMenu_chevron_bottom::after { - content: ''; - width: 0; - height: 0; - border-left: 7px solid transparent; - border-top: 7px solid $menu-bg-color; - border-right: 7px solid transparent; - position: absolute; - left: -7px; - bottom: 1px; -} - .mx_ContextualMenu_spinner { display: block; margin: 0 auto; From 6dcdad028e94f7e9fc3d5fbb5a315c12f726ab7e Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 20 Jun 2019 12:03:22 +0100 Subject: [PATCH 02/70] Clone ContextualMenu to InteractiveTooltip As part of reactions and editing work, we're adding a new style of tooltip that allows interacting with the content of the tooltip. `ContextualMenu` is closest out of the things we have today, but it doesn't position in quite the way we want and it's already quite complex. To get started, let's first clone that to a new `InteractiveTooltip`. Part of https://github.com/vector-im/riot-web/issues/9753 Part of https://github.com/vector-im/riot-web/issues/9716 --- res/css/_components.scss | 1 + .../views/elements/_InteractiveTooltip.scss | 164 ++++++++++++++++++ .../views/elements/InteractiveTooltip.js | 141 +++++++++++++++ 3 files changed, 306 insertions(+) create mode 100644 res/css/views/elements/_InteractiveTooltip.scss create mode 100644 src/components/views/elements/InteractiveTooltip.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 582dc59517..fa388c4e6a 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -86,6 +86,7 @@ @import "./views/elements/_Field.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InlineSpinner.scss"; +@import "./views/elements/_InteractiveTooltip.scss"; @import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_MemberEventListSummary.scss"; @import "./views/elements/_MessageEditor.scss"; diff --git a/res/css/views/elements/_InteractiveTooltip.scss b/res/css/views/elements/_InteractiveTooltip.scss new file mode 100644 index 0000000000..11f548fa18 --- /dev/null +++ b/res/css/views/elements/_InteractiveTooltip.scss @@ -0,0 +1,164 @@ +/* +Copyright 2019 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. +*/ + +.mx_InteractiveTooltip_wrapper { + position: fixed; + z-index: 5000; +} + +.mx_InteractiveTooltip_background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 1.0; + z-index: 5000; +} + +.mx_InteractiveTooltip { + border-radius: 4px; + box-shadow: 4px 4px 12px 0 $menu-box-shadow-color; + background-color: $menu-bg-color; + color: $primary-fg-color; + position: absolute; + font-size: 14px; + z-index: 5001; +} + +.mx_InteractiveTooltip_right { + right: 0; +} + +.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_right { + right: 8px; +} + +.mx_InteractiveTooltip_chevron_right { + position: absolute; + right: -8px; + top: 0px; + width: 0; + height: 0; + border-top: 8px solid transparent; + border-left: 8px solid $menu-bg-color; + border-bottom: 8px solid transparent; +} + +.mx_InteractiveTooltip_chevron_right::after { + content: ''; + width: 0; + height: 0; + border-top: 7px solid transparent; + border-left: 7px solid $menu-bg-color; + border-bottom: 7px solid transparent; + position: absolute; + top: -7px; + right: 1px; +} + +.mx_InteractiveTooltip_left { + left: 0; +} + +.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_left { + left: 8px; +} + +.mx_InteractiveTooltip_chevron_left { + position: absolute; + left: -8px; + top: 0px; + width: 0; + height: 0; + border-top: 8px solid transparent; + border-right: 8px solid $menu-bg-color; + border-bottom: 8px solid transparent; +} + +.mx_InteractiveTooltip_chevron_left::after { + content: ''; + width: 0; + height: 0; + border-top: 7px solid transparent; + border-right: 7px solid $menu-bg-color; + border-bottom: 7px solid transparent; + position: absolute; + top: -7px; + left: 1px; +} + +.mx_InteractiveTooltip_top { + top: 0; +} + +.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_top { + top: 8px; +} + +.mx_InteractiveTooltip_chevron_top { + position: absolute; + left: 0px; + top: -8px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-bottom: 8px solid $menu-bg-color; + border-right: 8px solid transparent; +} + +.mx_InteractiveTooltip_chevron_top::after { + content: ''; + width: 0; + height: 0; + border-left: 7px solid transparent; + border-bottom: 7px solid $menu-bg-color; + border-right: 7px solid transparent; + position: absolute; + left: -7px; + top: 1px; +} + +.mx_InteractiveTooltip_bottom { + bottom: 0; +} + +.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_bottom { + bottom: 8px; +} + +.mx_InteractiveTooltip_chevron_bottom { + position: absolute; + left: 0px; + bottom: -8px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-top: 8px solid $menu-bg-color; + border-right: 8px solid transparent; +} + +.mx_InteractiveTooltip_chevron_bottom::after { + content: ''; + width: 0; + height: 0; + border-left: 7px solid transparent; + border-top: 7px solid $menu-bg-color; + border-right: 7px solid transparent; + position: absolute; + left: -7px; + bottom: 1px; +} diff --git a/src/components/views/elements/InteractiveTooltip.js b/src/components/views/elements/InteractiveTooltip.js new file mode 100644 index 0000000000..7c582a2b71 --- /dev/null +++ b/src/components/views/elements/InteractiveTooltip.js @@ -0,0 +1,141 @@ +/* +Copyright 2019 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 ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +const InteractiveTooltipContainerId = "mx_InteractiveTooltip_Container"; + +function getOrCreateContainer() { + let container = document.getElementById(InteractiveTooltipContainerId); + + if (!container) { + container = document.createElement("div"); + container.id = InteractiveTooltipContainerId; + document.body.appendChild(container); + } + + return container; +} + +export default class InteractiveTooltip extends React.Component { + propTypes: { + top: PropTypes.number, + bottom: PropTypes.number, + left: PropTypes.number, + right: PropTypes.number, + chevronOffset: PropTypes.number, + chevronFace: PropTypes.string, // top, bottom, left, right or none + // Function to be called on menu close + onFinished: PropTypes.func, + + // If true, insert an invisible screen-sized element behind the + // menu that when clicked will close it. + hasBackground: PropTypes.bool, + + // The component to render as the context menu + elementClass: PropTypes.element.isRequired, + // on resize callback + windowResize: PropTypes.func, + // method to close menu + closeTooltip: PropTypes.func, + }; + + render() { + const position = {}; + let chevronFace = null; + const props = this.props; + + if (props.top) { + position.top = props.top; + } else { + position.bottom = props.bottom; + } + + if (props.left) { + position.left = props.left; + chevronFace = 'left'; + } else { + position.right = props.right; + chevronFace = 'right'; + } + + const chevronOffset = {}; + if (props.chevronFace) { + chevronFace = props.chevronFace; + } + const hasChevron = chevronFace && chevronFace !== "none"; + + if (chevronFace === 'top' || chevronFace === 'bottom') { + chevronOffset.left = props.chevronOffset; + } else { + chevronOffset.top = props.chevronOffset; + } + + const chevron = hasChevron ? +
: + undefined; + const className = 'mx_InteractiveTooltip_wrapper'; + + const menuClasses = classNames({ + 'mx_InteractiveTooltip': true, + 'mx_InteractiveTooltip_left': !hasChevron && position.left, + 'mx_InteractiveTooltip_right': !hasChevron && position.right, + 'mx_InteractiveTooltip_top': !hasChevron && position.top, + 'mx_InteractiveTooltip_bottom': !hasChevron && position.bottom, + 'mx_InteractiveTooltip_withChevron_left': chevronFace === 'left', + 'mx_InteractiveTooltip_withChevron_right': chevronFace === 'right', + 'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top', + 'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom', + }); + + const ElementClass = props.elementClass; + + return
+
+ { chevron } + +
+ { props.hasBackground &&
} +
; + } +} + +export function createTooltip(ElementClass, props, hasBackground=true) { + const closeTooltip = function(...args) { + ReactDOM.unmountComponentAtNode(getOrCreateContainer()); + + if (props && props.onFinished) { + props.onFinished.apply(null, args); + } + }; + + // We only reference closeTooltip once per call to createTooltip + const menu = ; + + ReactDOM.render(menu, getOrCreateContainer()); + + return {close: closeTooltip}; +} From 32bf4588dd3fa38c0169d9b1594de6e8fec39603 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 20 Jun 2019 18:33:45 +0100 Subject: [PATCH 03/70] Center tooltip along top or bottom of target This adjusts the positioning to work more the way we want: * Tooltip is position on the top or bottom edge of the target depending on where space is available * Tooltip and chevron are centered In addition, more bits borrowed from `ContextualMenu` are not needed, so they have been removed for simplicity. Part of https://github.com/vector-im/riot-web/issues/9753 Part of https://github.com/vector-im/riot-web/issues/9716 --- .../views/elements/_InteractiveTooltip.scss | 74 +-------------- .../views/elements/InteractiveTooltip.js | 92 ++++++++++--------- 2 files changed, 50 insertions(+), 116 deletions(-) diff --git a/res/css/views/elements/_InteractiveTooltip.scss b/res/css/views/elements/_InteractiveTooltip.scss index 11f548fa18..3ec20be928 100644 --- a/res/css/views/elements/_InteractiveTooltip.scss +++ b/res/css/views/elements/_InteractiveTooltip.scss @@ -39,79 +39,13 @@ limitations under the License. z-index: 5001; } -.mx_InteractiveTooltip_right { - right: 0; -} - -.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_right { - right: 8px; -} - -.mx_InteractiveTooltip_chevron_right { - position: absolute; - right: -8px; - top: 0px; - width: 0; - height: 0; - border-top: 8px solid transparent; - border-left: 8px solid $menu-bg-color; - border-bottom: 8px solid transparent; -} - -.mx_InteractiveTooltip_chevron_right::after { - content: ''; - width: 0; - height: 0; - border-top: 7px solid transparent; - border-left: 7px solid $menu-bg-color; - border-bottom: 7px solid transparent; - position: absolute; - top: -7px; - right: 1px; -} - -.mx_InteractiveTooltip_left { - left: 0; -} - -.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_left { - left: 8px; -} - -.mx_InteractiveTooltip_chevron_left { - position: absolute; - left: -8px; - top: 0px; - width: 0; - height: 0; - border-top: 8px solid transparent; - border-right: 8px solid $menu-bg-color; - border-bottom: 8px solid transparent; -} - -.mx_InteractiveTooltip_chevron_left::after { - content: ''; - width: 0; - height: 0; - border-top: 7px solid transparent; - border-right: 7px solid $menu-bg-color; - border-bottom: 7px solid transparent; - position: absolute; - top: -7px; - left: 1px; -} - -.mx_InteractiveTooltip_top { - top: 0; -} - .mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_top { top: 8px; } .mx_InteractiveTooltip_chevron_top { position: absolute; - left: 0px; + left: calc(50% - 8px); top: -8px; width: 0; height: 0; @@ -132,17 +66,13 @@ limitations under the License. top: 1px; } -.mx_InteractiveTooltip_bottom { - bottom: 0; -} - .mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_bottom { bottom: 8px; } .mx_InteractiveTooltip_chevron_bottom { position: absolute; - left: 0px; + left: calc(50% - 8px); bottom: -8px; width: 0; height: 0; diff --git a/src/components/views/elements/InteractiveTooltip.js b/src/components/views/elements/InteractiveTooltip.js index 7c582a2b71..b3d0b32fa7 100644 --- a/src/components/views/elements/InteractiveTooltip.js +++ b/src/components/views/elements/InteractiveTooltip.js @@ -33,21 +33,19 @@ function getOrCreateContainer() { return container; } +/* + * This style of tooltip takes a `target` element's rect and centers the tooltip + * along one edge of the target. + */ export default class InteractiveTooltip extends React.Component { propTypes: { - top: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - right: PropTypes.number, - chevronOffset: PropTypes.number, - chevronFace: PropTypes.string, // top, bottom, left, right or none + // A DOMRect from the target element + targetRect: PropTypes.object.isRequired, // Function to be called on menu close onFinished: PropTypes.func, - // If true, insert an invisible screen-sized element behind the // menu that when clicked will close it. hasBackground: PropTypes.bool, - // The component to render as the context menu elementClass: PropTypes.element.isRequired, // on resize callback @@ -56,58 +54,64 @@ export default class InteractiveTooltip extends React.Component { closeTooltip: PropTypes.func, }; + constructor() { + super(); + + this.state = { + contentRect: null, + }; + } + + collectContentRect = (element) => { + // We don't need to clean up when unmounting, so ignore + if (!element) return; + + this.setState({ + contentRect: element.getBoundingClientRect(), + }); + } + render() { + const props = this.props; + const { targetRect } = props; + + // The window X and Y offsets are to adjust position when zoomed in to page + const targetLeft = targetRect.left + window.pageXOffset; + const targetBottom = targetRect.bottom + window.pageYOffset; + const targetTop = targetRect.top + window.pageYOffset; + + // Align the tooltip vertically on whichever side of the target has more + // space available. const position = {}; let chevronFace = null; - const props = this.props; - - if (props.top) { - position.top = props.top; + if (targetBottom < window.innerHeight / 2) { + position.top = targetBottom; + chevronFace = "top"; } else { - position.bottom = props.bottom; + position.bottom = window.innerHeight - targetTop; + chevronFace = "bottom"; } - if (props.left) { - position.left = props.left; - chevronFace = 'left'; - } else { - position.right = props.right; - chevronFace = 'right'; - } + // Center the tooltip horizontally with the target's center. + position.left = targetLeft + targetRect.width / 2; - const chevronOffset = {}; - if (props.chevronFace) { - chevronFace = props.chevronFace; - } - const hasChevron = chevronFace && chevronFace !== "none"; - - if (chevronFace === 'top' || chevronFace === 'bottom') { - chevronOffset.left = props.chevronOffset; - } else { - chevronOffset.top = props.chevronOffset; - } - - const chevron = hasChevron ? -
: - undefined; - const className = 'mx_InteractiveTooltip_wrapper'; + const chevron =
; const menuClasses = classNames({ 'mx_InteractiveTooltip': true, - 'mx_InteractiveTooltip_left': !hasChevron && position.left, - 'mx_InteractiveTooltip_right': !hasChevron && position.right, - 'mx_InteractiveTooltip_top': !hasChevron && position.top, - 'mx_InteractiveTooltip_bottom': !hasChevron && position.bottom, - 'mx_InteractiveTooltip_withChevron_left': chevronFace === 'left', - 'mx_InteractiveTooltip_withChevron_right': chevronFace === 'right', 'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top', 'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom', }); + const menuStyle = {}; + if (this.state.contentRect) { + menuStyle.left = `-${this.state.contentRect.width / 2}px`; + } + const ElementClass = props.elementClass; - return
-
+ return
+
{ chevron }
From 3bd247ebaa02acf973f992cb6c01b221cd03c90f Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 21 Jun 2019 11:41:19 +0100 Subject: [PATCH 04/70] Tweak interactive tooltip to match design This tweaks the tooltip to match the color, spacing, etc. seen in the designs. Part of https://github.com/vector-im/riot-web/issues/9753 Part of https://github.com/vector-im/riot-web/issues/9716 --- .../views/elements/_InteractiveTooltip.scss | 43 +++++-------------- res/themes/dark/css/_dark.scss | 3 ++ res/themes/light/css/_light.scss | 3 ++ 3 files changed, 16 insertions(+), 33 deletions(-) diff --git a/res/css/views/elements/_InteractiveTooltip.scss b/res/css/views/elements/_InteractiveTooltip.scss index 3ec20be928..a949941dd8 100644 --- a/res/css/views/elements/_InteractiveTooltip.scss +++ b/res/css/views/elements/_InteractiveTooltip.scss @@ -30,17 +30,18 @@ limitations under the License. } .mx_InteractiveTooltip { - border-radius: 4px; - box-shadow: 4px 4px 12px 0 $menu-box-shadow-color; - background-color: $menu-bg-color; - color: $primary-fg-color; + border-radius: 3px; + background-color: $interactive-tooltip-bg-color; + color: $interactive-tooltip-fg-color; position: absolute; - font-size: 14px; + font-size: 10px; + font-weight: 600; + padding: 6px; z-index: 5001; } .mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_top { - top: 8px; + top: 10px; // 8px chevron + 2px spacing } .mx_InteractiveTooltip_chevron_top { @@ -50,24 +51,12 @@ limitations under the License. width: 0; height: 0; border-left: 8px solid transparent; - border-bottom: 8px solid $menu-bg-color; + border-bottom: 8px solid $interactive-tooltip-bg-color; border-right: 8px solid transparent; } -.mx_InteractiveTooltip_chevron_top::after { - content: ''; - width: 0; - height: 0; - border-left: 7px solid transparent; - border-bottom: 7px solid $menu-bg-color; - border-right: 7px solid transparent; - position: absolute; - left: -7px; - top: 1px; -} - .mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_bottom { - bottom: 8px; + bottom: 10px; // 8px chevron + 2px spacing } .mx_InteractiveTooltip_chevron_bottom { @@ -77,18 +66,6 @@ limitations under the License. width: 0; height: 0; border-left: 8px solid transparent; - border-top: 8px solid $menu-bg-color; + border-top: 8px solid $interactive-tooltip-bg-color; border-right: 8px solid transparent; } - -.mx_InteractiveTooltip_chevron_bottom::after { - content: ''; - width: 0; - height: 0; - border-left: 7px solid transparent; - border-top: 7px solid $menu-bg-color; - border-right: 7px solid transparent; - position: absolute; - left: -7px; - bottom: 1px; -} diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index bdccf71540..ed1cc162a0 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -160,6 +160,9 @@ $reaction-row-button-selected-border-color: $accent-color; $tooltip-timeline-bg-color: $tagpanel-bg-color; $tooltip-timeline-fg-color: #ffffff; +$interactive-tooltip-bg-color: $base-color; +$interactive-tooltip-fg-color: #ffffff; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 8244485ee3..361f6fa408 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -272,6 +272,9 @@ $reaction-row-button-selected-border-color: $accent-color; $tooltip-timeline-bg-color: $tagpanel-bg-color; $tooltip-timeline-fg-color: #ffffff; +$interactive-tooltip-bg-color: #27303a; +$interactive-tooltip-fg-color: #ffffff; + // ***** Mixins! ***** @define-mixin mx_DialogButton { From 80d73d84308411638681da11342c6c3d88c6f3e6 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 21 Jun 2019 13:48:20 +0100 Subject: [PATCH 05/70] Add optional rounded chevron for tooltip We'd like to have a rounded point on the chevron for an extra level of polish. This implements that look for browsers that support `clip-path`. Part of https://github.com/vector-im/riot-web/issues/9716 --- .../views/elements/_InteractiveTooltip.scss | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/res/css/views/elements/_InteractiveTooltip.scss b/res/css/views/elements/_InteractiveTooltip.scss index a949941dd8..a3f5b6edc2 100644 --- a/res/css/views/elements/_InteractiveTooltip.scss +++ b/res/css/views/elements/_InteractiveTooltip.scss @@ -55,6 +55,21 @@ limitations under the License. border-right: 8px solid transparent; } +// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path +// by Sebastiano Guerriero (@guerriero_se) +@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) { + .mx_InteractiveTooltip_chevron_top { + height: 16px; + width: 16px; + background-color: inherit; + border: none; + clip-path: polygon(0% 0%, 100% 100%, 0% 100%); + transform: rotate(135deg); + border-radius: 0 0 0 3px; + top: calc(-8px / 1.414); // sqrt(2) because of rotation + } +} + .mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_bottom { bottom: 10px; // 8px chevron + 2px spacing } @@ -69,3 +84,18 @@ limitations under the License. border-top: 8px solid $interactive-tooltip-bg-color; border-right: 8px solid transparent; } + +// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path +// by Sebastiano Guerriero (@guerriero_se) +@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) { + .mx_InteractiveTooltip_chevron_bottom { + height: 16px; + width: 16px; + background-color: inherit; + border: none; + clip-path: polygon(0% 0%, 100% 100%, 0% 100%); + transform: rotate(-45deg); + border-radius: 0 0 0 3px; + bottom: calc(-8px / 1.414); // sqrt(2) because of rotation + } +} From 5fa3f70fb4ceaad13dca05def150f849efa605c7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 21 Jun 2019 18:32:15 +0200 Subject: [PATCH 06/70] feature flag for displaying edits as well --- src/components/structures/MessagePanel.js | 3 ++- src/components/views/rooms/EventTile.js | 2 +- src/shouldHideEvent.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 8352872a2d..6713d41574 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -576,6 +576,7 @@ module.exports = React.createClass({ const scrollToken = mxEv.status ? undefined : eventId; const readReceipts = this._readReceiptsByEvent[eventId]; + const editingEnabled = SettingsStore.isFeatureEnabled("feature_message_editing"); ret.push(
  • Date: Sun, 23 Jun 2019 22:39:04 +0100 Subject: [PATCH 07/70] When joining from room directory, use auto_join Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomDirectory.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 5342276e63..54d3235200 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -373,7 +373,10 @@ module.exports = React.createClass({ showRoom: function(room, room_alias) { this.props.onFinished(); - const payload = {action: 'view_room'}; + const payload = { + action: 'view_room', + auto_join: true, + }; if (room) { // Don't let the user view a room they won't be able to either // peek or join: fail earlier so they don't have to click back From 8394e162ad7d1b8b5b253b81401bce2e70518a47 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 24 Jun 2019 16:53:31 +0200 Subject: [PATCH 08/70] cache setting where it's easy --- src/components/structures/MessagePanel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 6713d41574..4238e22bd9 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -108,6 +108,7 @@ module.exports = React.createClass({ }, componentWillMount: function() { + this._editingEnabled = SettingsStore.isFeatureEnabled("feature_message_editing"); // the event after which we put a visible unread marker on the last // render cycle; null if readMarkerVisible was false or the RM was // suppressed (eg because it was at the end of the timeline) @@ -576,7 +577,6 @@ module.exports = React.createClass({ const scrollToken = mxEv.status ? undefined : eventId; const readReceipts = this._readReceiptsByEvent[eventId]; - const editingEnabled = SettingsStore.isFeatureEnabled("feature_message_editing"); ret.push(
  • Date: Mon, 24 Jun 2019 16:53:42 +0200 Subject: [PATCH 09/70] adjust comment --- src/shouldHideEvent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js index d82647dbdf..7a98c0dba6 100644 --- a/src/shouldHideEvent.js +++ b/src/shouldHideEvent.js @@ -46,7 +46,7 @@ export default function shouldHideEvent(ev) { // Hide redacted events if (ev.isRedacted() && !isEnabled('showRedactions')) return true; - // Hide replacement events since they update the original tile + // Hide replacement events since they update the original tile (if enabled) if (ev.isRelation("m.replace") && SettingsStore.isFeatureEnabled("feature_message_editing")) return true; const eventDiff = memberEventDiff(ev); From 72bfc3b5ea2c5b79a3b454c5cb3418a51a1b97ff Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 24 Jun 2019 17:03:27 +0100 Subject: [PATCH 10/70] Improve API and interactivity of new tooltip This reworks the API the `InteractiveTooltip` component so that it's more natural to use just like other React components. You can now supply the target component as a child and the tooltip content as a prop. In addition, this tweaks the interactivity to keep the tooltip on screen until you move the mouse away from the tooltip and its target. Part of https://github.com/vector-im/riot-web/issues/9753 Part of https://github.com/vector-im/riot-web/issues/9716 --- .../views/elements/InteractiveTooltip.js | 151 ++++++++++++------ 1 file changed, 102 insertions(+), 49 deletions(-) diff --git a/src/components/views/elements/InteractiveTooltip.js b/src/components/views/elements/InteractiveTooltip.js index b3d0b32fa7..42b3561135 100644 --- a/src/components/views/elements/InteractiveTooltip.js +++ b/src/components/views/elements/InteractiveTooltip.js @@ -33,25 +33,30 @@ function getOrCreateContainer() { return container; } +function isInRect(x, y, rect, buffer = 10) { + const { top, right, bottom, left } = rect; + + if (x < (left - buffer) || x > (right + buffer)) { + return false; + } + + if (y < (top - buffer) || y > (bottom + buffer)) { + return false; + } + + return true; +} + /* - * This style of tooltip takes a `target` element's rect and centers the tooltip - * along one edge of the target. + * This style of tooltip takes a "target" element as its child and centers the + * tooltip along one edge of the target. */ export default class InteractiveTooltip extends React.Component { propTypes: { - // A DOMRect from the target element - targetRect: PropTypes.object.isRequired, - // Function to be called on menu close - onFinished: PropTypes.func, - // If true, insert an invisible screen-sized element behind the - // menu that when clicked will close it. - hasBackground: PropTypes.bool, - // The component to render as the context menu - elementClass: PropTypes.element.isRequired, - // on resize callback - windowResize: PropTypes.func, - // method to close menu - closeTooltip: PropTypes.func, + // Content to show in the tooltip + content: PropTypes.node.isRequired, + // Function to call when visibility of the tooltip changes + onVisibilityChange: PropTypes.func, }; constructor() { @@ -59,9 +64,20 @@ export default class InteractiveTooltip extends React.Component { this.state = { contentRect: null, + visible: false, }; } + componentDidUpdate() { + // Whenever this passthrough component updates, also render the tooltip + // in a separate DOM tree. This allows the tooltip content to participate + // the normal React rendering cycle: when this component re-renders, the + // tooltip content re-renders. + // Once we upgrade to React 16, this could be done a bit more naturally + // using the portals feature instead. + this.renderTooltip(); + } + collectContentRect = (element) => { // We don't need to clean up when unmounting, so ignore if (!element) return; @@ -71,9 +87,55 @@ export default class InteractiveTooltip extends React.Component { }); } - render() { - const props = this.props; - const { targetRect } = props; + collectTarget = (element) => { + this.target = element; + } + + onBackgroundClick = (ev) => { + this.hideTooltip(); + } + + onBackgroundMouseMove = (ev) => { + const { clientX: x, clientY: y } = ev; + const { contentRect } = this.state; + const targetRect = this.target.getBoundingClientRect(); + + if (!isInRect(x, y, contentRect) && !isInRect(x, y, targetRect)) { + this.hideTooltip(); + return; + } + } + + onTargetMouseOver = (ev) => { + this.showTooltip(); + } + + showTooltip() { + this.setState({ + visible: true, + }); + if (this.props.onVisibilityChange) { + this.props.onVisibilityChange(true); + } + } + + hideTooltip() { + this.setState({ + visible: false, + }); + if (this.props.onVisibilityChange) { + this.props.onVisibilityChange(false); + } + } + + renderTooltip() { + const { visible } = this.state; + if (!visible) { + ReactDOM.unmountComponentAtNode(getOrCreateContainer()); + return null; + } + + const targetRect = this.target.getBoundingClientRect(); // The window X and Y offsets are to adjust position when zoomed in to page const targetLeft = targetRect.left + window.pageXOffset; @@ -108,38 +170,29 @@ export default class InteractiveTooltip extends React.Component { menuStyle.left = `-${this.state.contentRect.width / 2}px`; } - const ElementClass = props.elementClass; - - return
    -
    - { chevron } - + const tooltip =
    +
    +
    + {chevron} + {this.props.content}
    - { props.hasBackground &&
    }
    ; + + ReactDOM.render(tooltip, getOrCreateContainer()); + } + + render() { + // We use `cloneElement` here to append some props to the child content + // without using a wrapper element which could disrupt layout. + return React.cloneElement(this.props.children, { + ref: this.collectTarget, + onMouseOver: this.onTargetMouseOver, + }); } } - -export function createTooltip(ElementClass, props, hasBackground=true) { - const closeTooltip = function(...args) { - ReactDOM.unmountComponentAtNode(getOrCreateContainer()); - - if (props && props.onFinished) { - props.onFinished.apply(null, args); - } - }; - - // We only reference closeTooltip once per call to createTooltip - const menu = ; - - ReactDOM.render(menu, getOrCreateContainer()); - - return {close: closeTooltip}; -} From 7391796eabb8037e91494ea87fb7d34f6338e04e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Jun 2019 10:18:47 +0100 Subject: [PATCH 11/70] Only autojoin using the search box enter/join btn --- src/components/structures/RoomDirectory.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 54d3235200..a98afdce2a 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -333,7 +333,7 @@ module.exports = React.createClass({ if (alias.indexOf(':') == -1) { alias = alias + ':' + this.state.roomServer; } - this.showRoomAlias(alias); + this.showRoomAlias(alias, true); } else { // This is a 3rd party protocol. Let's see if we can join it const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId); @@ -349,7 +349,7 @@ module.exports = React.createClass({ } MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => { if (resp.length > 0 && resp[0].alias) { - this.showRoomAlias(resp[0].alias); + this.showRoomAlias(resp[0].alias, true); } else { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Room not found', '', ErrorDialog, { @@ -367,15 +367,15 @@ module.exports = React.createClass({ } }, - showRoomAlias: function(alias) { - this.showRoom(null, alias); + showRoomAlias: function(alias, autoJoin=false) { + this.showRoom(null, alias, autoJoin); }, - showRoom: function(room, room_alias) { + showRoom: function(room, room_alias, autoJoin=false) { this.props.onFinished(); const payload = { action: 'view_room', - auto_join: true, + auto_join: autoJoin, }; if (room) { // Don't let the user view a room they won't be able to either From 67130cb45f0bbf2f54fa09f6e58446c96d445100 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 25 Jun 2019 11:57:55 +0100 Subject: [PATCH 12/70] Condense isInRect --- src/components/views/elements/InteractiveTooltip.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/views/elements/InteractiveTooltip.js b/src/components/views/elements/InteractiveTooltip.js index 42b3561135..b56fd62556 100644 --- a/src/components/views/elements/InteractiveTooltip.js +++ b/src/components/views/elements/InteractiveTooltip.js @@ -35,16 +35,8 @@ function getOrCreateContainer() { function isInRect(x, y, rect, buffer = 10) { const { top, right, bottom, left } = rect; - - if (x < (left - buffer) || x > (right + buffer)) { - return false; - } - - if (y < (top - buffer) || y > (bottom + buffer)) { - return false; - } - - return true; + return x >= (left - buffer) && x <= (right + buffer) + && y >= (top - buffer) && y <= (bottom + buffer); } /* From 8926992feb6549935c8f7a0d12a533df5d01962c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 19 Jun 2019 14:45:01 +0100 Subject: [PATCH 13/70] Add react button to action bar This adds a (temporarily non-functional) react button to the action bar. Part of https://github.com/vector-im/riot-web/issues/9753 --- res/css/views/messages/_MessageActionBar.scss | 4 ++++ res/img/react.svg | 10 ++++++++++ src/components/views/messages/MessageActionBar.js | 13 +++++++++++++ src/i18n/strings/en_EN.json | 1 + 4 files changed, 28 insertions(+) create mode 100644 res/img/react.svg diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 685c2bb018..7ac0e95e81 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -67,6 +67,10 @@ limitations under the License. background-color: $message-action-bar-fg-color; } +.mx_MessageActionBar_reactButton::after { + mask-image: url('$(res)/img/react.svg'); +} + .mx_MessageActionBar_replyButton::after { mask-image: url('$(res)/img/reply.svg'); } diff --git a/res/img/react.svg b/res/img/react.svg new file mode 100644 index 0000000000..dd23c41c2c --- /dev/null +++ b/res/img/react.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 80f0ba538c..3fb82febce 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -131,6 +131,16 @@ export default class MessageActionBar extends React.PureComponent { return SettingsStore.isFeatureEnabled("feature_message_editing"); } + renderReactButton() { + if (!this.isReactionsEnabled()) { + return null; + } + + return ; + } + renderAgreeDimension() { if (!this.isReactionsEnabled()) { return null; @@ -160,12 +170,14 @@ export default class MessageActionBar extends React.PureComponent { } render() { + let reactButton; let agreeDimensionReactionButtons; let likeDimensionReactionButtons; let replyButton; let editButton; if (isContentActionable(this.props.mxEvent)) { + reactButton = this.renderReactButton(); agreeDimensionReactionButtons = this.renderAgreeDimension(); likeDimensionReactionButtons = this.renderLikeDimension(); replyButton = + {reactButton} {agreeDimensionReactionButtons} {likeDimensionReactionButtons} {replyButton} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3edaaf6241..9564c45172 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -930,6 +930,7 @@ "Error decrypting audio": "Error decrypting audio", "Agree or Disagree": "Agree or Disagree", "Like or Dislike": "Like or Dislike", + "React": "React", "Reply": "Reply", "Edit": "Edit", "Options": "Options", From 91f707341a6d54ad7e1bfea85950bc1ccf5e365d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 19 Jun 2019 15:08:49 +0100 Subject: [PATCH 14/70] Tweak handler name to match others --- src/components/views/messages/MessageActionBar.js | 4 ++-- src/components/views/rooms/EventTile.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 3fb82febce..aaedf46200 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -57,7 +57,7 @@ export default class MessageActionBar extends React.PureComponent { this.props.onFocusChange(focused); } - onCryptoClicked = () => { + onCryptoClick = () => { const event = this.props.mxEvent; Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', import('../../../async-components/views/dialogs/EncryptedEventDialog'), @@ -89,7 +89,7 @@ export default class MessageActionBar extends React.PureComponent { let e2eInfoCallback = null; if (this.props.mxEvent.isEncrypted()) { - e2eInfoCallback = () => this.onCryptoClicked(); + e2eInfoCallback = () => this.onCryptoClick(); } const menuOptions = { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index a997e7a3de..988bf7eb3c 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -404,7 +404,7 @@ module.exports = withMatrixClient(React.createClass({ }); }, - onCryptoClicked: function(e) { + onCryptoClick: function(e) { const event = this.props.mxEvent; Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', @@ -440,7 +440,7 @@ module.exports = withMatrixClient(React.createClass({ _renderE2EPadlock: function() { const ev = this.props.mxEvent; - const props = {onClick: this.onCryptoClicked}; + const props = {onClick: this.onCryptoClick}; // event could not be decrypted if (ev.getContent().msgtype === 'm.bad.encrypted') { From fd2723585fb22d08f202652d4971c10a85106480 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 25 Jun 2019 15:10:41 +0100 Subject: [PATCH 15/70] Add quick reaction buttons in tooltip This adds the set of quick reactions as buttons in a new tooltip accessed via the react action in the message action bar. Part of https://github.com/vector-im/riot-web/issues/9753 --- res/css/_components.scss | 1 + .../messages/_ReactionTooltipButton.scss | 24 +++ .../views/messages/MessageActionBar.js | 9 +- .../views/messages/ReactMessageAction.js | 97 +++++++++++ .../views/messages/ReactionTooltipButton.js | 67 ++++++++ .../views/messages/ReactionsQuickTooltip.js | 151 ++++++++++++++++++ src/i18n/strings/en_EN.json | 7 +- 7 files changed, 353 insertions(+), 3 deletions(-) create mode 100644 res/css/views/messages/_ReactionTooltipButton.scss create mode 100644 src/components/views/messages/ReactMessageAction.js create mode 100644 src/components/views/messages/ReactionTooltipButton.js create mode 100644 src/components/views/messages/ReactionsQuickTooltip.js diff --git a/res/css/_components.scss b/res/css/_components.scss index fa388c4e6a..fac149380e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -118,6 +118,7 @@ @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_ReactionDimension.scss"; +@import "./views/messages/_ReactionTooltipButton.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; @import "./views/messages/_ReactionsRowButtonTooltip.scss"; diff --git a/res/css/views/messages/_ReactionTooltipButton.scss b/res/css/views/messages/_ReactionTooltipButton.scss new file mode 100644 index 0000000000..7cb754a8fd --- /dev/null +++ b/res/css/views/messages/_ReactionTooltipButton.scss @@ -0,0 +1,24 @@ +/* +Copyright 2019 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. +*/ + +.mx_ReactionTooltipButton { + font-size: 16px; + user-select: none; +} + +.mx_ReactionTooltipButton_selected { + opacity: 0.4; +} diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index aaedf46200..410c0762b3 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -136,8 +136,13 @@ export default class MessageActionBar extends React.PureComponent { return null; } - return ; } diff --git a/src/components/views/messages/ReactMessageAction.js b/src/components/views/messages/ReactMessageAction.js new file mode 100644 index 0000000000..804a154c9c --- /dev/null +++ b/src/components/views/messages/ReactMessageAction.js @@ -0,0 +1,97 @@ +/* +Copyright 2019 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 PropTypes from 'prop-types'; + +import sdk from '../../../index'; + +export default class ReactMessageAction extends React.PureComponent { + static propTypes = { + mxEvent: PropTypes.object.isRequired, + // The Relations model from the JS SDK for reactions to `mxEvent` + reactions: PropTypes.object, + onFocusChange: PropTypes.func, + } + + constructor(props) { + super(props); + + if (props.reactions) { + props.reactions.on("Relations.add", this.onReactionsChange); + props.reactions.on("Relations.remove", this.onReactionsChange); + props.reactions.on("Relations.redaction", this.onReactionsChange); + } + } + + onFocusChange = (focused) => { + if (!this.props.onFocusChange) { + return; + } + this.props.onFocusChange(focused); + } + + componentDidUpdate(prevProps) { + if (prevProps.reactions !== this.props.reactions) { + this.props.reactions.on("Relations.add", this.onReactionsChange); + this.props.reactions.on("Relations.remove", this.onReactionsChange); + this.props.reactions.on("Relations.redaction", this.onReactionsChange); + this.onReactionsChange(); + } + } + + componentWillUnmount() { + if (this.props.reactions) { + this.props.reactions.removeListener( + "Relations.add", + this.onReactionsChange, + ); + this.props.reactions.removeListener( + "Relations.remove", + this.onReactionsChange, + ); + this.props.reactions.removeListener( + "Relations.redaction", + this.onReactionsChange, + ); + } + } + + onReactionsChange = () => { + // Force a re-render of the tooltip because a change in the reactions + // set means the event tile's layout may have changed and possibly + // altered the location where the tooltip should be shown. + this.forceUpdate(); + } + + render() { + const ReactionsQuickTooltip = sdk.getComponent('messages.ReactionsQuickTooltip'); + const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip'); + const { mxEvent, reactions } = this.props; + + const content = ; + + return + + ; + } +} diff --git a/src/components/views/messages/ReactionTooltipButton.js b/src/components/views/messages/ReactionTooltipButton.js new file mode 100644 index 0000000000..a3f0256758 --- /dev/null +++ b/src/components/views/messages/ReactionTooltipButton.js @@ -0,0 +1,67 @@ +/* +Copyright 2019 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 PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import MatrixClientPeg from '../../../MatrixClientPeg'; + +export default class ReactionTooltipButton extends React.PureComponent { + static propTypes = { + mxEvent: PropTypes.object.isRequired, + // The reaction content / key / emoji + content: PropTypes.string.isRequired, + title: PropTypes.string, + // A possible Matrix event if the current user has voted for this type + myReactionEvent: PropTypes.object, + }; + + onClick = (ev) => { + const { mxEvent, myReactionEvent, content } = this.props; + if (myReactionEvent) { + MatrixClientPeg.get().redactEvent( + mxEvent.getRoomId(), + myReactionEvent.getId(), + ); + } else { + MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", { + "m.relates_to": { + "rel_type": "m.annotation", + "event_id": mxEvent.getId(), + "key": content, + }, + }); + } + } + + render() { + const { content, myReactionEvent } = this.props; + + const classes = classNames({ + mx_ReactionTooltipButton: true, + mx_ReactionTooltipButton_selected: !!myReactionEvent, + }); + + return + {content} + ; + } +} diff --git a/src/components/views/messages/ReactionsQuickTooltip.js b/src/components/views/messages/ReactionsQuickTooltip.js new file mode 100644 index 0000000000..74cfe82baf --- /dev/null +++ b/src/components/views/messages/ReactionsQuickTooltip.js @@ -0,0 +1,151 @@ +/* +Copyright 2019 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 PropTypes from 'prop-types'; + +import { _t } from '../../../languageHandler'; +import sdk from '../../../index'; +import MatrixClientPeg from '../../../MatrixClientPeg'; + +export default class ReactionsQuickTooltip extends React.PureComponent { + static propTypes = { + mxEvent: PropTypes.object.isRequired, + // The Relations model from the JS SDK for reactions to `mxEvent` + reactions: PropTypes.object, + }; + + constructor(props) { + super(props); + + if (props.reactions) { + props.reactions.on("Relations.add", this.onReactionsChange); + props.reactions.on("Relations.remove", this.onReactionsChange); + props.reactions.on("Relations.redaction", this.onReactionsChange); + } + + this.state = { + myReactions: this.getMyReactions(), + }; + } + + componentDidUpdate(prevProps) { + if (prevProps.reactions !== this.props.reactions) { + this.props.reactions.on("Relations.add", this.onReactionsChange); + this.props.reactions.on("Relations.remove", this.onReactionsChange); + this.props.reactions.on("Relations.redaction", this.onReactionsChange); + this.onReactionsChange(); + } + } + + componentWillUnmount() { + if (this.props.reactions) { + this.props.reactions.removeListener( + "Relations.add", + this.onReactionsChange, + ); + this.props.reactions.removeListener( + "Relations.remove", + this.onReactionsChange, + ); + this.props.reactions.removeListener( + "Relations.redaction", + this.onReactionsChange, + ); + } + } + + onReactionsChange = () => { + this.setState({ + myReactions: this.getMyReactions(), + }); + } + + getMyReactions() { + const reactions = this.props.reactions; + if (!reactions) { + return null; + } + const userId = MatrixClientPeg.get().getUserId(); + const myReactions = reactions.getAnnotationsBySender()[userId]; + if (!myReactions) { + return null; + } + return [...myReactions.values()]; + } + + render() { + const { mxEvent } = this.props; + const { myReactions } = this.state; + const ReactionTooltipButton = sdk.getComponent('messages.ReactionTooltipButton'); + + const items = [ + { + content: "๐Ÿ‘", + title: _t("Agree"), + }, + { + content: "๐Ÿ‘Ž", + title: _t("Disagree"), + }, + { + content: "๐Ÿ˜„", + title: _t("Happy"), + }, + { + content: "๐ŸŽ‰", + title: _t("Party Popper"), + }, + { + content: "๐Ÿ˜•", + title: _t("Confused"), + }, + { + content: "โค๏ธ", + title: _t("Heart"), + }, + { + content: "๐Ÿš€", + title: _t("Rocket"), + }, + { + content: "๐Ÿ‘€", + title: _t("Eyes"), + }, + ]; + + const buttons = items.map(({ content, title }) => { + const myReactionEvent = myReactions && myReactions.find(mxEvent => { + if (mxEvent.isRedacted()) { + return false; + } + return mxEvent.getRelation().key === content; + }); + + return ; + }); + + return
    + {buttons} +
    ; + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9564c45172..02c0658bcb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -930,7 +930,6 @@ "Error decrypting audio": "Error decrypting audio", "Agree or Disagree": "Agree or Disagree", "Like or Dislike": "Like or Dislike", - "React": "React", "Reply": "Reply", "Edit": "Edit", "Options": "Options", @@ -941,6 +940,12 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Error decrypting video": "Error decrypting video", + "Agree": "Agree", + "Disagree": "Disagree", + "Happy": "Happy", + "Party Popper": "Party Popper", + "Confused": "Confused", + "Eyes": "Eyes", "reacted with %(shortName)s": "reacted with %(shortName)s", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", From c6f2bb4ba7419b6788a35ad0faf25932eb888a67 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 25 Jun 2019 15:40:31 +0100 Subject: [PATCH 16/70] Arrange buttons in a grid --- res/css/_components.scss | 1 + .../views/messages/_ReactionQuickTooltip.scss | 20 +++++++++++++++++++ .../messages/_ReactionTooltipButton.scss | 1 + 3 files changed, 22 insertions(+) create mode 100644 res/css/views/messages/_ReactionQuickTooltip.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index fac149380e..8b86579bf3 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -118,6 +118,7 @@ @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_ReactionDimension.scss"; +@import "./views/messages/_ReactionQuickTooltip.scss"; @import "./views/messages/_ReactionTooltipButton.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; diff --git a/res/css/views/messages/_ReactionQuickTooltip.scss b/res/css/views/messages/_ReactionQuickTooltip.scss new file mode 100644 index 0000000000..5fb7987b6f --- /dev/null +++ b/res/css/views/messages/_ReactionQuickTooltip.scss @@ -0,0 +1,20 @@ +/* +Copyright 2019 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. +*/ + +.mx_ReactionsQuickTooltip { + display: grid; + grid-template-columns: repeat(4, auto); +} diff --git a/res/css/views/messages/_ReactionTooltipButton.scss b/res/css/views/messages/_ReactionTooltipButton.scss index 7cb754a8fd..36b8abd2a4 100644 --- a/res/css/views/messages/_ReactionTooltipButton.scss +++ b/res/css/views/messages/_ReactionTooltipButton.scss @@ -16,6 +16,7 @@ limitations under the License. .mx_ReactionTooltipButton { font-size: 16px; + padding: 6px; user-select: none; } From 088bbbfb91e02f13ded6f9ccad9beedef26cfa64 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 25 Jun 2019 15:54:58 +0100 Subject: [PATCH 17/70] Scale up reaction buttons on hover --- res/css/views/messages/_ReactionTooltipButton.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/res/css/views/messages/_ReactionTooltipButton.scss b/res/css/views/messages/_ReactionTooltipButton.scss index 36b8abd2a4..bf1c25e126 100644 --- a/res/css/views/messages/_ReactionTooltipButton.scss +++ b/res/css/views/messages/_ReactionTooltipButton.scss @@ -18,6 +18,11 @@ limitations under the License. font-size: 16px; padding: 6px; user-select: none; + transition: transform 0.25s; + + &:hover { + transform: scale(1.2); + } } .mx_ReactionTooltipButton_selected { From c1821fabd394747c7f273119b5e3ec1ef3e65a97 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 25 Jun 2019 16:17:02 +0100 Subject: [PATCH 18/70] Remove toggling reaction dimensions This removes the v1 Reactions UX which only allowed you to choose only one emoji out of each pair. It is replaced by a different UX inside a tooltip and without these constraints. Part of https://github.com/vector-im/riot-web/issues/9753 --- res/css/_components.scss | 1 - .../views/messages/_ReactionDimension.scss | 25 --- .../views/messages/MessageActionBar.js | 34 ---- .../views/messages/ReactionDimension.js | 176 ------------------ src/i18n/strings/en_EN.json | 2 - 5 files changed, 238 deletions(-) delete mode 100644 res/css/views/messages/_ReactionDimension.scss delete mode 100644 src/components/views/messages/ReactionDimension.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 8b86579bf3..4986ca837f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -117,7 +117,6 @@ @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; -@import "./views/messages/_ReactionDimension.scss"; @import "./views/messages/_ReactionQuickTooltip.scss"; @import "./views/messages/_ReactionTooltipButton.scss"; @import "./views/messages/_ReactionsRow.scss"; diff --git a/res/css/views/messages/_ReactionDimension.scss b/res/css/views/messages/_ReactionDimension.scss deleted file mode 100644 index 9a891d05cf..0000000000 --- a/res/css/views/messages/_ReactionDimension.scss +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2019 New Vector Ltd - -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. -*/ - -.mx_ReactionDimension { - width: 42px; - display: flex; - justify-content: space-evenly; -} - -.mx_ReactionDimension_disabled { - opacity: 0.4; -} diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 410c0762b3..e7843c1505 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -146,45 +146,13 @@ export default class MessageActionBar extends React.PureComponent { />; } - renderAgreeDimension() { - if (!this.isReactionsEnabled()) { - return null; - } - - const ReactionDimension = sdk.getComponent('messages.ReactionDimension'); - return ; - } - - renderLikeDimension() { - if (!this.isReactionsEnabled()) { - return null; - } - - const ReactionDimension = sdk.getComponent('messages.ReactionDimension'); - return ; - } - render() { let reactButton; - let agreeDimensionReactionButtons; - let likeDimensionReactionButtons; let replyButton; let editButton; if (isContentActionable(this.props.mxEvent)) { reactButton = this.renderReactButton(); - agreeDimensionReactionButtons = this.renderAgreeDimension(); - likeDimensionReactionButtons = this.renderLikeDimension(); replyButton = {reactButton} - {agreeDimensionReactionButtons} - {likeDimensionReactionButtons} {replyButton} {editButton} { - this.setState(this.getSelection()); - } - - getSelection() { - const myReactions = this.getMyReactions(); - if (!myReactions) { - return { - selectedOption: null, - selectedReactionEvent: null, - }; - } - const { options } = this.props; - let selectedOption = null; - let selectedReactionEvent = null; - for (const option of options) { - const reactionForOption = myReactions.find(mxEvent => { - if (mxEvent.isRedacted()) { - return false; - } - return mxEvent.getRelation().key === option; - }); - if (!reactionForOption) { - continue; - } - if (selectedOption) { - // If there are multiple selected values (only expected to occur via - // non-Riot clients), then act as if none are selected. - return { - selectedOption: null, - selectedReactionEvent: null, - }; - } - selectedOption = option; - selectedReactionEvent = reactionForOption; - } - return { selectedOption, selectedReactionEvent }; - } - - getMyReactions() { - const reactions = this.props.reactions; - if (!reactions) { - return null; - } - const userId = MatrixClientPeg.get().getUserId(); - const myReactions = reactions.getAnnotationsBySender()[userId]; - if (!myReactions) { - return null; - } - return [...myReactions.values()]; - } - - onOptionClick = (ev) => { - const { key } = ev.target.dataset; - this.toggleDimension(key); - } - - toggleDimension(key) { - const { selectedOption, selectedReactionEvent } = this.state; - const newSelectedOption = selectedOption !== key ? key : null; - this.setState({ - selectedOption: newSelectedOption, - }); - if (selectedReactionEvent) { - MatrixClientPeg.get().redactEvent( - this.props.mxEvent.getRoomId(), - selectedReactionEvent.getId(), - ); - } - if (newSelectedOption) { - MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), "m.reaction", { - "m.relates_to": { - "rel_type": "m.annotation", - "event_id": this.props.mxEvent.getId(), - "key": newSelectedOption, - }, - }); - } - } - - render() { - const { selectedOption } = this.state; - const { options } = this.props; - - const items = options.map(option => { - const disabled = selectedOption && selectedOption !== option; - const classes = classNames({ - mx_ReactionDimension_disabled: disabled, - }); - return - {option} - ; - }); - - return - {items} - ; - } -} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 02c0658bcb..010ad29da0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -928,8 +928,6 @@ "Today": "Today", "Yesterday": "Yesterday", "Error decrypting audio": "Error decrypting audio", - "Agree or Disagree": "Agree or Disagree", - "Like or Dislike": "Like or Dislike", "Reply": "Reply", "Edit": "Edit", "Options": "Options", From 93384f91f585d898fae9fd5591411128e2d4bac7 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 25 Jun 2019 18:15:03 +0100 Subject: [PATCH 19/70] Show reaction title and shortcode on hover This shows the title and shortcode for the hovered reaction at the bottom of the tooltip. If nothing is hovered, a blank space is shown for now, but will eventually become a link to a full emoji picker in future work. Part of https://github.com/vector-im/riot-web/issues/9753 --- .../views/messages/_ReactionQuickTooltip.scss | 11 +++- .../views/messages/ReactionTooltipButton.js | 1 + .../views/messages/ReactionsQuickTooltip.js | 60 ++++++++++++++++--- 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/res/css/views/messages/_ReactionQuickTooltip.scss b/res/css/views/messages/_ReactionQuickTooltip.scss index 5fb7987b6f..7b1611483b 100644 --- a/res/css/views/messages/_ReactionQuickTooltip.scss +++ b/res/css/views/messages/_ReactionQuickTooltip.scss @@ -14,7 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ReactionsQuickTooltip { +.mx_ReactionsQuickTooltip_buttons { display: grid; grid-template-columns: repeat(4, auto); } + +.mx_ReactionsQuickTooltip_label { + text-align: center; +} + +.mx_ReactionsQuickTooltip_shortcode { + padding-left: 6px; + opacity: 0.7; +} diff --git a/src/components/views/messages/ReactionTooltipButton.js b/src/components/views/messages/ReactionTooltipButton.js index a3f0256758..e09b9ade69 100644 --- a/src/components/views/messages/ReactionTooltipButton.js +++ b/src/components/views/messages/ReactionTooltipButton.js @@ -57,6 +57,7 @@ export default class ReactionTooltipButton extends React.PureComponent { }); return { + const { key } = ev.target.dataset; + const item = this.items.find(({ content }) => content === key); + this.setState({ + hoveredItem: item, + }); + } - const items = [ + onMouseOut = (ev) => { + this.setState({ + hoveredItem: null, + }); + } + + get items() { + return [ { content: "๐Ÿ‘", title: _t("Agree"), @@ -126,8 +138,14 @@ export default class ReactionsQuickTooltip extends React.PureComponent { title: _t("Eyes"), }, ]; + } - const buttons = items.map(({ content, title }) => { + render() { + const { mxEvent } = this.props; + const { myReactions, hoveredItem } = this.state; + const ReactionTooltipButton = sdk.getComponent('messages.ReactionTooltipButton'); + + const buttons = this.items.map(({ content, title }) => { const myReactionEvent = myReactions && myReactions.find(mxEvent => { if (mxEvent.isRedacted()) { return false; @@ -144,8 +162,34 @@ export default class ReactionsQuickTooltip extends React.PureComponent { />; }); - return
    - {buttons} + let label = "ย "; // non-breaking space to keep layout the same when empty + if (hoveredItem) { + const { content, title } = hoveredItem; + + let shortcodeLabel; + const shortcode = unicodeToShortcode(content); + if (shortcode) { + shortcodeLabel = + {shortcode} + ; + } + + label =
    + + {title} + + {shortcodeLabel} +
    ; + } + + return
    +
    + {buttons} +
    + {label}
    ; } } From b6242dbad2ef370fed9ba7e02562a29ac21abda9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Jun 2019 13:54:48 -0600 Subject: [PATCH 20/70] Fix spelling --- src/components/views/dialogs/RoomUpgradeDialog.js | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.js index ce8b93f693..45c242fea5 100644 --- a/src/components/views/dialogs/RoomUpgradeDialog.js +++ b/src/components/views/dialogs/RoomUpgradeDialog.js @@ -92,7 +92,7 @@ export default React.createClass({

    {_t( "Upgrading this room requires closing down the current " + - "instance of the room and creating a new room it its place. " + + "instance of the room and creating a new room in its place. " + "To give room members the best possible experience, we will:", )}

    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3edaaf6241..76447831aa 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1204,7 +1204,7 @@ "The room upgrade could not be completed": "The room upgrade could not be completed", "Upgrade this room to version %(version)s": "Upgrade this room to version %(version)s", "Upgrade Room Version": "Upgrade Room Version", - "Upgrading this room requires closing down the current instance of the room and creating a new room it its place. To give room members the best possible experience, we will:": "Upgrading this room requires closing down the current instance of the room and creating a new room it its place. To give room members the best possible experience, we will:", + "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:", "Create a new room with the same name, description and avatar": "Create a new room with the same name, description and avatar", "Update any local room aliases to point to the new room": "Update any local room aliases to point to the new room", "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room", From 0701d89bbeb7f6c6cfee9e583ed7cb16cfc05e06 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Jun 2019 15:40:22 -0600 Subject: [PATCH 21/70] Fix upgrade warning being chopped off The flex box was behaving a bit strange, so we just wrap the content and change `height: 235px` to `max-height: 235px` to get scrollbars. --- .../views/rooms/_RoomUpgradeWarningBar.scss | 17 +++++++---- .../views/rooms/RoomUpgradeWarningBar.js | 30 ++++++++++--------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/res/css/views/rooms/_RoomUpgradeWarningBar.scss b/res/css/views/rooms/_RoomUpgradeWarningBar.scss index fe81d3801a..1c477cedfe 100644 --- a/res/css/views/rooms/_RoomUpgradeWarningBar.scss +++ b/res/css/views/rooms/_RoomUpgradeWarningBar.scss @@ -15,17 +15,22 @@ limitations under the License. */ .mx_RoomUpgradeWarningBar { + max-height: 235px; + background-color: $preview-bar-bg-color; + padding-left: 20px; + padding-right: 20px; + overflow: scroll; +} + +.mx_RoomUpgradeWarningBar_wrapped { + width: 100%; + height: 100%; + display: flex; text-align: center; - height: 235px; - background-color: $event-selected-color; align-items: center; flex-direction: column; justify-content: center; - display: flex; - background-color: $preview-bar-bg-color; -webkit-align-items: center; - padding-left: 20px; - padding-right: 20px; } .mx_RoomUpgradeWarningBar_header { diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.js index c2e2ba89d4..edde0a6865 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.js +++ b/src/components/views/rooms/RoomUpgradeWarningBar.js @@ -97,20 +97,22 @@ module.exports = React.createClass({ return (
    -
    - {_t( - "This room is running room version , which this homeserver has " + - "marked as unstable.", - {}, - { - "roomVersion": () => {this.props.room.getVersion()}, - "i": (sub) => {sub}, - }, - )} -
    - {doUpgradeWarnings} -
    - {_t("Only room administrators will see this warning")} +
    +
    + {_t( + "This room is running room version , which this homeserver has " + + "marked as unstable.", + {}, + { + "roomVersion": () => {this.props.room.getVersion()}, + "i": (sub) => {sub}, + }, + )} +
    + {doUpgradeWarnings} +
    + {_t("Only room administrators will see this warning")} +
    ); From 7b00d29ea68f80dd0ca761038d35c54060ada4c9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Jun 2019 16:08:11 -0600 Subject: [PATCH 22/70] Don't boost trackpad users in breadcrumbs Fixes https://github.com/vector-im/riot-web/issues/10005 --- src/components/structures/IndicatorScrollbar.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 03ce258d37..b11e655f0d 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -38,6 +38,8 @@ export default class IndicatorScrollbar extends React.Component { this.checkOverflow = this.checkOverflow.bind(this); this._scrollElement = null; this._autoHideScrollbar = null; + this._likelyTrackpadUser = null; + this._checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser this.state = { leftIndicatorOffset: 0, @@ -129,6 +131,16 @@ export default class IndicatorScrollbar extends React.Component { // the harshness of the scroll behaviour. Should be a value between 0 and 1. const yRetention = 1.0; + // Check for trackpad users every so often to avoid boosting their scroll. + // See https://github.com/vector-im/riot-web/issues/10005 + const now = new Date().getTime(); + if (now >= this._checkAgainForTrackpad) { + this._likelyTrackpadUser = Math.abs(e.deltaX) > 0; + this._checkAgainForTrackpad = now + (15 * 60 * 1000); // 15min + } + + const safeToBoost = !this._likelyTrackpadUser; + if (Math.abs(e.deltaX) <= xyThreshold) { // HACK: We increase the amount of scroll to counteract smooth scrolling browsers. // Smooth scrolling browsers (Firefox) use the relative area to determine the scroll @@ -140,7 +152,7 @@ export default class IndicatorScrollbar extends React.Component { const additionalScroll = e.deltaY < 0 ? -50 : 50; // noinspection JSSuspiciousNameCombination - const val = Math.abs(e.deltaY) < 25 ? (e.deltaY + additionalScroll) : e.deltaY; + const val = Math.abs(e.deltaY) < 25 && safeToBoost ? (e.deltaY + additionalScroll) : e.deltaY; this._scrollElement.scrollLeft += val * yRetention; } } From a73436e1a18467c2954dcfccae426f74c0bcf7e4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Jun 2019 20:31:20 -0600 Subject: [PATCH 23/70] Don't use oobData if there is none Fixes the "buttons don't work" problem on https://github.com/vector-im/riot-web/issues/10114 --- src/components/views/rooms/RoomPreviewBar.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index cbc44d0933..ac52f60347 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -238,9 +238,9 @@ module.exports = React.createClass({ params: { email: this.props.invitedEmail, signurl: this.props.signUrl, - room_name: this.props.oobData.room_name, - room_avatar_url: this.props.oobData.avatarUrl, - inviter_name: this.props.oobData.inviterName, + room_name: this.props.oobData ? this.props.oobData.room_name : null, + room_avatar_url: this.props.oobData ? this.props.oobData.avatarUrl : null, + inviter_name: this.props.oobData ? this.props.oobData.inviterName : null, } }; }, From ca6ddf324fb12ac72179bd10432591bc23783685 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Jun 2019 20:56:33 -0600 Subject: [PATCH 24/70] Show a loading state for slow peeks --- res/css/views/rooms/_RoomPreviewBar.scss | 10 ++++++++++ src/components/structures/RoomView.js | 1 + src/components/views/rooms/RoomPreviewBar.js | 16 +++++++++++++++- src/i18n/strings/en_EN.json | 1 + 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss index ea3b787971..6ac5546f78 100644 --- a/res/css/views/rooms/_RoomPreviewBar.scss +++ b/res/css/views/rooms/_RoomPreviewBar.scss @@ -39,6 +39,16 @@ limitations under the License. margin: 10px 10px 10px 0; flex: 0 0 auto; } + + .mx_RoomPreviewBar_footer { + font-size: 12px; + line-height: 20px; + + .mx_Spinner { + vertical-align: middle; + display: inline-block; + } + } } .mx_RoomPreviewBar_dark { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index cda3f60fce..f02c4d45c3 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1523,6 +1523,7 @@ module.exports = React.createClass({
    + + {_t("Loading room preview")} +
    + ); + } break; } case MessageCase.Kicked: { @@ -433,7 +445,6 @@ module.exports = React.createClass({ } const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const Spinner = sdk.getComponent('elements.Spinner'); let subTitleElements; if (subTitle) { @@ -484,6 +495,9 @@ module.exports = React.createClass({ { secondaryButton } { primaryButton }
    +
    + { footer } +
    ); }, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3edaaf6241..8a54a4ba53 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -835,6 +835,7 @@ "Join the conversation with an account": "Join the conversation with an account", "Sign Up": "Sign Up", "Sign In": "Sign In", + "Loading room preview": "Loading room preview", "You were kicked from %(roomName)s by %(memberName)s": "You were kicked from %(roomName)s by %(memberName)s", "Reason: %(reason)s": "Reason: %(reason)s", "Forget this room": "Forget this room", From e5c3e5988b37084d8534adc43dd8883d2c785576 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Jun 2019 20:31:20 -0600 Subject: [PATCH 25/70] Revert "Don't use oobData if there is none" This reverts commit a73436e1a18467c2954dcfccae426f74c0bcf7e4. --- src/components/views/rooms/RoomPreviewBar.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index ac52f60347..cbc44d0933 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -238,9 +238,9 @@ module.exports = React.createClass({ params: { email: this.props.invitedEmail, signurl: this.props.signUrl, - room_name: this.props.oobData ? this.props.oobData.room_name : null, - room_avatar_url: this.props.oobData ? this.props.oobData.avatarUrl : null, - inviter_name: this.props.oobData ? this.props.oobData.inviterName : null, + room_name: this.props.oobData.room_name, + room_avatar_url: this.props.oobData.avatarUrl, + inviter_name: this.props.oobData.inviterName, } }; }, From 5f242f02857c25c2fd9fe47c352781368a4d7df8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Jun 2019 20:57:07 -0600 Subject: [PATCH 26/70] Supply oobData instead of erroring --- src/components/structures/RoomView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index cda3f60fce..724450d965 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1526,6 +1526,7 @@ module.exports = React.createClass({ error={this.state.roomLoadError} loading={loading} joining={this.state.joining} + oobData={this.props.oobData} />
    ); From 2d9fbcab70a2f941de95d5aabfc3d57cd3417c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=BCrmann?= Date: Wed, 26 Jun 2019 10:49:46 +0200 Subject: [PATCH 27/70] Fix the scrollbar in the community bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently the scrollbar is always visible because the inner container is 5px bigger in height than the outer container. This is hereby fixed. Signed-off-by: Jonas Schรผrmann --- res/css/structures/_TagPanel.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index a818f52125..a01e5dd838 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -65,7 +65,7 @@ limitations under the License. align-items: center; margin-top: 5px; - height: 100%; + height: calc(100% - 5px); } .mx_TagPanel .mx_TagPanel_tagTileContainer > div { height: 40px; From ef71e6fd4fc2701d1fd462647f67b09262a8b19c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 29 May 2019 18:03:05 +0200 Subject: [PATCH 28/70] very basic & hackish edit history dialog --- res/css/_components.scss | 1 + .../dialogs/_MessageEditHistoryDialog.scss | 38 +++++++++ .../views/dialogs/MessageEditHistoryDialog.js | 80 +++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + 4 files changed, 120 insertions(+) create mode 100644 res/css/views/dialogs/_MessageEditHistoryDialog.scss create mode 100644 src/components/views/dialogs/MessageEditHistoryDialog.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 4986ca837f..d30684993d 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -61,6 +61,7 @@ @import "./views/dialogs/_EncryptedEventDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; +@import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_RestoreKeyBackupDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; diff --git a/res/css/views/dialogs/_MessageEditHistoryDialog.scss b/res/css/views/dialogs/_MessageEditHistoryDialog.scss new file mode 100644 index 0000000000..ad51f4237f --- /dev/null +++ b/res/css/views/dialogs/_MessageEditHistoryDialog.scss @@ -0,0 +1,38 @@ +/* +Copyright 2019 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. +*/ + +.mx_MessageEditHistoryDialog ul { + list-style-type: none; + + &>li.edit { + display: flex; + + &>strong { + flex: 0 0 100px; + font-weight: bold; + } + + &>p { + + } + + } + + ul, ol { + list-style-type: circle; + } +} + diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js new file mode 100644 index 0000000000..958c7457e0 --- /dev/null +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -0,0 +1,80 @@ +/* +Copyright 2019 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 PropTypes from 'prop-types'; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import { _t } from '../../../languageHandler'; +import sdk from "../../../index"; +import * as HtmlUtils from '../../../HtmlUtils'; +import {wantsDateSeparator, formatTime} from '../../../DateUtils'; + +export default class MessageEditHistoryDialog extends React.Component { + static propTypes = { + mxEvent: PropTypes.object.isRequired, + }; + + componentWillMount() { + this.setState({edits: [this.props.mxEvent], isLoading: true}); + } + + async componentDidMount() { + const roomId = this.props.mxEvent.getRoomId(); + const eventId = this.props.mxEvent.getId(); + let edits = await MatrixClientPeg.get(). + relations(roomId, eventId, "m.replace", "m.room.message"); + edits = edits.slice().reverse(); + edits.unshift(this.props.mxEvent); + this.setState({edits, isLoading: false}); + } + + _renderEdit(event) { + const timestamp = formatTime(new Date(event.getTs()), true); + const content = event.event.content["m.new_content"] || event.event.content; + return
  • {timestamp}

    {HtmlUtils.bodyToHtml(content)}

  • ; + } + + _renderEdits() { + const DateSeparator = sdk.getComponent('messages.DateSeparator'); + const nodes = []; + let lastEvent; + this.state.edits.forEach(e => { + if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) { + nodes.push(
  • ); + } + nodes.push(this._renderEdit(e)); + lastEvent = e; + }); + return nodes; + } + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + let spinner; + const edits = this._renderEdits(); + if (this.state.isLoading) { + const Spinner = sdk.getComponent("elements.Spinner"); + spinner = ; + } + return ( + +
      {edits}
    + {spinner} +
    + ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 010ad29da0..01d84324ce 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1199,6 +1199,7 @@ "Manually export keys": "Manually export keys", "You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages", "Are you sure you want to sign out?": "Are you sure you want to sign out?", + "Message edits": "Message edits", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", "Report bugs & give feedback": "Report bugs & give feedback", From 8b5f07e63d20cc72f780eadd08eec0ee954253fe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 29 May 2019 18:23:18 +0200 Subject: [PATCH 29/70] open edit dialog on clicking (edited) --- src/components/views/messages/TextualBody.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index d76956d193..d95f0d88df 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -454,6 +454,11 @@ module.exports = React.createClass({ this.setState({editedMarkerHovered: false}); }, + _openHistoryDialog: async function() { + const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog"); + Modal.createDialog(MessageEditHistoryDialog, {mxEvent: this.props.mxEvent}); + }, + _renderEditedMarker: function() { let editedTooltip; if (this.state.editedMarkerHovered) { @@ -468,6 +473,7 @@ module.exports = React.createClass({ return (
    {editedTooltip}{`(${_t("edited")})`}
    From e54881aa2486f688a91461277648f9ca7b8bd095 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 31 May 2019 16:52:27 +0200 Subject: [PATCH 30/70] WIP --- src/components/views/messages/TextualBody.js | 103 +------------------ src/utils/pillify.js | 103 +++++++++++++++++++ 2 files changed, 105 insertions(+), 101 deletions(-) create mode 100644 src/utils/pillify.js diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index d95f0d88df..db5d178c96 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -30,12 +30,11 @@ import Modal from '../../../Modal'; import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler'; -import MatrixClientPeg from '../../../MatrixClientPeg'; import * as ContextualMenu from '../../structures/ContextualMenu'; import SettingsStore from "../../../settings/SettingsStore"; -import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; import ReplyThread from "../elements/ReplyThread"; import {host as matrixtoHost} from '../../../matrix-to'; +import {pillifyLinks} from '../../../utils/pillify'; module.exports = React.createClass({ displayName: 'TextualBody', @@ -99,7 +98,7 @@ module.exports = React.createClass({ // pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer // are still sent as plaintext URLs. If these are ever pillified in the composer, // we should be pillify them here by doing the linkifying BEFORE the pillifying. - this.pillifyLinks(this.refs.content.children); + pillifyLinks(this.refs.content.children, this.props.mxEvent); HtmlUtils.linkifyElement(this.refs.content); this.calculateUrlPreview(); @@ -184,104 +183,6 @@ module.exports = React.createClass({ } }, - pillifyLinks: function(nodes) { - const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); - let node = nodes[0]; - while (node) { - let pillified = false; - - if (node.tagName === "A" && node.getAttribute("href")) { - const href = node.getAttribute("href"); - - // If the link is a (localised) matrix.to link, replace it with a pill - const Pill = sdk.getComponent('elements.Pill'); - if (Pill.isMessagePillUrl(href)) { - const pillContainer = document.createElement('span'); - - const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); - const pill = ; - - ReactDOM.render(pill, pillContainer); - node.parentNode.replaceChild(pillContainer, node); - // Pills within pills aren't going to go well, so move on - pillified = true; - - // update the current node with one that's now taken its place - node = pillContainer; - } - } else if ( - node.nodeType === Node.TEXT_NODE && - // as applying pills happens outside of react, make sure we're not doubly - // applying @room pills here, as a rerender with the same content won't touch the DOM - // to clear the pills from the last run of pillifyLinks - !node.parentElement.classList.contains("mx_AtRoomPill") - ) { - const Pill = sdk.getComponent('elements.Pill'); - - let currentTextNode = node; - const roomNotifTextNodes = []; - - // Take a textNode and break it up to make all the instances of @room their - // own textNode, adding those nodes to roomNotifTextNodes - while (currentTextNode !== null) { - const roomNotifPos = Pill.roomNotifPos(currentTextNode.textContent); - let nextTextNode = null; - if (roomNotifPos > -1) { - let roomTextNode = currentTextNode; - - if (roomNotifPos > 0) roomTextNode = roomTextNode.splitText(roomNotifPos); - if (roomTextNode.textContent.length > Pill.roomNotifLen()) { - nextTextNode = roomTextNode.splitText(Pill.roomNotifLen()); - } - roomNotifTextNodes.push(roomTextNode); - } - currentTextNode = nextTextNode; - } - - if (roomNotifTextNodes.length > 0) { - const pushProcessor = new PushProcessor(MatrixClientPeg.get()); - const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif"); - if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) { - // Now replace all those nodes with Pills - for (const roomNotifTextNode of roomNotifTextNodes) { - // Set the next node to be processed to the one after the node - // we're adding now, since we've just inserted nodes into the structure - // we're iterating over. - // Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once - node = roomNotifTextNode.nextSibling; - - const pillContainer = document.createElement('span'); - const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); - const pill = ; - - ReactDOM.render(pill, pillContainer); - roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode); - } - // Nothing else to do for a text node (and we don't need to advance - // the loop pointer because we did it above) - continue; - } - } - } - - if (node.childNodes && node.childNodes.length && !pillified) { - this.pillifyLinks(node.childNodes); - } - - node = node.nextSibling; - } - }, - findLinks: function(nodes) { let links = []; diff --git a/src/utils/pillify.js b/src/utils/pillify.js new file mode 100644 index 0000000000..7744b6c15d --- /dev/null +++ b/src/utils/pillify.js @@ -0,0 +1,103 @@ +import ReactDOM from 'react-dom'; +import MatrixClientPeg from '../MatrixClientPeg'; +import SettingsStore from "../settings/SettingsStore"; +import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; +import sdk from '../index'; + + +export function pillifyLinks(nodes, mxEvent) { + const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); + const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); + let node = nodes[0]; + while (node) { + let pillified = false; + + if (node.tagName === "A" && node.getAttribute("href")) { + const href = node.getAttribute("href"); + + // If the link is a (localised) matrix.to link, replace it with a pill + const Pill = sdk.getComponent('elements.Pill'); + if (Pill.isMessagePillUrl(href)) { + const pillContainer = document.createElement('span'); + + const pill = ; + + ReactDOM.render(pill, pillContainer); + node.parentNode.replaceChild(pillContainer, node); + // Pills within pills aren't going to go well, so move on + pillified = true; + + // update the current node with one that's now taken its place + node = pillContainer; + } + } else if ( + node.nodeType === Node.TEXT_NODE && + // as applying pills happens outside of react, make sure we're not doubly + // applying @room pills here, as a rerender with the same content won't touch the DOM + // to clear the pills from the last run of pillifyLinks + !node.parentElement.classList.contains("mx_AtRoomPill") + ) { + const Pill = sdk.getComponent('elements.Pill'); + + let currentTextNode = node; + const roomNotifTextNodes = []; + + // Take a textNode and break it up to make all the instances of @room their + // own textNode, adding those nodes to roomNotifTextNodes + while (currentTextNode !== null) { + const roomNotifPos = Pill.roomNotifPos(currentTextNode.textContent); + let nextTextNode = null; + if (roomNotifPos > -1) { + let roomTextNode = currentTextNode; + + if (roomNotifPos > 0) roomTextNode = roomTextNode.splitText(roomNotifPos); + if (roomTextNode.textContent.length > Pill.roomNotifLen()) { + nextTextNode = roomTextNode.splitText(Pill.roomNotifLen()); + } + roomNotifTextNodes.push(roomTextNode); + } + currentTextNode = nextTextNode; + } + + if (roomNotifTextNodes.length > 0) { + const pushProcessor = new PushProcessor(MatrixClientPeg.get()); + const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif"); + if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, mxEvent)) { + // Now replace all those nodes with Pills + for (const roomNotifTextNode of roomNotifTextNodes) { + // Set the next node to be processed to the one after the node + // we're adding now, since we've just inserted nodes into the structure + // we're iterating over. + // Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once + node = roomNotifTextNode.nextSibling; + + const pillContainer = document.createElement('span'); + const pill = ; + + ReactDOM.render(pill, pillContainer); + roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode); + } + // Nothing else to do for a text node (and we don't need to advance + // the loop pointer because we did it above) + continue; + } + } + } + + if (node.childNodes && node.childNodes.length && !pillified) { + pillifyLinks(node.childNodes); + } + + node = node.nextSibling; + } +} From 19b4699bc2259f185031af1ddbdcbeacebace0f9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 3 Jun 2019 14:31:09 +0200 Subject: [PATCH 31/70] WIP for showing pills in edit history --- .../views/elements/EditHistoryMessage.js | 51 +++++++++++++++++++ src/utils/pillify.js | 2 +- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/components/views/elements/EditHistoryMessage.js diff --git a/src/components/views/elements/EditHistoryMessage.js b/src/components/views/elements/EditHistoryMessage.js new file mode 100644 index 0000000000..2b8ecb87aa --- /dev/null +++ b/src/components/views/elements/EditHistoryMessage.js @@ -0,0 +1,51 @@ +/* +Copyright 2019 New Vector Ltd +Copyright 2019 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 PropTypes from 'prop-types'; +import * as HtmlUtils from '../../../HtmlUtils'; +import {formatTime} from '../../../DateUtils'; +import {MatrixEvent} from 'matrix-js-sdk'; +import {pillifyLinks} from '../../../utils/pillify'; + +export default class EditHistoryMessage extends React.Component { + static propTypes = { + // the message event being edited + mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired, + }; + + constructor(props, context) { + super(props, context); + } + + componentDidMount() { + pillifyLinks(this.refs.content.children, this.props.mxEvent); + } + + componentDidUpdate() { + pillifyLinks(this.refs.content.children, this.props.mxEvent); + } + + render() { + const event = this.props.mxEvent; + const timestamp = formatTime(new Date(event.getTs()), true); + const content = event.event.content["m.new_content"] || event.event.content; + return
  • + {timestamp} +

    {HtmlUtils.bodyToHtml(content)}

    +
  • ; + } +} diff --git a/src/utils/pillify.js b/src/utils/pillify.js index 7744b6c15d..74eac560d3 100644 --- a/src/utils/pillify.js +++ b/src/utils/pillify.js @@ -95,7 +95,7 @@ export function pillifyLinks(nodes, mxEvent) { } if (node.childNodes && node.childNodes.length && !pillified) { - pillifyLinks(node.childNodes); + pillifyLinks(node.childNodes, mxEvent); } node = node.nextSibling; From c9aa7efe54be3bb1bcfc22236e8b85d26bab0d81 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 24 Jun 2019 17:45:29 +0200 Subject: [PATCH 32/70] don't require EventTile for default timestamp style --- res/css/views/messages/_MessageTimestamp.scss | 2 ++ res/css/views/rooms/_EventTile.scss | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/messages/_MessageTimestamp.scss b/res/css/views/messages/_MessageTimestamp.scss index e21189c59e..e5c228aa68 100644 --- a/res/css/views/messages/_MessageTimestamp.scss +++ b/res/css/views/messages/_MessageTimestamp.scss @@ -15,4 +15,6 @@ limitations under the License. */ .mx_MessageTimestamp { + color: $event-timestamp-color; + font-size: 10px; } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 62632eab27..1bd62e393e 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -93,8 +93,6 @@ limitations under the License. display: block; visibility: hidden; white-space: nowrap; - color: $event-timestamp-color; - font-size: 10px; left: 0px; width: 46px; /* 8 + 30 (avatar) + 8 */ text-align: center; From e7fc84d5dab3320a68e50446d913f577ad116b58 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 24 Jun 2019 17:45:57 +0200 Subject: [PATCH 33/70] render history items in own component including: - respect 12/24 hour setting - pillify --- .../views/dialogs/MessageEditHistoryDialog.js | 19 +++++++++---------- .../views/elements/EditHistoryMessage.js | 6 +++--- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js index 958c7457e0..d6198abea4 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.js +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -19,8 +19,8 @@ import PropTypes from 'prop-types'; import MatrixClientPeg from "../../../MatrixClientPeg"; import { _t } from '../../../languageHandler'; import sdk from "../../../index"; -import * as HtmlUtils from '../../../HtmlUtils'; -import {wantsDateSeparator, formatTime} from '../../../DateUtils'; +import {wantsDateSeparator} from '../../../DateUtils'; +import SettingsStore from '../../../settings/SettingsStore'; export default class MessageEditHistoryDialog extends React.Component { static propTypes = { @@ -28,7 +28,11 @@ export default class MessageEditHistoryDialog extends React.Component { }; componentWillMount() { - this.setState({edits: [this.props.mxEvent], isLoading: true}); + this.setState({ + edits: [this.props.mxEvent], + isLoading: true, + isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"), + }); } async componentDidMount() { @@ -41,13 +45,8 @@ export default class MessageEditHistoryDialog extends React.Component { this.setState({edits, isLoading: false}); } - _renderEdit(event) { - const timestamp = formatTime(new Date(event.getTs()), true); - const content = event.event.content["m.new_content"] || event.event.content; - return
  • {timestamp}

    {HtmlUtils.bodyToHtml(content)}

  • ; - } - _renderEdits() { + const EditHistoryMessage = sdk.getComponent('elements.EditHistoryMessage'); const DateSeparator = sdk.getComponent('messages.DateSeparator'); const nodes = []; let lastEvent; @@ -55,7 +54,7 @@ export default class MessageEditHistoryDialog extends React.Component { if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) { nodes.push(
  • ); } - nodes.push(this._renderEdit(e)); + nodes.push(); lastEvent = e; }); return nodes; diff --git a/src/components/views/elements/EditHistoryMessage.js b/src/components/views/elements/EditHistoryMessage.js index 2b8ecb87aa..1f18072fd8 100644 --- a/src/components/views/elements/EditHistoryMessage.js +++ b/src/components/views/elements/EditHistoryMessage.js @@ -41,10 +41,10 @@ export default class EditHistoryMessage extends React.Component { render() { const event = this.props.mxEvent; - const timestamp = formatTime(new Date(event.getTs()), true); + const timestamp = formatTime(new Date(event.getTs()), this.props.isTwelveHour); const content = event.event.content["m.new_content"] || event.event.content; - return
  • - {timestamp} + return
  • + {timestamp}

    {HtmlUtils.bodyToHtml(content)}

  • ; } From beb003b2d65f1b8e3a11953a0e69e9c5a27a6a95 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 24 Jun 2019 17:46:30 +0200 Subject: [PATCH 34/70] some preliminary styling --- res/css/views/dialogs/_MessageEditHistoryDialog.scss | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/res/css/views/dialogs/_MessageEditHistoryDialog.scss b/res/css/views/dialogs/_MessageEditHistoryDialog.scss index ad51f4237f..b917fe87fd 100644 --- a/res/css/views/dialogs/_MessageEditHistoryDialog.scss +++ b/res/css/views/dialogs/_MessageEditHistoryDialog.scss @@ -16,17 +16,18 @@ limitations under the License. .mx_MessageEditHistoryDialog ul { list-style-type: none; + font-size: 14px; &>li.edit { + margin: 10px 0; display: flex; - &>strong { - flex: 0 0 100px; - font-weight: bold; + &>.mx_MessageTimestamp { + flex: 0 0 50px; } &>p { - + margin: 0; } } From 0fe28cba4363125b52ec6351743b5f7248a2e9e6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jun 2019 10:18:27 +0200 Subject: [PATCH 35/70] support emotes in edit history --- .../views/elements/EditHistoryMessage.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/EditHistoryMessage.js b/src/components/views/elements/EditHistoryMessage.js index 1f18072fd8..749f478ba4 100644 --- a/src/components/views/elements/EditHistoryMessage.js +++ b/src/components/views/elements/EditHistoryMessage.js @@ -40,12 +40,23 @@ export default class EditHistoryMessage extends React.Component { } render() { - const event = this.props.mxEvent; - const timestamp = formatTime(new Date(event.getTs()), this.props.isTwelveHour); - const content = event.event.content["m.new_content"] || event.event.content; + const {mxEvent} = this.props; + const content = mxEvent.event.content["m.new_content"] || mxEvent.event.content; + const contentElements = HtmlUtils.bodyToHtml(content); + let contentContainer; + if (mxEvent.getContent().msgtype === "m.emote") { + const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); + contentContainer = (

    *  + { name } +  {contentElements} +

    ); + } else { + contentContainer = (

    {contentElements}

    ); + } + const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour); return
  • {timestamp} -

    {HtmlUtils.bodyToHtml(content)}

    + { contentContainer }
  • ; } } From 8c9a6ddf96e66b6239a9e672cc8c6acfd1eb8335 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jun 2019 14:50:56 +0200 Subject: [PATCH 36/70] support edits pagination in a ScrollPanel --- .../dialogs/_MessageEditHistoryDialog.scss | 11 ++++ .../views/dialogs/MessageEditHistoryDialog.js | 60 ++++++++++++++----- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/res/css/views/dialogs/_MessageEditHistoryDialog.scss b/res/css/views/dialogs/_MessageEditHistoryDialog.scss index b917fe87fd..bc4c2680ed 100644 --- a/res/css/views/dialogs/_MessageEditHistoryDialog.scss +++ b/res/css/views/dialogs/_MessageEditHistoryDialog.scss @@ -15,6 +15,17 @@ limitations under the License. */ .mx_MessageEditHistoryDialog ul { +.mx_MessageEditHistoryDialog { + display: flex; + flex-direction: column; + max-height: 60vh; +} + +.mx_MessageEditHistoryDialog_scrollPanel { + flex: 1 1 auto; +} + +.mx_MessageEditHistoryDialog_edits { list-style-type: none; font-size: 14px; diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js index d6198abea4..37f49026ac 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.js +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -28,21 +28,41 @@ export default class MessageEditHistoryDialog extends React.Component { }; componentWillMount() { + this.loadMoreEdits = this.loadMoreEdits.bind(this); this.setState({ - edits: [this.props.mxEvent], + events: [], + nextBatch: null, isLoading: true, isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"), }); } - async componentDidMount() { + async loadMoreEdits(backwards) { + if (backwards || (!this.state.nextBatch && !this.state.isLoading)) { + // bail out on backwards as we only paginate in one direction + return false; + } + const opts = {token: this.state.nextBatch}; const roomId = this.props.mxEvent.getRoomId(); const eventId = this.props.mxEvent.getId(); - let edits = await MatrixClientPeg.get(). - relations(roomId, eventId, "m.replace", "m.room.message"); - edits = edits.slice().reverse(); - edits.unshift(this.props.mxEvent); - this.setState({edits, isLoading: false}); + const result = await MatrixClientPeg.get().relations( + roomId, eventId, "m.replace", "m.room.message", opts); + //console.log(`loadMoreEdits: got ${result.}`) + let resolve; + const promise = new Promise(r => resolve = r); + this.setState({ + events: this.state.events.concat(result.events), + nextBatch: result.nextBatch, + isLoading: false, + }, () => { + const hasMoreResults = !!this.state.nextBatch; + resolve(hasMoreResults); + }); + return promise; + } + + componentDidMount() { + this.loadMoreEdits(); } _renderEdits() { @@ -50,7 +70,7 @@ export default class MessageEditHistoryDialog extends React.Component { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const nodes = []; let lastEvent; - this.state.edits.forEach(e => { + this.state.events.forEach(e => { if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) { nodes.push(
  • ); } @@ -61,18 +81,28 @@ export default class MessageEditHistoryDialog extends React.Component { } render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - let spinner; - const edits = this._renderEdits(); - if (this.state.isLoading) { + let content; + if (this.state.error) { + content = this.state.error; + } else if (this.state.isLoading) { const Spinner = sdk.getComponent("elements.Spinner"); - spinner = ; + content = ; + } else { + const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); + content = ( +
      {this._renderEdits()}
    +
    ); } + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( -
      {edits}
    - {spinner} + {content}
    ); } From ee03a0f31dd5c2f5f390e23cbe6a54a646dc139d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 11:56:58 +0200 Subject: [PATCH 37/70] recycle EventTile css to make history items look mostly similar --- .../dialogs/_MessageEditHistoryDialog.scss | 20 ++++--------------- .../views/elements/EditHistoryMessage.js | 14 +++++++------ 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/res/css/views/dialogs/_MessageEditHistoryDialog.scss b/res/css/views/dialogs/_MessageEditHistoryDialog.scss index bc4c2680ed..2da72a042e 100644 --- a/res/css/views/dialogs/_MessageEditHistoryDialog.scss +++ b/res/css/views/dialogs/_MessageEditHistoryDialog.scss @@ -28,23 +28,11 @@ limitations under the License. .mx_MessageEditHistoryDialog_edits { list-style-type: none; font-size: 14px; + padding: 0; + color: $primary-fg-color; - &>li.edit { - margin: 10px 0; - display: flex; - - &>.mx_MessageTimestamp { - flex: 0 0 50px; - } - - &>p { - margin: 0; - } - - } - - ul, ol { - list-style-type: circle; + .mx_EventTile_line, .mx_EventTile_content { + margin-right: 0px; } } diff --git a/src/components/views/elements/EditHistoryMessage.js b/src/components/views/elements/EditHistoryMessage.js index 749f478ba4..a6cbfbdfd4 100644 --- a/src/components/views/elements/EditHistoryMessage.js +++ b/src/components/views/elements/EditHistoryMessage.js @@ -46,17 +46,19 @@ export default class EditHistoryMessage extends React.Component { let contentContainer; if (mxEvent.getContent().msgtype === "m.emote") { const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); - contentContainer = (

    *  + contentContainer = (

    { name }  {contentElements} -

    ); +
    ); } else { - contentContainer = (

    {contentElements}

    ); + contentContainer = (
    {contentElements}
    ); } const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour); - return
  • - {timestamp} - { contentContainer } + return
  • +
    + {timestamp} + { contentContainer } +
  • ; } } From fffdfde8baa91e5bb6368427dd25a73370b3cd12 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 11:57:29 +0200 Subject: [PATCH 38/70] center dialog title --- res/css/views/dialogs/_MessageEditHistoryDialog.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/res/css/views/dialogs/_MessageEditHistoryDialog.scss b/res/css/views/dialogs/_MessageEditHistoryDialog.scss index 2da72a042e..b80742bd24 100644 --- a/res/css/views/dialogs/_MessageEditHistoryDialog.scss +++ b/res/css/views/dialogs/_MessageEditHistoryDialog.scss @@ -14,7 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MessageEditHistoryDialog ul { +.mx_MessageEditHistoryDialog .mx_Dialog_header > .mx_Dialog_title { + text-align: center; +} + .mx_MessageEditHistoryDialog { display: flex; flex-direction: column; From fe3be39fe772a1d5b6f80b633d23a5739a826e17 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 11:57:49 +0200 Subject: [PATCH 39/70] don't hide timestamps --- src/components/views/dialogs/MessageEditHistoryDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js index 37f49026ac..1f5408e94c 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.js +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -95,7 +95,7 @@ export default class MessageEditHistoryDialog extends React.Component { stickyBottom={false} startAtBottom={false} > -
      {this._renderEdits()}
    +
      {this._renderEdits()}
    ); } const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); From ddae4de7bd11c541c25190ce62010ded43f7ce9d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 11:58:03 +0200 Subject: [PATCH 40/70] cleanup: RoomDirectory doesn't use gemini anymore --- res/css/structures/_RoomDirectory.scss | 7 ------- 1 file changed, 7 deletions(-) diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index bcfe3aefd6..1df0a61a2b 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -35,13 +35,6 @@ limitations under the License. flex: 1; } -.mx_RoomDirectory .gm-scroll-view { - // little hack because gemini doesn't seem to detect - // the scrollbar width well in this instance - // when using css scrollbars - scrollbar-width: thin; -} - .mx_RoomDirectory_createRoom { background-color: $button-bg-color; border-radius: 4px; From 498db2597dd87d52784f6d35ccf4a3f1934fcc1b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 12:15:13 +0200 Subject: [PATCH 41/70] show hand on hovering (edited) marker --- res/css/views/rooms/_EventTile.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 1bd62e393e..1f75373be8 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -401,6 +401,7 @@ limitations under the License. color: $roomtopic-color; display: inline-block; margin-left: 9px; + cursor: pointer; } /* Various markdown overrides */ From d20b765e2742ba345f83cd6cd99b8bc059b4b413 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 14:34:26 +0200 Subject: [PATCH 42/70] rename $accent-color-50pct to $accent-color-darker we'll use $accent-color-50pct for 50% transparent accent color --- res/css/views/auth/_InteractiveAuthEntryComponents.scss | 2 +- res/themes/light/css/_light.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss index 972acdc2ab..85007aeecb 100644 --- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss +++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss @@ -49,7 +49,7 @@ limitations under the License. } .mx_InteractiveAuthEntryComponents_termsSubmit:disabled { - background-color: $accent-color-50pct; + background-color: $accent-color-darker; cursor: default; } diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 361f6fa408..3ef2ddc97f 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -28,7 +28,7 @@ $focus-bg-color: #dddddd; // button UI (white-on-green in light skin) $accent-fg-color: #ffffff; -$accent-color-50pct: #92caad; +$accent-color-darker: #92caad; $accent-color-alt: #238CF5; $selection-fg-color: $primary-bg-color; From 2e3a6b3c0ba2c1dc3cdf562299b36e8d2788365b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 14:40:33 +0200 Subject: [PATCH 43/70] set 50% transparent accent color as editor focus border --- res/css/views/elements/_MessageEditor.scss | 4 ++++ res/themes/light/css/_light.scss | 1 + 2 files changed, 5 insertions(+) diff --git a/res/css/views/elements/_MessageEditor.scss b/res/css/views/elements/_MessageEditor.scss index e721b267fa..7fd99bae17 100644 --- a/res/css/views/elements/_MessageEditor.scss +++ b/res/css/views/elements/_MessageEditor.scss @@ -34,6 +34,10 @@ limitations under the License. max-height: 200px; overflow-x: auto; + &:focus { + border-color: $accent-color-50pct; + } + span.mx_UserPill, span.mx_RoomPill { padding-left: 21px; position: relative; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 3ef2ddc97f..2dd193b8c5 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -28,6 +28,7 @@ $focus-bg-color: #dddddd; // button UI (white-on-green in light skin) $accent-fg-color: #ffffff; +$accent-color-50pct: rgba(3, 179, 129, 0.5); //#03b381 in rgb $accent-color-darker: #92caad; $accent-color-alt: #238CF5; From 92c2a119c94b1ead60909d780e79556884f5a374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=BCrmann?= Date: Wed, 26 Jun 2019 15:27:52 +0200 Subject: [PATCH 44/70] Remove top margin of community panel container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a simpler fix for the scrollbar problem than changing the definition for height, which was my previous attempt at solving this problem. Additionally, the top and bottom margins are now consistent. Signed-off-by: Jonas Schรผrmann --- res/css/structures/_TagPanel.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index a01e5dd838..b03d36a592 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -63,9 +63,8 @@ limitations under the License. display: flex; flex-direction: column; align-items: center; - margin-top: 5px; - height: calc(100% - 5px); + height: 100%; } .mx_TagPanel .mx_TagPanel_tagTileContainer > div { height: 40px; From fa0319f14b005fc72d5d9368bf590b5afa69f848 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 15:49:24 +0200 Subject: [PATCH 45/70] apply renamed (token -> from) option --- src/components/views/dialogs/MessageEditHistoryDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js index 1f5408e94c..dd6ef0d866 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.js +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -42,7 +42,7 @@ export default class MessageEditHistoryDialog extends React.Component { // bail out on backwards as we only paginate in one direction return false; } - const opts = {token: this.state.nextBatch}; + const opts = {from: this.state.nextBatch}; const roomId = this.props.mxEvent.getRoomId(); const eventId = this.props.mxEvent.getId(); const result = await MatrixClientPeg.get().relations( From f4b86ca2658f70017c34037f183f8f6b63c5bd5f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 15:51:28 +0200 Subject: [PATCH 46/70] don't bind --- src/components/views/dialogs/MessageEditHistoryDialog.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js index dd6ef0d866..45d46b49f2 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.js +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -28,7 +28,6 @@ export default class MessageEditHistoryDialog extends React.Component { }; componentWillMount() { - this.loadMoreEdits = this.loadMoreEdits.bind(this); this.setState({ events: [], nextBatch: null, @@ -37,7 +36,7 @@ export default class MessageEditHistoryDialog extends React.Component { }); } - async loadMoreEdits(backwards) { + loadMoreEdits = async (backwards) => { if (backwards || (!this.state.nextBatch && !this.state.isLoading)) { // bail out on backwards as we only paginate in one direction return false; From 39c96b15d8bfb98723c0164230a6a38ac78859d4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 15:51:46 +0200 Subject: [PATCH 47/70] set state in ctor --- src/components/views/dialogs/MessageEditHistoryDialog.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js index 45d46b49f2..9b11fadf83 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.js +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -27,13 +27,14 @@ export default class MessageEditHistoryDialog extends React.Component { mxEvent: PropTypes.object.isRequired, }; - componentWillMount() { - this.setState({ + constructor(props) { + super(props); + this.state = { events: [], nextBatch: null, isLoading: true, isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"), - }); + }; } loadMoreEdits = async (backwards) => { From 929020a13964f956c423d53d50f61acf1fa39741 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 15:51:55 +0200 Subject: [PATCH 48/70] remove leftover logging --- src/components/views/dialogs/MessageEditHistoryDialog.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js index 9b11fadf83..81a3732fe6 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.js +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -47,7 +47,6 @@ export default class MessageEditHistoryDialog extends React.Component { const eventId = this.props.mxEvent.getId(); const result = await MatrixClientPeg.get().relations( roomId, eventId, "m.replace", "m.room.message", opts); - //console.log(`loadMoreEdits: got ${result.}`) let resolve; const promise = new Promise(r => resolve = r); this.setState({ From d606c966ea7194854af5d22bd91e77bf76315de4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 16:12:44 +0200 Subject: [PATCH 49/70] use PureComponent --- src/components/views/dialogs/MessageEditHistoryDialog.js | 2 +- src/components/views/elements/EditHistoryMessage.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js index 81a3732fe6..786f1f43a0 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.js +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -22,7 +22,7 @@ import sdk from "../../../index"; import {wantsDateSeparator} from '../../../DateUtils'; import SettingsStore from '../../../settings/SettingsStore'; -export default class MessageEditHistoryDialog extends React.Component { +export default class MessageEditHistoryDialog extends React.PureComponent { static propTypes = { mxEvent: PropTypes.object.isRequired, }; diff --git a/src/components/views/elements/EditHistoryMessage.js b/src/components/views/elements/EditHistoryMessage.js index a6cbfbdfd4..b4b87bd246 100644 --- a/src/components/views/elements/EditHistoryMessage.js +++ b/src/components/views/elements/EditHistoryMessage.js @@ -21,7 +21,7 @@ import {formatTime} from '../../../DateUtils'; import {MatrixEvent} from 'matrix-js-sdk'; import {pillifyLinks} from '../../../utils/pillify'; -export default class EditHistoryMessage extends React.Component { +export default class EditHistoryMessage extends React.PureComponent { static propTypes = { // the message event being edited mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired, From c987f4e8d8ac71289820d1636d6721ef32320b54 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 16:12:57 +0200 Subject: [PATCH 50/70] remove passthrough ctor --- src/components/views/elements/EditHistoryMessage.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/views/elements/EditHistoryMessage.js b/src/components/views/elements/EditHistoryMessage.js index b4b87bd246..2807bedeb8 100644 --- a/src/components/views/elements/EditHistoryMessage.js +++ b/src/components/views/elements/EditHistoryMessage.js @@ -27,10 +27,6 @@ export default class EditHistoryMessage extends React.PureComponent { mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired, }; - constructor(props, context) { - super(props, context); - } - componentDidMount() { pillifyLinks(this.refs.content.children, this.props.mxEvent); } From a1548285b55688097a743a3dd2132308664dbd0a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 16:13:15 +0200 Subject: [PATCH 51/70] fix copyright header and whitespace --- .../views/elements/EditHistoryMessage.js | 2 +- src/utils/pillify.js | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/EditHistoryMessage.js b/src/components/views/elements/EditHistoryMessage.js index 2807bedeb8..85a704641a 100644 --- a/src/components/views/elements/EditHistoryMessage.js +++ b/src/components/views/elements/EditHistoryMessage.js @@ -1,5 +1,4 @@ /* -Copyright 2019 New Vector Ltd Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +13,7 @@ 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 PropTypes from 'prop-types'; import * as HtmlUtils from '../../../HtmlUtils'; diff --git a/src/utils/pillify.js b/src/utils/pillify.js index 74eac560d3..e943cfe657 100644 --- a/src/utils/pillify.js +++ b/src/utils/pillify.js @@ -1,10 +1,25 @@ +/* +Copyright 2019 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 ReactDOM from 'react-dom'; import MatrixClientPeg from '../MatrixClientPeg'; import SettingsStore from "../settings/SettingsStore"; import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; import sdk from '../index'; - export function pillifyLinks(nodes, mxEvent) { const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); From 54de0b298b36bdfc68d48b628373b21ed2779e75 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 16:13:32 +0200 Subject: [PATCH 52/70] add "Click to see edits." to tooltip --- src/components/views/messages/TextualBody.js | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index db5d178c96..25316844df 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -368,7 +368,7 @@ module.exports = React.createClass({ const date = editEvent && formatDate(editEvent.getDate()); editedTooltip = ; } return ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 01d84324ce..15dbd95d8c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -954,7 +954,7 @@ "Failed to copy": "Failed to copy", "Add an Integration": "Add an Integration", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", - "Edited at %(date)s": "Edited at %(date)s", + "Edited at %(date)s. Click to view edits.": "Edited at %(date)s. Click to view edits.", "edited": "edited", "Removed or unknown message type": "Removed or unknown message type", "Message removed by %(userId)s": "Message removed by %(userId)s", From c9c84016cbc0a438e3a2c02b7d01987c0b77da7d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 16:17:25 +0200 Subject: [PATCH 53/70] move EditHistoryMessage to messages directory --- src/components/views/dialogs/MessageEditHistoryDialog.js | 2 +- .../views/{elements => messages}/EditHistoryMessage.js | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/components/views/{elements => messages}/EditHistoryMessage.js (100%) diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js index 786f1f43a0..9d533eab56 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.js +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -65,7 +65,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent { } _renderEdits() { - const EditHistoryMessage = sdk.getComponent('elements.EditHistoryMessage'); + const EditHistoryMessage = sdk.getComponent('messages.EditHistoryMessage'); const DateSeparator = sdk.getComponent('messages.DateSeparator'); const nodes = []; let lastEvent; diff --git a/src/components/views/elements/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js similarity index 100% rename from src/components/views/elements/EditHistoryMessage.js rename to src/components/views/messages/EditHistoryMessage.js From f6e0cd9a03914546dc3abe130c359a14cbb55d65 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jun 2019 18:54:15 +0200 Subject: [PATCH 54/70] don't show error dialog when user has no webcam instead, retry with just audio. Also when mounted, check if the user has given enough permissions to return non-empty labels for the devices, something both ff & chrome do if you haven't going through the permissions popup yet. If not, show the permissions button. --- src/CallMediaHandler.js | 5 ++ .../tabs/user/VoiceUserSettingsTab.js | 77 +++++++++++-------- src/i18n/strings/en_EN.json | 3 - 3 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js index 55b8764a9e..a0364f798a 100644 --- a/src/CallMediaHandler.js +++ b/src/CallMediaHandler.js @@ -18,6 +18,11 @@ import * as Matrix from 'matrix-js-sdk'; import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; export default { + hasAnyLabeledDevices: async function() { + const devices = await navigator.mediaDevices.enumerateDevices(); + return devices.some(d => !!d.label); + }, + getDevices: function() { // Only needed for Electron atm, though should work in modern browsers // once permission has been granted to the webapp diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index 31a11b13ea..0cafbacf8d 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -29,48 +29,64 @@ export default class VoiceUserSettingsTab extends React.Component { super(); this.state = { - mediaDevices: null, + mediaDevices: false, activeAudioOutput: null, activeAudioInput: null, activeVideoInput: null, }; } - componentWillMount(): void { - this._refreshMediaDevices(); + async componentDidMount() { + const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices(); + if (canSeeDeviceLabels) { + this._refreshMediaDevices(); + } } _refreshMediaDevices = async (stream) => { - if (stream) { - // kill stream so that we don't leave it lingering around with webcam enabled etc - // as here we called gUM to ask user for permission to their device names only - stream.getTracks().forEach((track) => track.stop()); - } - this.setState({ mediaDevices: await CallMediaHandler.getDevices(), activeAudioOutput: CallMediaHandler.getAudioOutput(), activeAudioInput: CallMediaHandler.getAudioInput(), activeVideoInput: CallMediaHandler.getVideoInput(), }); + if (stream) { + // kill stream (after we've enumerated the devices, otherwise we'd get empty labels again) + // so that we don't leave it lingering around with webcam enabled etc + // as here we called gUM to ask user for permission to their device names only + stream.getTracks().forEach((track) => track.stop()); + } }; - _requestMediaPermissions = () => { - const getUserMedia = ( - window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia - ); - if (getUserMedia) { - return getUserMedia.apply(window.navigator, [ - { video: true, audio: true }, - this._refreshMediaDevices, - function() { - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createTrackedDialog('No media permissions', '', ErrorDialog, { - title: _t('No media permissions'), - description: _t('You may need to manually permit Riot to access your microphone/webcam'), - }); - }, - ]); + _requestMediaPermissions = async () => { + let constraints; + let stream; + let error; + try { + constraints = {video: true, audio: true}; + stream = await navigator.mediaDevices.getUserMedia(constraints); + } catch (err) { + // user likely doesn't have a webcam, + // we should still allow to select a microphone + if (err.name === "NotFoundError") { + constraints = { audio: true }; + try { + stream = await navigator.mediaDevices.getUserMedia(constraints); + } catch (err) { + error = err; + } + } else { + error = err; + } + } + if (error) { + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('No media permissions', '', ErrorDialog, { + title: _t('No media permissions'), + description: _t('You may need to manually permit Riot to access your microphone/webcam'), + }); + } else { + this._refreshMediaDevices(stream); } }; @@ -101,15 +117,7 @@ export default class VoiceUserSettingsTab extends React.Component { _renderDeviceOptions(devices, category) { return devices.map((d) => { - let label = d.label; - if (!label) { - switch (d.kind) { - case "audioinput": label = _t("Unnamed microphone"); break; - case "audiooutput": label = _t("Unnamed audio output"); break; - case "videoinput": label = _t("Unnamed camera"); break; - } - } - return (); + return (); }); } @@ -120,6 +128,7 @@ export default class VoiceUserSettingsTab extends React.Component { let speakerDropdown = null; let microphoneDropdown = null; let webcamDropdown = null; + console.log({mediaDevices: this.state.mediaDevices}); if (this.state.mediaDevices === false) { requestButton = (
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 010ad29da0..d3d32a6e89 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -615,9 +615,6 @@ "Learn more about how we use analytics.": "Learn more about how we use analytics.", "No media permissions": "No media permissions", "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam", - "Unnamed microphone": "Unnamed microphone", - "Unnamed audio output": "Unnamed audio output", - "Unnamed camera": "Unnamed camera", "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.", "Request media permissions": "Request media permissions", "No Audio Outputs detected": "No Audio Outputs detected", From e8fba4f77091cfe58d7dff32ea7ea0ef3ed84f38 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 26 Jun 2019 18:16:38 +0100 Subject: [PATCH 55/70] Change interactive tooltip to only flip when required This changes the interactive tooltip to only flip around when the tooltip content would be near the window edge. Fixes https://github.com/vector-im/riot-web/issues/10176 --- .../views/elements/InteractiveTooltip.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/InteractiveTooltip.js b/src/components/views/elements/InteractiveTooltip.js index b56fd62556..52d51e0b39 100644 --- a/src/components/views/elements/InteractiveTooltip.js +++ b/src/components/views/elements/InteractiveTooltip.js @@ -21,6 +21,10 @@ import classNames from 'classnames'; const InteractiveTooltipContainerId = "mx_InteractiveTooltip_Container"; +// If the distance from tooltip to window edge is below this value, the tooltip +// will flip around to the other side of the target. +const MIN_SAFE_DISTANCE_TO_WINDOW_EDGE = 20; + function getOrCreateContainer() { let container = document.getElementById(InteractiveTooltipContainerId); @@ -121,7 +125,7 @@ export default class InteractiveTooltip extends React.Component { } renderTooltip() { - const { visible } = this.state; + const { contentRect, visible } = this.state; if (!visible) { ReactDOM.unmountComponentAtNode(getOrCreateContainer()); return null; @@ -134,11 +138,12 @@ export default class InteractiveTooltip extends React.Component { const targetBottom = targetRect.bottom + window.pageYOffset; const targetTop = targetRect.top + window.pageYOffset; - // Align the tooltip vertically on whichever side of the target has more - // space available. + // Place the tooltip above the target by default. If we find that the + // tooltip content would extend past the safe area towards the window + // edge, flip around to below the target. const position = {}; let chevronFace = null; - if (targetBottom < window.innerHeight / 2) { + if (contentRect && (targetTop - contentRect.height <= MIN_SAFE_DISTANCE_TO_WINDOW_EDGE)) { position.top = targetBottom; chevronFace = "top"; } else { @@ -158,8 +163,8 @@ export default class InteractiveTooltip extends React.Component { }); const menuStyle = {}; - if (this.state.contentRect) { - menuStyle.left = `-${this.state.contentRect.width / 2}px`; + if (contentRect) { + menuStyle.left = `-${contentRect.width / 2}px`; } const tooltip =
    From debcafd7606ecf569192faa84fd93c45b1bf3416 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 26 Jun 2019 18:37:59 +0100 Subject: [PATCH 56/70] if on trackpad, don't mess with horizontal scrolling. trackpad heuristic is 'if 15 minutes of no horizontal scrollwheel events, assume user may have switched to mousewheel' --- .../structures/IndicatorScrollbar.js | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index b11e655f0d..52e0c9b8ac 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -131,17 +131,41 @@ export default class IndicatorScrollbar extends React.Component { // the harshness of the scroll behaviour. Should be a value between 0 and 1. const yRetention = 1.0; - // Check for trackpad users every so often to avoid boosting their scroll. + // whenever see horizontal scrolling, assume the user is on a trackpad + // for at least the next 15 minutes. + const now = new Date().getTime(); + if (Math.abs(e.deltaX) > 0) { + this._likelyTrackpadUser = true; + this._checkAgainForTrackpad = now + (15 * 60 * 1000); // 15min + } + else { + // if we haven't seen any horizontal scrolling for >15 minutes, assume + // the user might have plugged in a mousewheel + if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) { + this._likelyTrackpadUser = false; + } + } + + // don't mess with the horizontal scroll for trackpad users + // See https://github.com/vector-im/riot-web/issues/10005 + if (this._likelyTrackpadUser) { + return; + } + + // Every 15 minutes, start checking to Check for trackpad users every so often to messing with their scroll // See https://github.com/vector-im/riot-web/issues/10005 const now = new Date().getTime(); - if (now >= this._checkAgainForTrackpad) { - this._likelyTrackpadUser = Math.abs(e.deltaX) > 0; + if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) { + if (Math.abs(e.deltaX) > 0) { + this._likelyTrackpadUser = true; + } this._checkAgainForTrackpad = now + (15 * 60 * 1000); // 15min } - const safeToBoost = !this._likelyTrackpadUser; + if (this._likelyTrackpadUser) return; + + if (Math.abs(e.deltaX) <= xyThreshold) { // we are vertically scrolling. - if (Math.abs(e.deltaX) <= xyThreshold) { // HACK: We increase the amount of scroll to counteract smooth scrolling browsers. // Smooth scrolling browsers (Firefox) use the relative area to determine the scroll // amount, which means the likely small area of content results in a small amount of @@ -152,7 +176,7 @@ export default class IndicatorScrollbar extends React.Component { const additionalScroll = e.deltaY < 0 ? -50 : 50; // noinspection JSSuspiciousNameCombination - const val = Math.abs(e.deltaY) < 25 && safeToBoost ? (e.deltaY + additionalScroll) : e.deltaY; + const val = Math.abs(e.deltaY) < 25 ? (e.deltaY + additionalScroll) : 0; this._scrollElement.scrollLeft += val * yRetention; } } From 3d11eb430b59c77a53a925506f1a22cb34c061c7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 26 Jun 2019 18:38:46 +0100 Subject: [PATCH 57/70] oops, remove old code --- src/components/structures/IndicatorScrollbar.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 52e0c9b8ac..762a626c02 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -152,18 +152,6 @@ export default class IndicatorScrollbar extends React.Component { return; } - // Every 15 minutes, start checking to Check for trackpad users every so often to messing with their scroll - // See https://github.com/vector-im/riot-web/issues/10005 - const now = new Date().getTime(); - if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) { - if (Math.abs(e.deltaX) > 0) { - this._likelyTrackpadUser = true; - } - this._checkAgainForTrackpad = now + (15 * 60 * 1000); // 15min - } - - if (this._likelyTrackpadUser) return; - if (Math.abs(e.deltaX) <= xyThreshold) { // we are vertically scrolling. // HACK: We increase the amount of scroll to counteract smooth scrolling browsers. From 7fc5d229d64823c142b74074b537760ea5324106 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 26 Jun 2019 21:13:17 +0100 Subject: [PATCH 58/70] fix stuff as per review --- src/components/structures/IndicatorScrollbar.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 762a626c02..21a23e5670 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -131,15 +131,15 @@ export default class IndicatorScrollbar extends React.Component { // the harshness of the scroll behaviour. Should be a value between 0 and 1. const yRetention = 1.0; - // whenever see horizontal scrolling, assume the user is on a trackpad - // for at least the next 15 minutes. + // whenever we see horizontal scrolling, assume the user is on a trackpad + // for at least the next 1 minute. const now = new Date().getTime(); if (Math.abs(e.deltaX) > 0) { this._likelyTrackpadUser = true; - this._checkAgainForTrackpad = now + (15 * 60 * 1000); // 15min + this._checkAgainForTrackpad = now + (1 * 60 * 1000); } else { - // if we haven't seen any horizontal scrolling for >15 minutes, assume + // if we haven't seen any horizontal scrolling for a while, assume // the user might have plugged in a mousewheel if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) { this._likelyTrackpadUser = false; @@ -164,7 +164,7 @@ export default class IndicatorScrollbar extends React.Component { const additionalScroll = e.deltaY < 0 ? -50 : 50; // noinspection JSSuspiciousNameCombination - const val = Math.abs(e.deltaY) < 25 ? (e.deltaY + additionalScroll) : 0; + const val = Math.abs(e.deltaY) < 25 ? (e.deltaY + additionalScroll) : e.deltaY; this._scrollElement.scrollLeft += val * yRetention; } } From 3873dc724aaa4072d367e0980293d11e33239b2a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 26 Jun 2019 21:47:55 +0100 Subject: [PATCH 59/70] stupid linter >:( --- src/components/structures/IndicatorScrollbar.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 21a23e5670..1895d2089d 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -137,8 +137,7 @@ export default class IndicatorScrollbar extends React.Component { if (Math.abs(e.deltaX) > 0) { this._likelyTrackpadUser = true; this._checkAgainForTrackpad = now + (1 * 60 * 1000); - } - else { + } else { // if we haven't seen any horizontal scrolling for a while, assume // the user might have plugged in a mousewheel if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) { From e287362a8b63ea7feef7bc1c531241efbe390277 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 27 Jun 2019 11:19:36 +0100 Subject: [PATCH 60/70] Reaction buttons should use pointer cursor --- res/css/views/messages/_ReactionTooltipButton.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/messages/_ReactionTooltipButton.scss b/res/css/views/messages/_ReactionTooltipButton.scss index bf1c25e126..59244ab63b 100644 --- a/res/css/views/messages/_ReactionTooltipButton.scss +++ b/res/css/views/messages/_ReactionTooltipButton.scss @@ -18,6 +18,7 @@ limitations under the License. font-size: 16px; padding: 6px; user-select: none; + cursor: pointer; transition: transform 0.25s; &:hover { From c0e9edcf409a71ea14db9fe368c8f1f753be240a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Jun 2019 12:33:29 +0200 Subject: [PATCH 61/70] get decrypted content if needed --- src/components/views/messages/EditHistoryMessage.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js index 85a704641a..fef9c362c6 100644 --- a/src/components/views/messages/EditHistoryMessage.js +++ b/src/components/views/messages/EditHistoryMessage.js @@ -37,7 +37,8 @@ export default class EditHistoryMessage extends React.PureComponent { render() { const {mxEvent} = this.props; - const content = mxEvent.event.content["m.new_content"] || mxEvent.event.content; + const originalContent = mxEvent.getOriginalContent(); + const content = originalContent["m.new_content"] || originalContent; const contentElements = HtmlUtils.bodyToHtml(content); let contentContainer; if (mxEvent.getContent().msgtype === "m.emote") { From 25aa65ac0d22d6cd8365f735d71a443185268ec9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Jun 2019 14:13:37 +0200 Subject: [PATCH 62/70] remove leftover logging --- src/components/views/settings/tabs/user/VoiceUserSettingsTab.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index 0cafbacf8d..eb85fe4e44 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -128,7 +128,6 @@ export default class VoiceUserSettingsTab extends React.Component { let speakerDropdown = null; let microphoneDropdown = null; let webcamDropdown = null; - console.log({mediaDevices: this.state.mediaDevices}); if (this.state.mediaDevices === false) { requestButton = (
    From 59b4a3398d60469dfaddf5e3fd3ac2dabb538676 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 27 Jun 2019 13:05:04 +0100 Subject: [PATCH 63/70] Limit reactions row on initial display This limits the reactions row below messages to initially show at most 8 keys. For those messages with more than that, a "Show all" option appears to reveal all the keys. Fixes https://github.com/vector-im/riot-web/issues/9570 --- res/css/views/messages/_ReactionsRow.scss | 14 +++++++++ src/components/views/messages/ReactionsRow.js | 30 +++++++++++++++++-- src/i18n/strings/en_EN.json | 1 + 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/res/css/views/messages/_ReactionsRow.scss b/res/css/views/messages/_ReactionsRow.scss index 3b764e97b4..57c02ed3e5 100644 --- a/res/css/views/messages/_ReactionsRow.scss +++ b/res/css/views/messages/_ReactionsRow.scss @@ -18,3 +18,17 @@ limitations under the License. margin: 6px 0; color: $primary-fg-color; } + +.mx_ReactionsRow_showAll { + text-decoration: none; + font-size: 10px; + font-weight: 600; + margin-left: 6px; + vertical-align: top; + + &:hover, + &:link, + &:visited { + color: $accent-color; + } +} diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js index 51f62807a5..f210266c66 100644 --- a/src/components/views/messages/ReactionsRow.js +++ b/src/components/views/messages/ReactionsRow.js @@ -18,10 +18,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; import { isContentActionable } from '../../../utils/EventUtils'; import { isSingleEmoji } from '../../../HtmlUtils'; import MatrixClientPeg from '../../../MatrixClientPeg'; +// The maximum number of reactions to initially show on a message. +const MAX_ITEMS_WHEN_LIMITED = 8; + export default class ReactionsRow extends React.PureComponent { static propTypes = { // The event we're displaying reactions for @@ -41,6 +45,7 @@ export default class ReactionsRow extends React.PureComponent { this.state = { myReactions: this.getMyReactions(), + showAll: false, }; } @@ -94,16 +99,22 @@ export default class ReactionsRow extends React.PureComponent { return [...myReactions.values()]; } + onShowAllClick = () => { + this.setState({ + showAll: true, + }); + } + render() { const { mxEvent, reactions } = this.props; - const { myReactions } = this.state; + const { myReactions, showAll } = this.state; if (!reactions || !isContentActionable(mxEvent)) { return null; } const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton'); - const items = reactions.getSortedAnnotationsByKey().map(([content, events]) => { + let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => { if (!isSingleEmoji(content)) { return null; } @@ -125,10 +136,23 @@ export default class ReactionsRow extends React.PureComponent { reactionEvents={events} myReactionEvent={myReactionEvent} />; - }); + }).filter(item => !!item); + + let showAllLink; + if (items.length > MAX_ITEMS_WHEN_LIMITED && !showAll) { + items = items.slice(0, MAX_ITEMS_WHEN_LIMITED); + showAllLink = + {_t("Show all")} + ; + } return
    {items} + {showAllLink}
    ; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index de6a06e9e4..2e9f746054 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -944,6 +944,7 @@ "Party Popper": "Party Popper", "Confused": "Confused", "Eyes": "Eyes", + "Show all": "Show all", "reacted with %(shortName)s": "reacted with %(shortName)s", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", From 804a0c8507674277c66c9dacf69c88892c836e28 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 27 Jun 2019 18:56:22 +0100 Subject: [PATCH 64/70] Fix weird scrollbar when devtools is in a narrow browser Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/dialogs/_DevtoolsDialog.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 8e669acd10..c63a1b8e7d 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -81,10 +81,6 @@ limitations under the License. padding: 10px; } -.mx_DevTools_content .mx_Field_input { - display: inline-block; -} - .mx_DevTools_eventTypeStateKeyGroup { display: flex; flex-wrap: wrap; From 7e8eda33fce2153e1ee64f410ca9dab5e0013a88 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 27 Jun 2019 12:10:58 -0600 Subject: [PATCH 65/70] Flexboxify generic error page --- res/css/structures/_GenericErrorPage.scss | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/res/css/structures/_GenericErrorPage.scss b/res/css/structures/_GenericErrorPage.scss index 2b9e9f5e7d..7e9d7bbdaa 100644 --- a/res/css/structures/_GenericErrorPage.scss +++ b/res/css/structures/_GenericErrorPage.scss @@ -2,17 +2,15 @@ width: 100%; height: 100%; background-color: #fff; + display: flex; + align-items: center; + justify-content: center; } .mx_GenericErrorPage_box { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - margin: auto; + display: inline; width: 500px; - height: 125px; + min-height: 125px; border: 1px solid #f22; padding: 10px 10px 20px; background-color: #fcc; From 5473f7ba452e60c47c493bf5ce3397ab87227aac Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 27 Jun 2019 20:36:45 +0100 Subject: [PATCH 66/70] Unpin HLJS Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 96ec129f28..14d833df96 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279", "gfm.css": "^1.1.1", "glob": "^5.0.14", - "highlight.js": "9.14.2", + "highlight.js": "^9.15.8", "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.6", diff --git a/yarn.lock b/yarn.lock index 62868d0a92..7b949781d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3694,10 +3694,10 @@ he@1.1.1: resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= -highlight.js@9.14.2: - version "9.14.2" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.14.2.tgz#efbfb22dc701406e4da406056ef8c2b70ebe5b26" - integrity sha512-Nc6YNECYpxyJABGYJAyw7dBAYbXEuIzwzkqoJnwbc1nIpCiN+3ioYf0XrBnLiyyG0JLuJhpPtt2iTSbXiKLoyA== +highlight.js@^9.15.8: + version "9.15.8" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.8.tgz#f344fda123f36f1a65490e932cf90569e4999971" + integrity sha512-RrapkKQWwE+wKdF73VsOa2RQdIoO3mxwJ4P8mhbI6KYJUraUHRKM5w5zQQKXNk0xNL4UVRdulV9SBJcmzJNzVA== hmac-drbg@^1.0.0: version "1.0.1" From 527e1e94a4372e3f586dca8842d195e54e9bfbb4 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 28 Jun 2019 13:44:14 +0100 Subject: [PATCH 67/70] Update config.json docs location Part of https://github.com/vector-im/riot-web/pull/10195 --- src/utils/AutoDiscoveryUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index 06823e5d2a..e83e0348ca 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -67,7 +67,7 @@ export default class AutoDiscoveryUtils { {}, { a: (sub) => { return {sub}; From 04398b7853d9fcf3ad38376bac61d533943231e7 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 28 Jun 2019 14:46:57 +0100 Subject: [PATCH 68/70] Tweak limits so show all reveals more space than itself --- src/components/views/messages/ReactionsRow.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js index f210266c66..4bae8c0ca1 100644 --- a/src/components/views/messages/ReactionsRow.js +++ b/src/components/views/messages/ReactionsRow.js @@ -138,8 +138,11 @@ export default class ReactionsRow extends React.PureComponent { />; }).filter(item => !!item); + // Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items. + // The "+ 1" ensure that the "show all" reveals something that takes up + // more space than the button itself. let showAllLink; - if (items.length > MAX_ITEMS_WHEN_LIMITED && !showAll) { + if ((items.length > MAX_ITEMS_WHEN_LIMITED + 1) && !showAll) { items = items.slice(0, MAX_ITEMS_WHEN_LIMITED); showAllLink = Date: Fri, 28 Jun 2019 14:47:41 +0100 Subject: [PATCH 69/70] Rename link to button --- src/components/views/messages/ReactionsRow.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js index 4bae8c0ca1..57d2afc429 100644 --- a/src/components/views/messages/ReactionsRow.js +++ b/src/components/views/messages/ReactionsRow.js @@ -141,10 +141,10 @@ export default class ReactionsRow extends React.PureComponent { // Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items. // The "+ 1" ensure that the "show all" reveals something that takes up // more space than the button itself. - let showAllLink; + let showAllButton; if ((items.length > MAX_ITEMS_WHEN_LIMITED + 1) && !showAll) { items = items.slice(0, MAX_ITEMS_WHEN_LIMITED); - showAllLink = {items} - {showAllLink} + {showAllButton}
    ; } } From 00dfdfe7f171fad3822efde3a9bdf2851f1cd910 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 28 Jun 2019 15:16:44 +0100 Subject: [PATCH 70/70] Fix linter warning --- src/components/structures/IndicatorScrollbar.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 1895d2089d..d6efe8bee2 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -152,7 +152,6 @@ export default class IndicatorScrollbar extends React.Component { } if (Math.abs(e.deltaX) <= xyThreshold) { // we are vertically scrolling. - // HACK: We increase the amount of scroll to counteract smooth scrolling browsers. // Smooth scrolling browsers (Firefox) use the relative area to determine the scroll // amount, which means the likely small area of content results in a small amount of