mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 19:56:47 +03:00
Add comments and newlines back and resolve merge conflicts
This commit is contained in:
commit
4ea3fbc5ba
19 changed files with 493 additions and 263 deletions
112
CHANGELOG.md
112
CHANGELOG.md
|
@ -1,3 +1,115 @@
|
|||
Changes in [3.20.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0) (2021-05-10)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0-rc.1...v3.20.0)
|
||||
|
||||
* Upgrade to JS SDK 10.1.0
|
||||
* [Release] Don't use the event's metadata to calc the scale of an image
|
||||
[\#6004](https://github.com/matrix-org/matrix-react-sdk/pull/6004)
|
||||
|
||||
Changes in [3.20.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0-rc.1) (2021-05-04)
|
||||
===============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0...v3.20.0-rc.1)
|
||||
|
||||
* Upgrade to JS SDK 10.1.0-rc.1
|
||||
* Translations update from Weblate
|
||||
[\#5966](https://github.com/matrix-org/matrix-react-sdk/pull/5966)
|
||||
* Fix more space panel layout and hover behaviour issues
|
||||
[\#5965](https://github.com/matrix-org/matrix-react-sdk/pull/5965)
|
||||
* Fix edge case with space panel alignment with subspaces on ff
|
||||
[\#5964](https://github.com/matrix-org/matrix-react-sdk/pull/5964)
|
||||
* Fix saving room pill part to history
|
||||
[\#5951](https://github.com/matrix-org/matrix-react-sdk/pull/5951)
|
||||
* Generate room preview even when minimized
|
||||
[\#5948](https://github.com/matrix-org/matrix-react-sdk/pull/5948)
|
||||
* Another change from recovery passphrase to Security Phrase
|
||||
[\#5934](https://github.com/matrix-org/matrix-react-sdk/pull/5934)
|
||||
* Sort rooms in the add existing to space dialog based on recency
|
||||
[\#5943](https://github.com/matrix-org/matrix-react-sdk/pull/5943)
|
||||
* Inhibit sending RR when context switching to a room
|
||||
[\#5944](https://github.com/matrix-org/matrix-react-sdk/pull/5944)
|
||||
* Prevent room list keyboard handling from landing focus on hidden nodes
|
||||
[\#5950](https://github.com/matrix-org/matrix-react-sdk/pull/5950)
|
||||
* Make the text filter search all spaces instead of just the selected one
|
||||
[\#5942](https://github.com/matrix-org/matrix-react-sdk/pull/5942)
|
||||
* Enable indent rule and fix indent
|
||||
[\#5931](https://github.com/matrix-org/matrix-react-sdk/pull/5931)
|
||||
* Prevent peeking members from reacting
|
||||
[\#5946](https://github.com/matrix-org/matrix-react-sdk/pull/5946)
|
||||
* Disallow inline display maths
|
||||
[\#5939](https://github.com/matrix-org/matrix-react-sdk/pull/5939)
|
||||
* Space creation prompt user to add existing rooms for "Just Me" spaces
|
||||
[\#5923](https://github.com/matrix-org/matrix-react-sdk/pull/5923)
|
||||
* Add test coverage collection script
|
||||
[\#5937](https://github.com/matrix-org/matrix-react-sdk/pull/5937)
|
||||
* Fix joining room using via servers regression
|
||||
[\#5936](https://github.com/matrix-org/matrix-react-sdk/pull/5936)
|
||||
* Revert "Fixes the two Todays problem in Redaction"
|
||||
[\#5938](https://github.com/matrix-org/matrix-react-sdk/pull/5938)
|
||||
* Handle encoded matrix URLs
|
||||
[\#5903](https://github.com/matrix-org/matrix-react-sdk/pull/5903)
|
||||
* Render ignored users setting regardless of if there are any
|
||||
[\#5860](https://github.com/matrix-org/matrix-react-sdk/pull/5860)
|
||||
* Fix inserting trailing colon after mention/pill
|
||||
[\#5830](https://github.com/matrix-org/matrix-react-sdk/pull/5830)
|
||||
* Fixes the two Todays problem in Redaction
|
||||
[\#5917](https://github.com/matrix-org/matrix-react-sdk/pull/5917)
|
||||
* Fix page up/down scrolling only half a page
|
||||
[\#5920](https://github.com/matrix-org/matrix-react-sdk/pull/5920)
|
||||
* Voice messages: Composer controls
|
||||
[\#5935](https://github.com/matrix-org/matrix-react-sdk/pull/5935)
|
||||
* Support MSC3086 asserted identity
|
||||
[\#5886](https://github.com/matrix-org/matrix-react-sdk/pull/5886)
|
||||
* Handle possible edge case with getting stuck in "unsent messages" bar
|
||||
[\#5930](https://github.com/matrix-org/matrix-react-sdk/pull/5930)
|
||||
* Fix suggested rooms not showing up regression from room list optimisation
|
||||
[\#5932](https://github.com/matrix-org/matrix-react-sdk/pull/5932)
|
||||
* Broadcast language change to ElectronPlatform
|
||||
[\#5913](https://github.com/matrix-org/matrix-react-sdk/pull/5913)
|
||||
* Fix VoIP PIP frame color
|
||||
[\#5701](https://github.com/matrix-org/matrix-react-sdk/pull/5701)
|
||||
* Convert some Flow-typed files to TypeScript
|
||||
[\#5912](https://github.com/matrix-org/matrix-react-sdk/pull/5912)
|
||||
* Initial SpaceStore tests work
|
||||
[\#5906](https://github.com/matrix-org/matrix-react-sdk/pull/5906)
|
||||
* Fix issues with space hierarchy in layout and with incompatible servers
|
||||
[\#5926](https://github.com/matrix-org/matrix-react-sdk/pull/5926)
|
||||
* Scale all mxc thumbs using device pixel ratio for hidpi
|
||||
[\#5928](https://github.com/matrix-org/matrix-react-sdk/pull/5928)
|
||||
* Fix add existing to space dialog no longer showing rooms for public spaces
|
||||
[\#5918](https://github.com/matrix-org/matrix-react-sdk/pull/5918)
|
||||
* Disable spaces context switching for when exploring a space
|
||||
[\#5924](https://github.com/matrix-org/matrix-react-sdk/pull/5924)
|
||||
* Autofocus search box in the add existing to space dialog
|
||||
[\#5921](https://github.com/matrix-org/matrix-react-sdk/pull/5921)
|
||||
* Use label element in add existing to space dialog for easier hit target
|
||||
[\#5922](https://github.com/matrix-org/matrix-react-sdk/pull/5922)
|
||||
* Dynamic max and min zoom in the new ImageView
|
||||
[\#5916](https://github.com/matrix-org/matrix-react-sdk/pull/5916)
|
||||
* Improve message error states
|
||||
[\#5897](https://github.com/matrix-org/matrix-react-sdk/pull/5897)
|
||||
* Check for null room in `VisibilityProvider`
|
||||
[\#5914](https://github.com/matrix-org/matrix-react-sdk/pull/5914)
|
||||
* Add unit tests for various collection-based utility functions
|
||||
[\#5910](https://github.com/matrix-org/matrix-react-sdk/pull/5910)
|
||||
* Spaces visual fixes
|
||||
[\#5909](https://github.com/matrix-org/matrix-react-sdk/pull/5909)
|
||||
* Remove reliance on DOM API to generated message preview
|
||||
[\#5908](https://github.com/matrix-org/matrix-react-sdk/pull/5908)
|
||||
* Expand upon voice message event & include overall waveform
|
||||
[\#5888](https://github.com/matrix-org/matrix-react-sdk/pull/5888)
|
||||
* Use floats for image background opacity
|
||||
[\#5905](https://github.com/matrix-org/matrix-react-sdk/pull/5905)
|
||||
* Show invites to spaces at the top of the space panel
|
||||
[\#5902](https://github.com/matrix-org/matrix-react-sdk/pull/5902)
|
||||
* Improve edge cases with spaces context switching
|
||||
[\#5899](https://github.com/matrix-org/matrix-react-sdk/pull/5899)
|
||||
* Fix spaces notification dots wrongly including upgraded (hidden) rooms
|
||||
[\#5900](https://github.com/matrix-org/matrix-react-sdk/pull/5900)
|
||||
* Iterate the spaces face pile design
|
||||
[\#5898](https://github.com/matrix-org/matrix-react-sdk/pull/5898)
|
||||
* Fix alignment issue with nested spaces being cut off wrong
|
||||
[\#5890](https://github.com/matrix-org/matrix-react-sdk/pull/5890)
|
||||
|
||||
Changes in [3.19.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0) (2021-04-26)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0-rc.1...v3.19.0)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.19.0",
|
||||
"version": "3.20.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -97,7 +97,7 @@
|
|||
"react-transition-group": "^4.4.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"rfc4648": "^1.4.0",
|
||||
"sanitize-html": "github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db",
|
||||
"sanitize-html": "^2.3.2",
|
||||
"tar-js": "^0.3.0",
|
||||
"text-encoding-utf-8": "^1.0.2",
|
||||
"url": "^0.11.0",
|
||||
|
|
|
@ -545,11 +545,13 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
if (!grouper) {
|
||||
const wantTile = this._shouldShowEvent(mxEv);
|
||||
const isGrouped = false;
|
||||
if (wantTile) {
|
||||
// make sure we unpack the array returned by _getTilesForEvent,
|
||||
// otherwise react will auto-generate keys and we will end up
|
||||
// replacing all of the DOM elements every time we paginate.
|
||||
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextTile));
|
||||
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, isGrouped,
|
||||
nextEvent, nextTile));
|
||||
prevEvent = mxEv;
|
||||
}
|
||||
|
||||
|
@ -570,7 +572,7 @@ export default class MessagePanel extends React.Component {
|
|||
_${mxEv.getId()}`) !== null;
|
||||
}
|
||||
|
||||
_getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) {
|
||||
_getTilesForEvent(prevEvent, mxEv, last, isGrouped=false, nextEvent, nextEventWithTile) {
|
||||
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
|
@ -593,7 +595,7 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
// do we need a date separator since the last event?
|
||||
const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate);
|
||||
if (wantsDateSeparator) {
|
||||
if (wantsDateSeparator && !isGrouped) {
|
||||
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
|
||||
ret.push(dateSeparator);
|
||||
}
|
||||
|
@ -977,9 +979,9 @@ class CreationGrouper {
|
|||
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
||||
|
||||
const panel = this.panel;
|
||||
const ret = [];
|
||||
const isGrouped = true;
|
||||
const createEvent = this.createEvent;
|
||||
const lastShownEvent = this.lastShownEvent;
|
||||
|
||||
|
@ -993,12 +995,12 @@ class CreationGrouper {
|
|||
// If this m.room.create event should be shown (room upgrade) then show it before the summary
|
||||
if (panel._shouldShowEvent(createEvent)) {
|
||||
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
|
||||
ret.push(...panel._getTilesForEvent(createEvent, createEvent, false));
|
||||
ret.push(...panel._getTilesForEvent(createEvent, createEvent));
|
||||
}
|
||||
|
||||
for (const ejected of this.ejectedEvents) {
|
||||
ret.push(...panel._getTilesForEvent(
|
||||
createEvent, ejected, createEvent === lastShownEvent,
|
||||
createEvent, ejected, createEvent === lastShownEvent, isGrouped,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -1007,7 +1009,7 @@ class CreationGrouper {
|
|||
// of EventListSummary, render each member event as if the previous
|
||||
// one was itself. This way, the timestamp of the previous event === the
|
||||
// timestamp of the current event, and no DateSeparator is inserted.
|
||||
return panel._getTilesForEvent(e, e, e === lastShownEvent);
|
||||
return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
|
||||
}).reduce((a, b) => a.concat(b), []);
|
||||
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
|
||||
const ev = this.events[this.events.length - 1];
|
||||
|
@ -1092,7 +1094,7 @@ class RedactionGrouper {
|
|||
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
||||
|
||||
const isGrouped = true;
|
||||
const panel = this.panel;
|
||||
const ret = [];
|
||||
const lastShownEvent = this.lastShownEvent;
|
||||
|
@ -1112,7 +1114,8 @@ class RedactionGrouper {
|
|||
let eventTiles = this.events.map((e, i) => {
|
||||
senders.add(e.sender);
|
||||
const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1];
|
||||
return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent, this.nextEvent, this.nextEventTile);
|
||||
return panel._getTilesForEvent(
|
||||
prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile);
|
||||
}).reduce((a, b) => a.concat(b), []);
|
||||
|
||||
if (eventTiles.length === 0) {
|
||||
|
@ -1191,7 +1194,7 @@ class MemberGrouper {
|
|||
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||
|
||||
const isGrouped = true;
|
||||
const panel = this.panel;
|
||||
const lastShownEvent = this.lastShownEvent;
|
||||
const ret = [];
|
||||
|
@ -1224,7 +1227,7 @@ class MemberGrouper {
|
|||
// of MemberEventListSummary, render each member event as if the previous
|
||||
// one was itself. This way, the timestamp of the previous event === the
|
||||
// timestamp of the current event, and no DateSeparator is inserted.
|
||||
return panel._getTilesForEvent(e, e, e === lastShownEvent);
|
||||
return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
|
||||
}).reduce((a, b) => a.concat(b), []);
|
||||
|
||||
if (eventTiles.length === 0) {
|
||||
|
|
|
@ -27,8 +27,8 @@ import { Action } from "../../dispatcher/actions";
|
|||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
|
||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import SpaceStore, {UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../stores/SpaceStore";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/SpaceStore";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -16,20 +16,23 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import filesize from "filesize";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { getBlobSafeMimeType } from '../../../utils/blobs';
|
||||
|
||||
interface IProps {
|
||||
file: File;
|
||||
currentIndex: number;
|
||||
totalFiles?: number;
|
||||
onFinished: (uploadConfirmed: boolean, uploadAll?: boolean) => void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.UploadConfirmDialog")
|
||||
export default class UploadConfirmDialog extends React.Component {
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
currentIndex: PropTypes.number,
|
||||
totalFiles: PropTypes.number,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
export default class UploadConfirmDialog extends React.Component<IProps> {
|
||||
private objectUrl: string;
|
||||
private mimeType: string;
|
||||
|
||||
static defaultProps = {
|
||||
totalFiles: 1,
|
||||
|
@ -38,22 +41,28 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._objectUrl = URL.createObjectURL(props.file);
|
||||
// Create a fresh `Blob` for previewing (even though `File` already is
|
||||
// one) so we can adjust the MIME type if needed.
|
||||
this.mimeType = getBlobSafeMimeType(props.file.type);
|
||||
const blob = new Blob([props.file], { type:
|
||||
this.mimeType,
|
||||
});
|
||||
this.objectUrl = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._objectUrl) URL.revokeObjectURL(this._objectUrl);
|
||||
if (this.objectUrl) URL.revokeObjectURL(this.objectUrl);
|
||||
}
|
||||
|
||||
_onCancelClick = () => {
|
||||
private onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onUploadClick = () => {
|
||||
private onUploadClick = () => {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
_onUploadAllClick = () => {
|
||||
private onUploadAllClick = () => {
|
||||
this.props.onFinished(true, true);
|
||||
}
|
||||
|
||||
|
@ -75,10 +84,10 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
}
|
||||
|
||||
let preview;
|
||||
if (this.props.file.type.startsWith('image/')) {
|
||||
if (this.mimeType.startsWith('image/')) {
|
||||
preview = <div className="mx_UploadConfirmDialog_previewOuter">
|
||||
<div className="mx_UploadConfirmDialog_previewInner">
|
||||
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this._objectUrl} /></div>
|
||||
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this.objectUrl} /></div>
|
||||
<div>{this.props.file.name} ({filesize(this.props.file.size)})</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
@ -95,7 +104,7 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
|
||||
let uploadAllButton;
|
||||
if (this.props.currentIndex + 1 < this.props.totalFiles) {
|
||||
uploadAllButton = <button onClick={this._onUploadAllClick}>
|
||||
uploadAllButton = <button onClick={this.onUploadAllClick}>
|
||||
{_t("Upload all")}
|
||||
</button>;
|
||||
}
|
||||
|
@ -103,7 +112,7 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
return (
|
||||
<BaseDialog className='mx_UploadConfirmDialog'
|
||||
fixedWidth={false}
|
||||
onFinished={this._onCancelClick}
|
||||
onFinished={this.onCancelClick}
|
||||
title={title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
|
@ -113,7 +122,7 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
|
||||
<DialogButtons primaryButton={_t('Upload')}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this._onUploadClick}
|
||||
onPrimaryButtonClick={this.onUploadClick}
|
||||
focus={true}
|
||||
>
|
||||
{uploadAllButton}
|
|
@ -108,8 +108,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
window.addEventListener("resize", this.calculateZoom);
|
||||
// After the image loads for the first time we want to calculate the zoom
|
||||
this.image.current.addEventListener("load", this.calculateZoom);
|
||||
// Try to precalculate the zoom from width and height props
|
||||
this.calculateZoom();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -122,11 +120,8 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
const image = this.image.current;
|
||||
const imageWrapper = this.imageWrapper.current;
|
||||
|
||||
const width = this.props.width || image.naturalWidth;
|
||||
const height = this.props.height || image.naturalHeight;
|
||||
|
||||
const zoomX = imageWrapper.clientWidth / width;
|
||||
const zoomY = imageWrapper.clientHeight / height;
|
||||
const zoomX = imageWrapper.clientWidth / image.naturalWidth;
|
||||
const zoomY = imageWrapper.clientHeight / image.naturalHeight;
|
||||
|
||||
// If the image is smaller in both dimensions set its the zoom to 1 to
|
||||
// display it in its original size
|
||||
|
|
|
@ -187,9 +187,15 @@ function DeviceItem({userId, device}: {userId: string, device: IDevice}) {
|
|||
verifyDevice(cli.getUser(userId), device);
|
||||
};
|
||||
|
||||
const deviceName = device.ambiguous ?
|
||||
(device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" :
|
||||
device.getDisplayName();
|
||||
let deviceName;
|
||||
if (!device.getDisplayName()?.trim()) {
|
||||
deviceName = device.deviceId;
|
||||
} else {
|
||||
deviceName = device.ambiguous ?
|
||||
device.getDisplayName() + " (" + device.deviceId + ")" :
|
||||
device.getDisplayName();
|
||||
}
|
||||
|
||||
let trustedLabel = null;
|
||||
if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted");
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import EditorModel from '../../../editor/model';
|
||||
|
@ -24,17 +24,19 @@ import {getCaretOffsetAndText} from '../../../editor/dom';
|
|||
import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize';
|
||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||
import {parseEvent} from '../../../editor/deserialize';
|
||||
import {PartCreator} from '../../../editor/parts';
|
||||
import {CommandPartCreator} from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import classNames from 'classnames';
|
||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {CommandCategories, getCommand} from '../../../SlashCommands';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SendHistoryManager from '../../../SendHistoryManager';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
function _isReply(mxEvent) {
|
||||
const relatesTo = mxEvent.getContent()["m.relates_to"];
|
||||
|
@ -212,6 +214,22 @@ export default class EditMessageComposer extends React.Component {
|
|||
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
|
||||
}
|
||||
|
||||
_isSlashCommand() {
|
||||
const parts = this.model.parts;
|
||||
const firstPart = parts[0];
|
||||
if (firstPart) {
|
||||
if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
|
||||
&& (firstPart.type === "plain" || firstPart.type === "pill-candidate")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_isContentModified(newContent) {
|
||||
// if nothing has changed then bail
|
||||
const oldContent = this.props.editState.getEvent().getContent();
|
||||
|
@ -224,19 +242,114 @@ export default class EditMessageComposer extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
_sendEdit = () => {
|
||||
_getSlashCommand() {
|
||||
const commandText = this.model.parts.reduce((text, part) => {
|
||||
// use mxid to textify user pills in a command
|
||||
if (part.type === "user-pill") {
|
||||
return text + part.resourceId;
|
||||
}
|
||||
return text + part.text;
|
||||
}, "");
|
||||
const {cmd, args} = getCommand(commandText);
|
||||
return [cmd, args, commandText];
|
||||
}
|
||||
|
||||
async _runSlashCommand(cmd, args, roomId) {
|
||||
const result = cmd.run(roomId, args);
|
||||
let messageContent;
|
||||
let error = result.error;
|
||||
if (result.promise) {
|
||||
try {
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
messageContent = await result.promise;
|
||||
} else {
|
||||
await result.promise;
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
console.error("Command failure: %s", error);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// assume the error is a server error when the command is async
|
||||
const isServerError = !!result.promise;
|
||||
const title = isServerError ? _td("Server error") : _td("Command error");
|
||||
|
||||
let errText;
|
||||
if (typeof error === 'string') {
|
||||
errText = error;
|
||||
} else if (error.message) {
|
||||
errText = error.message;
|
||||
} else {
|
||||
errText = _t("Server unavailable, overloaded, or something else went wrong.");
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog(title, '', ErrorDialog, {
|
||||
title: _t(title),
|
||||
description: errText,
|
||||
});
|
||||
} else {
|
||||
console.log("Command success.");
|
||||
if (messageContent) return messageContent;
|
||||
}
|
||||
}
|
||||
|
||||
_sendEdit = async () => {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const editedEvent = this.props.editState.getEvent();
|
||||
const editContent = createEditContent(this.model, editedEvent);
|
||||
const newContent = editContent["m.new_content"];
|
||||
|
||||
let shouldSend = true;
|
||||
|
||||
// If content is modified then send an updated event into the room
|
||||
if (this._isContentModified(newContent)) {
|
||||
const roomId = editedEvent.getRoomId();
|
||||
this._cancelPreviousPendingEdit();
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
dis.dispatch({action: "message_sent"});
|
||||
this._clearStoredEditorState();
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
if (!containsEmote(this.model) && this._isSlashCommand()) {
|
||||
const [cmd, args, commandText] = this._getSlashCommand();
|
||||
if (cmd) {
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
editContent["m.new_content"] = await this._runSlashCommand(cmd, args, roomId);
|
||||
} else {
|
||||
this._runSlashCommand(cmd, args, roomId);
|
||||
shouldSend = false;
|
||||
}
|
||||
} else {
|
||||
// ask the user if their unknown command should be sent as a message
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, {
|
||||
title: _t("Unknown Command"),
|
||||
description: <div>
|
||||
<p>
|
||||
{ _t("Unrecognised command: %(commandText)s", {commandText}) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("You can use <code>/help</code> to list available commands. " +
|
||||
"Did you mean to send this as a message?", {}, {
|
||||
code: t => <code>{ t }</code>,
|
||||
}) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("Hint: Begin your message with <code>//</code> to start it with a slash.", {}, {
|
||||
code: t => <code>{ t }</code>,
|
||||
}) }
|
||||
</p>
|
||||
</div>,
|
||||
button: _t('Send as message'),
|
||||
});
|
||||
const [sendAnyway] = await finished;
|
||||
// if !sendAnyway bail to let the user edit the composer and try again
|
||||
if (!sendAnyway) return;
|
||||
}
|
||||
}
|
||||
if (shouldSend) {
|
||||
this._cancelPreviousPendingEdit();
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
this._clearStoredEditorState();
|
||||
dis.dispatch({action: "message_sent"});
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
}
|
||||
}
|
||||
|
||||
// close the event editing and focus composer
|
||||
|
@ -278,13 +391,14 @@ export default class EditMessageComposer extends React.Component {
|
|||
_createEditorModel() {
|
||||
const {editState} = this.props;
|
||||
const room = this._getRoom();
|
||||
const partCreator = new PartCreator(room, this.context);
|
||||
const partCreator = new CommandPartCreator(room, this.context);
|
||||
let parts;
|
||||
if (editState.hasEditorState()) {
|
||||
// if restoring state from a previous editor,
|
||||
// restore serialized parts from the state
|
||||
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
|
||||
} else {
|
||||
//otherwise, either restore serialized parts from localStorage or parse the body of the event
|
||||
parts = this._restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator);
|
||||
}
|
||||
this.model = new EditorModel(parts, partCreator);
|
||||
|
|
|
@ -26,13 +26,11 @@ import {SpaceItem} from "./SpaceTreeLevel";
|
|||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore, {
|
||||
HOME_SPACE,
|
||||
UPDATE_INVITED_SPACES,
|
||||
UPDATE_SELECTED_SPACE,
|
||||
UPDATE_TOP_LEVEL_SPACES,
|
||||
} from "../../../stores/SpaceStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
|
||||
import NotificationBadge from "../rooms/NotificationBadge";
|
||||
import {
|
||||
RovingAccessibleButton,
|
||||
|
@ -40,13 +38,15 @@ import {
|
|||
RovingTabIndexProvider,
|
||||
} from "../../../accessibility/RovingTabIndex";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {RoomNotificationStateStore} from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import {NotificationState} from "../../../stores/notifications/NotificationState";
|
||||
|
||||
interface IButtonProps {
|
||||
space?: Room;
|
||||
className?: string;
|
||||
selected?: boolean;
|
||||
tooltip?: string;
|
||||
notificationState?: SpaceNotificationState;
|
||||
notificationState?: NotificationState;
|
||||
isNarrow?: boolean;
|
||||
onClick(): void;
|
||||
}
|
||||
|
@ -212,8 +212,8 @@ const SpacePanel = () => {
|
|||
className="mx_SpaceButton_home"
|
||||
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
||||
selected={!activeSpace}
|
||||
tooltip={_t("Home")}
|
||||
notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
|
||||
tooltip={_t("All rooms")}
|
||||
notificationState={RoomNotificationStateStore.instance.globalState}
|
||||
isNarrow={isPanelCollapsed}
|
||||
/>
|
||||
{ invites.map(s => <SpaceItem
|
||||
|
|
|
@ -1012,7 +1012,7 @@
|
|||
"Create": "Create",
|
||||
"Expand space panel": "Expand space panel",
|
||||
"Collapse space panel": "Collapse space panel",
|
||||
"Home": "Home",
|
||||
"All rooms": "All rooms",
|
||||
"Click to copy": "Click to copy",
|
||||
"Copied!": "Copied!",
|
||||
"Failed to copy": "Failed to copy",
|
||||
|
@ -1441,6 +1441,13 @@
|
|||
"Someone is using an unknown session": "Someone is using an unknown session",
|
||||
"This room is end-to-end encrypted": "This room is end-to-end encrypted",
|
||||
"Everyone in this room is verified": "Everyone in this room is verified",
|
||||
"Server error": "Server error",
|
||||
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
||||
"Unknown Command": "Unknown Command",
|
||||
"Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s",
|
||||
"You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?",
|
||||
"Hint: Begin your message with <code>//</code> to start it with a slash.": "Hint: Begin your message with <code>//</code> to start it with a slash.",
|
||||
"Send as message": "Send as message",
|
||||
"Edit message": "Edit message",
|
||||
"Mod": "Mod",
|
||||
"This event could not be displayed": "This event could not be displayed",
|
||||
|
@ -1631,13 +1638,6 @@
|
|||
"This Room": "This Room",
|
||||
"All Rooms": "All Rooms",
|
||||
"Search…": "Search…",
|
||||
"Server error": "Server error",
|
||||
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
||||
"Unknown Command": "Unknown Command",
|
||||
"Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s",
|
||||
"You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?",
|
||||
"Hint: Begin your message with <code>//</code> to start it with a slash.": "Hint: Begin your message with <code>//</code> to start it with a slash.",
|
||||
"Send as message": "Send as message",
|
||||
"Failed to connect to integration manager": "Failed to connect to integration manager",
|
||||
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
|
||||
"Add some now": "Add some now",
|
||||
|
@ -2016,10 +2016,10 @@
|
|||
"Continue with %(provider)s": "Continue with %(provider)s",
|
||||
"Sign in with single sign-on": "Sign in with single sign-on",
|
||||
"And %(count)s more...|other": "And %(count)s more...",
|
||||
"Home": "Home",
|
||||
"Enter a server name": "Enter a server name",
|
||||
"Looks good": "Looks good",
|
||||
"Can't find this server or its room list": "Can't find this server or its room list",
|
||||
"All rooms": "All rooms",
|
||||
"Your server": "Your server",
|
||||
"Are you sure you want to remove <b>%(serverName)s</b>": "Are you sure you want to remove <b>%(serverName)s</b>",
|
||||
"Remove server": "Remove server",
|
||||
|
|
|
@ -31,28 +31,23 @@ import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateS
|
|||
import {DefaultTagID} from "./room-list/models";
|
||||
import {EnhancedMap, mapDiff} from "../utils/maps";
|
||||
import {setHasDiff} from "../utils/sets";
|
||||
import {objectDiff} from "../utils/objects";
|
||||
import {arrayHasDiff} from "../utils/arrays";
|
||||
import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory";
|
||||
import RoomViewStore from "./RoomViewStore";
|
||||
|
||||
type SpaceKey = string | symbol;
|
||||
|
||||
interface IState {}
|
||||
|
||||
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
|
||||
|
||||
export const HOME_SPACE = Symbol("home-space");
|
||||
export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
|
||||
|
||||
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
|
||||
export const UPDATE_INVITED_SPACES = Symbol("invited-spaces");
|
||||
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
|
||||
// Space Room ID/HOME_SPACE will be emitted when a Space's children change
|
||||
// Space Room ID will be emitted when a Space's children change
|
||||
|
||||
const MAX_SUGGESTED_ROOMS = 20;
|
||||
|
||||
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "home_space"}`;
|
||||
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "ALL_ROOMS"}`;
|
||||
|
||||
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
|
||||
return arr.reduce((result, room: Room) => {
|
||||
|
@ -86,15 +81,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
|
||||
// The spaces representing the roots of the various tree-like hierarchies
|
||||
private rootSpaces: Room[] = [];
|
||||
// The list of rooms not present in any currently joined spaces
|
||||
private orphanedRooms = new Set<string>();
|
||||
// Map from room ID to set of spaces which list it as a child
|
||||
private parentMap = new EnhancedMap<string, Set<string>>();
|
||||
// Map from space key to SpaceNotificationState instance representing that space
|
||||
private notificationStateMap = new Map<SpaceKey, SpaceNotificationState>();
|
||||
// Map from spaceId to SpaceNotificationState instance representing that space
|
||||
private notificationStateMap = new Map<string, SpaceNotificationState>();
|
||||
// Map from space key to Set of room IDs that should be shown as part of that space's filter
|
||||
private spaceFilteredRooms = new Map<string | symbol, Set<string>>();
|
||||
// The space currently selected in the Space Panel - if null then `Home` is selected
|
||||
private spaceFilteredRooms = new Map<string, Set<string>>();
|
||||
// The space currently selected in the Space Panel - if null then All Rooms is selected
|
||||
private _activeSpace?: Room = null;
|
||||
private _suggestedRooms: ISpaceSummaryRoom[] = [];
|
||||
private _invitedSpaces = new Set<Room>();
|
||||
|
@ -244,7 +237,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
|
||||
return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
|
||||
if (!space) {
|
||||
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
|
||||
}
|
||||
return this.spaceFilteredRooms.get(space.roomId) || new Set();
|
||||
};
|
||||
|
||||
private rebuild = throttle(() => {
|
||||
|
@ -275,7 +271,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
});
|
||||
});
|
||||
|
||||
const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren));
|
||||
const [rootSpaces] = partitionSpacesAndRooms(Array.from(unseenChildren));
|
||||
|
||||
// somewhat algorithm to handle full-cycles
|
||||
const detachedNodes = new Set<Room>(spaces);
|
||||
|
@ -316,7 +312,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
// rootSpaces.push(space);
|
||||
// });
|
||||
|
||||
this.orphanedRooms = new Set(orphanedRooms);
|
||||
this.rootSpaces = rootSpaces;
|
||||
this.parentMap = backrefs;
|
||||
|
||||
|
@ -337,25 +332,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
this.rebuild();
|
||||
}
|
||||
|
||||
private showInHomeSpace = (room: Room) => {
|
||||
if (room.isSpaceRoom()) return false;
|
||||
return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
|
||||
|| DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
|
||||
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites
|
||||
};
|
||||
|
||||
// Update a given room due to its tag changing (e.g DM-ness or Fav-ness)
|
||||
// This can only change whether it shows up in the HOME_SPACE or not
|
||||
private onRoomUpdate = (room: Room) => {
|
||||
if (this.showInHomeSpace(room)) {
|
||||
this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId);
|
||||
this.emit(HOME_SPACE);
|
||||
} else if (!this.orphanedRooms.has(room.roomId)) {
|
||||
this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId);
|
||||
this.emit(HOME_SPACE);
|
||||
}
|
||||
};
|
||||
|
||||
private onSpaceMembersChange = (ev: MatrixEvent) => {
|
||||
// skip this update if we do not have a DM with this user
|
||||
if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return;
|
||||
|
@ -369,16 +345,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
const oldFilteredRooms = this.spaceFilteredRooms;
|
||||
this.spaceFilteredRooms = new Map();
|
||||
|
||||
// put all room invites in the Home Space
|
||||
const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite");
|
||||
this.spaceFilteredRooms.set(HOME_SPACE, new Set<string>(invites.map(room => room.roomId)));
|
||||
|
||||
visibleRooms.forEach(room => {
|
||||
if (this.showInHomeSpace(room)) {
|
||||
this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId);
|
||||
}
|
||||
});
|
||||
|
||||
this.rootSpaces.forEach(s => {
|
||||
// traverse each space tree in DFS to build up the supersets as you go up,
|
||||
// reusing results from like subtrees.
|
||||
|
@ -425,13 +391,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
// Update NotificationStates
|
||||
this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => {
|
||||
if (roomIds.has(room.roomId)) {
|
||||
// Don't aggregate notifications for DMs except in the Home Space
|
||||
if (s !== HOME_SPACE) {
|
||||
return !DMRoomMap.shared().getUserIdForRoomId(room.roomId)
|
||||
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite);
|
||||
}
|
||||
|
||||
return true;
|
||||
return !DMRoomMap.shared().getUserIdForRoomId(room.roomId)
|
||||
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -513,8 +474,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
// TODO confirm this after implementing parenting behaviour
|
||||
if (room.isSpaceRoom()) {
|
||||
this.onSpaceUpdate();
|
||||
} else {
|
||||
this.onRoomUpdate(room);
|
||||
}
|
||||
this.emit(room.roomId);
|
||||
break;
|
||||
|
@ -527,38 +486,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => {
|
||||
if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) {
|
||||
// If the room was in favourites and now isn't or the opposite then update its position in the trees
|
||||
const oldTags = lastEvent?.getContent()?.tags || {};
|
||||
const newTags = ev.getContent()?.tags || {};
|
||||
if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) {
|
||||
this.onRoomUpdate(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => {
|
||||
if (ev.getType() === EventType.Direct) {
|
||||
const lastContent = lastEvent.getContent();
|
||||
const content = ev.getContent();
|
||||
|
||||
const diff = objectDiff<Record<string, string[]>>(lastContent, content);
|
||||
// filter out keys which changed by reference only by checking whether the sets differ
|
||||
const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k]));
|
||||
// DM tag changes, refresh relevant rooms
|
||||
new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => {
|
||||
const room = this.matrixClient?.getRoom(roomId);
|
||||
if (room) {
|
||||
this.onRoomUpdate(room);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
protected async reset() {
|
||||
this.rootSpaces = [];
|
||||
this.orphanedRooms = new Set();
|
||||
this.parentMap = new EnhancedMap();
|
||||
this.notificationStateMap = new Map();
|
||||
this.spaceFilteredRooms = new Map();
|
||||
|
@ -573,8 +502,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
this.matrixClient.removeListener("Room", this.onRoom);
|
||||
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
|
||||
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
|
||||
this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
|
||||
this.matrixClient.removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
await this.reset();
|
||||
}
|
||||
|
@ -584,8 +511,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
this.matrixClient.on("Room", this.onRoom);
|
||||
this.matrixClient.on("Room.myMembership", this.onRoom);
|
||||
this.matrixClient.on("RoomState.events", this.onRoomState);
|
||||
this.matrixClient.on("Room.accountData", this.onRoomAccountData);
|
||||
this.matrixClient.on("accountData", this.onAccountData);
|
||||
|
||||
await this.onSpaceUpdate(); // trigger an initial update
|
||||
|
||||
|
@ -610,7 +535,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
// Don't context switch when navigating to the space room
|
||||
// as it will cause you to end up in the wrong room
|
||||
this.setActiveSpace(room, false);
|
||||
} else if (!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) {
|
||||
} else if (this.activeSpace && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) {
|
||||
this.switchToRelatedSpace(roomId);
|
||||
}
|
||||
|
||||
|
@ -628,7 +553,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
}
|
||||
|
||||
public getNotificationState(key: SpaceKey): SpaceNotificationState {
|
||||
public getNotificationState(key: string): SpaceNotificationState {
|
||||
if (this.notificationStateMap.has(key)) {
|
||||
return this.notificationStateMap.get(key);
|
||||
}
|
||||
|
|
|
@ -680,7 +680,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
promise = this.recalculatePrefiltering();
|
||||
} else {
|
||||
this.filterConditions.push(filter);
|
||||
// Runtime filters with spaces disable prefiltering for the search all spaces effect
|
||||
// Runtime filters with spaces disable prefiltering for the search all spaces feature
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
// this has to be awaited so that `setKnownRooms` is called in time for the `addFilterCondition` below
|
||||
// this way the runtime filters are only evaluated on one dataset and not both.
|
||||
|
@ -712,10 +712,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
|
||||
if (this.algorithm) {
|
||||
this.algorithm.removeFilterCondition(filter);
|
||||
// Runtime filters with spaces disable prefiltering for the search all spaces effect
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
promise = this.recalculatePrefiltering();
|
||||
}
|
||||
}
|
||||
// Runtime filters with spaces disable prefiltering for the search all spaces feature
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
promise = this.recalculatePrefiltering();
|
||||
}
|
||||
}
|
||||
idx = this.prefilterConditions.indexOf(filter);
|
||||
|
|
|
@ -24,26 +24,34 @@ import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore";
|
|||
* Watches for changes in spaces to manage the filter on the provided RoomListStore
|
||||
*/
|
||||
export class SpaceWatcher {
|
||||
private filter = new SpaceFilterCondition();
|
||||
private filter: SpaceFilterCondition;
|
||||
private activeSpace: Room = SpaceStore.instance.activeSpace;
|
||||
|
||||
constructor(private store: RoomListStoreClass) {
|
||||
this.updateFilter(); // get the filter into a consistent state
|
||||
store.addFilter(this.filter);
|
||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
|
||||
}
|
||||
|
||||
private onSelectedSpaceUpdated = (activeSpace: Room) => {
|
||||
private onSelectedSpaceUpdated = (activeSpace?: Room) => {
|
||||
this.activeSpace = activeSpace;
|
||||
this.updateFilter();
|
||||
|
||||
if (this.filter) {
|
||||
if (activeSpace) {
|
||||
this.updateFilter();
|
||||
} else {
|
||||
this.store.removeFilter(this.filter);
|
||||
this.filter = null;
|
||||
}
|
||||
} else if (activeSpace) {
|
||||
this.filter = new SpaceFilterCondition();
|
||||
this.updateFilter();
|
||||
this.store.addFilter(this.filter);
|
||||
}
|
||||
};
|
||||
|
||||
private updateFilter = () => {
|
||||
if (this.activeSpace) {
|
||||
SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
|
||||
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
|
||||
});
|
||||
}
|
||||
SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
|
||||
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
|
||||
});
|
||||
this.filter.updateSpace(this.activeSpace);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
|
||||
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
|
||||
import { IDestroyable } from "../../../utils/IDestroyable";
|
||||
import SpaceStore, {HOME_SPACE} from "../../SpaceStore";
|
||||
import SpaceStore from "../../SpaceStore";
|
||||
import { setHasDiff } from "../../../utils/sets";
|
||||
|
||||
/**
|
||||
|
@ -55,10 +55,12 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
|
|||
}
|
||||
};
|
||||
|
||||
private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE;
|
||||
private getSpaceEventKey = (space: Room) => space.roomId;
|
||||
|
||||
public updateSpace(space: Room) {
|
||||
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
|
||||
if (this.space) {
|
||||
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
|
||||
}
|
||||
SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate);
|
||||
this.onStoreUpdate(); // initial update from the change to the space
|
||||
}
|
||||
|
|
|
@ -17,63 +17,8 @@ limitations under the License.
|
|||
// Pull in the encryption lib so that we can decrypt attachments.
|
||||
import encrypt from 'browser-encrypt-attachment';
|
||||
import {mediaFromContent} from "../customisations/Media";
|
||||
import {IEncryptedFile} from "../customisations/models/IMediaEventContent";
|
||||
|
||||
// WARNING: We have to be very careful about what mime-types we allow into blobs,
|
||||
// as for performance reasons these are now rendered via URL.createObjectURL()
|
||||
// rather than by converting into data: URIs.
|
||||
//
|
||||
// This means that the content is rendered using the origin of the script which
|
||||
// called createObjectURL(), and so if the content contains any scripting then it
|
||||
// will pose a XSS vulnerability when the browser renders it. This is particularly
|
||||
// bad if the user right-clicks the URI and pastes it into a new window or tab,
|
||||
// as the blob will then execute with access to Element's full JS environment(!)
|
||||
//
|
||||
// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647
|
||||
// for details.
|
||||
//
|
||||
// We mitigate this by only allowing mime-types into blobs which we know don't
|
||||
// contain any scripting, and instantiate all others as application/octet-stream
|
||||
// regardless of what mime-type the event claimed. Even if the payload itself
|
||||
// is some malicious HTML, the fact we instantiate it with a media mimetype or
|
||||
// application/octet-stream means the browser doesn't try to render it as such.
|
||||
//
|
||||
// One interesting edge case is image/svg+xml, which empirically *is* rendered
|
||||
// correctly if the blob is set to the src attribute of an img tag (for thumbnails)
|
||||
// *even if the mimetype is application/octet-stream*. However, empirically JS
|
||||
// in the SVG isn't executed in this scenario, so we seem to be okay.
|
||||
//
|
||||
// Tested on Chrome 65 and Firefox 60
|
||||
//
|
||||
// The list below is taken mainly from
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
|
||||
// N.B. Matrix doesn't currently specify which mimetypes are valid in given
|
||||
// events, so we pick the ones which HTML5 browsers should be able to display
|
||||
//
|
||||
// For the record, mime-types which must NEVER enter this list below include:
|
||||
// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
|
||||
|
||||
const ALLOWED_BLOB_MIMETYPES = [
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/png',
|
||||
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
|
||||
'audio/mp4',
|
||||
'audio/webm',
|
||||
'audio/aac',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/wave',
|
||||
'audio/wav',
|
||||
'audio/x-wav',
|
||||
'audio/x-pn-wav',
|
||||
'audio/flac',
|
||||
'audio/x-flac',
|
||||
];
|
||||
import { IEncryptedFile } from "../customisations/models/IMediaEventContent";
|
||||
import { getBlobSafeMimeType } from "./blobs";
|
||||
|
||||
/**
|
||||
* Decrypt a file attached to a matrix event.
|
||||
|
@ -100,9 +45,7 @@ export function decryptFile(file: IEncryptedFile): Promise<Blob> {
|
|||
// browser (e.g. by copying the URI into a new tab or window.)
|
||||
// See warning at top of file.
|
||||
let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : '';
|
||||
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
|
||||
mimetype = 'application/octet-stream';
|
||||
}
|
||||
mimetype = getBlobSafeMimeType(mimetype);
|
||||
|
||||
return new Blob([dataArray], {type: mimetype});
|
||||
});
|
||||
|
|
78
src/utils/blobs.ts
Normal file
78
src/utils/blobs.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
|
||||
// WARNING: We have to be very careful about what mime-types we allow into blobs,
|
||||
// as for performance reasons these are now rendered via URL.createObjectURL()
|
||||
// rather than by converting into data: URIs.
|
||||
//
|
||||
// This means that the content is rendered using the origin of the script which
|
||||
// called createObjectURL(), and so if the content contains any scripting then it
|
||||
// will pose a XSS vulnerability when the browser renders it. This is particularly
|
||||
// bad if the user right-clicks the URI and pastes it into a new window or tab,
|
||||
// as the blob will then execute with access to Element's full JS environment(!)
|
||||
//
|
||||
// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647
|
||||
// for details.
|
||||
//
|
||||
// We mitigate this by only allowing mime-types into blobs which we know don't
|
||||
// contain any scripting, and instantiate all others as application/octet-stream
|
||||
// regardless of what mime-type the event claimed. Even if the payload itself
|
||||
// is some malicious HTML, the fact we instantiate it with a media mimetype or
|
||||
// application/octet-stream means the browser doesn't try to render it as such.
|
||||
//
|
||||
// One interesting edge case is image/svg+xml, which empirically *is* rendered
|
||||
// correctly if the blob is set to the src attribute of an img tag (for thumbnails)
|
||||
// *even if the mimetype is application/octet-stream*. However, empirically JS
|
||||
// in the SVG isn't executed in this scenario, so we seem to be okay.
|
||||
//
|
||||
// Tested on Chrome 65 and Firefox 60
|
||||
//
|
||||
// The list below is taken mainly from
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
|
||||
// N.B. Matrix doesn't currently specify which mimetypes are valid in given
|
||||
// events, so we pick the ones which HTML5 browsers should be able to display
|
||||
//
|
||||
// For the record, mime-types which must NEVER enter this list below include:
|
||||
// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
|
||||
|
||||
const ALLOWED_BLOB_MIMETYPES = [
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/png',
|
||||
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
|
||||
'audio/mp4',
|
||||
'audio/webm',
|
||||
'audio/aac',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/wave',
|
||||
'audio/wav',
|
||||
'audio/x-wav',
|
||||
'audio/x-pn-wav',
|
||||
'audio/flac',
|
||||
'audio/x-flac',
|
||||
];
|
||||
|
||||
export function getBlobSafeMimeType(mimetype: string): string {
|
||||
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
return mimetype;
|
||||
}
|
|
@ -77,7 +77,7 @@ describe('MessagePanel', function() {
|
|||
DMRoomMap.makeShared();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
clock.uninstall();
|
||||
});
|
||||
|
||||
|
@ -88,7 +88,21 @@ describe('MessagePanel', function() {
|
|||
events.push(test_utils.mkMessage(
|
||||
{
|
||||
event: true, room: "!room:id", user: "@user:id",
|
||||
ts: ts0 + i*1000,
|
||||
ts: ts0 + i * 1000,
|
||||
}));
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
// Just to avoid breaking Dateseparator tests that might run at 00hrs
|
||||
function mkOneDayEvents() {
|
||||
const events = [];
|
||||
const ts0 = Date.parse('09 May 2004 00:12:00 GMT');
|
||||
for (let i = 0; i < 10; i++) {
|
||||
events.push(test_utils.mkMessage(
|
||||
{
|
||||
event: true, room: "!room:id", user: "@user:id",
|
||||
ts: ts0 + i * 1000,
|
||||
}));
|
||||
}
|
||||
return events;
|
||||
|
@ -104,7 +118,7 @@ describe('MessagePanel', function() {
|
|||
let i = 0;
|
||||
events.push(test_utils.mkMessage({
|
||||
event: true, room: "!room:id", user: "@user:id",
|
||||
ts: ts0 + ++i*1000,
|
||||
ts: ts0 + ++i * 1000,
|
||||
}));
|
||||
|
||||
for (i = 0; i < 10; i++) {
|
||||
|
@ -151,7 +165,7 @@ describe('MessagePanel', function() {
|
|||
},
|
||||
getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
|
||||
},
|
||||
ts: ts0 + i*1000,
|
||||
ts: ts0 + i * 1000,
|
||||
mship: 'join',
|
||||
prevMship: 'join',
|
||||
name: 'A user',
|
||||
|
@ -250,7 +264,6 @@ describe('MessagePanel', function() {
|
|||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function isReadMarkerVisible(rmContainer) {
|
||||
return rmContainer && rmContainer.children.length > 0;
|
||||
}
|
||||
|
@ -437,4 +450,17 @@ describe('MessagePanel', function() {
|
|||
// read marker should be hidden given props and at the last event
|
||||
expect(isReadMarkerVisible(rm)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should render Date separators for the events', function () {
|
||||
const events = mkOneDayEvents();
|
||||
const res = mount(
|
||||
<WrappedMessagePanel
|
||||
className="cls"
|
||||
events={events}
|
||||
/>,
|
||||
);
|
||||
const Dates = res.find(sdk.getComponent('messages.DateSeparator'));
|
||||
|
||||
expect(Dates.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -101,6 +101,7 @@ const invite1 = "!invite1:server";
|
|||
const invite2 = "!invite2:server";
|
||||
const room1 = "!room1:server";
|
||||
const room2 = "!room2:server";
|
||||
const room3 = "!room3:server";
|
||||
const space1 = "!space1:server";
|
||||
const space2 = "!space2:server";
|
||||
const space3 = "!space3:server";
|
||||
|
@ -361,8 +362,8 @@ describe("SpaceStore", () => {
|
|||
expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("home space does not contain rooms/low priority from rooms within spaces", () => {
|
||||
expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy();
|
||||
it("home space does contain rooms/low priority even if they are also shown in a space", () => {
|
||||
expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("space contains child rooms", () => {
|
||||
|
@ -614,8 +615,8 @@ describe("SpaceStore", () => {
|
|||
|
||||
describe("space auto switching tests", () => {
|
||||
beforeEach(async () => {
|
||||
[room1, room2, orphan1].forEach(mkRoom);
|
||||
mkSpace(space1, [room1, room2]);
|
||||
[room1, room2, room3, orphan1].forEach(mkRoom);
|
||||
mkSpace(space1, [room1, room2, room3]);
|
||||
mkSpace(space2, [room1, room2]);
|
||||
|
||||
client.getRoom(room2).currentState.getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||
|
@ -641,15 +642,15 @@ describe("SpaceStore", () => {
|
|||
|
||||
it("switch to canonical parent space for room", async () => {
|
||||
viewRoom(room1);
|
||||
await store.setActiveSpace(null, false);
|
||||
await store.setActiveSpace(client.getRoom(space2), false);
|
||||
viewRoom(room2);
|
||||
expect(store.activeSpace).toBe(client.getRoom(space2));
|
||||
});
|
||||
|
||||
it("switch to first containing space for room", async () => {
|
||||
viewRoom(room2);
|
||||
await store.setActiveSpace(null, false);
|
||||
viewRoom(room1);
|
||||
await store.setActiveSpace(client.getRoom(space2), false);
|
||||
viewRoom(room3);
|
||||
expect(store.activeSpace).toBe(client.getRoom(space1));
|
||||
});
|
||||
|
||||
|
@ -659,6 +660,13 @@ describe("SpaceStore", () => {
|
|||
viewRoom(orphan1);
|
||||
expect(store.activeSpace).toBeNull();
|
||||
});
|
||||
|
||||
it("when switching rooms in the all rooms home space don't switch to related space", async () => {
|
||||
viewRoom(room2);
|
||||
await store.setActiveSpace(null, false);
|
||||
viewRoom(room1);
|
||||
expect(store.activeSpace).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("traverseSpace", () => {
|
||||
|
|
31
yarn.lock
31
yarn.lock
|
@ -4226,9 +4226,9 @@ hoist-non-react-statics@^3.3.0:
|
|||
react-is "^16.7.0"
|
||||
|
||||
hosted-git-info@^2.1.4:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||
|
||||
hosted-git-info@^3.0.6:
|
||||
version "3.0.7"
|
||||
|
@ -5677,8 +5677,8 @@ mathml-tag-names@^2.1.3:
|
|||
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||
version "10.0.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c8f69c0b7937b9064938c134d708c4d064b71315"
|
||||
version "10.1.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2d73805ca3d8c5a140fe05e574f826696de1656a"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
another-json "^0.2.0"
|
||||
|
@ -6307,6 +6307,11 @@ parse-json@^5.0.0:
|
|||
json-parse-even-better-errors "^2.3.0"
|
||||
lines-and-columns "^1.1.6"
|
||||
|
||||
parse-srcset@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
|
||||
integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=
|
||||
|
||||
parse5-htmlparser2-tree-adapter@^6.0.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
|
||||
|
@ -7248,17 +7253,18 @@ sane@^4.0.3:
|
|||
minimist "^1.1.1"
|
||||
walker "~1.0.5"
|
||||
|
||||
"sanitize-html@github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db":
|
||||
version "2.0.0-rc.3"
|
||||
resolved "https://codeload.github.com/apostrophecms/sanitize-html/tar.gz/3c7f93f2058f696f5359e3e58d464161647226db"
|
||||
sanitize-html@^2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.3.3.tgz#3db382c9a621cce4c46d90f10c64f1e9da9e8353"
|
||||
integrity sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA==
|
||||
dependencies:
|
||||
deepmerge "^4.2.2"
|
||||
escape-string-regexp "^4.0.0"
|
||||
htmlparser2 "^4.1.0"
|
||||
htmlparser2 "^6.0.0"
|
||||
is-plain-object "^5.0.0"
|
||||
klona "^2.0.3"
|
||||
parse-srcset "^1.0.2"
|
||||
postcss "^8.0.2"
|
||||
srcset "^3.0.0"
|
||||
|
||||
saxes@^5.0.0:
|
||||
version "5.0.1"
|
||||
|
@ -7515,11 +7521,6 @@ sprintf-js@~1.0.2:
|
|||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
||||
|
||||
srcset@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/srcset/-/srcset-3.0.0.tgz#8afd8b971362dfc129ae9c1a99b3897301ce6441"
|
||||
integrity sha512-D59vF08Qzu/C4GAOXVgMTLfgryt5fyWo93FZyhEWANo0PokFz/iWdDe13mX3O5TRf6l8vMTqckAfR4zPiaH0yQ==
|
||||
|
||||
sshpk@^1.7.0:
|
||||
version "1.16.1"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
|
||||
|
|
Loading…
Reference in a new issue