{_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 = (
;
+ }
+}
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(
- "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"