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/res/css/_components.scss b/res/css/_components.scss index 582dc59517..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"; @@ -86,6 +87,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"; @@ -116,7 +118,8 @@ @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"; @import "./views/messages/_ReactionsRowButton.scss"; @import "./views/messages/_ReactionsRowButtonTooltip.scss"; 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; 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; 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; diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index a818f52125..b03d36a592 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -63,7 +63,6 @@ limitations under the License. display: flex; flex-direction: column; align-items: center; - margin-top: 5px; height: 100%; } 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/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; diff --git a/res/css/views/dialogs/_MessageEditHistoryDialog.scss b/res/css/views/dialogs/_MessageEditHistoryDialog.scss new file mode 100644 index 0000000000..b80742bd24 --- /dev/null +++ b/res/css/views/dialogs/_MessageEditHistoryDialog.scss @@ -0,0 +1,41 @@ +/* +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 .mx_Dialog_header > .mx_Dialog_title { + text-align: center; +} + +.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; + padding: 0; + color: $primary-fg-color; + + .mx_EventTile_line, .mx_EventTile_content { + margin-right: 0px; + } +} + diff --git a/res/css/views/elements/_InteractiveTooltip.scss b/res/css/views/elements/_InteractiveTooltip.scss new file mode 100644 index 0000000000..a3f5b6edc2 --- /dev/null +++ b/res/css/views/elements/_InteractiveTooltip.scss @@ -0,0 +1,101 @@ +/* +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: 3px; + background-color: $interactive-tooltip-bg-color; + color: $interactive-tooltip-fg-color; + position: absolute; + font-size: 10px; + font-weight: 600; + padding: 6px; + z-index: 5001; +} + +.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_top { + top: 10px; // 8px chevron + 2px spacing +} + +.mx_InteractiveTooltip_chevron_top { + position: absolute; + left: calc(50% - 8px); + top: -8px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-bottom: 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_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 +} + +.mx_InteractiveTooltip_chevron_bottom { + position: absolute; + left: calc(50% - 8px); + bottom: -8px; + width: 0; + height: 0; + border-left: 8px solid transparent; + 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 + } +} 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/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/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/messages/_ReactionQuickTooltip.scss b/res/css/views/messages/_ReactionQuickTooltip.scss new file mode 100644 index 0000000000..7b1611483b --- /dev/null +++ b/res/css/views/messages/_ReactionQuickTooltip.scss @@ -0,0 +1,29 @@ +/* +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_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/res/css/views/messages/_ReactionDimension.scss b/res/css/views/messages/_ReactionTooltipButton.scss similarity index 66% rename from res/css/views/messages/_ReactionDimension.scss rename to res/css/views/messages/_ReactionTooltipButton.scss index 9a891d05cf..59244ab63b 100644 --- a/res/css/views/messages/_ReactionDimension.scss +++ b/res/css/views/messages/_ReactionTooltipButton.scss @@ -1,5 +1,5 @@ /* -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. @@ -14,12 +14,18 @@ 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_ReactionTooltipButton { + font-size: 16px; + padding: 6px; + user-select: none; + cursor: pointer; + transition: transform 0.25s; + + &:hover { + transform: scale(1.2); + } } -.mx_ReactionDimension_disabled { +.mx_ReactionTooltipButton_selected { opacity: 0.4; } 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/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 62632eab27..1f75373be8 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; @@ -403,6 +401,7 @@ limitations under the License. color: $roomtopic-color; display: inline-block; margin-left: 9px; + cursor: pointer; } /* Various markdown overrides */ 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/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/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/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..2dd193b8c5 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -28,7 +28,8 @@ $focus-bg-color: #dddddd; // button UI (white-on-green in light skin) $accent-fg-color: #ffffff; -$accent-color-50pct: #92caad; +$accent-color-50pct: rgba(3, 179, 129, 0.5); //#03b381 in rgb +$accent-color-darker: #92caad; $accent-color-alt: #238CF5; $selection-fg-color: $primary-bg-color; @@ -272,6 +273,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 { 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/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 03ce258d37..d6efe8bee2 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,7 +131,27 @@ 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; - if (Math.abs(e.deltaX) <= xyThreshold) { + // 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 + (1 * 60 * 1000); + } 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) { + 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; + } + + 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 diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 8352872a2d..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) @@ -585,7 +586,7 @@ module.exports = React.createClass({ { 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,13 +367,16 @@ 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'}; + const payload = { + action: 'view_room', + auto_join: autoJoin, + }; 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 diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index cda3f60fce..03b371ef7e 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1523,9 +1523,11 @@ module.exports = React.createClass({
); diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js new file mode 100644 index 0000000000..9d533eab56 --- /dev/null +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -0,0 +1,108 @@ +/* +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 {wantsDateSeparator} from '../../../DateUtils'; +import SettingsStore from '../../../settings/SettingsStore'; + +export default class MessageEditHistoryDialog extends React.PureComponent { + static propTypes = { + mxEvent: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + events: [], + nextBatch: null, + isLoading: true, + isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"), + }; + } + + loadMoreEdits = async (backwards) => { + if (backwards || (!this.state.nextBatch && !this.state.isLoading)) { + // bail out on backwards as we only paginate in one direction + return false; + } + const opts = {from: this.state.nextBatch}; + const roomId = this.props.mxEvent.getRoomId(); + const eventId = this.props.mxEvent.getId(); + const result = await MatrixClientPeg.get().relations( + roomId, eventId, "m.replace", "m.room.message", opts); + 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() { + const EditHistoryMessage = sdk.getComponent('messages.EditHistoryMessage'); + const DateSeparator = sdk.getComponent('messages.DateSeparator'); + const nodes = []; + let lastEvent; + this.state.events.forEach(e => { + if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) { + nodes.push(
  • ); + } + nodes.push(); + lastEvent = e; + }); + return nodes; + } + + render() { + let content; + if (this.state.error) { + content = this.state.error; + } else if (this.state.isLoading) { + const Spinner = sdk.getComponent("elements.Spinner"); + content = ; + } else { + const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); + content = ( +
      {this._renderEdits()}
    +
    ); + } + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + return ( + + {content} + + ); + } +} 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/components/views/elements/InteractiveTooltip.js b/src/components/views/elements/InteractiveTooltip.js new file mode 100644 index 0000000000..52d51e0b39 --- /dev/null +++ b/src/components/views/elements/InteractiveTooltip.js @@ -0,0 +1,195 @@ +/* +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"; + +// 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); + + if (!container) { + container = document.createElement("div"); + container.id = InteractiveTooltipContainerId; + document.body.appendChild(container); + } + + return container; +} + +function isInRect(x, y, rect, buffer = 10) { + const { top, right, bottom, left } = rect; + return x >= (left - buffer) && x <= (right + buffer) + && y >= (top - buffer) && y <= (bottom + buffer); +} + +/* + * 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: { + // Content to show in the tooltip + content: PropTypes.node.isRequired, + // Function to call when visibility of the tooltip changes + onVisibilityChange: PropTypes.func, + }; + + constructor() { + super(); + + 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; + + this.setState({ + contentRect: element.getBoundingClientRect(), + }); + } + + 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 { contentRect, 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; + const targetBottom = targetRect.bottom + window.pageYOffset; + const targetTop = targetRect.top + window.pageYOffset; + + // 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 (contentRect && (targetTop - contentRect.height <= MIN_SAFE_DISTANCE_TO_WINDOW_EDGE)) { + position.top = targetBottom; + chevronFace = "top"; + } else { + position.bottom = window.innerHeight - targetTop; + chevronFace = "bottom"; + } + + // Center the tooltip horizontally with the target's center. + position.left = targetLeft + targetRect.width / 2; + + const chevron =
    ; + + const menuClasses = classNames({ + 'mx_InteractiveTooltip': true, + 'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top', + 'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom', + }); + + const menuStyle = {}; + if (contentRect) { + menuStyle.left = `-${contentRect.width / 2}px`; + } + + const tooltip =
    +
    +
    + {chevron} + {this.props.content} +
    +
    ; + + 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, + }); + } +} diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js new file mode 100644 index 0000000000..fef9c362c6 --- /dev/null +++ b/src/components/views/messages/EditHistoryMessage.js @@ -0,0 +1,61 @@ +/* +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.PureComponent { + static propTypes = { + // the message event being edited + mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired, + }; + + componentDidMount() { + pillifyLinks(this.refs.content.children, this.props.mxEvent); + } + + componentDidUpdate() { + pillifyLinks(this.refs.content.children, this.props.mxEvent); + } + + render() { + const {mxEvent} = this.props; + const originalContent = mxEvent.getOriginalContent(); + const content = originalContent["m.new_content"] || originalContent; + 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} + { contentContainer } +
    +
  • ; + } +} diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 80f0ba538c..e7843c1505 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 = { @@ -131,43 +131,28 @@ export default class MessageActionBar extends React.PureComponent { return SettingsStore.isFeatureEnabled("feature_message_editing"); } - renderAgreeDimension() { + renderReactButton() { if (!this.isReactionsEnabled()) { return null; } - const ReactionDimension = sdk.getComponent('messages.ReactionDimension'); - return ; - } + const ReactMessageAction = sdk.getComponent('messages.ReactMessageAction'); + const { mxEvent, reactions } = this.props; - renderLikeDimension() { - if (!this.isReactionsEnabled()) { - return null; - } - - const ReactionDimension = sdk.getComponent('messages.ReactionDimension'); - return ; } render() { - let agreeDimensionReactionButtons; - let likeDimensionReactionButtons; + let reactButton; let replyButton; let editButton; if (isContentActionable(this.props.mxEvent)) { - agreeDimensionReactionButtons = this.renderAgreeDimension(); - likeDimensionReactionButtons = this.renderLikeDimension(); + reactButton = this.renderReactButton(); replyButton = - {agreeDimensionReactionButtons} - {likeDimensionReactionButtons} + {reactButton} {replyButton} {editButton} { + 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/ReactionDimension.js b/src/components/views/messages/ReactionDimension.js deleted file mode 100644 index de33ad1a57..0000000000 --- a/src/components/views/messages/ReactionDimension.js +++ /dev/null @@ -1,176 +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. -*/ - -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import MatrixClientPeg from '../../../MatrixClientPeg'; - -export default class ReactionDimension extends React.PureComponent { - static propTypes = { - mxEvent: PropTypes.object.isRequired, - // Array of strings containing the emoji for each option - options: PropTypes.array.isRequired, - title: PropTypes.string, - // The Relations model from the JS SDK for reactions - reactions: PropTypes.object, - }; - - constructor(props) { - super(props); - - this.state = this.getSelection(); - - if (props.reactions) { - props.reactions.on("Relations.add", this.onReactionsChange); - props.reactions.on("Relations.remove", this.onReactionsChange); - props.reactions.on("Relations.redaction", this.onReactionsChange); - } - } - - 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(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/components/views/messages/ReactionTooltipButton.js b/src/components/views/messages/ReactionTooltipButton.js new file mode 100644 index 0000000000..e09b9ade69 --- /dev/null +++ b/src/components/views/messages/ReactionTooltipButton.js @@ -0,0 +1,68 @@ +/* +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..0505bbd2df --- /dev/null +++ b/src/components/views/messages/ReactionsQuickTooltip.js @@ -0,0 +1,195 @@ +/* +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'; +import { unicodeToShortcode } from '../../../HtmlUtils'; + +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 = { + hoveredItem: null, + 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()]; + } + + onMouseOver = (ev) => { + const { key } = ev.target.dataset; + const item = this.items.find(({ content }) => content === key); + this.setState({ + hoveredItem: item, + }); + } + + onMouseOut = (ev) => { + this.setState({ + hoveredItem: null, + }); + } + + get items() { + return [ + { + 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"), + }, + ]; + } + + 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; + } + return mxEvent.getRelation().key === content; + }); + + return ; + }); + + 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} +
    ; + } +} diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js index 51f62807a5..57d2afc429 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,26 @@ export default class ReactionsRow extends React.PureComponent { reactionEvents={events} myReactionEvent={myReactionEvent} />; - }); + }).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 showAllButton; + if ((items.length > MAX_ITEMS_WHEN_LIMITED + 1) && !showAll) { + items = items.slice(0, MAX_ITEMS_WHEN_LIMITED); + showAllButton = + {_t("Show all")} + ; + } return
    {items} + {showAllButton}
    ; } } diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index d76956d193..25316844df 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 = []; @@ -454,6 +355,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) { @@ -462,12 +368,13 @@ module.exports = React.createClass({ const date = editEvent && formatDate(editEvent.getDate()); editedTooltip = ; } return (
    {editedTooltip}{`(${_t("edited")})`}
    diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 7d11ddac61..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') { @@ -829,7 +829,7 @@ module.exports.haveTileForEvent = function(e) { if (e.isRedacted() && !isMessageEvent(e)) return false; // No tile for replacement events since they update the original tile - if (e.isRelation("m.replace")) return false; + if (e.isRelation("m.replace") && SettingsStore.isFeatureEnabled("feature_message_editing")) return false; const handler = getHandlerTile(e); if (handler === undefined) return false; diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index cbc44d0933..fb5bc3ae0d 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -66,6 +66,7 @@ module.exports = React.createClass({ error: PropTypes.object, canPreview: PropTypes.bool, + previewLoading: PropTypes.bool, room: PropTypes.object, // When a spinner is present, a spinnerState can be specified to indicate the @@ -254,6 +255,8 @@ module.exports = React.createClass({ }, render: function() { + const Spinner = sdk.getComponent('elements.Spinner'); + let showSpinner = false; let darkStyle = false; let title; @@ -262,6 +265,7 @@ module.exports = React.createClass({ let primaryActionLabel; let secondaryActionHandler; let secondaryActionLabel; + let footer; const messageCase = this._getMessageCase(); switch (messageCase) { @@ -287,6 +291,14 @@ module.exports = React.createClass({ primaryActionHandler = this.onRegisterClick; secondaryActionLabel = _t("Sign In"); secondaryActionHandler = this.onLoginClick; + if (this.props.previewLoading) { + footer = ( +
    + + {_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/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")} +
    ); diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index 31a11b13ea..eb85fe4e44 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 (); }); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2881ca83fe..24ddaa22eb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -616,9 +616,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", @@ -836,6 +833,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", @@ -929,8 +927,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", @@ -941,6 +937,13 @@ "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", + "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.", @@ -951,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", @@ -1200,6 +1203,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", @@ -1209,7 +1213,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", diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js index 5ecb91b305..7a98c0dba6 100644 --- a/src/shouldHideEvent.js +++ b/src/shouldHideEvent.js @@ -46,8 +46,8 @@ export default function shouldHideEvent(ev) { // Hide redacted events if (ev.isRedacted() && !isEnabled('showRedactions')) return true; - // Hide replacement events since they update the original tile - if (ev.isRelation("m.replace")) return true; + // 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); 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}; diff --git a/src/utils/pillify.js b/src/utils/pillify.js new file mode 100644 index 0000000000..e943cfe657 --- /dev/null +++ b/src/utils/pillify.js @@ -0,0 +1,118 @@ +/* +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"); + 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, mxEvent); + } + + node = node.nextSibling; + } +} 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"