Merge branch 'develop' into export-conversations

This commit is contained in:
Jaiwanth 2021-06-08 18:43:03 +05:30 committed by GitHub
commit dbb3614374
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1236 additions and 569 deletions

View file

@ -18,7 +18,7 @@ module.exports = {
},
overrides: [{
"files": ["src/**/*.{ts,tsx}"],
"files": ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
"extends": ["matrix-org/ts"],
"rules": {
// We're okay being explicit at the moment

View file

@ -1,3 +1,109 @@
Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0)
* Upgrade to JS SDK 11.2.0
* [Release] Fix notif panel timestamp padding
[\#6158](https://github.com/matrix-org/matrix-react-sdk/pull/6158)
Changes in [3.23.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0-rc.1) (2021-06-01)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0...v3.23.0-rc.1)
* Upgrade to JS SDK 11.2.0-rc.1
* Translations update from Weblate
[\#6128](https://github.com/matrix-org/matrix-react-sdk/pull/6128)
* Fix all DMs wrongly appearing in room list when `m.direct` is changed
[\#6122](https://github.com/matrix-org/matrix-react-sdk/pull/6122)
* Update way of checking for registration disabled
[\#6123](https://github.com/matrix-org/matrix-react-sdk/pull/6123)
* Fix the ability to remove avatar from a space via settings
[\#6126](https://github.com/matrix-org/matrix-react-sdk/pull/6126)
* Switch to stable endpoint/fields for MSC2858
[\#6125](https://github.com/matrix-org/matrix-react-sdk/pull/6125)
* Clear stored editor state when canceling editing using a shortcut
[\#6117](https://github.com/matrix-org/matrix-react-sdk/pull/6117)
* Respect newlines in space topics
[\#6124](https://github.com/matrix-org/matrix-react-sdk/pull/6124)
* Add url param `defaultUsername` to prefill the login username field
[\#5674](https://github.com/matrix-org/matrix-react-sdk/pull/5674)
* Bump ws from 7.4.2 to 7.4.6
[\#6115](https://github.com/matrix-org/matrix-react-sdk/pull/6115)
* Sticky headers repositioning without layout trashing
[\#6110](https://github.com/matrix-org/matrix-react-sdk/pull/6110)
* Handle user_busy in voip calls
[\#6112](https://github.com/matrix-org/matrix-react-sdk/pull/6112)
* Avoid showing warning modals from the invite dialog after it unmounts
[\#6105](https://github.com/matrix-org/matrix-react-sdk/pull/6105)
* Fix misleading child counts in spaces
[\#6109](https://github.com/matrix-org/matrix-react-sdk/pull/6109)
* Close creation menu when expanding space panel via expand hierarchy
[\#6090](https://github.com/matrix-org/matrix-react-sdk/pull/6090)
* Prevent having duplicates in pending room state
[\#6108](https://github.com/matrix-org/matrix-react-sdk/pull/6108)
* Update reactions row on event decryption
[\#6106](https://github.com/matrix-org/matrix-react-sdk/pull/6106)
* Destroy playback instance on voice message unmount
[\#6101](https://github.com/matrix-org/matrix-react-sdk/pull/6101)
* Fix message preview not up to date
[\#6102](https://github.com/matrix-org/matrix-react-sdk/pull/6102)
* Convert some Flow typed files to TS (round 2)
[\#6076](https://github.com/matrix-org/matrix-react-sdk/pull/6076)
* Remove unused middlePanelResized event listener
[\#6086](https://github.com/matrix-org/matrix-react-sdk/pull/6086)
* Fix accessing currentState on an invalid joinedRoom
[\#6100](https://github.com/matrix-org/matrix-react-sdk/pull/6100)
* Remove Promise allSettled polyfill as js-sdk uses it directly
[\#6097](https://github.com/matrix-org/matrix-react-sdk/pull/6097)
* Prevent DecoratedRoomAvatar to update its state for the same value
[\#6099](https://github.com/matrix-org/matrix-react-sdk/pull/6099)
* Skip generatePreview if event is not part of the live timeline
[\#6098](https://github.com/matrix-org/matrix-react-sdk/pull/6098)
* fix sticky headers when results num get displayed
[\#6095](https://github.com/matrix-org/matrix-react-sdk/pull/6095)
* Improve addEventsToTimeline performance scoping WhoIsTypingTile::setState
[\#6094](https://github.com/matrix-org/matrix-react-sdk/pull/6094)
* Safeguards to prevent layout trashing for window dimensions
[\#6092](https://github.com/matrix-org/matrix-react-sdk/pull/6092)
* Use local room state to render space hierarchy if the room is known
[\#6089](https://github.com/matrix-org/matrix-react-sdk/pull/6089)
* Add spinner in UserMenu to list pending long running actions
[\#6085](https://github.com/matrix-org/matrix-react-sdk/pull/6085)
* Stop overscroll in Firefox Nightly for macOS
[\#6093](https://github.com/matrix-org/matrix-react-sdk/pull/6093)
* Move SettingsStore watchers/monitors over to ES6 maps for performance
[\#6063](https://github.com/matrix-org/matrix-react-sdk/pull/6063)
* Bump libolm version.
[\#6080](https://github.com/matrix-org/matrix-react-sdk/pull/6080)
* Improve styling of the message action bar
[\#6066](https://github.com/matrix-org/matrix-react-sdk/pull/6066)
* Improve explore rooms when no results are found
[\#6070](https://github.com/matrix-org/matrix-react-sdk/pull/6070)
* Remove logo spinner
[\#6078](https://github.com/matrix-org/matrix-react-sdk/pull/6078)
* Fix add reaction prompt showing even when user is not joined to room
[\#6073](https://github.com/matrix-org/matrix-react-sdk/pull/6073)
* Vectorize spinners
[\#5680](https://github.com/matrix-org/matrix-react-sdk/pull/5680)
* Fix handling of via servers for suggested rooms
[\#6077](https://github.com/matrix-org/matrix-react-sdk/pull/6077)
* Upgrade showChatEffects to room-level setting exposure
[\#6075](https://github.com/matrix-org/matrix-react-sdk/pull/6075)
* Delete RoomView dead code
[\#6071](https://github.com/matrix-org/matrix-react-sdk/pull/6071)
* Reduce noise in tests
[\#6074](https://github.com/matrix-org/matrix-react-sdk/pull/6074)
* Fix room name issues in right panel summary card
[\#6069](https://github.com/matrix-org/matrix-react-sdk/pull/6069)
* Cache normalized room name
[\#6072](https://github.com/matrix-org/matrix-react-sdk/pull/6072)
* Update MemberList to reflect changes for invite permission change
[\#6061](https://github.com/matrix-org/matrix-react-sdk/pull/6061)
* Delete RoomView dead code
[\#6065](https://github.com/matrix-org/matrix-react-sdk/pull/6065)
* Show subspace rooms count even if it is 0 for consistency
[\#6067](https://github.com/matrix-org/matrix-react-sdk/pull/6067)
Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0)

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "3.22.0",
"version": "3.23.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {

View file

@ -76,6 +76,7 @@
@import "./views/dialogs/_DevtoolsDialog.scss";
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
@import "./views/dialogs/_FeedbackDialog.scss";
@import "./views/dialogs/_ForwardDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_HostSignupDialog.scss";
@import "./views/dialogs/_IncomingSasDialog.scss";

View file

@ -82,7 +82,6 @@ limitations under the License.
color: $primary-fg-color;
font-size: $font-12px;
display: inline;
padding-left: 0px;
}
.mx_NotificationPanel .mx_EventTile_senderDetails {
@ -103,6 +102,7 @@ limitations under the License.
visibility: visible;
position: initial;
display: inline;
padding-left: 5px;
}
.mx_NotificationPanel .mx_EventTile_line {

View file

@ -365,6 +365,45 @@ $SpaceRoomViewInnerWidth: 428px;
}
}
.mx_SpaceRoomView_betaWarning {
padding: 12px 12px 12px 54px;
position: relative;
font-size: $font-15px;
line-height: $font-24px;
width: 432px;
border-radius: 8px;
background-color: $info-plinth-bg-color;
color: $secondary-fg-color;
box-sizing: border-box;
> h3 {
font-weight: $font-semi-bold;
font-size: inherit;
line-height: inherit;
margin: 0;
}
> p {
font-size: inherit;
line-height: inherit;
margin: 0;
}
&::before {
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
content: '';
width: 20px;
height: 20px;
position: absolute;
top: 14px;
left: 14px;
background-color: $secondary-fg-color;
}
}
.mx_SpaceRoomView_inviteTeammates {
// XXX remove this when spaces leaves Beta
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {

View file

@ -0,0 +1,159 @@
/*
Copyright 2021 Robin Townsend <robin@robin.town>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ForwardDialog {
width: 520px;
color: $primary-fg-color;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-height: 0;
height: 80vh;
> h3 {
margin: 0 0 6px;
color: $secondary-fg-color;
font-size: $font-12px;
font-weight: $font-semi-bold;
line-height: $font-15px;
}
> .mx_ForwardDialog_preview {
max-height: 30%;
flex-shrink: 0;
overflow: scroll;
div {
pointer-events: none;
}
.mx_EventTile_msgOption {
display: none;
}
// When forwarding messages from encrypted rooms, EventTile will complain
// that our preview is unencrypted, which doesn't actually matter
.mx_EventTile_e2eIcon_unencrypted {
display: none;
}
// We also hide download links to not encourage users to try interacting
.mx_MFileBody_download {
display: none;
}
}
> hr {
width: 100%;
border: none;
border-top: 1px solid $input-border-color;
margin: 12px 0;
}
> .mx_ForwardList {
display: contents;
.mx_SearchBox {
// To match the space around the title
margin: 0 0 15px 0;
flex-grow: 0;
}
.mx_ForwardList_content {
flex-grow: 1;
}
.mx_ForwardList_noResults {
display: block;
margin-top: 24px;
}
.mx_ForwardList_results {
&:not(:first-child) {
margin-top: 24px;
}
.mx_ForwardList_entry {
display: flex;
justify-content: space-between;
height: 32px;
padding: 6px;
border-radius: 8px;
&:hover {
background-color: $groupFilterPanel-bg-color;
}
.mx_ForwardList_roomButton {
display: flex;
margin-right: 12px;
min-width: 0;
.mx_DecoratedRoomAvatar {
margin-right: 12px;
}
.mx_ForwardList_entry_name {
font-size: $font-15px;
line-height: 30px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 12px;
}
}
.mx_ForwardList_sendButton {
position: relative;
&:not(.mx_ForwardList_canSend) .mx_ForwardList_sendLabel {
// Hide the "Send" label while preserving button size
visibility: hidden;
}
.mx_ForwardList_sendIcon, .mx_NotificationBadge {
position: absolute;
}
.mx_NotificationBadge {
// Match the failed to send indicator's color with the disabled button
background-color: $button-danger-disabled-fg-color;
}
&.mx_ForwardList_sending .mx_ForwardList_sendIcon {
background-color: $button-primary-bg-color;
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: 14px;
width: 14px;
height: 14px;
}
&.mx_ForwardList_sent .mx_ForwardList_sendIcon {
background-color: $button-primary-bg-color;
mask-image: url('$(res)/img/element-icons/circle-sent.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: 14px;
width: 14px;
height: 14px;
}
}
}
}
}
}

View file

@ -22,6 +22,7 @@ limitations under the License.
}
.mx_ImageView_image_wrapper {
pointer-events: initial;
display: flex;
justify-content: center;
align-items: center;
@ -30,7 +31,6 @@ limitations under the License.
}
.mx_ImageView_image {
pointer-events: all;
flex-shrink: 0;
}
@ -43,7 +43,7 @@ limitations under the License.
}
.mx_ImageView_info_wrapper {
pointer-events: all;
pointer-events: initial;
padding-left: 32px;
display: flex;
flex-direction: row;
@ -63,7 +63,7 @@ limitations under the License.
.mx_ImageView_toolbar {
padding-right: 16px;
pointer-events: all;
pointer-events: initial;
display: flex;
align-items: center;
}

View file

@ -85,12 +85,11 @@ $left-gutter: 64px;
}
.mx_EventTile_isEditing .mx_MessageTimestamp {
visibility: hidden !important;
visibility: hidden;
}
.mx_EventTile .mx_MessageTimestamp {
display: block;
visibility: hidden;
white-space: nowrap;
left: 0px;
text-align: center;
@ -142,29 +141,11 @@ $left-gutter: 64px;
line-height: 57px !important;
}
.mx_MessagePanel_alwaysShowTimestamps .mx_MessageTimestamp {
visibility: visible;
}
.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
left: 3px;
width: auto;
}
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
// The first set is to handle the 'group layout' (default) and the second for the IRC layout
.mx_EventTile_last > div > a > .mx_MessageTimestamp,
.mx_EventTile:hover > div > a > .mx_MessageTimestamp,
.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp,
.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_ReplyThread .mx_EventTile > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp {
visibility: visible;
}
.mx_EventTile:hover .mx_MessageActionBar,
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,

View file

@ -1,6 +1,5 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2015-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.
@ -15,52 +14,61 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, { ComponentType } from "react";
import * as sdk from './index';
import PropTypes from 'prop-types';
import { _t } from './languageHandler';
import { IDialogProps } from "./components/views/dialogs/IDialogProps";
type AsyncImport<T> = { default: T };
interface IProps extends IDialogProps {
// A promise which resolves with the real component
prom: Promise<ComponentType | AsyncImport<ComponentType>>;
}
interface IState {
component?: ComponentType;
error?: Error;
}
/**
* Wrap an asynchronous loader function with a react component which shows a
* spinner until the real component loads.
*/
export default class AsyncWrapper extends React.Component {
static propTypes = {
/** A promise which resolves with the real component
*/
prom: PropTypes.object.isRequired,
};
export default class AsyncWrapper extends React.Component<IProps, IState> {
private unmounted = false;
state = {
public state = {
component: null,
error: null,
};
componentDidMount() {
this._unmounted = false;
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/element-web/issues/3148
console.log('Starting load of AsyncWrapper for modal');
this.props.prom.then((result) => {
if (this._unmounted) {
return;
}
if (this.unmounted) return;
// Take the 'default' member if it's there, then we support
// passing in just an import()ed module, since ES6 async import
// always returns a module *namespace*.
const component = result.default ? result.default : result;
this.setState({component});
const component = (result as AsyncImport<ComponentType>).default
? (result as AsyncImport<ComponentType>).default
: result as ComponentType;
this.setState({ component });
}).catch((e) => {
console.warn('AsyncWrapper promise failed', e);
this.setState({error: e});
this.setState({ error: e });
});
}
componentWillUnmount() {
this._unmounted = true;
this.unmounted = true;
}
_onWrapperCancelClick = () => {
private onWrapperCancelClick = () => {
this.props.onFinished(false);
};
@ -71,12 +79,10 @@ export default class AsyncWrapper extends React.Component {
} else if (this.state.error) {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <BaseDialog onFinished={this.props.onFinished}
title={_t("Error")}
>
{_t("Unable to load! Check your network connectivity and try again.")}
return <BaseDialog onFinished={this.props.onFinished} title={_t("Error")}>
{ _t("Unable to load! Check your network connectivity and try again.") }
<DialogButtons primaryButton={_t("Dismiss")}
onPrimaryButtonClick={this._onWrapperCancelClick}
onPrimaryButtonClick={this.onWrapperCancelClick}
hasCancel={false}
/>
</BaseDialog>;

View file

@ -1,6 +1,5 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2015-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.
@ -15,35 +14,37 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {MatrixClientPeg} from './MatrixClientPeg';
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
import { Room } from 'matrix-js-sdk/src/models/room';
import { MatrixClientPeg } from './MatrixClientPeg';
import dis from './dispatcher/dispatcher';
import { EventStatus } from 'matrix-js-sdk/src/models/event';
export default class Resend {
static resendUnsentEvents(room) {
return Promise.all(room.getPendingEvents().filter(function(ev) {
static resendUnsentEvents(room: Room): Promise<void[]> {
return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) {
return ev.status === EventStatus.NOT_SENT;
}).map(function(event) {
}).map(function(event: MatrixEvent) {
return Resend.resend(event);
}));
}
static cancelUnsentEvents(room) {
room.getPendingEvents().filter(function(ev) {
static cancelUnsentEvents(room: Room): void {
room.getPendingEvents().filter(function(ev: MatrixEvent) {
return ev.status === EventStatus.NOT_SENT;
}).forEach(function(event) {
}).forEach(function(event: MatrixEvent) {
Resend.removeFromQueue(event);
});
}
static resend(event) {
static resend(event: MatrixEvent): Promise<void> {
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
return MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
dis.dispatch({
action: 'message_sent',
event: event,
});
}, function(err) {
}, function(err: Error) {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/element-web/issues/3148
console.log('Resend got send failure: ' + err.name + '(' + err + ')');
@ -55,7 +56,7 @@ export default class Resend {
});
}
static removeFromQueue(event) {
static removeFromQueue(event: MatrixEvent): void {
MatrixClientPeg.get().cancelPendingEvent(event);
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -24,12 +24,12 @@ limitations under the License.
* A similar thing could also be achieved via `pushState` with a state object,
* but keeping it separate like this seems easier in case we do want to extend.
*/
const aliasToIDMap = new Map();
const aliasToIDMap = new Map<string, string>();
export function storeRoomAliasInCache(alias, id) {
export function storeRoomAliasInCache(alias: string, id: string): void {
aliasToIDMap.set(alias, id);
}
export function getCachedRoomIDForAlias(alias) {
export function getCachedRoomIDForAlias(alias: string): string {
return aliasToIDMap.get(alias);
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2017, 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.
@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import dis from '../dispatcher/dispatcher';
import { MatrixClient } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
import dis from "../dispatcher/dispatcher";
import {ActionPayload} from "../dispatcher/payloads";
// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events
// become dispatches in the same place.
@ -27,7 +33,7 @@ import dis from '../dispatcher/dispatcher';
* @param {string} prevState the previous sync state.
* @returns {Object} an action of type MatrixActions.sync.
*/
function createSyncAction(matrixClient, state, prevState) {
function createSyncAction(matrixClient: MatrixClient, state: string, prevState: string): ActionPayload {
return {
action: 'MatrixActions.sync',
state,
@ -53,7 +59,7 @@ function createSyncAction(matrixClient, state, prevState) {
* @param {MatrixEvent} accountDataEvent the account data event.
* @returns {AccountDataAction} an action of type MatrixActions.accountData.
*/
function createAccountDataAction(matrixClient, accountDataEvent) {
function createAccountDataAction(matrixClient: MatrixClient, accountDataEvent: MatrixEvent): ActionPayload {
return {
action: 'MatrixActions.accountData',
event: accountDataEvent,
@ -81,7 +87,11 @@ function createAccountDataAction(matrixClient, accountDataEvent) {
* @param {Room} room the room where account data was changed
* @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData.
*/
function createRoomAccountDataAction(matrixClient, accountDataEvent, room) {
function createRoomAccountDataAction(
matrixClient: MatrixClient,
accountDataEvent: MatrixEvent,
room: Room,
): ActionPayload {
return {
action: 'MatrixActions.Room.accountData',
event: accountDataEvent,
@ -106,7 +116,7 @@ function createRoomAccountDataAction(matrixClient, accountDataEvent, room) {
* @param {Room} room the Room that was stored.
* @returns {RoomAction} an action of type `MatrixActions.Room`.
*/
function createRoomAction(matrixClient, room) {
function createRoomAction(matrixClient: MatrixClient, room: Room): ActionPayload {
return { action: 'MatrixActions.Room', room };
}
@ -127,7 +137,7 @@ function createRoomAction(matrixClient, room) {
* @param {Room} room the Room whose tags were changed.
* @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`.
*/
function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
function createRoomTagsAction(matrixClient: MatrixClient, roomTagsEvent: MatrixEvent, room: Room): ActionPayload {
return { action: 'MatrixActions.Room.tags', room };
}
@ -140,7 +150,7 @@ function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
* @param {Room} room the room the receipt happened in.
* @returns {Object} an action of type MatrixActions.Room.receipt.
*/
function createRoomReceiptAction(matrixClient, event, room) {
function createRoomReceiptAction(matrixClient: MatrixClient, event: MatrixEvent, room: Room): ActionPayload {
return {
action: 'MatrixActions.Room.receipt',
event,
@ -178,7 +188,17 @@ function createRoomReceiptAction(matrixClient, event, room) {
* @param {EventTimeline} data.timeline the timeline being altered.
* @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`.
*/
function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTimeline, removed, data) {
function createRoomTimelineAction(
matrixClient: MatrixClient,
timelineEvent: MatrixEvent,
room: Room,
toStartOfTimeline: boolean,
removed: boolean,
data: {
liveEvent: boolean;
timeline: EventTimeline;
},
): ActionPayload {
return {
action: 'MatrixActions.Room.timeline',
event: timelineEvent,
@ -208,8 +228,13 @@ function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTi
* @param {string} oldMembership the previous membership, can be null.
* @returns {RoomMembershipAction} an action of type `MatrixActions.Room.myMembership`.
*/
function createSelfMembershipAction(matrixClient, room, membership, oldMembership) {
return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership};
function createSelfMembershipAction(
matrixClient: MatrixClient,
room: Room,
membership: string,
oldMembership: string,
): ActionPayload {
return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership };
}
/**
@ -228,61 +253,65 @@ function createSelfMembershipAction(matrixClient, room, membership, oldMembershi
* @param {MatrixEvent} event the matrix event that was decrypted.
* @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`.
*/
function createEventDecryptedAction(matrixClient, event) {
function createEventDecryptedAction(matrixClient: MatrixClient, event: MatrixEvent): ActionPayload {
return { action: 'MatrixActions.Event.decrypted', event };
}
type Listener = () => void;
type ActionCreator = (matrixClient: MatrixClient, ...args: any) => ActionPayload;
// A list of callbacks to call to unregister all listeners added
let matrixClientListenersStop: Listener[] = [];
/**
* Start listening to events of type eventName on matrixClient and when they are emitted,
* dispatch an action created by the actionCreator function.
* @param {MatrixClient} matrixClient a MatrixClient to register a listener with.
* @param {string} eventName the event to listen to on MatrixClient.
* @param {function} actionCreator a function that should return an action to dispatch
* when given the MatrixClient as an argument as well as
* arguments emitted in the MatrixClient event.
*/
function addMatrixClientListener(matrixClient: MatrixClient, eventName: string, actionCreator: ActionCreator): void {
const listener: Listener = (...args) => {
const payload = actionCreator(matrixClient, ...args);
if (payload) {
dis.dispatch(payload, true);
}
};
matrixClient.on(eventName, listener);
matrixClientListenersStop.push(() => {
matrixClient.removeListener(eventName, listener);
});
}
/**
* This object is responsible for dispatching actions when certain events are emitted by
* the given MatrixClient.
*/
export default {
// A list of callbacks to call to unregister all listeners added
_matrixClientListenersStop: [],
/**
* Start listening to certain events from the MatrixClient and dispatch actions when
* they are emitted.
* @param {MatrixClient} matrixClient the MatrixClient to listen to events from
*/
start(matrixClient) {
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
this._addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
this._addMatrixClientListener(matrixClient, 'Room', createRoomAction);
this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
this._addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction);
this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
this._addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction);
this._addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction);
},
/**
* Start listening to events of type eventName on matrixClient and when they are emitted,
* dispatch an action created by the actionCreator function.
* @param {MatrixClient} matrixClient a MatrixClient to register a listener with.
* @param {string} eventName the event to listen to on MatrixClient.
* @param {function} actionCreator a function that should return an action to dispatch
* when given the MatrixClient as an argument as well as
* arguments emitted in the MatrixClient event.
*/
_addMatrixClientListener(matrixClient, eventName, actionCreator) {
const listener = (...args) => {
const payload = actionCreator(matrixClient, ...args);
if (payload) {
dis.dispatch(payload, true);
}
};
matrixClient.on(eventName, listener);
this._matrixClientListenersStop.push(() => {
matrixClient.removeListener(eventName, listener);
});
start(matrixClient: MatrixClient) {
addMatrixClientListener(matrixClient, 'sync', createSyncAction);
addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
addMatrixClientListener(matrixClient, 'Room', createRoomAction);
addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction);
addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction);
addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction);
},
/**
* Stop listening to events.
*/
stop() {
this._matrixClientListenersStop.forEach((stopListener) => stopListener());
matrixClientListenersStop.forEach((stopListener) => stopListener());
matrixClientListenersStop = [];
},
};

View file

@ -19,7 +19,6 @@ limitations under the License.
import React, {createRef} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import shouldHideEvent from '../../shouldHideEvent';
import {wantsDateSeparator} from '../../DateUtils';
import * as sdk from '../../index';
@ -616,10 +615,6 @@ export default class MessagePanel extends React.Component {
const eventId = mxEv.getId();
const highlight = (eventId === this.props.highlightedEventId);
// we can't use local echoes as scroll tokens, because their event IDs change.
// Local echos have a send "status".
const scrollToken = mxEv.status ? undefined : eventId;
const readReceipts = this._readReceiptsByEvent[eventId];
let isLastSuccessful = false;
@ -651,7 +646,6 @@ export default class MessagePanel extends React.Component {
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
<EventTile
as="li"
data-scroll-tokens={scrollToken}
ref={this._collectEventNode.bind(this, eventId)}
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
mxEvent={mxEv}
@ -854,13 +848,6 @@ export default class MessagePanel extends React.Component {
const style = this.props.hidden ? { display: 'none' } : {};
const className = classNames(
this.props.className,
{
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
},
);
let whoIsTyping;
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
whoIsTyping = (<WhoIsTypingTile
@ -884,7 +871,7 @@ export default class MessagePanel extends React.Component {
<ErrorBoundary>
<ScrollPanel
ref={this._scrollPanel}
className={className}
className={this.props.className}
onScroll={this.props.onScroll}
onUserScroll={this.props.onUserScroll}
onResize={this.onResize}

View file

@ -50,6 +50,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
showUrlPreview={false}
tileShape="notif"
empty={emptyState}
alwaysShowTimestamps={true}
/>
);
} else {

View file

@ -59,7 +59,6 @@ import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel";
import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
import ForwardMessage from "../views/rooms/ForwardMessage";
import SearchBar from "../views/rooms/SearchBar";
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import AuxPanel from "../views/rooms/AuxPanel";
@ -136,7 +135,6 @@ export interface IState {
// Whether to highlight the event scrolled to
isInitialEventHighlighted?: boolean;
replyToEvent?: MatrixEvent;
forwardingEvent?: MatrixEvent;
numUnreadMessages: number;
draggingFile: boolean;
searching: boolean;
@ -323,7 +321,6 @@ export default class RoomView extends React.Component<IProps, IState> {
initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
replyToEvent: RoomViewStore.getQuotingEvent(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
@ -1410,18 +1407,6 @@ export default class RoomView extends React.Component<IProps, IState> {
dis.dispatch({ action: "open_room_settings" });
};
private onCancelClick = () => {
console.log("updateTint from onCancelClick");
this.updateTint();
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'forward_event',
event: null,
});
}
dis.fire(Action.FocusComposer);
};
private onAppsClick = () => {
dis.dispatch({
action: "appsDrawer",
@ -1526,10 +1511,19 @@ export default class RoomView extends React.Component<IProps, IState> {
// jump down to the bottom of this room, where new events are arriving
private jumpToLiveTimeline = () => {
dis.dispatch({
action: 'view_room',
room_id: this.state.room.roomId,
});
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
// If we were viewing a highlighted event, firing view_room without
// an event will take care of both clearing the URL fragment and
// jumping to the bottom
dis.dispatch({
action: 'view_room',
room_id: this.state.room.roomId,
});
} else {
// Otherwise we have to jump manually
this.messagePanel.jumpToLiveTimeline();
dis.fire(Action.FocusComposer);
}
};
// jump up to wherever our read marker is
@ -1828,11 +1822,7 @@ export default class RoomView extends React.Component<IProps, IState> {
let aux = null;
let previewBar;
let hideCancel = false;
if (this.state.forwardingEvent) {
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
} else if (this.state.searching) {
hideCancel = true; // has own cancel
if (this.state.searching) {
aux = <SearchBar
searchInProgress={this.state.searchInProgress}
onCancelClick={this.onCancelSearchClick}
@ -1841,7 +1831,6 @@ export default class RoomView extends React.Component<IProps, IState> {
/>;
} else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
hideCancel = true;
} else if (myMembership !== "join") {
// We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it.
@ -1850,7 +1839,6 @@ export default class RoomView extends React.Component<IProps, IState> {
inviterName = this.props.oobData.inviterName;
}
const invitedEmail = this.props.threepidInvite?.toEmail;
hideCancel = true;
previewBar = (
<RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
@ -1968,11 +1956,8 @@ export default class RoomView extends React.Component<IProps, IState> {
hideMessagePanel = true;
}
const shouldHighlight = this.state.isInitialEventHighlighted;
let highlightedEventId = null;
if (this.state.forwardingEvent) {
highlightedEventId = this.state.forwardingEvent.getId();
} else if (shouldHighlight) {
if (this.state.isInitialEventHighlighted) {
highlightedEventId = this.state.initialEventId;
}
@ -2061,7 +2046,6 @@ export default class RoomView extends React.Component<IProps, IState> {
inRoom={myMembership === 'join'}
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus}

View file

@ -587,6 +587,10 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
<h3>{ _t("Me and my teammates") }</h3>
<div>{ _t("A private space for you and your teammates") }</div>
</AccessibleButton>
<div className="mx_SpaceRoomView_betaWarning">
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
<p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p>
</div>
<SpaceFeedbackPrompt />
</div>;
};

View file

@ -120,6 +120,9 @@ class TimelinePanel extends React.Component {
// which layout to use
layout: LayoutPropType,
// whether to always show timestamps for an event
alwaysShowTimestamps: PropTypes.bool,
}
// a map from room id to read marker event timestamp
@ -1440,7 +1443,7 @@ class TimelinePanel extends React.Component {
onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={this.onMessageListUnfillRequest}
isTwelveHour={this.state.isTwelveHour}
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
className={this.props.className}
tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier}

View file

@ -32,6 +32,7 @@ import { MenuItem } from "../../structures/ContextMenu";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog";
export function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
@ -157,10 +158,10 @@ export default class MessageContextMenu extends React.Component {
};
onForwardClick = () => {
if (this.props.onCloseDialog) this.props.onCloseDialog();
dis.dispatch({
action: 'forward_event',
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
matrixClient: MatrixClientPeg.get(),
event: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator,
});
this.closeMenu();
};

View file

@ -40,6 +40,8 @@ interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
showUnpin?: boolean;
// override delete handler
onDeleteClick?(): void;
// override edit handler
onEditClick?(): void;
}
const WidgetContextMenu: React.FC<IProps> = ({
@ -47,6 +49,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
app,
userWidget,
onDeleteClick,
onEditClick,
showUnpin,
...props
}) => {
@ -89,12 +92,16 @@ const WidgetContextMenu: React.FC<IProps> = ({
let editButton;
if (canModify && WidgetUtils.isManagedByManager(app)) {
const onEditClick = () => {
WidgetUtils.editWidget(room, app);
const _onEditClick = () => {
if (onEditClick) {
onEditClick();
} else {
WidgetUtils.editWidget(room, app);
}
onFinished();
};
editButton = <IconizedContextMenuOption onClick={onEditClick} label={_t("Edit")} />;
editButton = <IconizedContextMenuOption onClick={_onEditClick} label={_t("Edit")} />;
}
let snapshotButton;
@ -116,24 +123,29 @@ const WidgetContextMenu: React.FC<IProps> = ({
let deleteButton;
if (onDeleteClick || canModify) {
const onDeleteClickDefault = () => {
// Show delete confirmation dialog
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
title: _t("Delete Widget"),
description: _t(
"Deleting a widget removes it for all users in this room." +
" Are you sure you want to delete this widget?"),
button: _t("Delete widget"),
onFinished: (confirmed) => {
if (!confirmed) return;
WidgetUtils.setRoomWidget(roomId, app.id);
},
});
const _onDeleteClick = () => {
if (onDeleteClick) {
onDeleteClick();
} else {
// Show delete confirmation dialog
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
title: _t("Delete Widget"),
description: _t(
"Deleting a widget removes it for all users in this room." +
" Are you sure you want to delete this widget?"),
button: _t("Delete widget"),
onFinished: (confirmed) => {
if (!confirmed) return;
WidgetUtils.setRoomWidget(roomId, app.id);
},
});
}
onFinished();
};
deleteButton = <IconizedContextMenuOption
onClick={onDeleteClick || onDeleteClickDefault}
onClick={_onDeleteClick}
label={userWidget ? _t("Remove") : _t("Remove for everyone")}
/>;
}

View file

@ -0,0 +1,247 @@
/*
Copyright 2021 Robin Townsend <robin@robin.town>
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, {useMemo, useState, useEffect} from "react";
import classnames from "classnames";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client";
import {_t} from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings";
import {UIFeature} from "../../../settings/UIFeature";
import {Layout} from "../../../settings/Layout";
import {IDialogProps} from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import {avatarUrlForUser} from "../../../Avatar";
import EventTile from "../rooms/EventTile";
import SearchBox from "../../structures/SearchBox";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {Alignment} from '../elements/Tooltip';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
import NotificationBadge from "../rooms/NotificationBadge";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
const AVATAR_SIZE = 30;
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
// The event to forward
event: MatrixEvent;
// We need a permalink creator for the source room to pass through to EventTile
// in case the event is a reply (even though the user can't get at the link)
permalinkCreator: RoomPermalinkCreator;
}
interface IEntryProps {
room: Room;
event: MatrixEvent;
matrixClient: MatrixClient;
onFinished(success: boolean): void;
}
enum SendState {
CanSend,
Sending,
Sent,
Failed,
}
const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinished }) => {
const [sendState, setSendState] = useState<SendState>(SendState.CanSend);
const jumpToRoom = () => {
dis.dispatch({
action: "view_room",
room_id: room.roomId,
});
onFinished(true);
};
const send = async () => {
setSendState(SendState.Sending);
try {
await cli.sendEvent(room.roomId, event.getType(), event.getContent());
setSendState(SendState.Sent);
} catch (e) {
setSendState(SendState.Failed);
}
};
let className;
let disabled = false;
let title;
let icon;
if (sendState === SendState.CanSend) {
className = "mx_ForwardList_canSend";
if (room.maySendMessage()) {
title = _t("Send");
} else {
disabled = true;
title = _t("You don't have permission to do this");
}
} else if (sendState === SendState.Sending) {
className = "mx_ForwardList_sending";
disabled = true;
title = _t("Sending");
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
} else if (sendState === SendState.Sent) {
className = "mx_ForwardList_sent";
disabled = true;
title = _t("Sent");
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
} else {
className = "mx_ForwardList_sendFailed";
disabled = true;
title = _t("Failed to send");
icon = <NotificationBadge
notification={StaticNotificationState.RED_EXCLAMATION}
/>;
}
return <div className="mx_ForwardList_entry">
<AccessibleTooltipButton
className="mx_ForwardList_roomButton"
onClick={jumpToRoom}
title={_t("Open link")}
yOffset={-20}
alignment={Alignment.Top}
>
<DecoratedRoomAvatar room={room} avatarSize={32} />
<span className="mx_ForwardList_entry_name">{ room.name }</span>
</AccessibleTooltipButton>
<AccessibleTooltipButton
kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
className={`mx_ForwardList_sendButton ${className}`}
onClick={send}
disabled={disabled}
title={title}
yOffset={-20}
alignment={Alignment.Top}
>
<div className="mx_ForwardList_sendLabel">{ _t("Send") }</div>
{ icon }
</AccessibleTooltipButton>
</div>;
};
const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => {
const userId = cli.getUserId();
const [profileInfo, setProfileInfo] = useState<any>({});
useEffect(() => {
cli.getProfileInfo(userId).then(info => setProfileInfo(info));
}, [cli, userId]);
// For the message preview we fake the sender as ourselves
const mockEvent = new MatrixEvent({
type: "m.room.message",
sender: userId,
content: event.getContent(),
unsigned: {
age: 97,
},
event_id: "$9999999999999999999999999999999999999999999",
room_id: event.getRoomId(),
});
mockEvent.sender = {
name: profileInfo.displayname || userId,
userId,
getAvatarUrl: (..._) => {
return avatarUrlForUser(
{ avatarUrl: profileInfo.avatar_url },
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
},
getMxcAvatarUrl: () => profileInfo.avatar_url,
};
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();
const spacesEnabled = useFeatureEnabled("feature_spaces");
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
const previewLayout = useSettingValue<Layout>("layout");
let rooms = useMemo(() => sortRooms(
cli.getVisibleRooms().filter(
room => room.getMyMembership() === "join" &&
!(spacesEnabled && room.isSpaceRoom()),
),
), [cli, spacesEnabled]);
if (lcQuery) {
rooms = new QueryMatcher<Room>(rooms, {
keys: ["name"],
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
shouldMatchWordsOnly: false,
}).match(lcQuery);
}
return <BaseDialog
title={_t("Forward message")}
className="mx_ForwardDialog"
contentId="mx_ForwardList"
onFinished={onFinished}
fixedWidth={false}
>
<h3>{ _t("Message preview") }</h3>
<div className={classnames("mx_ForwardDialog_preview", {
"mx_IRCLayout": previewLayout == Layout.IRC,
"mx_GroupLayout": previewLayout == Layout.Group,
})}>
<EventTile
mxEvent={mockEvent}
layout={previewLayout}
enableFlair={flairEnabled}
permalinkCreator={permalinkCreator}
as="div"
/>
</div>
<hr />
<div className="mx_ForwardList" id="mx_ForwardList">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={_t("Search for rooms or people")}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_ForwardList_content">
{ rooms.length > 0 ? (
<div className="mx_ForwardList_results">
{ rooms.map(room =>
<Entry
key={room.roomId}
room={room}
event={event}
matrixClient={cli}
onFinished={onFinished}
/>,
) }
</div>
) : <span className="mx_ForwardList_noResults">
{ _t("No results") }
</span> }
</AutoHideScrollbar>
</div>
</BaseDialog>;
};
export default ForwardDialog;

View file

@ -15,5 +15,5 @@ limitations under the License.
*/
export interface IDialogProps {
onFinished: (bool) => void;
onFinished(...args: any): void;
}

View file

@ -159,7 +159,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
stickyBottom={false}
startAtBottom={false}
>
<ul className="mx_MessageEditHistoryDialog_edits mx_MessagePanel_alwaysShowTimestamps">{this._renderEdits()}</ul>
<ul className="mx_MessageEditHistoryDialog_edits">{this._renderEdits()}</ul>
</ScrollPanel>);
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

View file

@ -19,7 +19,7 @@ import React from 'react';
import classNames from 'classnames';
import AccessibleButton from "./AccessibleButton";
import Tooltip from './Tooltip';
import Tooltip, {Alignment} from './Tooltip';
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
@ -28,6 +28,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
tooltipClassName?: string;
forceHide?: boolean;
yOffset?: number;
alignment?: Alignment;
}
interface IState {
@ -66,13 +67,14 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, ...props} = this.props;
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, ...props} = this.props;
const tip = this.state.hover ? <Tooltip
className="mx_AccessibleTooltipButton_container"
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
label={tooltip || title}
yOffset={yOffset}
alignment={alignment}
/> : null;
return (
<AccessibleButton

View file

@ -417,6 +417,8 @@ export default class AppTile extends React.Component {
onFinished={this._closeContextMenu}
showUnpin={!this.props.userWidget}
userWidget={this.props.userWidget}
onEditClick={this.props.onEditClick}
onDeleteClick={this.props.onDeleteClick}
/>
);
}

View file

@ -63,9 +63,9 @@ const EventListSummary: React.FC<IProps> = ({
// If we are only given few events then just pass them through
if (events.length < threshold) {
return (
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
{ children }
</div>
</li>
);
}

View file

@ -19,20 +19,20 @@ limitations under the License.
import React, { createRef } from 'react';
import { _t } from '../../../languageHandler';
import AccessibleTooltipButton from "./AccessibleTooltipButton";
import {Key} from "../../../Keyboard";
import { Key } from "../../../Keyboard";
import FocusLock from "react-focus-lock";
import MemberAvatar from "../avatars/MemberAvatar";
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import MessageContextMenu from "../context_menus/MessageContextMenu";
import {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu';
import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu';
import MessageTimestamp from "../messages/MessageTimestamp";
import SettingsStore from "../../../settings/SettingsStore";
import {formatFullDate} from "../../../DateUtils";
import { formatFullDate } from "../../../DateUtils";
import dis from '../../../dispatcher/dispatcher';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {normalizeWheelEvent} from "../../../utils/Mouse";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { normalizeWheelEvent } from "../../../utils/Mouse";
// Max scale to keep gaps around the image
const MAX_SCALE = 0.95;
@ -95,8 +95,6 @@ export default class ImageView extends React.Component<IProps, IState> {
private initX = 0;
private initY = 0;
private lastX = 0;
private lastY = 0;
private previousX = 0;
private previousY = 0;
@ -105,23 +103,35 @@ export default class ImageView extends React.Component<IProps, IState> {
// needs to be passive in order to work with Chromium
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
// We want to recalculate zoom whenever the window's size changes
window.addEventListener("resize", this.calculateZoom);
window.addEventListener("resize", this.recalculateZoom);
// After the image loads for the first time we want to calculate the zoom
this.image.current.addEventListener("load", this.calculateZoom);
this.image.current.addEventListener("load", this.recalculateZoom);
}
componentWillUnmount() {
this.focusLock.current.removeEventListener('wheel', this.onWheel);
window.removeEventListener("resize", this.calculateZoom);
this.image.current.removeEventListener("load", this.calculateZoom);
window.removeEventListener("resize", this.recalculateZoom);
this.image.current.removeEventListener("load", this.recalculateZoom);
}
private calculateZoom = () => {
private recalculateZoom = () => {
this.setZoomAndRotation();
}
private setZoomAndRotation = (inputRotation?: number) => {
const image = this.image.current;
const imageWrapper = this.imageWrapper.current;
const zoomX = imageWrapper.clientWidth / image.naturalWidth;
const zoomY = imageWrapper.clientHeight / image.naturalHeight;
const rotation = inputRotation || this.state.rotation;
const imageIsNotFlipped = rotation % 180 === 0;
// If the image is rotated take it into account
const width = imageIsNotFlipped ? image.naturalWidth : image.naturalHeight;
const height = imageIsNotFlipped ? image.naturalHeight : image.naturalWidth;
const zoomX = imageWrapper.clientWidth / width;
const zoomY = imageWrapper.clientHeight / height;
// If the image is smaller in both dimensions set its the zoom to 1 to
// display it in its original size
@ -130,6 +140,7 @@ export default class ImageView extends React.Component<IProps, IState> {
zoom: 1,
minZoom: 1,
maxZoom: 1,
rotation: rotation,
});
return;
}
@ -138,10 +149,14 @@ export default class ImageView extends React.Component<IProps, IState> {
// image by default
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;
if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom});
// If zoom is smaller than minZoom don't go below that value
const zoom = (this.state.zoom <= this.state.minZoom) ? minZoom : this.state.zoom;
this.setState({
minZoom: minZoom,
maxZoom: 1,
rotation: rotation,
zoom: zoom,
});
}
@ -157,7 +172,7 @@ export default class ImageView extends React.Component<IProps, IState> {
return;
}
if (newZoom >= this.state.maxZoom) {
this.setState({zoom: this.state.maxZoom});
this.setState({ zoom: this.state.maxZoom });
return;
}
@ -170,7 +185,7 @@ export default class ImageView extends React.Component<IProps, IState> {
ev.stopPropagation();
ev.preventDefault();
const {deltaY} = normalizeWheelEvent(ev);
const { deltaY } = normalizeWheelEvent(ev);
this.zoom(-(deltaY * ZOOM_COEFFICIENT));
};
@ -192,14 +207,12 @@ export default class ImageView extends React.Component<IProps, IState> {
private onRotateCounterClockwiseClick = () => {
const cur = this.state.rotation;
const rotationDegrees = cur - 90;
this.setState({ rotation: rotationDegrees });
this.setZoomAndRotation(cur - 90);
};
private onRotateClockwiseClick = () => {
const cur = this.state.rotation;
const rotationDegrees = cur + 90;
this.setState({ rotation: rotationDegrees });
this.setZoomAndRotation(cur + 90);
};
private onDownloadClick = () => {
@ -246,15 +259,15 @@ export default class ImageView extends React.Component<IProps, IState> {
// Zoom in if we are completely zoomed out
if (this.state.zoom === this.state.minZoom) {
this.setState({zoom: this.state.maxZoom});
this.setState({ zoom: this.state.maxZoom });
return;
}
this.setState({moving: true});
this.setState({ moving: true });
this.previousX = this.state.translationX;
this.previousY = this.state.translationY;
this.initX = ev.pageX - this.lastX;
this.initY = ev.pageY - this.lastY;
this.initX = ev.pageX - this.state.translationX;
this.initY = ev.pageY - this.state.translationY;
};
private onMoving = (ev: React.MouseEvent) => {
@ -263,11 +276,9 @@ export default class ImageView extends React.Component<IProps, IState> {
if (!this.state.moving) return;
this.lastX = ev.pageX - this.initX;
this.lastY = ev.pageY - this.initY;
this.setState({
translationX: this.lastX,
translationY: this.lastY,
translationX: ev.pageX - this.initX,
translationY: ev.pageY - this.initY,
});
};
@ -283,8 +294,10 @@ export default class ImageView extends React.Component<IProps, IState> {
translationX: 0,
translationY: 0,
});
this.initX = 0;
this.initY = 0;
}
this.setState({moving: false});
this.setState({ moving: false });
};
private renderContextMenu() {
@ -355,7 +368,7 @@ export default class ImageView extends React.Component<IProps, IState> {
const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
const sender = (
<div className="mx_ImageView_info_sender">
{senderName}
{ senderName }
</div>
);
const messageTimestamp = (
@ -382,10 +395,10 @@ export default class ImageView extends React.Component<IProps, IState> {
info = (
<div className="mx_ImageView_info_wrapper">
{avatar}
{ avatar }
<div className="mx_ImageView_info">
{sender}
{messageTimestamp}
{ sender }
{ messageTimestamp }
</div>
</div>
);
@ -425,7 +438,7 @@ export default class ImageView extends React.Component<IProps, IState> {
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_zoomIn"
title={_t("Zoom in")}
onClick={ this.onZoomInClick }>
onClick={this.onZoomInClick}>
</AccessibleTooltipButton>
);
}
@ -441,7 +454,7 @@ export default class ImageView extends React.Component<IProps, IState> {
ref={this.focusLock}
>
<div className="mx_ImageView_panel">
{info}
{ info }
<div className="mx_ImageView_toolbar">
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
@ -453,25 +466,30 @@ export default class ImageView extends React.Component<IProps, IState> {
title={_t("Rotate Right")}
onClick={this.onRotateClockwiseClick}>
</AccessibleTooltipButton>
{zoomOutButton}
{zoomInButton}
{ zoomOutButton }
{ zoomInButton }
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_download"
title={_t("Download")}
onClick={ this.onDownloadClick }>
</AccessibleTooltipButton>
{contextMenuButton}
{ contextMenuButton }
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_close"
title={_t("Close")}
onClick={ this.props.onFinished }>
</AccessibleTooltipButton>
{this.renderContextMenu()}
{ this.renderContextMenu() }
</div>
</div>
<div
className="mx_ImageView_image_wrapper"
ref={this.imageWrapper}>
ref={this.imageWrapper}
onMouseDown={this.props.onFinished}
onMouseMove={this.onMoving}
onMouseUp={this.onEndMoving}
onMouseLeave={this.onEndMoving}
>
<img
src={this.props.src}
title={this.props.name}
@ -480,9 +498,6 @@ export default class ImageView extends React.Component<IProps, IState> {
className="mx_ImageView_image"
draggable={true}
onMouseDown={this.onStartMoving}
onMouseMove={this.onMoving}
onMouseUp={this.onEndMoving}
onMouseLeave={this.onEndMoving}
/>
</div>
</FocusLock>

View file

@ -47,6 +47,10 @@ export default class ReplyThread extends React.Component {
// Specifies which layout to use.
layout: LayoutPropType,
forExport: PropTypes.bool,
// Whether to always show a timestamp
alwaysShowTimestamps: PropTypes.bool,
};
static contextType = MatrixClientContext;
@ -216,7 +220,7 @@ export default class ReplyThread extends React.Component {
};
}
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, forExport) {
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, forExport, alwaysShowTimestamps) {
if (!ReplyThread.getParentEventId(parentEv)) {
return null;
}
@ -227,6 +231,7 @@ export default class ReplyThread extends React.Component {
ref={ref}
permalinkCreator={permalinkCreator}
layout={layout}
alwaysShowTimestamps={alwaysShowTimestamps}
/>;
}
@ -396,6 +401,7 @@ export default class ReplyThread extends React.Component {
isRedacted={ev.isRedacted()}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
layout={this.props.layout}
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
replacingEventId={ev.replacingEventId()}
/>

View file

@ -916,6 +916,12 @@ export default class EventTile extends React.Component<IProps, IState> {
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
}
// we can't use local echoes as scroll tokens, because their event IDs change.
// Local echos have a send "status".
const scrollToken = this.props.mxEvent.status
? undefined
: this.props.mxEvent.getId();
let avatar;
let sender;
let avatarSize;
@ -989,7 +995,8 @@ export default class EventTile extends React.Component<IProps, IState> {
onFocusChange={this.onActionBarFocusChange}
/> : undefined;
const showTimestamp = this.props.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.state.hover);
const showTimestamp = this.props.mxEvent.getTs() &&
(this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused);
const timestamp = showTimestamp ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
@ -1060,7 +1067,7 @@ export default class EventTile extends React.Component<IProps, IState> {
case 'notif': {
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
return (
<div className={classes} aria-live={ariaLive} aria-atomic="true">
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
<div className="mx_EventTile_roomName">
<RoomAvatar room={room} width={28} height={28} />
<a href={permalink} onClick={this.onPermalinkClicked}>
@ -1083,12 +1090,12 @@ export default class EventTile extends React.Component<IProps, IState> {
onHeightChanged={this.props.onHeightChanged}
/>
</div>
</div>
</li>
);
}
case 'file_grid': {
return (
<div className={classes} aria-live={ariaLive} aria-atomic="true">
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
<div className="mx_EventTile_line">
<EventTileType ref={this.tile}
mxEvent={this.props.mxEvent}
@ -1109,7 +1116,7 @@ export default class EventTile extends React.Component<IProps, IState> {
{ timestamp }
</div>
</a>
</div>
</li>
);
}
@ -1122,10 +1129,12 @@ export default class EventTile extends React.Component<IProps, IState> {
this.props.onHeightChanged,
this.props.permalinkCreator,
this.replyThread,
null,
this.props.alwaysShowTimestamps || this.state.hover,
);
}
return (
<div className={classes} aria-live={ariaLive} aria-atomic="true">
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
{ ircTimestamp }
{ avatar }
{ sender }
@ -1143,7 +1152,7 @@ export default class EventTile extends React.Component<IProps, IState> {
showUrlPreview={false}
/>
</div>
</div>
</li>
);
}
default: {
@ -1154,17 +1163,18 @@ export default class EventTile extends React.Component<IProps, IState> {
this.replyThread,
this.props.layout,
this.props.forExport,
this.props.alwaysShowTimestamps || this.state.hover,
);
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return (
React.createElement(this.props.as || "div", {
React.createElement(this.props.as || "li", {
"ref": this.ref,
"className": classes,
"tabIndex": -1,
"aria-live": ariaLive,
"aria-atomic": "true",
"data-scroll-tokens": this.props["data-scroll-tokens"],
"data-scroll-tokens": scrollToken,
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
}, [

View file

@ -1,53 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017 Michael Telatynski
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {Key} from '../../../Keyboard';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.ForwardMessage")
export default class ForwardMessage extends React.Component {
static propTypes = {
onCancelClick: PropTypes.func.isRequired,
};
componentDidMount() {
document.addEventListener('keydown', this._onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this._onKeyDown);
}
_onKeyDown = ev => {
switch (ev.key) {
case Key.ESCAPE:
this.props.onCancelClick();
break;
}
};
render() {
return (
<div className="mx_ForwardMessage">
<h1>{ _t('Please select the destination room for this message') }</h1>
</div>
);
}
}

View file

@ -89,7 +89,7 @@ export default class ReplyPreview extends React.Component {
</div>
<div className="mx_ReplyPreview_clear" />
<EventTile
last={true}
alwaysShowTimestamps={true}
tileShape="reply_preview"
mxEvent={this.state.event}
permalinkCreator={this.props.permalinkCreator}

View file

@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RateLimitedFunc from '../../../ratelimitedfunc';
import { CancelButton } from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon';
@ -44,7 +43,6 @@ export default class RoomHeader extends React.Component {
onSettingsClick: PropTypes.func,
onSearchClick: PropTypes.func,
onLeaveClick: PropTypes.func,
onCancelClick: PropTypes.func,
e2eStatus: PropTypes.string,
onAppsClick: PropTypes.func,
appsShown: PropTypes.bool,
@ -54,7 +52,6 @@ export default class RoomHeader extends React.Component {
static defaultProps = {
editing: false,
inRoom: false,
onCancelClick: null,
};
componentDidMount() {
@ -90,11 +87,6 @@ export default class RoomHeader extends React.Component {
render() {
let searchStatus = null;
let cancelButton = null;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
// don't display the search count until the search completes and
// gives us a valid (possibly zero) searchCount.
@ -220,7 +212,6 @@ export default class RoomHeader extends React.Component {
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
{ name }
{ topicElement }
{ cancelButton }
{ rightRow }
<RoomHeaderButtons room={this.props.room} />
</div>

View file

@ -47,6 +47,7 @@ export default class SearchResultTile extends React.Component {
const ts1 = mxEv.getTs();
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
const timeline = result.context.getTimeline();
for (let j = 0; j < timeline.length; j++) {
@ -67,6 +68,7 @@ export default class SearchResultTile extends React.Component {
highlightLink={this.props.resultLink}
onHeightChanged={this.props.onHeightChanged}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
alwaysShowTimestamps={alwaysShowTimestamps}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
/>
));

View file

@ -16,23 +16,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import AccessibleButton from '../elements/AccessibleButton';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
// cancel button which is shared between room header and simple room header
export function CancelButton(props) {
const {onClick} = props;
return (
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
<img src={require("../../../../res/img/cancel.svg")} className='mx_filterFlipColor'
width="18" height="18" alt={_t("Cancel")} />
</AccessibleButton>
);
}
/*
* A stripped-down room header used for things like the user settings
* and room directory.
@ -41,18 +27,13 @@ export function CancelButton(props) {
export default class SimpleRoomHeader extends React.Component {
static propTypes = {
title: PropTypes.string,
onCancelClick: PropTypes.func,
// `src` to a TintableSvg. Optional.
icon: PropTypes.string,
};
render() {
let cancelButton;
let icon;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
if (this.props.icon) {
const TintableSvg = sdk.getComponent('elements.TintableSvg');
icon = <TintableSvg
@ -66,7 +47,6 @@ export default class SimpleRoomHeader extends React.Component {
<div className="mx_RoomHeader_simpleHeader">
{ icon }
{ this.props.title }
{ cancelButton }
</div>
</div>
);

View file

@ -367,7 +367,7 @@ export default class Stickerpicker extends React.PureComponent {
/**
* Launch the integration manager on the stickers integration page
*/
_launchManageIntegrations() {
_launchManageIntegrations = () => {
// TODO: Open the right integration manager for the widget
if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll(
@ -382,7 +382,7 @@ export default class Stickerpicker extends React.PureComponent {
this.state.widgetId,
);
}
}
};
render() {
let stickerPicker;
@ -401,7 +401,7 @@ export default class Stickerpicker extends React.PureComponent {
key="controls_hide_stickers"
className={className}
onClick={this._onHideStickersClick}
active={this.state.showStickers}
active={this.state.showStickers.toString()}
title={_t("Hide Stickers")}
>
</AccessibleButton>;

View file

@ -35,8 +35,8 @@ export const useSettingValue = <T>(settingName: string, roomId: string = null, e
};
// Hook to fetch whether a feature is enabled and dynamically update when that changes
export const useFeatureEnabled = (featureName: string, roomId: string = null) => {
const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId));
export const useFeatureEnabled = (featureName: string, roomId: string = null): boolean => {
const [enabled, setEnabled] = useState(SettingsStore.getValue<boolean>(featureName, roomId));
useEffect(() => {
const ref = SettingsStore.watchSetting(featureName, roomId, () => {

View file

@ -1477,7 +1477,6 @@
"Encrypting your message...": "Encrypting your message...",
"Your message was sent": "Your message was sent",
"Failed to send": "Failed to send",
"Please select the destination room for this message": "Please select the destination room for this message",
"Scroll to most recent messages": "Scroll to most recent messages",
"Close preview": "Close preview",
"and %(count)s others...|other": "and %(count)s others...",
@ -2209,6 +2208,13 @@
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
"Report a bug": "Report a bug",
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
"You don't have permission to do this": "You don't have permission to do this",
"Sending": "Sending",
"Sent": "Sent",
"Open link": "Open link",
"Forward message": "Forward message",
"Message preview": "Message preview",
"Search for rooms or people": "Search for rooms or people",
"Confirm abort of host creation": "Confirm abort of host creation",
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
"Abort": "Abort",
@ -2667,7 +2673,6 @@
"Some of your messages have not been sent": "Some of your messages have not been sent",
"Delete all": "Delete all",
"Retry all": "Retry all",
"Sending": "Sending",
"You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
@ -2728,6 +2733,8 @@
"A private space to organise your rooms": "A private space to organise your rooms",
"Me and my teammates": "Me and my teammates",
"A private space for you and your teammates": "A private space for you and your teammates",
"Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.",
"We're working on this as part of the beta, but just want to let you know.": "We're working on this as part of the beta, but just want to let you know.",
"Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
"Inviting...": "Inviting...",
"Invite your teammates": "Invite your teammates",

View file

@ -35,7 +35,7 @@ export interface MatrixProfile {
export interface CrawlerCheckpoint {
roomId: string;
token: string;
fullCrawl: boolean;
fullCrawl?: boolean;
direction: string;
}
@ -73,14 +73,14 @@ export interface EventAndProfile {
export interface LoadArgs {
roomId: string;
limit: number;
fromEvent: string;
direction: string;
fromEvent?: string;
direction?: string;
}
export interface IndexStats {
size: number;
event_count: number;
room_count: number;
eventCount: number;
roomCount: number;
}
/**

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -14,33 +14,42 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventEmitter } from "events";
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
import { Room } from 'matrix-js-sdk/src/models/room';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
import PlatformPeg from "../PlatformPeg";
import {MatrixClientPeg} from "../MatrixClientPeg";
import {RoomMember} from 'matrix-js-sdk/src/models/room-member';
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
import {sleep} from "../utils/promise";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { sleep } from "../utils/promise";
import SettingsStore from "../settings/SettingsStore";
import {EventEmitter} from "events";
import {SettingLevel} from "../settings/SettingLevel";
import { SettingLevel } from "../settings/SettingLevel";
import {CrawlerCheckpoint, LoadArgs, SearchArgs} from "./BaseEventIndexManager";
// The time in ms that the crawler will wait loop iterations if there
// have not been any checkpoints to consume in the last iteration.
const CRAWLER_IDLE_TIME = 5000;
// The maximum number of events our crawler should fetch in a single crawl.
const EVENTS_PER_CRAWL = 100;
interface ICrawler {
cancel(): void;
}
/*
* Event indexing class that wraps the platform specific event indexing.
*/
export default class EventIndex extends EventEmitter {
constructor() {
super();
this.crawlerCheckpoints = [];
// The time in ms that the crawler will wait loop iterations if there
// have not been any checkpoints to consume in the last iteration.
this._crawlerIdleTime = 5000;
// The maximum number of events our crawler should fetch in a single
// crawl.
this._eventsPerCrawl = 100;
this._crawler = null;
this._currentCheckpoint = null;
}
private crawlerCheckpoints: CrawlerCheckpoint[] = [];
private crawler: ICrawler = null;
private currentCheckpoint: CrawlerCheckpoint = null;
async init() {
public async init() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
this.crawlerCheckpoints = await indexManager.loadCheckpoints();
@ -52,7 +61,7 @@ export default class EventIndex extends EventEmitter {
/**
* Register event listeners that are necessary for the event index to work.
*/
registerListeners() {
public registerListeners() {
const client = MatrixClientPeg.get();
client.on('sync', this.onSync);
@ -66,7 +75,7 @@ export default class EventIndex extends EventEmitter {
/**
* Remove the event index specific event listeners.
*/
removeListeners() {
public removeListeners() {
const client = MatrixClientPeg.get();
if (client === null) return;
@ -81,7 +90,7 @@ export default class EventIndex extends EventEmitter {
/**
* Get crawler checkpoints for the encrypted rooms and store them in the index.
*/
async addInitialCheckpoints() {
public async addInitialCheckpoints() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
const client = MatrixClientPeg.get();
const rooms = client.getRooms();
@ -102,14 +111,14 @@ export default class EventIndex extends EventEmitter {
const timeline = room.getLiveTimeline();
const token = timeline.getPaginationToken("b");
const backCheckpoint = {
const backCheckpoint: CrawlerCheckpoint = {
roomId: room.roomId,
token: token,
direction: "b",
fullCrawl: true,
};
const forwardCheckpoint = {
const forwardCheckpoint: CrawlerCheckpoint = {
roomId: room.roomId,
token: token,
direction: "f",
@ -146,7 +155,7 @@ export default class EventIndex extends EventEmitter {
* - Every other sync, tell the event index to commit all the queued up
* live events
*/
onSync = async (state, prevState, data) => {
private onSync = async (state: string, prevState: string, data: object) => {
const indexManager = PlatformPeg.get().getEventIndexingManager();
if (prevState === "PREPARED" && state === "SYNCING") {
@ -176,7 +185,15 @@ export default class EventIndex extends EventEmitter {
* otherwise we save their event id and wait for them in the Event.decrypted
* listener.
*/
onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => {
private onRoomTimeline = async (
ev: MatrixEvent,
room: Room,
toStartOfTimeline: boolean,
removed: boolean,
data: {
liveEvent: boolean;
},
) => {
const client = MatrixClientPeg.get();
// We only index encrypted rooms locally.
@ -194,7 +211,7 @@ export default class EventIndex extends EventEmitter {
await this.addLiveEventToIndex(ev);
}
onRoomStateEvent = async (ev, state) => {
private onRoomStateEvent = async (ev: MatrixEvent, state: RoomState) => {
if (!MatrixClientPeg.get().isRoomEncrypted(state.roomId)) return;
if (ev.getType() === "m.room.encryption" && !await this.isRoomIndexed(state.roomId)) {
@ -209,7 +226,7 @@ export default class EventIndex extends EventEmitter {
* Checks if the event was marked for addition in the Room.timeline
* listener, if so queues it up to be added to the index.
*/
onEventDecrypted = async (ev, err) => {
private onEventDecrypted = async (ev: MatrixEvent, err: Error) => {
// If the event isn't in our live event set, ignore it.
if (err) return;
await this.addLiveEventToIndex(ev);
@ -220,7 +237,7 @@ export default class EventIndex extends EventEmitter {
*
* Removes a redacted event from our event index.
*/
onRedaction = async (ev, room) => {
private onRedaction = async (ev: MatrixEvent, room: Room) => {
// We only index encrypted rooms locally.
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
const indexManager = PlatformPeg.get().getEventIndexingManager();
@ -238,7 +255,7 @@ export default class EventIndex extends EventEmitter {
* Listens for timeline resets that are caused by a limited timeline to
* re-add checkpoints for rooms that need to be crawled again.
*/
onTimelineReset = async (room, timelineSet, resetAllTimelines) => {
private onTimelineReset = async (room: Room, timelineSet: EventTimelineSet, resetAllTimelines: boolean) => {
if (room === null) return;
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
@ -258,7 +275,7 @@ export default class EventIndex extends EventEmitter {
* @returns {bool} Returns true if the event can be indexed, false
* otherwise.
*/
isValidEvent(ev) {
private isValidEvent(ev: MatrixEvent) {
const isUsefulType = ["m.room.message", "m.room.name", "m.room.topic"].includes(ev.getType());
const validEventType = isUsefulType && !ev.isRedacted() && !ev.isDecryptionFailure();
@ -282,7 +299,7 @@ export default class EventIndex extends EventEmitter {
return validEventType && validMsgType && hasContentValue;
}
eventToJson(ev) {
private eventToJson(ev: MatrixEvent) {
const jsonEvent = ev.toJSON();
const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
@ -314,7 +331,7 @@ export default class EventIndex extends EventEmitter {
*
* @param {MatrixEvent} ev The event that should be added to the index.
*/
async addLiveEventToIndex(ev) {
private async addLiveEventToIndex(ev: MatrixEvent) {
const indexManager = PlatformPeg.get().getEventIndexingManager();
if (!this.isValidEvent(ev)) return;
@ -333,11 +350,11 @@ export default class EventIndex extends EventEmitter {
* Emmit that the crawler has changed the checkpoint that it's currently
* handling.
*/
emitNewCheckpoint() {
private emitNewCheckpoint() {
this.emit("changedCheckpoint", this.currentRoom());
}
async addEventsFromLiveTimeline(timeline) {
private async addEventsFromLiveTimeline(timeline: EventTimeline) {
const events = timeline.getEvents();
for (let i = 0; i < events.length; i++) {
@ -346,7 +363,7 @@ export default class EventIndex extends EventEmitter {
}
}
async addRoomCheckpoint(roomId, fullCrawl = false) {
private async addRoomCheckpoint(roomId: string, fullCrawl = false) {
const indexManager = PlatformPeg.get().getEventIndexingManager();
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
@ -396,16 +413,16 @@ export default class EventIndex extends EventEmitter {
* crawl, otherwise create a new checkpoint and push it to the
* crawlerCheckpoints queue so we go through them in a round-robin way.
*/
async crawlerFunc() {
private async crawlerFunc() {
let cancelled = false;
const client = MatrixClientPeg.get();
const indexManager = PlatformPeg.get().getEventIndexingManager();
this._crawler = {};
this._crawler.cancel = () => {
cancelled = true;
this.crawler = {
cancel: () => {
cancelled = true;
},
};
let idle = false;
@ -417,11 +434,11 @@ export default class EventIndex extends EventEmitter {
sleepTime = Math.max(sleepTime, 100);
if (idle) {
sleepTime = this._crawlerIdleTime;
sleepTime = CRAWLER_IDLE_TIME;
}
if (this._currentCheckpoint !== null) {
this._currentCheckpoint = null;
if (this.currentCheckpoint !== null) {
this.currentCheckpoint = null;
this.emitNewCheckpoint();
}
@ -440,7 +457,7 @@ export default class EventIndex extends EventEmitter {
continue;
}
this._currentCheckpoint = checkpoint;
this.currentCheckpoint = checkpoint;
this.emitNewCheckpoint();
idle = false;
@ -454,8 +471,11 @@ export default class EventIndex extends EventEmitter {
try {
res = await client.createMessagesRequest(
checkpoint.roomId, checkpoint.token, this._eventsPerCrawl,
checkpoint.direction);
checkpoint.roomId,
checkpoint.token,
EVENTS_PER_CRAWL,
checkpoint.direction,
);
} catch (e) {
if (e.httpStatus === 403) {
console.log("EventIndex: Removing checkpoint as we don't have ",
@ -612,23 +632,23 @@ export default class EventIndex extends EventEmitter {
}
}
this._crawler = null;
this.crawler = null;
}
/**
* Start the crawler background task.
*/
startCrawler() {
if (this._crawler !== null) return;
public startCrawler() {
if (this.crawler !== null) return;
this.crawlerFunc();
}
/**
* Stop the crawler background task.
*/
stopCrawler() {
if (this._crawler === null) return;
this._crawler.cancel();
public stopCrawler() {
if (this.crawler === null) return;
this.crawler.cancel();
}
/**
@ -637,7 +657,7 @@ export default class EventIndex extends EventEmitter {
* This removes all the MatrixClient event listeners, stops the crawler
* task, and closes the index.
*/
async close() {
public async close() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
this.removeListeners();
this.stopCrawler();
@ -654,7 +674,7 @@ export default class EventIndex extends EventEmitter {
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
* of search results once the search is done.
*/
async search(searchArgs) {
public async search(searchArgs: SearchArgs) {
const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.searchEventIndex(searchArgs);
}
@ -680,11 +700,16 @@ export default class EventIndex extends EventEmitter {
* @returns {Promise<MatrixEvent[]>} Resolves to an array of events that
* contain URLs.
*/
async loadFileEvents(room, limit = 10, fromEvent = null, direction = EventTimeline.BACKWARDS) {
public async loadFileEvents(
room: Room,
limit = 10,
fromEvent: string = null,
direction: string = EventTimeline.BACKWARDS,
) {
const client = MatrixClientPeg.get();
const indexManager = PlatformPeg.get().getEventIndexingManager();
const loadArgs = {
const loadArgs: LoadArgs = {
roomId: room.roomId,
limit: limit,
};
@ -772,13 +797,13 @@ export default class EventIndex extends EventEmitter {
* @returns {Promise<boolean>} Resolves to true if events were added to the
* timeline, false otherwise.
*/
async populateFileTimeline(
timelineSet,
timeline,
room,
public async populateFileTimeline(
timelineSet: EventTimelineSet,
timeline: EventTimeline,
room: Room,
limit = 10,
fromEvent = null,
direction = EventTimeline.BACKWARDS,
fromEvent: string = null,
direction: string = EventTimeline.BACKWARDS,
) {
const matrixEvents = await this.loadFileEvents(room, limit, fromEvent, direction);
@ -837,7 +862,7 @@ export default class EventIndex extends EventEmitter {
* @returns {Promise<boolean>} Resolves to a boolean which is true if more
* events were successfully retrieved.
*/
paginateTimelineWindow(room, timelineWindow, direction, limit) {
public paginateTimelineWindow(room: Room, timelineWindow: TimelineWindow, direction: string, limit: number) {
const tl = timelineWindow.getTimelineIndex(direction);
if (!tl) return Promise.resolve(false);
@ -871,7 +896,7 @@ export default class EventIndex extends EventEmitter {
* @return {Promise<IndexStats>} A promise that will resolve to the index
* statistics.
*/
async getStats() {
public async getStats() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.getStats();
}
@ -885,7 +910,7 @@ export default class EventIndex extends EventEmitter {
* @return {Promise<boolean>} Returns true if the index contains events for
* the given room, false otherwise.
*/
async isRoomIndexed(roomId) {
public async isRoomIndexed(roomId) {
const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.isRoomIndexed(roomId);
}
@ -896,21 +921,21 @@ export default class EventIndex extends EventEmitter {
* @returns {Room} A MatrixRoom that is being currently crawled, null
* if no room is currently being crawled.
*/
currentRoom() {
if (this._currentCheckpoint === null && this.crawlerCheckpoints.length === 0) {
public currentRoom() {
if (this.currentCheckpoint === null && this.crawlerCheckpoints.length === 0) {
return null;
}
const client = MatrixClientPeg.get();
if (this._currentCheckpoint !== null) {
return client.getRoom(this._currentCheckpoint.roomId);
if (this.currentCheckpoint !== null) {
return client.getRoom(this.currentCheckpoint.roomId);
} else {
return client.getRoom(this.crawlerCheckpoints[0].roomId);
}
}
crawlingRooms() {
public crawlingRooms() {
const totalRooms = new Set();
const crawlingRooms = new Set();
@ -918,14 +943,14 @@ export default class EventIndex extends EventEmitter {
crawlingRooms.add(checkpoint.roomId);
});
if (this._currentCheckpoint !== null) {
crawlingRooms.add(this._currentCheckpoint.roomId);
if (this.currentCheckpoint !== null) {
crawlingRooms.add(this.currentCheckpoint.roomId);
}
const client = MatrixClientPeg.get();
const rooms = client.getRooms();
const isRoomEncrypted = (room) => {
const isRoomEncrypted = (room: Room) => {
return client.isRoomEncrypted(room.roomId);
};
@ -934,6 +959,6 @@ export default class EventIndex extends EventEmitter {
totalRooms.add(room.roomId);
});
return {crawlingRooms, totalRooms};
return { crawlingRooms, totalRooms };
}
}

View file

@ -63,8 +63,7 @@ export class WatchManager {
if (!inRoomId) {
// Fire updates to all the individual room watchers too, as they probably care about the change higher up.
const callbacks = Array.from(roomWatchers.values()).flat(1);
callbacks.push(...callbacks);
callbacks.push(...Array.from(roomWatchers.values()).flat(1));
} else if (roomWatchers.has(IRRELEVANT_ROOM)) {
callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM));
}

View file

@ -1,7 +1,5 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2017-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.
@ -15,11 +13,18 @@ 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 { Store } from 'flux/utils';
import dis from '../dispatcher/dispatcher';
import {Store} from 'flux/utils';
import { ActionPayload } from "../dispatcher/payloads";
interface IState {
deferredAction: any;
}
const INITIAL_STATE = {
deferred_action: null,
deferredAction: null,
};
/**
@ -27,39 +32,38 @@ const INITIAL_STATE = {
* store that listens for actions and updates its state accordingly, informing any
* listeners (views) of state changes.
*/
class LifecycleStore extends Store {
class LifecycleStore extends Store<ActionPayload> {
private state: IState = INITIAL_STATE;
constructor() {
super(dis);
// Initialise state
this._state = INITIAL_STATE;
}
_setState(newState) {
this._state = Object.assign(this._state, newState);
private setState(newState: Partial<IState>) {
this.state = Object.assign(this.state, newState);
this.__emitChange();
}
__onDispatch(payload) {
protected __onDispatch(payload: ActionPayload) {
switch (payload.action) {
case 'do_after_sync_prepared':
this._setState({
deferred_action: payload.deferred_action,
this.setState({
deferredAction: payload.deferred_action,
});
break;
case 'cancel_after_sync_prepared':
this._setState({
deferred_action: null,
this.setState({
deferredAction: null,
});
break;
case 'sync_state': {
case 'syncstate': {
if (payload.state !== 'PREPARED') {
break;
}
if (!this._state.deferred_action) break;
const deferredAction = Object.assign({}, this._state.deferred_action);
this._setState({
deferred_action: null,
if (!this.state.deferredAction) break;
const deferredAction = Object.assign({}, this.state.deferredAction);
this.setState({
deferredAction: null,
});
dis.dispatch(deferredAction);
break;
@ -71,8 +75,8 @@ class LifecycleStore extends Store {
}
}
reset() {
this._state = Object.assign({}, INITIAL_STATE);
private reset() {
this.state = Object.assign({}, INITIAL_STATE);
}
}

View file

@ -54,8 +54,6 @@ const INITIAL_STATE = {
// Any error that has occurred during loading
roomLoadError: null,
forwardingEvent: null,
quotingEvent: null,
replyingToEvent: null,
@ -150,11 +148,6 @@ class RoomViewStore extends Store<ActionPayload> {
case 'on_logged_out':
this.reset();
break;
case 'forward_event':
this.setState({
forwardingEvent: payload.event,
});
break;
case 'reply_to_event':
// If currently viewed room does not match the room in which we wish to reply then change rooms
// this can happen when performing a search across all rooms
@ -187,7 +180,6 @@ class RoomViewStore extends Store<ActionPayload> {
roomAlias: payload.room_alias,
initialEventId: payload.event_id,
isInitialEventHighlighted: payload.highlighted,
forwardingEvent: null,
roomLoading: false,
roomLoadError: null,
// should peek by default
@ -207,14 +199,6 @@ class RoomViewStore extends Store<ActionPayload> {
newState.replyingToEvent = payload.replyingToEvent;
}
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'send_event',
room_id: newState.roomId,
event: this.state.forwardingEvent,
});
}
this.setState(newState);
if (payload.auto_join) {
@ -428,11 +412,6 @@ class RoomViewStore extends Store<ActionPayload> {
return this.state.joinError;
}
// The mxEvent if one is about to be forwarded
public getForwardingEvent() {
return this.state.forwardingEvent;
}
// The mxEvent if one is currently being replied to/quoted
public getQuotingEvent() {
return this.state.replyingToEvent;

View file

@ -332,7 +332,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
}
public getContainerWidgets(room: Room, container: Container): IApp[] {
return this.byRoom[room.roomId]?.[container]?.ordered || [];
return this.byRoom[room?.roomId]?.[container]?.ordered || [];
}
public isInContainer(room: Room, widget: IApp, container: Container): boolean {

View file

@ -1,28 +0,0 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Automatically focuses the captured reference when receiving a non-null
* object. Useful in scenarios where componentDidMount does not have a
* useful reference to an element, but one needs to focus the element on
* first render. Example usage: ref={focusCapturedRef}
* @param {function} ref The React reference to focus on, if not null
*/
export function focusCapturedRef(ref) {
if (ref) {
ref.focus();
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -22,7 +22,7 @@ import url from "url";
* @param {string} u The url to be abbreviated
* @returns {string} The abbreviated url
*/
export function abbreviateUrl(u) {
export function abbreviateUrl(u: string): string {
if (!u) return '';
const parsedUrl = url.parse(u);
@ -37,7 +37,7 @@ export function abbreviateUrl(u) {
return u;
}
export function unabbreviateUrl(u) {
export function unabbreviateUrl(u: string): string {
if (!u) return '';
let longUrl = u;

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");
you may not use this file except in compliance with the License.
@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {_t} from "../languageHandler";
import { _t } from "../languageHandler";
// These are the constants we use for when to break the text
const MILLISECONDS_RECENT = 15000;
const MILLISECONDS_1_MIN = 75000;
const MINUTES_UNDER_1_HOUR = 45;
const MINUTES_1_HOUR = 75;
const HOURS_UNDER_1_DAY = 23;
const HOURS_1_DAY = 26;
/**
* Converts a timestamp into human-readable, translated, text.
* @param {number} timeMillis The time in millis to compare against.
* @returns {string} The humanized time.
*/
export function humanizeTime(timeMillis) {
// These are the constants we use for when to break the text
const MILLISECONDS_RECENT = 15000;
const MILLISECONDS_1_MIN = 75000;
const MINUTES_UNDER_1_HOUR = 45;
const MINUTES_1_HOUR = 75;
const HOURS_UNDER_1_DAY = 23;
const HOURS_1_DAY = 26;
export function humanizeTime(timeMillis: number): string {
const now = (new Date()).getTime();
let msAgo = now - timeMillis;
const minutes = Math.abs(Math.ceil(msAgo / 60000));

View file

@ -19,11 +19,11 @@ limitations under the License.
* TODO: Convert this to a real TypeScript interface
*/
export default class PermalinkConstructor {
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
forEvent(roomId: string, eventId: string, serverCandidates: string[] = []): string {
throw new Error("Not implemented");
}
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
forRoom(roomIdOrAlias: string, serverCandidates: string[] = []): string {
throw new Error("Not implemented");
}
@ -73,12 +73,12 @@ export class PermalinkParts {
return new PermalinkParts(null, null, null, groupId, null);
}
static forRoom(roomIdOrAlias: string, viaServers: string[]): PermalinkParts {
return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers || []);
static forRoom(roomIdOrAlias: string, viaServers: string[] = []): PermalinkParts {
return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers);
}
static forEvent(roomId: string, eventId: string, viaServers: string[]): PermalinkParts {
return new PermalinkParts(roomId, eventId, null, null, viaServers || []);
static forEvent(roomId: string, eventId: string, viaServers: string[] = []): PermalinkParts {
return new PermalinkParts(roomId, eventId, null, null, viaServers);
}
get primaryEntityId(): string {

View file

@ -149,7 +149,7 @@ export class RoomPermalinkCreator {
// Prefer to use canonical alias for permalink if possible
const alias = this.room.getCanonicalAlias();
if (alias) {
return getPermalinkConstructor().forRoom(alias, this._serverCandidates);
return getPermalinkConstructor().forRoom(alias);
}
}
return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
@ -302,7 +302,7 @@ export function makeRoomPermalink(roomId: string): string {
}
const permalinkCreator = new RoomPermalinkCreator(room);
permalinkCreator.load();
return permalinkCreator.forRoom();
return permalinkCreator.forShareableRoom();
}
export function makeGroupPermalink(groupId: string): string {

View file

@ -25,7 +25,6 @@ import DMRoomMap from '../src/utils/DMRoomMap';
import EventEmitter from 'events';
import SdkConfig from '../src/SdkConfig';
import { ActionPayload } from '../src/dispatcher/payloads';
import { Actions } from '../src/notifications/types';
import { Action } from '../src/dispatcher/actions';
const REAL_ROOM_ID = '$room1:example.org';

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager';
const assert = require('assert');
function mockKeyEvent(key: string, modifiers?: {
ctrlKey?: boolean,
@ -28,7 +27,7 @@ function mockKeyEvent(key: string, modifiers?: {
ctrlKey: modifiers?.ctrlKey ?? false,
altKey: modifiers?.altKey ?? false,
shiftKey: modifiers?.shiftKey ?? false,
metaKey: modifiers?.metaKey ?? false
metaKey: modifiers?.metaKey ?? false,
} as KeyboardEvent;
}
@ -37,9 +36,8 @@ describe('KeyBindingsManager', () => {
const combo1: KeyCombo = {
key: 'k',
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo1, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n'), combo1, false), false);
expect(isKeyComboMatch(mockKeyEvent('k'), combo1, false)).toBe(true);
expect(isKeyComboMatch(mockKeyEvent('n'), combo1, false)).toBe(false);
});
it('should match key + modifier key combo', () => {
@ -47,38 +45,38 @@ describe('KeyBindingsManager', () => {
key: 'k',
ctrlKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false), false);
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true);
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k'), combo, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false)).toBe(false);
const combo2: KeyCombo = {
key: 'k',
metaKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo2, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false), false);
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false)).toBe(true);
expect(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k'), combo2, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false)).toBe(false);
const combo3: KeyCombo = {
key: 'k',
altKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo3, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false), false);
expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false)).toBe(true);
expect(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k'), combo3, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false)).toBe(false);
const combo4: KeyCombo = {
key: 'k',
shiftKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo4, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false), false);
expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false)).toBe(true);
expect(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k'), combo4, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false)).toBe(false);
});
it('should match key + multiple modifiers key combo', () => {
@ -87,11 +85,11 @@ describe('KeyBindingsManager', () => {
ctrlKey: true,
altKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo,
false), false);
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true);
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo,
false)).toBe(false);
const combo2: KeyCombo = {
key: 'k',
@ -99,13 +97,13 @@ describe('KeyBindingsManager', () => {
shiftKey: true,
altKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k',
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false), false);
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
false)).toBe(true);
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k',
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false)).toBe(false);
const combo3: KeyCombo = {
key: 'k',
@ -114,12 +112,12 @@ describe('KeyBindingsManager', () => {
altKey: true,
metaKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k',
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n',
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k',
{ ctrlKey: true, shiftKey: true, altKey: true }), combo3, false), false);
expect(isKeyComboMatch(mockKeyEvent('k',
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(true);
expect(isKeyComboMatch(mockKeyEvent('n',
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('k',
{ ctrlKey: true, shiftKey: true, altKey: true }), combo3, false)).toBe(false);
});
it('should match ctrlOrMeta key combo', () => {
@ -128,13 +126,13 @@ describe('KeyBindingsManager', () => {
ctrlOrCmd: true,
};
// PC:
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false);
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true);
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false);
// MAC:
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true), false);
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true)).toBe(true);
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true)).toBe(false);
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true)).toBe(false);
});
it('should match advanced ctrlOrMeta key combo', () => {
@ -144,10 +142,10 @@ describe('KeyBindingsManager', () => {
altKey: true,
};
// PC:
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false), false);
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true);
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false)).toBe(false);
// MAC:
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true), false);
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true)).toBe(true);
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true)).toBe(false);
});
});

View file

@ -0,0 +1,163 @@
/*
Copyright 2021 Robin Townsend <robin@robin.town>
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 "../../../skinned-sdk";
import React from "react";
import {configure, mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import {act} from "react-dom/test-utils";
import * as TestUtils from "../../../test-utils";
import {MatrixClientPeg} from "../../../../src/MatrixClientPeg";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import {RoomPermalinkCreator} from "../../../../src/utils/permalinks/Permalinks";
import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog";
configure({ adapter: new Adapter() });
describe("ForwardDialog", () => {
const sourceRoom = "!111111111111111111:example.org";
const defaultMessage = TestUtils.mkMessage({
room: sourceRoom,
user: "@alice:example.org",
msg: "Hello world!",
event: true,
});
const defaultRooms = ["a", "A", "b"].map(name => TestUtils.mkStubRoom(name, name));
const mountForwardDialog = async (message = defaultMessage, rooms = defaultRooms) => {
const client = MatrixClientPeg.get();
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
let wrapper;
await act(async () => {
wrapper = mount(
<ForwardDialog
matrixClient={client}
event={message}
permalinkCreator={new RoomPermalinkCreator(undefined, sourceRoom)}
onFinished={jest.fn()}
/>,
);
// Wait one tick for our profile data to load so the state update happens within act
await new Promise(resolve => setImmediate(resolve));
});
return wrapper;
};
beforeEach(() => {
TestUtils.stubClient();
DMRoomMap.makeShared();
MatrixClientPeg.get().getUserId = jest.fn().mockReturnValue("@bob:example.org");
});
it("shows a preview with us as the sender", async () => {
const wrapper = await mountForwardDialog();
const previewBody = wrapper.find(".mx_EventTile_body");
expect(previewBody.text()).toBe("Hello world!");
// We would just test SenderProfile for the user ID, but it's stubbed
const previewAvatar = wrapper.find(".mx_EventTile_avatar .mx_BaseAvatar_image");
expect(previewAvatar.prop("title")).toBe("@bob:example.org");
});
it("filters the rooms", async () => {
const wrapper = await mountForwardDialog();
expect(wrapper.find("Entry")).toHaveLength(3);
const searchInput = wrapper.find("SearchBox input");
searchInput.instance().value = "a";
searchInput.simulate("change");
expect(wrapper.find("Entry")).toHaveLength(2);
});
it("tracks message sending progress across multiple rooms", async () => {
const wrapper = await mountForwardDialog();
// Make sendEvent require manual resolution so we can see the sending state
let finishSend;
let cancelSend;
MatrixClientPeg.get().sendEvent = jest.fn(() => new Promise((resolve, reject) => {
finishSend = resolve;
cancelSend = reject;
}));
const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
expect(firstButton.render().is(".mx_ForwardList_canSend")).toBe(true);
act(() => { firstButton.simulate("click"); });
expect(firstButton.render().is(".mx_ForwardList_sending")).toBe(true);
await act(async () => {
cancelSend();
// Wait one tick for the button to realize the send failed
await new Promise(resolve => setImmediate(resolve));
});
expect(firstButton.render().is(".mx_ForwardList_sendFailed")).toBe(true);
const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").at(1);
expect(secondButton.render().is(".mx_ForwardList_canSend")).toBe(true);
act(() => { secondButton.simulate("click"); });
expect(secondButton.render().is(".mx_ForwardList_sending")).toBe(true);
await act(async () => {
finishSend();
// Wait one tick for the button to realize the send succeeded
await new Promise(resolve => setImmediate(resolve));
});
expect(secondButton.render().is(".mx_ForwardList_sent")).toBe(true);
});
it("can render replies", async () => {
const replyMessage = TestUtils.mkEvent({
type: "m.room.message",
room: "!111111111111111111:example.org",
user: "@alice:example.org",
content: {
"msgtype": "m.text",
"body": "> <@bob:example.org> Hi Alice!\n\nHi Bob!",
"m.relates_to": {
"m.in_reply_to": {
event_id: "$2222222222222222222222222222222222222222222",
},
},
},
event: true,
});
const wrapper = await mountForwardDialog(replyMessage);
expect(wrapper.find("ReplyThread")).toBeTruthy();
});
it("disables buttons for rooms without send permissions", async () => {
const readOnlyRoom = TestUtils.mkStubRoom("a", "a");
readOnlyRoom.maySendMessage = jest.fn().mockReturnValue(false);
const rooms = [readOnlyRoom, TestUtils.mkStubRoom("b", "b")];
const wrapper = await mountForwardDialog(undefined, rooms);
const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
expect(firstButton.prop("disabled")).toBe(true);
const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").last();
expect(secondButton.prop("disabled")).toBe(false);
});
});

View file

@ -21,7 +21,7 @@ import "../skinned-sdk"; // Must be first for skinning to work
import SpaceStore, {
UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES
UPDATE_TOP_LEVEL_SPACES,
} from "../../src/stores/SpaceStore";
import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils";
import { mkEvent, mkStubRoom, stubClient } from "../test-utils";

View file

@ -219,7 +219,7 @@ export function mkMessage(opts) {
return mkEvent(opts);
}
export function mkStubRoom(roomId = null) {
export function mkStubRoom(roomId = null, name) {
const stubTimeline = { getEvents: () => [] };
return {
roomId,
@ -238,6 +238,7 @@ export function mkStubRoom(roomId = null) {
getPendingEvents: () => [],
getLiveTimeline: () => stubTimeline,
getUnfilteredTimelineSet: () => null,
findEventById: () => null,
getAccountData: () => null,
hasMembershipState: () => null,
getVersion: () => '1',
@ -255,13 +256,17 @@ export function mkStubRoom(roomId = null) {
tags: {},
setBlacklistUnverifiedDevices: jest.fn(),
on: jest.fn(),
off: jest.fn(),
removeListener: jest.fn(),
getDMInviter: jest.fn(),
name,
getAvatarUrl: () => 'mxc://avatar.url/room.png',
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
isSpaceRoom: jest.fn(() => false),
getUnreadNotificationCount: jest.fn(() => 0),
getEventReadUpTo: jest.fn(() => null),
getCanonicalAlias: jest.fn(),
getAltAliases: jest.fn().mockReturnValue([]),
timeline: [],
};
}

View file

@ -34,7 +34,7 @@ function mockRoom(roomId, members, serverACL) {
return {
roomId,
getCanonicalAlias: () => roomId,
getCanonicalAlias: () => null,
getJoinedMembers: () => members,
getMember: (userId) => members.find(m => m.userId === userId),
currentState: {

View file

@ -5701,8 +5701,8 @@ mathml-tag-names@^2.1.3:
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
version "11.1.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/acb9bc8cc5234326a7583514a8e120a4ac42eedc"
version "11.2.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/35ecbed29d16982deff27a8c37b05167738225a2"
dependencies:
"@babel/runtime" "^7.12.5"
another-json "^0.2.0"