Add comments and newlines back and resolve merge conflicts

This commit is contained in:
Jaiwanth 2021-05-11 11:05:54 +05:30
commit 4ea3fbc5ba
19 changed files with 493 additions and 263 deletions

View file

@ -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)

View file

@ -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",

View file

@ -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) {

View file

@ -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;

View file

@ -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}

View file

@ -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

View file

@ -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");

View file

@ -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);

View file

@ -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

View file

@ -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",

View file

@ -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);
}

View file

@ -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);

View file

@ -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);
};
}

View file

@ -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
}

View file

@ -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
View 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;
}

View file

@ -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);
});
});

View file

@ -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", () => {

View file

@ -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"