Merge branch 'develop' into gsouquet/switch-rooms

pull/21833/head
Germain Souquet 2021-05-19 11:09:15 +01:00
commit 282b9f9e0f
53 changed files with 1044 additions and 618 deletions

View File

@ -1,3 +1,113 @@
Changes in [3.21.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0) (2021-05-17)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0-rc.1...v3.21.0)
## Security notice
matrix-react-sdk 3.21.0 fixes a low severity issue (GHSA-8796-gc9j-63rv)
related to file upload. When uploading a file, the local file preview can lead
to execution of scripts embedded in the uploaded file, but only after several
user interactions to open the preview in a separate tab. This only impacts the
local user while in the process of uploading. It cannot be exploited remotely
or by other users. Thanks to [Muhammad Zaid Ghifari](https://github.com/MR-ZHEEV)
for responsibly disclosing this via Matrix's Security Disclosure Policy.
## All changes
* Upgrade to JS SDK 11.0.0
* [Release] Add missing space on beta feedback dialog
[\#6019](https://github.com/matrix-org/matrix-react-sdk/pull/6019)
* [Release] Add feedback mechanism for beta features, namely Spaces
[\#6013](https://github.com/matrix-org/matrix-react-sdk/pull/6013)
* Add feedback mechanism for beta features, namely Spaces
[\#6012](https://github.com/matrix-org/matrix-react-sdk/pull/6012)
Changes in [3.21.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0-rc.1) (2021-05-11)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0...v3.21.0-rc.1)
* Upgrade to JS SDK 11.0.0-rc.1
* Add disclaimer about subspaces being experimental in add existing dialog
[\#5978](https://github.com/matrix-org/matrix-react-sdk/pull/5978)
* Spaces Beta release
[\#5933](https://github.com/matrix-org/matrix-react-sdk/pull/5933)
* Improve permissions error when adding new server to room directory
[\#6009](https://github.com/matrix-org/matrix-react-sdk/pull/6009)
* Allow user to progress through space creation & setup using Enter
[\#6006](https://github.com/matrix-org/matrix-react-sdk/pull/6006)
* Upgrade sanitize types
[\#6008](https://github.com/matrix-org/matrix-react-sdk/pull/6008)
* Upgrade `cheerio` and resolve type errors
[\#6007](https://github.com/matrix-org/matrix-react-sdk/pull/6007)
* Add slash commands support to edit message composer
[\#5865](https://github.com/matrix-org/matrix-react-sdk/pull/5865)
* Fix the two todays problem
[\#5940](https://github.com/matrix-org/matrix-react-sdk/pull/5940)
* Switch the Home Space out for an All rooms space
[\#5969](https://github.com/matrix-org/matrix-react-sdk/pull/5969)
* Show device ID in UserInfo when there is no device name
[\#5985](https://github.com/matrix-org/matrix-react-sdk/pull/5985)
* Switch back to release version of `sanitize-html`
[\#6005](https://github.com/matrix-org/matrix-react-sdk/pull/6005)
* Bump hosted-git-info from 2.8.8 to 2.8.9
[\#5998](https://github.com/matrix-org/matrix-react-sdk/pull/5998)
* Don't use the event's metadata to calc the scale of an image
[\#5982](https://github.com/matrix-org/matrix-react-sdk/pull/5982)
* Adjust MIME type of upload confirmation if needed
[\#5981](https://github.com/matrix-org/matrix-react-sdk/pull/5981)
* Forbid redaction of encryption events
[\#5991](https://github.com/matrix-org/matrix-react-sdk/pull/5991)
* Fix voice message playback being squished up against send button
[\#5988](https://github.com/matrix-org/matrix-react-sdk/pull/5988)
* Improve style of notification badges on the space panel
[\#5983](https://github.com/matrix-org/matrix-react-sdk/pull/5983)
* Add dev dependency for parse5 typings
[\#5990](https://github.com/matrix-org/matrix-react-sdk/pull/5990)
* Iterate Spaces admin UX around room management
[\#5977](https://github.com/matrix-org/matrix-react-sdk/pull/5977)
* Guard all isSpaceRoom calls behind the labs flag
[\#5979](https://github.com/matrix-org/matrix-react-sdk/pull/5979)
* Bump lodash from 4.17.20 to 4.17.21
[\#5986](https://github.com/matrix-org/matrix-react-sdk/pull/5986)
* Bump lodash from 4.17.19 to 4.17.21 in /test/end-to-end-tests
[\#5987](https://github.com/matrix-org/matrix-react-sdk/pull/5987)
* Bump ua-parser-js from 0.7.23 to 0.7.28
[\#5984](https://github.com/matrix-org/matrix-react-sdk/pull/5984)
* Update visual style of plain files in the timeline
[\#5971](https://github.com/matrix-org/matrix-react-sdk/pull/5971)
* Support for multiple streams (not MSC3077)
[\#5833](https://github.com/matrix-org/matrix-react-sdk/pull/5833)
* Update space ordering behaviour to match updates in MSC
[\#5963](https://github.com/matrix-org/matrix-react-sdk/pull/5963)
* Improve performance of search all spaces and space switching
[\#5976](https://github.com/matrix-org/matrix-react-sdk/pull/5976)
* Update colours and sizing for voice messages
[\#5970](https://github.com/matrix-org/matrix-react-sdk/pull/5970)
* Update link to Android SDK
[\#5973](https://github.com/matrix-org/matrix-react-sdk/pull/5973)
* Add cleanup functions for image view
[\#5962](https://github.com/matrix-org/matrix-react-sdk/pull/5962)
* Add a note about sharing your IP in P2P calls
[\#5961](https://github.com/matrix-org/matrix-react-sdk/pull/5961)
* Only aggregate DM notifications on the Space Panel in the Home Space
[\#5968](https://github.com/matrix-org/matrix-react-sdk/pull/5968)
* Add retry mechanism and progress bar to add existing to space dialog
[\#5975](https://github.com/matrix-org/matrix-react-sdk/pull/5975)
* Warn on access token reveal
[\#5755](https://github.com/matrix-org/matrix-react-sdk/pull/5755)
* Fix newly joined room appearing under the wrong space
[\#5945](https://github.com/matrix-org/matrix-react-sdk/pull/5945)
* Early rendering for voice messages in the timeline
[\#5955](https://github.com/matrix-org/matrix-react-sdk/pull/5955)
* Calculate the real waveform in the Playback class for voice messages
[\#5956](https://github.com/matrix-org/matrix-react-sdk/pull/5956)
* Don't recurse on arrayFastResample
[\#5957](https://github.com/matrix-org/matrix-react-sdk/pull/5957)
* Support a dark theme for voice messages
[\#5958](https://github.com/matrix-org/matrix-react-sdk/pull/5958)
* Handle no/blocked microphones in voice messages
[\#5959](https://github.com/matrix-org/matrix-react-sdk/pull/5959)
Changes in [3.20.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0) (2021-05-10) 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) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0-rc.1...v3.20.0)

View File

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "3.20.0", "version": "3.21.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -80,7 +80,7 @@
"linkifyjs": "^2.1.9", "linkifyjs": "^2.1.9",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^0.1.0-beta.13", "matrix-widget-api": "^0.1.0-beta.14",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"opus-recorder": "^8.0.3", "opus-recorder": "^8.0.3",
"pako": "^2.0.3", "pako": "^2.0.3",

View File

@ -54,7 +54,8 @@ limitations under the License.
display: flex; display: flex;
margin-top: 12px; margin-top: 12px;
.mx_BaseAvatar { // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling
.mx_DecoratedRoomAvatar {
margin-right: 12px; margin-right: 12px;
} }
@ -75,6 +76,10 @@ limitations under the License.
} }
.mx_AddExistingToSpace_section_spaces { .mx_AddExistingToSpace_section_spaces {
.mx_BaseAvatar {
margin-right: 12px;
}
.mx_BaseAvatar_image { .mx_BaseAvatar_image {
border-radius: 8px; border-radius: 8px;
} }
@ -105,6 +110,90 @@ limitations under the License.
mask-position: center; mask-position: center;
} }
} }
.mx_AddExistingToSpace_footer {
display: flex;
margin-top: 20px;
> span {
flex-grow: 1;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
.mx_ProgressBar {
height: 8px;
width: 100%;
@mixin ProgressBarBorderRadius 8px;
}
.mx_AddExistingToSpace_progressText {
margin-top: 8px;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
}
> * {
vertical-align: middle;
}
}
.mx_AddExistingToSpace_error {
padding-left: 12px;
> img {
align-self: center;
}
.mx_AddExistingToSpace_errorHeading {
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-18px;
color: $notice-primary-color;
}
.mx_AddExistingToSpace_errorCaption {
margin-top: 4px;
font-size: $font-12px;
line-height: $font-15px;
color: $primary-fg-color;
}
}
.mx_AccessibleButton {
display: inline-block;
align-self: center;
}
.mx_AccessibleButton_kind_primary {
padding: 8px 36px;
}
.mx_AddExistingToSpace_retryButton {
margin-left: 12px;
padding-left: 24px;
position: relative;
&::before {
content: '';
position: absolute;
background-color: $primary-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/retry.svg');
width: 18px;
height: 18px;
left: 0;
}
}
.mx_AccessibleButton_kind_link {
padding: 0;
}
}
} }
.mx_AddExistingToSpaceDialog { .mx_AddExistingToSpaceDialog {
@ -189,88 +278,4 @@ limitations under the License.
.mx_AddExistingToSpace { .mx_AddExistingToSpace {
display: contents; display: contents;
} }
.mx_AddExistingToSpaceDialog_footer {
display: flex;
margin-top: 20px;
> span {
flex-grow: 1;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
.mx_ProgressBar {
height: 8px;
width: 100%;
@mixin ProgressBarBorderRadius 8px;
}
.mx_AddExistingToSpaceDialog_progressText {
margin-top: 8px;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
}
> * {
vertical-align: middle;
}
}
.mx_AddExistingToSpaceDialog_error {
padding-left: 12px;
> img {
align-self: center;
}
.mx_AddExistingToSpaceDialog_errorHeading {
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-18px;
color: $notice-primary-color;
}
.mx_AddExistingToSpaceDialog_errorCaption {
margin-top: 4px;
font-size: $font-12px;
line-height: $font-15px;
color: $primary-fg-color;
}
}
.mx_AccessibleButton {
display: inline-block;
align-self: center;
}
.mx_AccessibleButton_kind_primary {
padding: 8px 36px;
}
.mx_AddExistingToSpaceDialog_retryButton {
margin-left: 12px;
padding-left: 24px;
position: relative;
&::before {
content: '';
position: absolute;
background-color: $primary-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/retry.svg');
width: 18px;
height: 18px;
left: 0;
}
}
.mx_AccessibleButton_kind_link {
padding: 0;
}
}
} }

View File

@ -20,7 +20,8 @@ limitations under the License.
.mx_ReactionsRow_addReactionButton { .mx_ReactionsRow_addReactionButton {
position: relative; position: relative;
display: none; // show on hover of the .mx_EventTile display: inline-block;
visibility: hidden; // show on hover of the .mx_EventTile
width: 24px; width: 24px;
height: 24px; height: 24px;
vertical-align: middle; vertical-align: middle;
@ -39,7 +40,7 @@ limitations under the License.
} }
&.mx_ReactionsRow_addReactionButton_active { &.mx_ReactionsRow_addReactionButton_active {
display: inline-block; // keep showing whilst the context menu is shown visibility: visible; // keep showing whilst the context menu is shown
} }
&:hover, &.mx_ReactionsRow_addReactionButton_active { &:hover, &.mx_ReactionsRow_addReactionButton_active {
@ -51,7 +52,7 @@ limitations under the License.
} }
.mx_EventTile:hover .mx_ReactionsRow_addReactionButton { .mx_EventTile:hover .mx_ReactionsRow_addReactionButton {
display: inline-block; visibility: visible;
} }
.mx_ReactionsRow_showAll { .mx_ReactionsRow_showAll {

View File

@ -98,7 +98,7 @@ limitations under the License.
position: relative; position: relative;
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 32px; border-radius: 8px;
&::before { &::before {
content: ''; content: '';
@ -114,6 +114,11 @@ limitations under the License.
} }
} }
.mx_RoomSublist_auxButton:hover,
.mx_RoomSublist_menuButton:hover {
background: $roomlist-button-bg-color;
}
// Hide the menu button by default // Hide the menu button by default
.mx_RoomSublist_menuButton { .mx_RoomSublist_menuButton {
visibility: hidden; visibility: hidden;

View File

@ -40,6 +40,8 @@ export function eventTriggersUnreadCount(ev) {
return false; return false;
} else if (ev.getType() == 'm.room.server_acl') { } else if (ev.getType() == 'm.room.server_acl') {
return false; return false;
} else if (ev.isRedacted()) {
return false;
} }
return haveTileForEvent(ev); return haveTileForEvent(ev);
} }

View File

@ -167,7 +167,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
const onKeyDownHandler = useCallback((ev) => { const onKeyDownHandler = useCallback((ev) => {
let handled = false; let handled = false;
// Don't interfere with input default keydown behaviour // Don't interfere with input default keydown behaviour
if (handleHomeEnd && ev.target.tagName !== "INPUT") { if (handleHomeEnd && ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
// check if we actually have any items // check if we actually have any items
switch (ev.key) { switch (ev.key) {
case Key.HOME: case Key.HOME:

View File

@ -26,6 +26,8 @@ import EmojiProvider from './EmojiProvider';
import NotifProvider from './NotifProvider'; import NotifProvider from './NotifProvider';
import {timeout} from "../utils/promise"; import {timeout} from "../utils/promise";
import AutocompleteProvider, {ICommand} from "./AutocompleteProvider"; import AutocompleteProvider, {ICommand} from "./AutocompleteProvider";
import SettingsStore from "../settings/SettingsStore";
import SpaceProvider from "./SpaceProvider";
export interface ISelectionRange { export interface ISelectionRange {
beginning?: boolean; // whether the selection is in the first block of the editor or not beginning?: boolean; // whether the selection is in the first block of the editor or not
@ -56,6 +58,11 @@ const PROVIDERS = [
DuckDuckGoProvider, DuckDuckGoProvider,
]; ];
// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here
if (SettingsStore.getValue("feature_spaces")) {
PROVIDERS.push(SpaceProvider);
}
// Providers will get rejected if they take longer than this. // Providers will get rejected if they take longer than this.
const PROVIDER_COMPLETION_TIMEOUT = 3000; const PROVIDER_COMPLETION_TIMEOUT = 3000;

View File

@ -1,8 +1,7 @@
/* /*
Copyright 2016 Aviral Dasgupta Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2017, 2018, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,17 +16,19 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from "react";
import {uniqBy, sortBy} from "lodash";
import Room from "matrix-js-sdk/src/models/room"; import Room from "matrix-js-sdk/src/models/room";
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import {MatrixClientPeg} from '../MatrixClientPeg'; import {MatrixClientPeg} from '../MatrixClientPeg';
import QueryMatcher from './QueryMatcher'; import QueryMatcher from './QueryMatcher';
import {PillCompletion} from './Components'; import {PillCompletion} from './Components';
import * as sdk from '../index';
import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
import {ICompletion, ISelectionRange} from "./Autocompleter"; import {ICompletion, ISelectionRange} from "./Autocompleter";
import {uniqBy, sortBy} from "lodash"; import RoomAvatar from '../components/views/avatars/RoomAvatar';
import SettingsStore from "../settings/SettingsStore";
const ROOM_REGEX = /\B#\S*/g; const ROOM_REGEX = /\B#\S*/g;
@ -49,7 +50,7 @@ function matcherObject(room: Room, displayedAlias: string, matchName = "") {
} }
export default class RoomProvider extends AutocompleteProvider { export default class RoomProvider extends AutocompleteProvider {
matcher: QueryMatcher<Room>; protected matcher: QueryMatcher<Room>;
constructor() { constructor() {
super(ROOM_REGEX); super(ROOM_REGEX);
@ -58,20 +59,28 @@ export default class RoomProvider extends AutocompleteProvider {
}); });
} }
protected getRooms() {
const cli = MatrixClientPeg.get();
let rooms = cli.getVisibleRooms();
if (SettingsStore.getValue("feature_spaces")) {
rooms = rooms.filter(r => !r.isSpaceRoom());
}
return rooms;
}
async getCompletions( async getCompletions(
query: string, query: string,
selection: ISelectionRange, selection: ISelectionRange,
force = false, force = false,
limit = -1, limit = -1,
): Promise<ICompletion[]> { ): Promise<ICompletion[]> {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
const client = MatrixClientPeg.get();
let completions = []; let completions = [];
const {command, range} = this.getCurrentCommand(query, selection, force); const {command, range} = this.getCurrentCommand(query, selection, force);
if (command) { if (command) {
// the only reason we need to do this is because Fuse only matches on properties // the only reason we need to do this is because Fuse only matches on properties
let matcherObjects = client.getVisibleRooms().reduce((aliases, room) => { let matcherObjects = this.getRooms().reduce((aliases, room) => {
if (room.getCanonicalAlias()) { if (room.getCanonicalAlias()) {
aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name)); aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name));
} }
@ -115,7 +124,7 @@ export default class RoomProvider extends AutocompleteProvider {
), ),
range, range,
}; };
}).filter((completion) => !!completion.completion && completion.completion.length > 0).slice(0, 4); }).filter((completion) => !!completion.completion && completion.completion.length > 0);
} }
return completions; return completions;
} }

View File

@ -0,0 +1,43 @@
/*
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.
*/
import React from "react";
import { _t } from '../languageHandler';
import {MatrixClientPeg} from '../MatrixClientPeg';
import RoomProvider from "./RoomProvider";
export default class SpaceProvider extends RoomProvider {
protected getRooms() {
return MatrixClientPeg.get().getVisibleRooms().filter(r => r.isSpaceRoom());
}
getName() {
return _t("Spaces");
}
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
return (
<div
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
role="listbox"
aria-label={_t("Space Autocomplete")}
>
{ completions }
</div>
);
}
}

View File

@ -222,10 +222,12 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
}; };
private onKeyDown = (ev: React.KeyboardEvent) => { private onKeyDown = (ev: React.KeyboardEvent) => {
// don't let keyboard handling escape the context menu
ev.stopPropagation();
if (!this.props.managed) { if (!this.props.managed) {
if (ev.key === Key.ESCAPE) { if (ev.key === Key.ESCAPE) {
this.props.onFinished(); this.props.onFinished();
ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
} }
return; return;
@ -258,7 +260,6 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
if (handled) { if (handled) {
// consume all other keys in context menu // consume all other keys in context menu
ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
} }
}; };

View File

@ -50,6 +50,9 @@ class FilePanel extends React.Component {
if (room?.roomId !== this.props?.roomId) return; if (room?.roomId !== this.props?.roomId) return;
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return; if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
const client = MatrixClientPeg.get();
client.decryptEventIfNeeded(ev);
if (ev.isBeingDecrypted()) { if (ev.isBeingDecrypted()) {
this.decryptingEvents.add(ev.getId()); this.decryptingEvents.add(ev.getId());
} else { } else {

View File

@ -27,7 +27,7 @@ import CallMediaHandler from '../../CallMediaHandler';
import { fixupColorFonts } from '../../utils/FontManager'; import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg'; import { IMatrixClientCreds } from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import TagOrderActions from '../../actions/TagOrderActions'; import TagOrderActions from '../../actions/TagOrderActions';
@ -219,16 +219,6 @@ class LoggedInView extends React.Component<IProps, IState> {
}); });
}; };
// Child components assume that the client peg will not be null, so give them some
// sort of assurance here by only allowing a re-render if the client is truthy.
//
// This is required because `LoggedInView` maintains its own state and if this state
// updates after the client peg has been made null (during logout), then it will
// attempt to re-render and the children will throw errors.
shouldComponentUpdate() {
return Boolean(MatrixClientPeg.get());
}
canResetTimelineInRoom = (roomId) => { canResetTimelineInRoom = (roomId) => {
if (!this._roomView.current) { if (!this._roomView.current) {
return true; return true;

View File

@ -473,7 +473,7 @@ export default class MessagePanel extends React.Component {
} }
get _roomHasPendingEdit() { get _roomHasPendingEdit() {
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`); return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
} }
_getEventTiles() { _getEventTiles() {

View File

@ -811,7 +811,7 @@ export default class RoomView extends React.Component<IProps, IState> {
}; };
private onEvent = (ev) => { private onEvent = (ev) => {
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || ev.shouldAttemptDecryption()) return;
this.handleEffects(ev); this.handleEffects(ev);
}; };

View File

@ -52,8 +52,6 @@ import {useStateToggle} from "../../hooks/useStateToggle";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import FacePile from "../views/elements/FacePile"; import FacePile from "../views/elements/FacePile";
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog"; import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
import {sleep} from "../../utils/promise";
import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu"; import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
import IconizedContextMenu, { import IconizedContextMenu, {
IconizedContextMenuOption, IconizedContextMenuOption,
@ -78,6 +76,7 @@ interface IProps {
interface IState { interface IState {
phase: Phase; phase: Phase;
createdRooms?: boolean; // internal state for the creation wizard
showRightPanel: boolean; showRightPanel: boolean;
myMembership: string; myMembership: string;
} }
@ -461,7 +460,8 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
setError(""); setError("");
setBusy(true); setBusy(true);
try { try {
await Promise.all(roomNames.map(name => name.trim()).filter(Boolean).map(name => { const filteredRoomNames = roomNames.map(name => name.trim()).filter(Boolean);
await Promise.all(filteredRoomNames.map(name => {
return createRoom({ return createRoom({
createOpts: { createOpts: {
preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat, preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat,
@ -474,7 +474,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
parentSpace: space, parentSpace: space,
}); });
})); }));
onFinished(); onFinished(filteredRoomNames.length > 0);
} catch (e) { } catch (e) {
console.error("Failed to create initial space rooms", e); console.error("Failed to create initial space rooms", e);
setError(_t("Failed to create initial space rooms")); setError(_t("Failed to create initial space rooms"));
@ -484,7 +484,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
let onClick = (ev) => { let onClick = (ev) => {
ev.preventDefault(); ev.preventDefault();
onFinished(); onFinished(false);
}; };
let buttonLabel = _t("Skip for now"); let buttonLabel = _t("Skip for now");
if (roomNames.some(name => name.trim())) { if (roomNames.some(name => name.trim())) {
@ -517,39 +517,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
}; };
const SpaceAddExistingRooms = ({ space, onFinished }) => { const SpaceAddExistingRooms = ({ space, onFinished }) => {
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
let onClick = onFinished;
let buttonLabel = _t("Skip for now");
if (selectedToAdd.size > 0) {
onClick = async () => {
setBusy(true);
for (const room of selectedToAdd) {
const via = calculateRoomVia(room);
try {
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
if (e.errcode === "M_LIMIT_EXCEEDED") {
await sleep(e.data.retry_after_ms);
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
}
throw e;
});
} catch (e) {
console.error("Failed to add rooms to space", e);
setError(_t("Failed to add rooms to space"));
break;
}
}
setBusy(false);
};
buttonLabel = busy ? _t("Adding...") : _t("Add");
}
return <div> return <div>
<h1>{ _t("What do you want to organise?") }</h1> <h1>{ _t("What do you want to organise?") }</h1>
<div className="mx_SpaceRoomView_description"> <div className="mx_SpaceRoomView_description">
@ -557,35 +524,24 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
"no one will be informed. You can add more later.") } "no one will be informed. You can add more later.") }
</div> </div>
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
<AddExistingToSpace <AddExistingToSpace
space={space} space={space}
selected={selectedToAdd} emptySelectionButton={
onChange={(checked, room) => { <AccessibleButton kind="primary" onClick={onFinished}>
if (checked) { { _t("Skip for now") }
selectedToAdd.add(room); </AccessibleButton>
} else {
selectedToAdd.delete(room);
} }
setSelectedToAdd(new Set(selectedToAdd)); onFinished={onFinished}
}}
/> />
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<AccessibleButton
kind="primary"
disabled={busy}
onClick={onClick}
>
{ buttonLabel }
</AccessibleButton>
</div> </div>
<SpaceFeedbackPrompt /> <SpaceFeedbackPrompt />
</div>; </div>;
}; };
const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished }) => { const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRooms }) => {
return <div className="mx_SpaceRoomView_publicShare"> return <div className="mx_SpaceRoomView_publicShare">
<h1>{ _t("Share %(name)s", { <h1>{ _t("Share %(name)s", {
name: justCreatedOpts?.createOpts?.name || space.name, name: justCreatedOpts?.createOpts?.name || space.name,
@ -598,7 +554,7 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished }) => {
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<AccessibleButton kind="primary" onClick={onFinished}> <AccessibleButton kind="primary" onClick={onFinished}>
{ _t("Go to my first room") } { createdRooms ? _t("Go to my first room") : _t("Go to my space") }
</AccessibleButton> </AccessibleButton>
</div> </div>
<SpaceFeedbackPrompt /> <SpaceFeedbackPrompt />
@ -891,13 +847,14 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
_t("Let's create a room for each of them.") + "\n" + _t("Let's create a room for each of them.") + "\n" +
_t("You can add more later too, including already existing ones.") _t("You can add more later too, including already existing ones.")
} }
onFinished={() => this.setState({ phase: Phase.PublicShare })} onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.PublicShare, createdRooms })}
/>; />;
case Phase.PublicShare: case Phase.PublicShare:
return <SpaceSetupPublicShare return <SpaceSetupPublicShare
justCreatedOpts={this.props.justCreatedOpts} justCreatedOpts={this.props.justCreatedOpts}
space={this.props.space} space={this.props.space}
onFinished={this.goToFirstRoom} onFinished={this.goToFirstRoom}
createdRooms={this.state.createdRooms}
/>; />;
case Phase.PrivateScope: case Phase.PrivateScope:
@ -919,7 +876,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
title={_t("What projects are you working on?")} title={_t("What projects are you working on?")}
description={_t("We'll create rooms for each of them. " + description={_t("We'll create rooms for each of them. " +
"You can add more later too, including already existing ones.")} "You can add more later too, including already existing ones.")}
onFinished={() => this.setState({ phase: Phase.Landing })} onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.Landing, createdRooms })}
/>; />;
case Phase.PrivateExistingRooms: case Phase.PrivateExistingRooms:
return <SpaceAddExistingRooms return <SpaceAddExistingRooms

View File

@ -1149,9 +1149,8 @@ class TimelinePanel extends React.Component {
arrayFastClone(events) arrayFastClone(events)
.reverse() .reverse()
.forEach(event => { .forEach(event => {
if (event.shouldAttemptDecryption()) { const client = MatrixClientPeg.get();
event.attemptDecryption(MatrixClientPeg.get()._crypto); client.decryptEventIfNeeded(event);
}
}); });
const firstVisibleEventIndex = this._checkForPreJoinUISI(events); const firstVisibleEventIndex = this._checkForPreJoinUISI(events);

View File

@ -20,7 +20,6 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { User } from "matrix-js-sdk/src/models/user"; import { User } from "matrix-js-sdk/src/models/user";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { TagID } from '../../../stores/room-list/models';
import RoomAvatar from "./RoomAvatar"; import RoomAvatar from "./RoomAvatar";
import NotificationBadge from '../rooms/NotificationBadge'; import NotificationBadge from '../rooms/NotificationBadge';
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
@ -35,7 +34,6 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps { interface IProps {
room: Room; room: Room;
avatarSize: number; avatarSize: number;
tag: TagID;
displayBadge?: boolean; displayBadge?: boolean;
forceCount?: boolean; forceCount?: boolean;
oobData?: object; oobData?: object;

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useContext, useMemo, useState} from "react"; import React, {ReactNode, useContext, useMemo, useState} from "react";
import classNames from "classnames"; import classNames from "classnames";
import {Room} from "matrix-js-sdk/src/models/room"; import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client"; import {MatrixClient} from "matrix-js-sdk/src/client";
@ -37,6 +37,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import ProgressBar from "../elements/ProgressBar"; import ProgressBar from "../elements/ProgressBar";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
@ -46,7 +47,10 @@ interface IProps extends IDialogProps {
const Entry = ({ room, checked, onChange }) => { const Entry = ({ room, checked, onChange }) => {
return <label className="mx_AddExistingToSpace_entry"> return <label className="mx_AddExistingToSpace_entry">
<RoomAvatar room={room} height={32} width={32} /> { room?.isSpaceRoom()
? <RoomAvatar room={room} height={32} width={32} />
: <DecoratedRoomAvatar room={room} avatarSize={32} />
}
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span> <span className="mx_AddExistingToSpace_entry_name">{ room.name }</span>
<StyledCheckbox <StyledCheckbox
onChange={onChange ? (e) => onChange(e.target.checked) : null} onChange={onChange ? (e) => onChange(e.target.checked) : null}
@ -58,14 +62,23 @@ const Entry = ({ room, checked, onChange }) => {
interface IAddExistingToSpaceProps { interface IAddExistingToSpaceProps {
space: Room; space: Room;
selected: Set<Room>; footerPrompt?: ReactNode;
onChange(checked: boolean, room: Room): void; emptySelectionButton?: ReactNode;
onFinished(added: boolean): void;
} }
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, selected, onChange }) => { export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
space,
footerPrompt,
emptySelectionButton,
onFinished,
}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]); const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]);
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [progress, setProgress] = useState<number>(null);
const [error, setError] = useState<Error>(null);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase(); const lcQuery = query.toLowerCase();
@ -93,120 +106,6 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
return arr; return arr;
}, [[], [], []]); }, [[], [], []]);
return <div className="mx_AddExistingToSpace">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Filter your rooms and spaces") }
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
{ rooms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3>
{ rooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : undefined }
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
<h3>{ _t("Spaces") }</h3>
<div className="mx_AddExistingToSpace_section_experimental">
<div>{ _t("Feeling experimental?") }</div>
<div>{ _t("You can add existing spaces to a space.") }</div>
</div>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selected.has(space)}
onChange={onChange ? (checked) => {
onChange(checked, space);
} : null}
/>;
}) }
</div>
) : null }
{ dms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Direct Messages") }</h3>
{ dms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : null }
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [progress, setProgress] = useState<number>(null);
const [error, setError] = useState<Error>(null);
let spaceOptionSection;
if (existingSubspaces.length > 0) {
const options = [space, ...existingSubspaces].map((space) => {
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
});
return <div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} />
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
});
spaceOptionSection = (
<Dropdown
id="mx_SpaceSelectDropdown"
onOptionChange={(key: string) => {
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
}}
value={selectedSpace.roomId}
label={_t("Space selection")}
>
{ options }
</Dropdown>
);
} else {
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
}
const title = <React.Fragment>
<RoomAvatar room={selectedSpace} height={40} width={40} />
<div>
<h1>{ _t("Add existing rooms") }</h1>
{ spaceOptionSection }
</div>
</React.Fragment>;
const addRooms = async () => { const addRooms = async () => {
setError(null); setError(null);
setProgress(0); setProgress(0);
@ -269,20 +168,145 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
</div> </div>
</span>; </span>;
} else { } else {
let button = emptySelectionButton;
if (!button || selectedToAdd.size > 0) {
button = <AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
{ _t("Add") }
</AccessibleButton>;
}
footer = <> footer = <>
<span> <span>
<div>{ _t("Want to add a new room instead?") }</div> { footerPrompt }
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
{ _t("Create a new room") }
</AccessibleButton>
</span> </span>
<AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}> { button }
{ _t("Add") }
</AccessibleButton>
</>; </>;
} }
const onChange = !busy && !error ? (checked, room) => {
if (checked) {
selectedToAdd.add(room);
} else {
selectedToAdd.delete(room);
}
setSelectedToAdd(new Set(selectedToAdd));
} : null;
return <div className="mx_AddExistingToSpace">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Filter your rooms and spaces") }
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
{ rooms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3>
{ rooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : undefined }
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
<h3>{ _t("Spaces") }</h3>
<div className="mx_AddExistingToSpace_section_experimental">
<div>{ _t("Feeling experimental?") }</div>
<div>{ _t("You can add existing spaces to a space.") }</div>
</div>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={onChange ? (checked) => {
onChange(checked, space);
} : null}
/>;
}) }
</div>
) : null }
{ dms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Direct Messages") }</h3>
{ dms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : null }
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
<div className="mx_AddExistingToSpace_footer">
{ footer }
</div>
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
let spaceOptionSection;
if (existingSubspaces.length > 0) {
const options = [space, ...existingSubspaces].map((space) => {
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
});
return <div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} />
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
});
spaceOptionSection = (
<Dropdown
id="mx_SpaceSelectDropdown"
onOptionChange={(key: string) => {
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
}}
value={selectedSpace.roomId}
label={_t("Space selection")}
>
{ options }
</Dropdown>
);
} else {
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
}
const title = <React.Fragment>
<RoomAvatar room={selectedSpace} height={40} width={40} />
<div>
<h1>{ _t("Add existing rooms") }</h1>
{ spaceOptionSection }
</div>
</React.Fragment>;
return <BaseDialog return <BaseDialog
title={title} title={title}
className="mx_AddExistingToSpaceDialog" className="mx_AddExistingToSpaceDialog"
@ -293,21 +317,16 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={cli}>
<AddExistingToSpace <AddExistingToSpace
space={space} space={space}
selected={selectedToAdd} onFinished={onFinished}
onChange={!busy && !error ? (checked, room) => { footerPrompt={<>
if (checked) { <div>{ _t("Want to add a new room instead?") }</div>
selectedToAdd.add(room); <AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
} else { { _t("Create a new room") }
selectedToAdd.delete(room); </AccessibleButton>
} </>}
setSelectedToAdd(new Set(selectedToAdd));
} : null}
/> />
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
<div className="mx_AddExistingToSpaceDialog_footer">
{ footer }
</div>
<SpaceFeedbackPrompt onClick={() => onFinished(false)} /> <SpaceFeedbackPrompt onClick={() => onFinished(false)} />
</BaseDialog>; </BaseDialog>;
}; };

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,7 +16,7 @@ limitations under the License.
import * as React from 'react'; import * as React from 'react';
import BaseDialog from './BaseDialog'; import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler'; import { _t, getUserLanguage } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { import {
ClientWidgetApi, ClientWidgetApi,
@ -39,6 +39,8 @@ import {OwnProfileStore} from "../../../stores/OwnProfileStore";
import { arrayFastClone } from "../../../utils/arrays"; import { arrayFastClone } from "../../../utils/arrays";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import {ELEMENT_CLIENT_ID} from "../../../identifiers";
import SettingsStore from "../../../settings/SettingsStore";
interface IProps { interface IProps {
widgetDefinition: IModalWidgetOpenRequestData; widgetDefinition: IModalWidgetOpenRequestData;
@ -129,6 +131,9 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
currentUserId: MatrixClientPeg.get().getUserId(), currentUserId: MatrixClientPeg.get().getUserId(),
userDisplayName: OwnProfileStore.instance.displayName, userDisplayName: OwnProfileStore.instance.displayName,
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(), userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
clientId: ELEMENT_CLIENT_ID,
clientTheme: SettingsStore.getValue("theme"),
clientLanguage: getUserLanguage(),
}); });
const parsed = new URL(templated); const parsed = new URL(templated);

View File

@ -217,6 +217,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
value={this.state.otherHomeserver} value={this.state.otherHomeserver}
validateOnChange={false} validateOnChange={false}
validateOnFocus={false} validateOnFocus={false}
id="mx_homeserverInput"
/> />
</StyledRadioButton> </StyledRadioButton>
<p> <p>

View File

@ -345,6 +345,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}> <form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
<input <input
type="password" type="password"
id="mx_passPhraseInput"
className="mx_AccessSecretStorageDialog_passPhraseInput" className="mx_AccessSecretStorageDialog_passPhraseInput"
onChange={this.onPassPhraseChange} onChange={this.onPassPhraseChange}
value={this.state.passPhrase} value={this.state.passPhrase}

View File

@ -37,7 +37,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
effect = new Effect(options); effect = new Effect(options);
effectsRef.current[name] = effect; effectsRef.current[name] = effect;
} catch (err) { } catch (err) {
console.warn('Unable to load effect module at \'../../../effects/${name}\'.', err); console.warn(`Unable to load effect module at '../../../effects/${name}.`, err);
} }
} }
return effect; return effect;

View File

@ -207,6 +207,7 @@ export default class ImageView extends React.Component<IProps, IState> {
a.href = this.props.src; a.href = this.props.src;
a.download = this.props.name; a.download = this.props.name;
a.target = "_blank"; a.target = "_blank";
a.rel = "noreferrer noopener";
a.click(); a.click();
}; };

View File

@ -58,13 +58,8 @@ export default class LanguageDropdown extends React.Component {
// If no value is given, we start with the first // If no value is given, we start with the first
// country selected, but our parent component // country selected, but our parent component
// doesn't know this, therefore we do this. // doesn't know this, therefore we do this.
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); const language = languageHandler.getUserLanguage();
if (language) {
this.props.onOptionChange(language); this.props.onOptionChange(language);
} else {
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
this.props.onOptionChange(language);
}
} }
} }

View File

@ -31,6 +31,7 @@ import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessi
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import {canCancel} from "../context_menus/MessageContextMenu"; import {canCancel} from "../context_menus/MessageContextMenu";
import Resend from "../../../Resend"; import Resend from "../../../Resend";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@ -122,6 +123,10 @@ export default class MessageActionBar extends React.PureComponent {
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) { if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
this.props.mxEvent.on("Event.status", this.onSent); this.props.mxEvent.on("Event.status", this.onSent);
} }
const client = MatrixClientPeg.get();
client.decryptEventIfNeeded(this.props.mxEvent);
if (this.props.mxEvent.isBeingDecrypted()) { if (this.props.mxEvent.isBeingDecrypted()) {
this.props.mxEvent.once("Event.decrypted", this.onDecrypted); this.props.mxEvent.once("Event.decrypted", this.onDecrypted);
} }

View File

@ -50,6 +50,10 @@ const ReactButton = ({ mxEvent, reactions }: IProps) => {
})} })}
title={_t("Add reaction")} title={_t("Add reaction")}
onClick={openMenu} onClick={openMenu}
onContextMenu={e => {
e.preventDefault();
openMenu();
}}
isExpanded={menuDisplayed} isExpanded={menuDisplayed}
inputRef={button} inputRef={button}
/> />
@ -174,6 +178,8 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
/>; />;
}).filter(item => !!item); }).filter(item => !!item);
if (!items.length) return null;
// Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items. // Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items.
// The "+ 1" ensure that the "show all" reveals something that takes up // The "+ 1" ensure that the "show all" reveals something that takes up
// more space than the button itself. // more space than the button itself.

View File

@ -18,6 +18,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@replaceableComponent("views.messages.ViewSourceEvent") @replaceableComponent("views.messages.ViewSourceEvent")
export default class ViewSourceEvent extends React.PureComponent { export default class ViewSourceEvent extends React.PureComponent {
@ -36,6 +37,10 @@ export default class ViewSourceEvent extends React.PureComponent {
componentDidMount() { componentDidMount() {
const {mxEvent} = this.props; const {mxEvent} = this.props;
const client = MatrixClientPeg.get();
client.decryptEventIfNeeded(mxEvent);
if (mxEvent.isBeingDecrypted()) { if (mxEvent.isBeingDecrypted()) {
mxEvent.once("Event.decrypted", () => this.forceUpdate()); mxEvent.once("Event.decrypted", () => this.forceUpdate());
} }

View File

@ -23,8 +23,6 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import Analytics from "../../../Analytics"; import Analytics from "../../../Analytics";
import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { CSSTransition } from "react-transition-group"; import { CSSTransition } from "react-transition-group";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import { DefaultTagID } from "../../../stores/room-list/models";
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
import Toolbar from "../../../accessibility/Toolbar"; import Toolbar from "../../../accessibility/Toolbar";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -84,8 +82,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
public render(): React.ReactElement { public render(): React.ReactElement {
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => { const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
const roomTags = RoomListStore.instance.getTagsForRoom(r);
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
return ( return (
<RovingAccessibleTooltipButton <RovingAccessibleTooltipButton
className="mx_RoomBreadcrumbs_crumb" className="mx_RoomBreadcrumbs_crumb"
@ -98,7 +94,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
<DecoratedRoomAvatar <DecoratedRoomAvatar
room={r} room={r}
avatarSize={32} avatarSize={32}
tag={roomTag}
displayBadge={true} displayBadge={true}
forceCount={true} forceCount={true}
/> />

View File

@ -27,7 +27,6 @@ import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {DefaultTagID} from "../../../stores/room-list/models";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import RoomTopic from "../elements/RoomTopic"; import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName"; import RoomName from "../elements/RoomName";
@ -177,7 +176,6 @@ export default class RoomHeader extends React.Component {
roomAvatar = <DecoratedRoomAvatar roomAvatar = <DecoratedRoomAvatar
room={this.props.room} room={this.props.room}
avatarSize={32} avatarSize={32}
tag={DefaultTagID.Untagged} // to apply room publicity badging
oobData={this.props.oobData} oobData={this.props.oobData}
viewAvatarOnClick={true} viewAvatarOnClick={true}
/>; />;

View File

@ -576,7 +576,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
const roomAvatar = <DecoratedRoomAvatar const roomAvatar = <DecoratedRoomAvatar
room={this.props.room} room={this.props.room}
avatarSize={32} avatarSize={32}
tag={this.props.tag}
displayBadge={this.props.isMinimized} displayBadge={this.props.isMinimized}
oobData={({avatarUrl: roomProfile.avatarMxc})} oobData={({avatarUrl: roomProfile.avatarMxc})}
/>; />;

View File

@ -23,6 +23,7 @@ import {copyPlaintext} from "../../../utils/strings";
import {sleep} from "../../../utils/promise"; import {sleep} from "../../../utils/promise";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import {showRoomInviteDialog} from "../../../RoomInvite"; import {showRoomInviteDialog} from "../../../RoomInvite";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
interface IProps { interface IProps {
space: Room; space: Room;
@ -50,7 +51,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
<h3>{ _t("Share invite link") }</h3> <h3>{ _t("Share invite link") }</h3>
<span>{ copiedText }</span> <span>{ copiedText }</span>
</AccessibleButton> </AccessibleButton>
<AccessibleButton { space.canInvite(MatrixClientPeg.get()?.getUserId()) ? <AccessibleButton
className="mx_SpacePublicShare_inviteButton" className="mx_SpacePublicShare_inviteButton"
onClick={() => { onClick={() => {
showRoomInviteDialog(space.roomId); showRoomInviteDialog(space.roomId);
@ -59,7 +60,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
> >
<h3>{ _t("Invite people") }</h3> <h3>{ _t("Invite people") }</h3>
<span>{ _t("Invite with email or username") }</span> <span>{ _t("Invite with email or username") }</span>
</AccessibleButton> </AccessibleButton> : null }
</div>; </div>;
}; };

View File

@ -209,7 +209,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
const userId = this.context.getUserId(); const userId = this.context.getUserId();
let inviteOption; let inviteOption;
if (this.props.space.canInvite(userId)) { if (this.props.space.getJoinRule() === "public" || this.props.space.canInvite(userId)) {
inviteOption = ( inviteOption = (
<IconizedContextMenuOption <IconizedContextMenuOption
className="mx_SpacePanel_contextMenu_inviteButton" className="mx_SpacePanel_contextMenu_inviteButton"

View File

@ -57,8 +57,8 @@ export default class PlaybackWaveform extends React.PureComponent<IProps, IState
}; };
private onTimeUpdate = (time: number[]) => { private onTimeUpdate = (time: number[]) => {
// Track percentages to very coarse precision, otherwise 0.002 ends up highlighting a bar. // Track percentages to a general precision to avoid over-waking the component.
const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(1)); const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(3));
this.setState({progress}); this.setState({progress});
}; };

43
src/effects/effect.ts Normal file
View File

@ -0,0 +1,43 @@
/*
Copyright 2020 Nurjin Jafar
Copyright 2020 Nordeck IT + Consulting GmbH.
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.
*/
export type Effect<TOptions extends { [key: string]: any }> = {
/**
* one or more emojis that will trigger this effect
*/
emojis: Array<string>;
/**
* the matrix message type that will trigger this effect
*/
msgType: string;
/**
* the room command to trigger this effect
*/
command: string;
/**
* a function that returns the translated description of the effect
*/
description: () => string;
/**
* a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message
*/
fallbackMessage: () => string;
/**
* animation options
*/
options: TOptions;
}

View File

@ -15,80 +15,11 @@
limitations under the License. limitations under the License.
*/ */
import { _t, _td } from "../languageHandler"; import { _t, _td } from "../languageHandler";
import { ConfettiOptions } from "./confetti";
export type Effect<TOptions extends { [key: string]: any }> = { import { Effect } from "./effect";
/** import { FireworksOptions } from "./fireworks";
* one or more emojis that will trigger this effect import { SnowfallOptions } from "./snowfall";
*/ import { SpaceInvadersOptions } from "./spaceinvaders";
emojis: Array<string>;
/**
* the matrix message type that will trigger this effect
*/
msgType: string;
/**
* the room command to trigger this effect
*/
command: string;
/**
* a function that returns the translated description of the effect
*/
description: () => string;
/**
* a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message
*/
fallbackMessage: () => string;
/**
* animation options
*/
options: TOptions;
}
type ConfettiOptions = {
/**
* max confetti count
*/
maxCount: number;
/**
* particle animation speed
*/
speed: number;
/**
* the confetti animation frame interval in milliseconds
*/
frameInterval: number;
/**
* the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
*/
alpha: number;
/**
* use gradient instead of solid particle color
*/
gradient: boolean;
};
type FireworksOptions = {
/**
* max fireworks count
*/
maxCount: number;
/**
* gravity value that firework adds to shift from it's start position
*/
gravity: number;
}
type SnowfallOptions = {
/**
* The maximum number of snowflakes to render at a given time
*/
maxCount: number;
/**
* The amount of gravity to apply to the snowflakes
*/
gravity: number;
/**
* The amount of drift (horizontal sway) to apply to the snowflakes. Each snowflake varies.
*/
maxDrift: number;
}
/** /**
* This configuration defines room effects that can be triggered by custom message types and emojis * This configuration defines room effects that can be triggered by custom message types and emojis
@ -131,6 +62,17 @@ export const CHAT_EFFECTS: Array<Effect<{ [key: string]: any }>> = [
maxDrift: 5, maxDrift: 5,
}, },
} as Effect<SnowfallOptions>, } as Effect<SnowfallOptions>,
{
emojis: ["👾", "🌌"],
msgType: "io.element.effects.space_invaders",
command: "spaceinvaders",
description: () => _td("Sends the given message with a space themed effect"),
fallbackMessage: () => _t("sends space invaders") + " 👾",
options: {
maxCount: 50,
gravity: 0.01,
},
} as Effect<SpaceInvadersOptions>,
]; ];

View File

@ -0,0 +1,119 @@
/*
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.
*/
import ICanvasEffect from '../ICanvasEffect';
import { arrayFastClone } from "../../utils/arrays";
export type SpaceInvadersOptions = {
/**
* The maximum number of invaders to render at a given time
*/
maxCount: number;
/**
* The amount of gravity to apply to the invaders
*/
gravity: number;
}
type Invader = {
x: number;
y: number;
xCol: number;
gravity: number;
}
export const DefaultOptions: SpaceInvadersOptions = {
maxCount: 50,
gravity: 0.005,
};
const KEY_FRAME_INTERVAL = 15; // 15ms, roughly
const GLYPH = "👾";
export default class SpaceInvaders implements ICanvasEffect {
private readonly options: SpaceInvadersOptions;
constructor(options: { [key: string]: any }) {
this.options = {...DefaultOptions, ...options};
}
private context: CanvasRenderingContext2D | null = null;
private particles: Array<Invader> = [];
private lastAnimationTime: number;
public isRunning: boolean;
public start = async (canvas: HTMLCanvasElement, timeout = 3000) => {
if (!canvas) {
return;
}
this.context = canvas.getContext('2d');
this.particles = [];
const count = this.options.maxCount;
while (this.particles.length < count) {
this.particles.push(this.resetParticle({} as Invader, canvas.width, canvas.height));
}
this.isRunning = true;
requestAnimationFrame(this.renderLoop);
if (timeout) {
window.setTimeout(this.stop, timeout);
}
}
public stop = async () => {
this.isRunning = false;
}
private resetParticle = (particle: Invader, width: number, height: number): Invader => {
particle.x = Math.random() * width;
particle.y = Math.random() * -height;
particle.xCol = particle.x;
particle.gravity = this.options.gravity + (Math.random() * 6) + 4;
return particle;
}
private renderLoop = (): void => {
if (!this.context || !this.context.canvas) {
return;
}
if (this.particles.length === 0) {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
} else {
const timeDelta = Date.now() - this.lastAnimationTime;
if (timeDelta >= KEY_FRAME_INTERVAL || !this.lastAnimationTime) {
// Clear the screen first
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
this.lastAnimationTime = Date.now();
this.animateAndRenderInvaders();
}
requestAnimationFrame(this.renderLoop);
}
};
private animateAndRenderInvaders() {
if (!this.context || !this.context.canvas) {
return;
}
this.context.font = "50px Twemoji";
for (const particle of arrayFastClone(this.particles)) {
particle.y += particle.gravity;
this.context.save();
this.context.fillText(GLYPH, particle.x, particle.y);
this.context.restore();
}
}
}

View File

@ -600,6 +600,10 @@
"See when the avatar changes in this room": "See when the avatar changes in this room", "See when the avatar changes in this room": "See when the avatar changes in this room",
"Change the avatar of your active room": "Change the avatar of your active room", "Change the avatar of your active room": "Change the avatar of your active room",
"See when the avatar changes in your active room": "See when the avatar changes in your active room", "See when the avatar changes in your active room": "See when the avatar changes in your active room",
"Kick, ban, or invite people to this room, and make you leave": "Kick, ban, or invite people to this room, and make you leave",
"See when people join, leave, or are invited to this room": "See when people join, leave, or are invited to this room",
"Kick, ban, or invite people to your active room, and make you leave": "Kick, ban, or invite people to your active room, and make you leave",
"See when people join, leave, or are invited to your active room": "See when people join, leave, or are invited to your active room",
"Send stickers to this room as you": "Send stickers to this room as you", "Send stickers to this room as you": "Send stickers to this room as you",
"See when a sticker is posted in this room": "See when a sticker is posted in this room", "See when a sticker is posted in this room": "See when a sticker is posted in this room",
"Send stickers to your active room as you": "Send stickers to your active room as you", "Send stickers to your active room as you": "Send stickers to your active room as you",
@ -880,6 +884,8 @@
"sends fireworks": "sends fireworks", "sends fireworks": "sends fireworks",
"Sends the given message with snowfall": "Sends the given message with snowfall", "Sends the given message with snowfall": "Sends the given message with snowfall",
"sends snowfall": "sends snowfall", "sends snowfall": "sends snowfall",
"Sends the given message with a space themed effect": "Sends the given message with a space themed effect",
"sends space invaders": "sends space invaders",
"unknown person": "unknown person", "unknown person": "unknown person",
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>", "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>", "You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
@ -2033,15 +2039,15 @@
"Add a new server...": "Add a new server...", "Add a new server...": "Add a new server...",
"%(networkName)s rooms": "%(networkName)s rooms", "%(networkName)s rooms": "%(networkName)s rooms",
"Matrix rooms": "Matrix rooms", "Matrix rooms": "Matrix rooms",
"Not all selected were added": "Not all selected were added",
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
"Filter your rooms and spaces": "Filter your rooms and spaces", "Filter your rooms and spaces": "Filter your rooms and spaces",
"Feeling experimental?": "Feeling experimental?", "Feeling experimental?": "Feeling experimental?",
"You can add existing spaces to a space.": "You can add existing spaces to a space.", "You can add existing spaces to a space.": "You can add existing spaces to a space.",
"Direct Messages": "Direct Messages", "Direct Messages": "Direct Messages",
"Space selection": "Space selection", "Space selection": "Space selection",
"Add existing rooms": "Add existing rooms", "Add existing rooms": "Add existing rooms",
"Not all selected were added": "Not all selected were added",
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
"Want to add a new room instead?": "Want to add a new room instead?", "Want to add a new room instead?": "Want to add a new room instead?",
"Create a new room": "Create a new room", "Create a new room": "Create a new room",
"Matrix ID": "Matrix ID", "Matrix ID": "Matrix ID",
@ -2704,13 +2710,12 @@
"Failed to create initial space rooms": "Failed to create initial space rooms", "Failed to create initial space rooms": "Failed to create initial space rooms",
"Skip for now": "Skip for now", "Skip for now": "Skip for now",
"Creating rooms...": "Creating rooms...", "Creating rooms...": "Creating rooms...",
"Failed to add rooms to space": "Failed to add rooms to space",
"Adding...": "Adding...",
"What do you want to organise?": "What do you want to organise?", "What do you want to organise?": "What do you want to organise?",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.", "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.",
"Share %(name)s": "Share %(name)s", "Share %(name)s": "Share %(name)s",
"It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.", "It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.",
"Go to my first room": "Go to my first room", "Go to my first room": "Go to my first room",
"Go to my space": "Go to my space",
"Who are you working with?": "Who are you working with?", "Who are you working with?": "Who are you working with?",
"Make sure the right people have access to %(name)s": "Make sure the right people have access to %(name)s", "Make sure the right people have access to %(name)s": "Make sure the right people have access to %(name)s",
"Just me": "Just me", "Just me": "Just me",
@ -2834,6 +2839,7 @@
"Room Notification": "Room Notification", "Room Notification": "Room Notification",
"Notification Autocomplete": "Notification Autocomplete", "Notification Autocomplete": "Notification Autocomplete",
"Room Autocomplete": "Room Autocomplete", "Room Autocomplete": "Room Autocomplete",
"Space Autocomplete": "Space Autocomplete",
"Users": "Users", "Users": "Users",
"User Autocomplete": "User Autocomplete", "User Autocomplete": "User Autocomplete",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.", "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.",

17
src/identifiers.ts Normal file
View File

@ -0,0 +1,17 @@
/*
* 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.
*/
export const ELEMENT_CLIENT_ID = "io.element.web";

View File

@ -177,8 +177,10 @@ export default class EventIndex extends EventEmitter {
* listener. * listener.
*/ */
onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => { onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => {
const client = MatrixClientPeg.get();
// We only index encrypted rooms locally. // We only index encrypted rooms locally.
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; if (!client.isRoomEncrypted(room.roomId)) return;
// If it isn't a live event or if it's redacted there's nothing to // If it isn't a live event or if it's redacted there's nothing to
// do. // do.
@ -187,10 +189,7 @@ export default class EventIndex extends EventEmitter {
return; return;
} }
if (ev.isBeingDecrypted()) { await client.decryptEventIfNeeded(ev);
// XXX: Private member access
await ev._decryptionPromise;
}
await this.addLiveEventToIndex(ev); await this.addLiveEventToIndex(ev);
} }
@ -518,19 +517,10 @@ export default class EventIndex extends EventEmitter {
const decryptionPromises = matrixEvents const decryptionPromises = matrixEvents
.filter(event => event.isEncrypted()) .filter(event => event.isEncrypted())
.map(event => { .map(event => {
if (event.shouldAttemptDecryption()) { return client.decryptEventIfNeeded(event, {
return event.attemptDecryption(client._crypto, {
isRetry: true, isRetry: true,
emit: false, emit: false,
}); });
} else {
// TODO the decryption promise is a private property, this
// should either be made public or we should convert the
// event that gets fired when decryption is done into a
// promise using the once event emitter method:
// https://nodejs.org/api/events.html#events_events_once_emitter_name
return event._decryptionPromise;
}
}); });
// Let us wait for all the events to get decrypted. // Let us wait for all the events to get decrypted.

View File

@ -56,6 +56,15 @@ export function newTranslatableError(message: string) {
return error; return error;
} }
export function getUserLanguage(): string {
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
if (language) {
return language;
} else {
return normalizeLanguageKey(getLanguageFromBrowser());
}
}
// Function which only purpose is to mark that a string is translatable // Function which only purpose is to mark that a string is translatable
// Does not actually do anything. It's helpful for automatic extraction of translatable strings // Does not actually do anything. It's helpful for automatic extraction of translatable strings
export function _td(s: string): string { export function _td(s: string): string {
@ -455,10 +464,14 @@ function getLangsJson(): Promise<object> {
request( request(
{ method: "GET", url }, { method: "GET", url },
(err, response, body) => { (err, response, body) => {
if (err || response.status < 200 || response.status >= 300) { if (err) {
reject(err); reject(err);
return; return;
} }
if (response.status < 200 || response.status >= 300) {
reject(new Error(`Failed to load ${url}, got ${response.status}`));
return;
}
resolve(JSON.parse(body)); resolve(JSON.parse(body));
}, },
); );
@ -498,10 +511,14 @@ function getLanguage(langPath: string): Promise<object> {
request( request(
{ method: "GET", url: langPath }, { method: "GET", url: langPath },
(err, response, body) => { (err, response, body) => {
if (err || response.status < 200 || response.status >= 300) { if (err) {
reject(err); reject(err);
return; return;
} }
if (response.status < 200 || response.status >= 300) {
reject(new Error(`Failed to load ${langPath}, got ${response.status}`));
return;
}
resolve(weblateToCounterpart(JSON.parse(body))); resolve(weblateToCounterpart(JSON.parse(body)));
}, },
); );

View File

@ -62,7 +62,7 @@ export const getOrder = (order: string, creationTs: number, roomId: string): Arr
if (typeof order === "string" && Array.from(order).every((c: string) => { if (typeof order === "string" && Array.from(order).every((c: string) => {
const charCode = c.charCodeAt(0); const charCode = c.charCodeAt(0);
return charCode >= 0x20 && charCode <= 0x7F; return charCode >= 0x20 && charCode <= 0x7E;
})) { })) {
validatedOrder = order; validatedOrder = order;
} }

View File

@ -34,6 +34,7 @@ import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
import { getListAlgorithmInstance } from "./list-ordering"; import { getListAlgorithmInstance } from "./list-ordering";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { VisibilityProvider } from "../filters/VisibilityProvider"; import { VisibilityProvider } from "../filters/VisibilityProvider";
import { MultiLock } from "../../../utils/MultiLock";
/** /**
* Fired when the Algorithm has determined a list has been updated. * Fired when the Algorithm has determined a list has been updated.
@ -77,6 +78,7 @@ export class Algorithm extends EventEmitter {
} = {}; } = {};
private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>(); private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
private allowedRoomsByFilters: Set<Room> = new Set<Room>(); private allowedRoomsByFilters: Set<Room> = new Set<Room>();
private handlerLock = new MultiLock();
/** /**
* Set to true to suspend emissions of algorithm updates. * Set to true to suspend emissions of algorithm updates.
@ -677,6 +679,12 @@ export class Algorithm extends EventEmitter {
* processing. * processing.
*/ */
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> { public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`Acquiring lock for ${room.roomId} with cause ${cause}`);
}
const release = await this.handlerLock.acquire(room.roomId);
try {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`); console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
@ -712,7 +720,7 @@ export class Algorithm extends EventEmitter {
// If we have tags for a room and don't have the room referenced, something went horribly // If we have tags for a room and don't have the room referenced, something went horribly
// wrong - the reference should have been updated above. // wrong - the reference should have been updated above.
if (hasTags && !knownRoomRef && !isSticky) { if (hasTags && !knownRoomRef && !isSticky) {
throw new Error(`${room.roomId} is missing from room array but is known - trying to find duplicate`); throw new Error(`${room.roomId} is missing from room array but is known`);
} }
// Like above, update the reference to the sticky room if we need to // Like above, update the reference to the sticky room if we need to
@ -800,7 +808,9 @@ export class Algorithm extends EventEmitter {
if (this.stickyRoom === room) { if (this.stickyRoom === room) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`); console.warn(
`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`,
);
} }
return false; return false;
} }
@ -862,8 +872,13 @@ export class Algorithm extends EventEmitter {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`); console.log(
`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`,
);
} }
return changed; return changed;
} finally {
release();
}
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2020 The Matrix.org Foundation C.I.C. * Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -52,6 +52,8 @@ import {getCustomTheme} from "../../theme";
import CountlyAnalytics from "../../CountlyAnalytics"; import CountlyAnalytics from "../../CountlyAnalytics";
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities"; import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { ELEMENT_CLIENT_ID } from "../../identifiers";
import { getUserLanguage } from "../../languageHandler";
// TODO: Destroy all of this code // TODO: Destroy all of this code
@ -194,6 +196,9 @@ export class StopGapWidget extends EventEmitter {
currentUserId: MatrixClientPeg.get().getUserId(), currentUserId: MatrixClientPeg.get().getUserId(),
userDisplayName: OwnProfileStore.instance.displayName, userDisplayName: OwnProfileStore.instance.displayName,
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(), userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
clientId: ELEMENT_CLIENT_ID,
clientTheme: SettingsStore.getValue("theme"),
clientLanguage: getUserLanguage(),
}, opts?.asPopout); }, opts?.asPopout);
const parsed = new URL(templated); const parsed = new URL(templated);

View File

@ -44,6 +44,7 @@ import { CHAT_EFFECTS } from "../../effects";
import { containsEmoji } from "../../effects/utils"; import { containsEmoji } from "../../effects/utils";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import {tryTransformPermalinkToLocalHref} from "../../utils/permalinks/Permalinks"; import {tryTransformPermalinkToLocalHref} from "../../utils/permalinks/Permalinks";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
// TODO: Purge this from the universe // TODO: Purge this from the universe
@ -144,6 +145,52 @@ export class StopGapWidgetDriver extends WidgetDriver {
return {roomId, eventId: r.event_id}; return {roomId, eventId: r.event_id};
} }
public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise<MatrixEvent[]> {
limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice
const client = MatrixClientPeg.get();
const roomId = ActiveRoomObserver.activeRoomId;
const room = client.getRoom(roomId);
if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client");
const results: MatrixEvent[] = [];
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
for (let i = events.length - 1; i > 0; i--) {
if (results.length >= limit) break;
const ev = events[i];
if (ev.getType() !== eventType) continue;
if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()['msgtype']) continue;
results.push(ev);
}
return results.map(e => e.event);
}
public async readStateEvents(
eventType: string, stateKey: string | undefined, limit: number,
): Promise<MatrixEvent[]> {
limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice
const client = MatrixClientPeg.get();
const roomId = ActiveRoomObserver.activeRoomId;
const room = client.getRoom(roomId);
if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client");
const results: MatrixEvent[] = [];
const state = room.currentState.events.get(eventType);
if (state) {
if (stateKey === "" || !!stateKey) {
const forKey = state.get(stateKey);
if (forKey) results.push(forKey);
} else {
results.push(...Array.from(state.values()));
}
}
return results.slice(0, limit).map(e => e.event);
}
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) { public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {
const oidcState = WidgetPermissionStore.instance.getOIDCState( const oidcState = WidgetPermissionStore.instance.getOIDCState(
this.forWidget, this.forWidgetKind, this.inRoomId, this.forWidget, this.forWidgetKind, this.inRoomId,

30
src/utils/MultiLock.ts Normal file
View File

@ -0,0 +1,30 @@
/*
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.
*/
import { EnhancedMap } from "./maps";
import AwaitLock from "await-lock";
export type DoneFn = () => void;
export class MultiLock {
private locks = new EnhancedMap<string, AwaitLock>();
public async acquire(key: string): Promise<DoneFn> {
const lock = this.locks.getOrCreate(key, new AwaitLock());
await lock.acquireAsync();
return () => lock.release();
}
}

View File

@ -75,7 +75,8 @@ export function arraySmoothingResample(input: number[], points: number): number[
for (let i = 1; i < input.length - 1; i += 2) { for (let i = 1; i < input.length - 1; i += 2) {
const prevPoint = input[i - 1]; const prevPoint = input[i - 1];
const nextPoint = input[i + 1]; const nextPoint = input[i + 1];
const average = (prevPoint + nextPoint) / 2; const currPoint = input[i];
const average = (prevPoint + nextPoint + currPoint) / 3;
samples.push(average); samples.push(average);
} }
input = samples; input = samples;

View File

@ -21,6 +21,7 @@ import {SimpleObservable} from "matrix-widget-api";
import { IDestroyable } from "../utils/IDestroyable"; import { IDestroyable } from "../utils/IDestroyable";
import { PlaybackClock } from "./PlaybackClock"; import { PlaybackClock } from "./PlaybackClock";
import { createAudioContext, decodeOgg } from "./compat"; import { createAudioContext, decodeOgg } from "./compat";
import { clamp } from "../utils/numbers";
export enum PlaybackState { export enum PlaybackState {
Decoding = "decoding", Decoding = "decoding",
@ -33,9 +34,20 @@ export const PLAYBACK_WAVEFORM_SAMPLES = 39;
const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
function makePlaybackWaveform(input: number[]): number[] { function makePlaybackWaveform(input: number[]): number[] {
// We use a smoothing resample to keep the rough shape of the waveform the user will be seeing. We // First, convert negative amplitudes to positive so we don't detect zero as "noisy".
// then rescale so the user can see the waveform properly (loud noises == 100%). const noiseWaveform = input.map(v => Math.abs(v));
return arrayRescale(arraySmoothingResample(input, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
// Next, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
// We also rescale the waveform to be 0-1 for the remaining function logic.
const resampled = arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
// Then, we'll do a high and low pass filter to isolate actual speaking volumes within the rescaled
// waveform. Most speech happens below the 0.5 mark.
const filtered = resampled.map(v => clamp(v, 0.1, 0.5));
// Finally, we'll rescale the filtered waveform (0.1-0.5 becomes 0-1 again) so the user sees something
// sensible. This is what we return to keep our contract of "values between zero and one".
return arrayRescale(filtered, 0, 1);
} }
export class Playback extends EventEmitter implements IDestroyable { export class Playback extends EventEmitter implements IDestroyable {
@ -126,6 +138,7 @@ export class Playback extends EventEmitter implements IDestroyable {
this.waveformObservable.update(this.resampledWaveform); this.waveformObservable.update(this.resampledWaveform);
this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore
this.clock.flagLoadTime(); // must happen first because setting the duration fires a clock update
this.clock.durationSeconds = this.audioBuf.duration; this.clock.durationSeconds = this.audioBuf.duration;
} }

View File

@ -54,6 +54,15 @@ export class PlaybackClock implements IDestroyable {
} }
}; };
/**
* Mark the time in the audio context where the clip starts/has been loaded.
* This is to ensure the clock isn't skewed into thinking it is ~0.5s into
* a clip when the duration is set.
*/
public flagLoadTime() {
this.clipStart = this.context.currentTime;
}
public flagStart() { public flagStart() {
if (this.stopped) { if (this.stopped) {
this.clipStart = this.context.currentTime; this.clipStart = this.context.currentTime;

View File

@ -96,6 +96,16 @@ export class CapabilityText {
[EventDirection.Receive]: _td("See when the avatar changes in your active room"), [EventDirection.Receive]: _td("See when the avatar changes in your active room"),
}, },
}, },
[EventType.RoomMember]: {
[WidgetKind.Room]: {
[EventDirection.Send]: _td("Kick, ban, or invite people to this room, and make you leave"),
[EventDirection.Receive]: _td("See when people join, leave, or are invited to this room"),
},
[GENERIC_WIDGET_KIND]: {
[EventDirection.Send]: _td("Kick, ban, or invite people to your active room, and make you leave"),
[EventDirection.Receive]: _td("See when people join, leave, or are invited to your active room"),
},
},
}; };
private static nonStateSendRecvCaps: ISendRecvStaticCapText = { private static nonStateSendRecvCaps: ISendRecvStaticCapText = {

View File

@ -95,6 +95,7 @@ export function createTestClient() {
getItem: jest.fn(), getItem: jest.fn(),
}, },
}, },
decryptEventIfNeeded: () => Promise.resolve(),
}; };
} }

View File

@ -73,10 +73,10 @@ describe('arrays', () => {
// we'd be feeding a thousand values in and seeing what a curve of 250 values looks like, // we'd be feeding a thousand values in and seeing what a curve of 250 values looks like,
// but that's not really feasible to manually verify accuracy. // but that's not really feasible to manually verify accuracy.
[ [
{input: [2, 2, 0, 2, 2, 0, 2, 2, 0], output: [1, 1, 2, 1]}, // Odd -> Even {input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3, 3]}, // Odd -> Even
{input: [2, 2, 0, 2, 2, 0, 2, 2, 0], output: [1, 1, 2]}, // Odd -> Odd {input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3]}, // Odd -> Odd
{input: [2, 2, 0, 2, 2, 0, 2, 2], output: [1, 1, 2]}, // Even -> Odd {input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3, 3]}, // Even -> Odd
{input: [2, 2, 0, 2, 2, 0, 2, 2], output: [1, 2]}, // Even -> Even {input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3]}, // Even -> Even
].forEach((c, i) => expectSample(i, c.input, c.output, true)); ].forEach((c, i) => expectSample(i, c.input, c.output, true));
}); });

View File

@ -5670,8 +5670,8 @@ mathml-tag-names@^2.1.3:
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
version "10.1.0" version "11.0.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2d73805ca3d8c5a140fe05e574f826696de1656a" resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/52a893a8116d60bb76f1b015d3161a15404b3628"
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
another-json "^0.2.0" another-json "^0.2.0"
@ -5704,10 +5704,10 @@ matrix-react-test-utils@^0.2.2:
"@babel/traverse" "^7.13.17" "@babel/traverse" "^7.13.17"
walk "^2.3.14" walk "^2.3.14"
matrix-widget-api@^0.1.0-beta.13: matrix-widget-api@^0.1.0-beta.14:
version "0.1.0-beta.13" version "0.1.0-beta.14"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.13.tgz#ebddc83eaef39bbb87b621a02a35902e1a29b9ef" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70"
integrity sha512-DJAvuX2E7gxc/a9rtJPDh17ba9xGIOAoBHcWirNTN3KGodzsrZ+Ns+M/BREFWMwGS5yEBZko5eq7uhXStEbnyQ== integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ==
dependencies: dependencies:
"@types/events" "^3.0.0" "@types/events" "^3.0.0"
events "^3.2.0" events "^3.2.0"