diff --git a/package.json b/package.json index 485e4d7872..9640682ec5 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "qrcode": "^1.4.4", "re-resizable": "^6.9.0", "react": "^17.0.2", - "react-beautiful-dnd": "^4.0.1", + "react-beautiful-dnd": "^13.1.0", "react-dom": "^17.0.2", "react-focus-lock": "^2.5.0", "react-transition-group": "^4.4.1", @@ -126,6 +126,7 @@ "@sinonjs/fake-timers": "^7.0.2", "@types/classnames": "^2.2.11", "@types/counterpart": "^0.18.1", + "@types/diff-match-patch": "^1.0.5", "@types/flux": "^3.1.9", "@types/jest": "^26.0.20", "@types/linkifyjs": "^2.1.3", @@ -135,8 +136,9 @@ "@types/pako": "^1.0.1", "@types/parse5": "^6.0.0", "@types/qrcode": "^1.3.5", - "@types/react": "^16.9", - "@types/react-dom": "^16.9.10", + "@types/react": "^17.0.2", + "@types/react-beautiful-dnd": "^13.0.0", + "@types/react-dom": "^17.0.2", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "^2.3.1", "@types/zxcvbn": "^4.4.0", @@ -170,9 +172,6 @@ "typescript": "^4.1.3", "walk": "^2.3.14" }, - "resolutions": { - "**/@types/react": "^16.14" - }, "jest": { "testEnvironment": "./__test-utils__/environment.js", "testMatch": [ diff --git a/res/css/_components.scss b/res/css/_components.scss index 56403ea190..ec3af8655e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -123,7 +123,6 @@ @import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_FacePile.scss"; @import "./views/elements/_Field.scss"; -@import "./views/elements/_FormButton.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InlineSpinner.scss"; diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index c433ccf275..e64057d16c 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -31,7 +31,6 @@ $activeBorderColor: $secondary-fg-color; // Create another flexbox so the Panel fills the container display: flex; flex-direction: column; - overflow-y: auto; .mx_SpacePanel_spaceTreeWrapper { flex: 1; @@ -69,6 +68,12 @@ $activeBorderColor: $secondary-fg-color; cursor: pointer; } + .mx_SpaceItem_dragging { + .mx_SpaceButton_toggleCollapse { + visibility: hidden; + } + } + .mx_SpaceTreeLevel { display: flex; flex-direction: column; diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 09f834a6e3..14e4c01389 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -134,8 +134,9 @@ limitations under the License. .mx_Toast_buttons { float: right; display: flex; + gap: 5px; - .mx_FormButton { + .mx_AccessibleButton { min-width: 96px; box-sizing: border-box; } diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss index 3463a653fc..1a8241b65f 100644 --- a/res/css/views/beta/_BetaCard.scss +++ b/res/css/views/beta/_BetaCard.scss @@ -19,49 +19,68 @@ limitations under the License. padding: 24px; background-color: $settings-profile-placeholder-bg-color; border-radius: 8px; - display: flex; box-sizing: border-box; - > div { - .mx_BetaCard_title { - font-weight: $font-semi-bold; - font-size: $font-18px; - line-height: $font-22px; - color: $primary-fg-color; - margin: 4px 0 14px; + .mx_BetaCard_columns { + display: flex; - .mx_BetaCard_betaPill { - margin-left: 12px; + > div { + .mx_BetaCard_title { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + color: $primary-fg-color; + margin: 4px 0 14px; + + .mx_BetaCard_betaPill { + margin-left: 12px; + } + } + + .mx_BetaCard_caption { + font-size: $font-15px; + line-height: $font-20px; + color: $secondary-fg-color; + margin-bottom: 20px; + } + + .mx_BetaCard_buttons .mx_AccessibleButton { + display: block; + margin: 12px 0; + padding: 7px 40px; + width: auto; + } + + .mx_BetaCard_disclaimer { + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + margin-top: 20px; } } - .mx_BetaCard_caption { - font-size: $font-15px; - line-height: $font-20px; - color: $secondary-fg-color; - margin-bottom: 20px; - } - - .mx_AccessibleButton { - display: block; - margin: 12px 0; - padding: 7px 40px; - width: auto; - } - - .mx_BetaCard_disclaimer { - font-size: $font-12px; - line-height: $font-15px; - color: $secondary-fg-color; - margin-top: 20px; + > img { + margin: auto 0 auto 20px; + width: 300px; + object-fit: contain; + height: 100%; } } - > img { - margin: auto 0 auto 20px; - width: 300px; - object-fit: contain; - height: 100%; + .mx_BetaCard_relatedSettings { + .mx_SettingsFlag { + margin: 16px 0 0; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + + .mx_SettingsFlag_microcopy { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + } + } } } diff --git a/res/css/views/context_menus/_TagTileContextMenu.scss b/res/css/views/context_menus/_TagTileContextMenu.scss index 8929c8906e..d707f4ce7c 100644 --- a/res/css/views/context_menus/_TagTileContextMenu.scss +++ b/res/css/views/context_menus/_TagTileContextMenu.scss @@ -38,6 +38,15 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/view-community.svg'); } +.mx_TagTileContextMenu_moveUp::before { + transform: rotate(180deg); + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); +} + +.mx_TagTileContextMenu_moveDown::before { + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); +} + .mx_TagTileContextMenu_hideCommunity::before { mask-image: url('$(res)/img/element-icons/hide.svg'); } diff --git a/res/css/views/dialogs/_SettingsDialog.scss b/res/css/views/dialogs/_SettingsDialog.scss index 6c4ed35c5a..b3b6802c3d 100644 --- a/res/css/views/dialogs/_SettingsDialog.scss +++ b/res/css/views/dialogs/_SettingsDialog.scss @@ -15,7 +15,7 @@ limitations under the License. */ // Not actually a component but things shared by settings components -.mx_UserSettingsDialog, .mx_RoomSettingsDialog { +.mx_UserSettingsDialog, .mx_RoomSettingsDialog, .mx_SpaceSettingsDialog { width: 90vw; max-width: 1000px; // set the height too since tabbed view scrolls itself. diff --git a/res/css/views/dialogs/_SpaceSettingsDialog.scss b/res/css/views/dialogs/_SpaceSettingsDialog.scss index 6e5fd9c8c8..fa074fdbe8 100644 --- a/res/css/views/dialogs/_SpaceSettingsDialog.scss +++ b/res/css/views/dialogs/_SpaceSettingsDialog.scss @@ -15,7 +15,6 @@ limitations under the License. */ .mx_SpaceSettingsDialog { - width: 480px; color: $primary-fg-color; .mx_SpaceSettings_errorText { @@ -32,8 +31,44 @@ limitations under the License. margin-left: 16px; } - .mx_AccessibleButton_kind_danger { - margin-top: 28px; + .mx_SettingsTab_section { + .mx_SettingsTab_section_caption { + margin-top: 12px; + margin-bottom: 20px; + } + + & + .mx_SettingsTab_subheading { + border-top: 1px solid $message-body-panel-bg-color; + margin-top: 0; + padding-top: 24px; + } + + .mx_RadioButton { + margin-top: 8px; + margin-bottom: 4px; + + .mx_RadioButton_content { + font-weight: $font-semi-bold; + line-height: $font-18px; + color: $primary-fg-color; + } + + & + span { + font-size: $font-15px; + line-height: $font-18px; + color: $secondary-fg-color; + margin-left: 26px; + } + } + + .mx_SettingsTab_showAdvanced { + margin: 16px 0; + padding: 0; + } + + .mx_SettingsFlag { + margin-top: 24px; + } } .mx_SpaceSettingsDialog_buttons { @@ -52,4 +87,14 @@ limitations under the License. .mx_AccessibleButton_hasKind { padding: 8px 22px; } + + .mx_TabbedView_tabLabel { + .mx_SpaceSettingsDialog_generalIcon::before { + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + + .mx_SpaceSettingsDialog_visibilityIcon::before { + mask-image: url('$(res)/img/element-icons/eye.svg'); + } + } } diff --git a/res/css/views/elements/_FormButton.scss b/res/css/views/elements/_FormButton.scss deleted file mode 100644 index eda201ff03..0000000000 --- a/res/css/views/elements/_FormButton.scss +++ /dev/null @@ -1,42 +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. -*/ - -.mx_FormButton { - line-height: $font-16px; - padding: 5px 15px; - font-size: $font-12px; - height: min-content; - - &:not(:last-child) { - margin-right: 8px; - } - - &.mx_AccessibleButton_kind_primary { - color: $accent-color; - background-color: $accent-bg-color; - } - - &.mx_AccessibleButton_kind_danger { - color: $notice-primary-color; - background-color: $notice-primary-bg-color; - } - - &.mx_AccessibleButton_kind_secondary { - color: $secondary-fg-color; - border: 1px solid $secondary-fg-color; - background-color: unset; - } -} diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 87420ae4e7..6632ccddf9 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -259,16 +259,6 @@ limitations under the License. .mx_AccessibleButton.mx_AccessibleButton_hasKind { padding: 8px 18px; - - &.mx_AccessibleButton_kind_primary { - color: $accent-color; - background-color: $accent-bg-color; - } - - &.mx_AccessibleButton_kind_danger { - color: $notice-primary-color; - background-color: $notice-primary-bg-color; - } } .mx_VerificationShowSas .mx_AccessibleButton, diff --git a/res/css/views/right_panel/_VerificationPanel.scss b/res/css/views/right_panel/_VerificationPanel.scss index a8466a1626..12148b09de 100644 --- a/res/css/views/right_panel/_VerificationPanel.scss +++ b/res/css/views/right_panel/_VerificationPanel.scss @@ -58,7 +58,7 @@ limitations under the License. } .mx_VerificationPanel_reciprocate_section { - .mx_FormButton { + .mx_AccessibleButton { width: 100%; box-sizing: border-box; padding: 10px; diff --git a/res/css/views/spaces/_SpaceBasicSettings.scss b/res/css/views/spaces/_SpaceBasicSettings.scss index 204ccab2b7..68e8723f11 100644 --- a/res/css/views/spaces/_SpaceBasicSettings.scss +++ b/res/css/views/spaces/_SpaceBasicSettings.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_SpaceBasicSettings { .mx_Field { - margin: 32px 0; + margin: 24px 0; } .mx_SpaceBasicSettings_avatarContainer { @@ -73,7 +73,7 @@ limitations under the License. } } - .mx_FormButton { + .mx_AccessibleButton { padding: 8px 22px; margin-left: auto; display: block; diff --git a/res/img/element-icons/eye.svg b/res/img/element-icons/eye.svg new file mode 100644 index 0000000000..0460a6201d --- /dev/null +++ b/res/img/element-icons/eye.svg @@ -0,0 +1,3 @@ + + + diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 02af402951..0990af70ce 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -24,19 +24,19 @@ clone() { # Try the PR author's branch in case it exists on the deps as well. # First we check if GITHUB_HEAD_REF is defined, # Then we check if BUILDKITE_BRANCH is defined, -# if it isn't we can assume this is a Netlify build -if [ -n ${GITHUB_HEAD_REF+x} ]; then - head=$GITHUB_HEAD_REF -elif [ -n ${BUILDKITE_BRANCH+x} ]; then +# if they aren't we can assume this is a Netlify build +if [ -n "$GITHUB_HEAD_REF" ]; then + head=$GITHUB_HEAD_REF +elif [ -n "$BUILDKITE_BRANCH" ]; then head=$BUILDKITE_BRANCH else - # Netlify doesn't give us info about the fork so we have to get it from GitHub API - apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" - apiEndpoint+=$REVIEW_ID - head=$(curl $apiEndpoint | jq -r '.head.label') + # Netlify doesn't give us info about the fork so we have to get it from GitHub API + apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" + apiEndpoint+=$REVIEW_ID + head=$(curl $apiEndpoint | jq -r '.head.label') fi -# If head is set, it will contain on BuilKite either: +# If head is set, it will contain on Buildkite either: # * "branch" when the author's branch and target branch are in the same repo # * "fork:branch" when the author's branch is in their fork or if this is a Netlify build # We can split on `:` into an array to check. @@ -44,19 +44,26 @@ fi # to determine whether the branch is from a fork or not BRANCH_ARRAY=(${head//:/ }) if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then - if [[ "$GITHUB_REPOSITORY" = "$deforg/$defrepo" ]]; then - clone $deforg $defrepo $head + + if [ -n "$GITHUB_HEAD_REF" ]; then + if [[ "$GITHUB_REPOSITORY" == "$deforg"* ]]; then + clone $deforg $defrepo $GITHUB_HEAD_REF + else + REPO_ARRAY=(${GITHUB_REPOSITORY//\// }) + clone $REPO_ARRAY[0] $defrepo $GITHUB_HEAD_REF + fi else - clone $GITHUB_ACTOR $defrepo $head + clone $deforg $defrepo $BUILDKITE_BRANCH fi + elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]} fi # Try the target branch of the push or PR. -if [ -n ${GITHUB_BASE_REF+x} ]; then +if [ -n $GITHUB_BASE_REF ]; then clone $deforg $defrepo $GITHUB_BASE_REF -elif [ -n ${BUILDKITE_PULL_REQUEST_BASE_BRANCH+x} ]; then +elif [ -n $BUILDKITE_PULL_REQUEST_BASE_BRANCH ]; then clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH fi diff --git a/src/@types/diff-dom.ts b/src/@types/diff-dom.ts new file mode 100644 index 0000000000..884ee6126d --- /dev/null +++ b/src/@types/diff-dom.ts @@ -0,0 +1,50 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +declare module "diff-dom" { + enum Action { + AddElement = "addElement", + AddTextElement = "addTextElement", + RemoveTextElement = "removeTextElement", + RemoveElement = "removeElement", + ReplaceElement = "replaceElement", + ModifyTextElement = "modifyTextElement", + AddAttribute = "addAttribute", + RemoveAttribute = "removeAttribute", + ModifyAttribute = "modifyAttribute", + } + + export interface IDiff { + action: Action; + name: string; + text?: string; + route: number[]; + value: string; + element: unknown; + oldValue: string; + newValue: string; + } + + interface IOpts { + } + + export class DiffDOM { + public constructor(opts?: IOpts); + public apply(tree: unknown, diffs: IDiff[]): unknown; + public undo(tree: unknown, diffs: IDiff[]): unknown; + public diff(a: HTMLElement | string, b: HTMLElement | string): IDiff[]; + } +} diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 0c6b63dd33..7eff341095 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -113,19 +113,6 @@ declare global { usageDetails?: {[key: string]: number}; } - export interface ISettledFulfilled { - status: "fulfilled"; - value: T; - } - export interface ISettledRejected { - status: "rejected"; - reason: any; - } - - interface PromiseConstructor { - allSettled(promises: Promise[]): Promise | ISettledRejected>>; - } - interface HTMLAudioElement { type?: string; // sinkId & setSinkId are experimental and typescript doesn't know about them diff --git a/src/Avatar.ts b/src/Avatar.ts index 8ea0b0c9fa..4c4bd1c265 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -17,13 +17,12 @@ limitations under the License. import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { User } from "matrix-js-sdk/src/models/user"; import { Room } from "matrix-js-sdk/src/models/room"; +import { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; import DMRoomMap from './utils/DMRoomMap'; import { mediaFromMxc } from "./customisations/Media"; import SettingsStore from "./settings/SettingsStore"; -export type ResizeMethod = "crop" | "scale"; - // Not to be used for BaseAvatar urls as that has similar default avatar fallback already export function avatarUrlForMember( member: RoomMember, diff --git a/src/DecryptionFailureTracker.js b/src/DecryptionFailureTracker.ts similarity index 79% rename from src/DecryptionFailureTracker.js rename to src/DecryptionFailureTracker.ts index b02a5e937b..07c0c546fe 100644 --- a/src/DecryptionFailureTracker.js +++ b/src/DecryptionFailureTracker.ts @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +Copyright 2018 - 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,34 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MatrixError } from "matrix-js-sdk/src/http-api"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + export class DecryptionFailure { - constructor(failedEventId, errorCode) { - this.failedEventId = failedEventId; - this.errorCode = errorCode; + public readonly ts: number; + + constructor(public readonly failedEventId: string, public readonly errorCode: string) { this.ts = Date.now(); } } +type TrackingFn = (count: number, trackedErrCode: string) => void; +type ErrCodeMapFn = (errcode: string) => string; + export class DecryptionFailureTracker { // Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list // is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did // are accumulated in `failureCounts`. - failures = []; + public failures: DecryptionFailure[] = []; // A histogram of the number of failures that will be tracked at the next tracking // interval, split by failure error code. - failureCounts = { + public failureCounts: Record = { // [errorCode]: 42 }; // Event IDs of failures that were tracked previously - trackedEventHashMap = { + public trackedEventHashMap: Record = { // [eventId]: true }; // Set to an interval ID when `start` is called - checkInterval = null; - trackInterval = null; + public checkInterval: NodeJS.Timeout = null; + public trackInterval: NodeJS.Timeout = null; // Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`. static TRACK_INTERVAL_MS = 60000; @@ -67,7 +73,7 @@ export class DecryptionFailureTracker { * @param {function?} errorCodeMapFn The function used to map error codes to the * trackedErrorCode. If not provided, the `.code` of errors will be used. */ - constructor(fn, errorCodeMapFn) { + constructor(private readonly fn: TrackingFn, private readonly errorCodeMapFn?: ErrCodeMapFn) { if (!fn || typeof fn !== 'function') { throw new Error('DecryptionFailureTracker requires tracking function'); } @@ -75,9 +81,6 @@ export class DecryptionFailureTracker { if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') { throw new Error('DecryptionFailureTracker second constructor argument should be a function'); } - - this._trackDecryptionFailure = fn; - this._mapErrorCode = errorCodeMapFn; } // loadTrackedEventHashMap() { @@ -88,7 +91,7 @@ export class DecryptionFailureTracker { // localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap)); // } - eventDecrypted(e, err) { + public eventDecrypted(e: MatrixEvent, err: MatrixError | Error): void { if (err) { this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code)); } else { @@ -97,18 +100,18 @@ export class DecryptionFailureTracker { } } - addDecryptionFailure(failure) { + public addDecryptionFailure(failure: DecryptionFailure): void { this.failures.push(failure); } - removeDecryptionFailuresForEvent(e) { + public removeDecryptionFailuresForEvent(e: MatrixEvent): void { this.failures = this.failures.filter((f) => f.failedEventId !== e.getId()); } /** * Start checking for and tracking failures. */ - start() { + public start(): void { this.checkInterval = setInterval( () => this.checkFailures(Date.now()), DecryptionFailureTracker.CHECK_INTERVAL_MS, @@ -123,7 +126,7 @@ export class DecryptionFailureTracker { /** * Clear state and stop checking for and tracking failures. */ - stop() { + public stop(): void { clearInterval(this.checkInterval); clearInterval(this.trackInterval); @@ -132,11 +135,11 @@ export class DecryptionFailureTracker { } /** - * Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be + * Mark failures that occurred before nowTs - GRACE_PERIOD_MS as failures that should be * tracked. Only mark one failure per event ID. * @param {number} nowTs the timestamp that represents the time now. */ - checkFailures(nowTs) { + public checkFailures(nowTs: number): void { const failuresGivenGrace = []; const failuresNotReady = []; while (this.failures.length > 0) { @@ -175,10 +178,10 @@ export class DecryptionFailureTracker { const dedupedFailures = dedupedFailuresMap.values(); - this._aggregateFailures(dedupedFailures); + this.aggregateFailures(dedupedFailures); } - _aggregateFailures(failures) { + private aggregateFailures(failures: DecryptionFailure[]): void { for (const failure of failures) { const errorCode = failure.errorCode; this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1; @@ -189,12 +192,12 @@ export class DecryptionFailureTracker { * If there are failures that should be tracked, call the given trackDecryptionFailure * function with the number of failures that should be tracked. */ - trackFailures() { + public trackFailures(): void { for (const errorCode of Object.keys(this.failureCounts)) { if (this.failureCounts[errorCode] > 0) { - const trackedErrorCode = this._mapErrorCode ? this._mapErrorCode(errorCode) : errorCode; + const trackedErrorCode = this.errorCodeMapFn ? this.errorCodeMapFn(errorCode) : errorCode; - this._trackDecryptionFailure(this.failureCounts[errorCode], trackedErrorCode); + this.fn(this.failureCounts[errorCode], trackedErrorCode); this.failureCounts[errorCode] = 0; } } diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index ef5ac383e3..983538d65b 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -17,11 +17,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import sanitizeHtml from 'sanitize-html'; -import { IExtendedSanitizeOptions } from './@types/sanitize-html'; +import cheerio from 'cheerio'; import * as linkify from 'linkifyjs'; -import linkifyMatrix from './linkify-matrix'; import _linkifyElement from 'linkifyjs/element'; import _linkifyString from 'linkifyjs/string'; import classNames from 'classnames'; @@ -29,13 +28,15 @@ import EMOJIBASE_REGEX from 'emojibase-regex'; import url from 'url'; import katex from 'katex'; import { AllHtmlEntities } from 'html-entities'; -import SettingsStore from './settings/SettingsStore'; -import cheerio from 'cheerio'; +import { IContent } from 'matrix-js-sdk/src/models/event'; -import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; -import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji"; +import { IExtendedSanitizeOptions } from './@types/sanitize-html'; +import linkifyMatrix from './linkify-matrix'; +import SettingsStore from './settings/SettingsStore'; +import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; +import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; -import {mediaFromMxc} from "./customisations/Media"; +import { mediaFromMxc } from "./customisations/Media"; linkifyMatrix(linkify); @@ -66,7 +67,7 @@ export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet' * need emojification. * unicodeToImage uses this function. */ -function mightContainEmoji(str: string) { +function mightContainEmoji(str: string): boolean { return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str); } @@ -76,7 +77,7 @@ function mightContainEmoji(str: string) { * @param {String} char The emoji character * @return {String} The shortcode (such as :thumbup:) */ -export function unicodeToShortcode(char: string) { +export function unicodeToShortcode(char: string): string { const data = getEmojiFromUnicode(char); return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); } @@ -87,7 +88,7 @@ export function unicodeToShortcode(char: string) { * @param {String} shortcode The shortcode (such as :thumbup:) * @return {String} The emoji character; null if none exists */ -export function shortcodeToUnicode(shortcode: string) { +export function shortcodeToUnicode(shortcode: string): string { shortcode = shortcode.slice(1, shortcode.length - 1); const data = SHORTCODE_TO_EMOJI.get(shortcode); return data ? data.unicode : null; @@ -124,13 +125,13 @@ export function processHtmlForSending(html: string): string { * Given an untrusted HTML string, return a React node with an sanitized version * of that HTML. */ -export function sanitizedHtmlNode(insaneHtml: string) { +export function sanitizedHtmlNode(insaneHtml: string): ReactNode { const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams); return
; } -export function getHtmlText(insaneHtml: string) { +export function getHtmlText(insaneHtml: string): string { return sanitizeHtml(insaneHtml, { allowedTags: [], allowedAttributes: {}, @@ -148,7 +149,7 @@ export function getHtmlText(insaneHtml: string) { * other places we need to sanitise URLs. * @return true if permitted, otherwise false */ -export function isUrlPermitted(inputUrl: string) { +export function isUrlPermitted(inputUrl: string): boolean { try { const parsed = url.parse(inputUrl); if (!parsed.protocol) return false; @@ -351,13 +352,6 @@ class HtmlHighlighter extends BaseHighlighter { } } -interface IContent { - format?: string; - // eslint-disable-next-line camelcase - formatted_body?: string; - body: string; -} - interface IOpts { highlightLink?: string; disableBigEmoji?: boolean; @@ -367,6 +361,14 @@ interface IOpts { ref?: React.Ref; } +export interface IOptsReturnNode extends IOpts { + returnString: false; +} + +export interface IOptsReturnString extends IOpts { + returnString: true; +} + /* turn a matrix event body into html * * content: 'content' of the MatrixEvent @@ -380,6 +382,8 @@ interface IOpts { * opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer * opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString) */ +export function bodyToHtml(content: IContent, highlights: string[], opts: IOptsReturnString): string; +export function bodyToHtml(content: IContent, highlights: string[], opts: IOptsReturnNode): ReactNode; export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts = {}) { const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body; let bodyHasEmoji = false; @@ -501,7 +505,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options * @returns {string} Linkified string */ -export function linkifyString(str: string, options = linkifyMatrix.options) { +export function linkifyString(str: string, options = linkifyMatrix.options): string { return _linkifyString(str, options); } @@ -512,7 +516,7 @@ export function linkifyString(str: string, options = linkifyMatrix.options) { * @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options * @returns {object} */ -export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options) { +export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options): HTMLElement { return _linkifyElement(element, options); } @@ -523,7 +527,7 @@ export function linkifyElement(element: HTMLElement, options = linkifyMatrix.opt * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options * @returns {string} */ -export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options) { +export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options): string { return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams); } @@ -534,7 +538,7 @@ export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatri * @param {Node} node * @returns {bool} */ -export function checkBlockNode(node: Node) { +export function checkBlockNode(node: Node): boolean { switch (node.nodeName) { case "H1": case "H2": diff --git a/src/Modal.tsx b/src/Modal.tsx index ce11c571b6..2f2d5a2d52 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -385,7 +385,7 @@ export class ModalManager {
); - ReactDOM.render(dialog, ModalManager.getOrCreateContainer()); + setImmediate(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer())); } else { // This is safe to call repeatedly if we happen to do that ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); diff --git a/src/Rooms.js b/src/Rooms.ts similarity index 89% rename from src/Rooms.js rename to src/Rooms.ts index 955498faaa..19d1c9ee05 100644 --- a/src/Rooms.js +++ b/src/Rooms.ts @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +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. @@ -14,7 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from './MatrixClientPeg'; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { MatrixClientPeg } from './MatrixClientPeg'; /** * Given a room object, return the alias we should use for it, @@ -25,11 +27,11 @@ import {MatrixClientPeg} from './MatrixClientPeg'; * @param {Object} room The room object * @returns {string} A display alias for the given room */ -export function getDisplayAliasForRoom(room) { +export function getDisplayAliasForRoom(room: Room): string { return room.getCanonicalAlias() || room.getAltAliases()[0]; } -export function looksLikeDirectMessageRoom(room, myUserId) { +export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolean { const myMembership = room.getMyMembership(); const me = room.getMember(myUserId); @@ -48,7 +50,7 @@ export function looksLikeDirectMessageRoom(room, myUserId) { return false; } -export function guessAndSetDMRoom(room, isDirect) { +export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise { let newTarget; if (isDirect) { const guessedUserId = guessDMRoomTargetId( @@ -70,7 +72,7 @@ export function guessAndSetDMRoom(room, isDirect) { this room as a DM room * @returns {object} A promise */ -export function setDMRoom(roomId, userId) { +export function setDMRoom(roomId: string, userId: string): Promise { if (MatrixClientPeg.get().isGuest()) { return Promise.resolve(); } @@ -114,7 +116,7 @@ export function setDMRoom(roomId, userId) { * @param {string} myUserId User ID of the current user * @returns {string} User ID of the user that the room is probably a DM with */ -function guessDMRoomTargetId(room, myUserId) { +function guessDMRoomTargetId(room: Room, myUserId: string): string { let oldestTs; let oldestUser; diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 9700c57d67..bd2133f3d9 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -17,8 +17,8 @@ See the License for the specific language governing permissions and limitations under the License. */ - import * as React from 'react'; +import { User } from "matrix-js-sdk/src/models/user"; import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers'; import {MatrixClientPeg} from './MatrixClientPeg'; @@ -1019,9 +1019,8 @@ export const Commands = [ const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId); dis.dispatch({ action: Action.ViewUser, - // XXX: We should be using a real member object and not assuming what the - // receiver wants. - member: member || {userId}, + // XXX: We should be using a real member object and not assuming what the receiver wants. + member: member || { userId } as User, }); return success(); }, diff --git a/src/Unread.js b/src/Unread.ts similarity index 75% rename from src/Unread.js rename to src/Unread.ts index 25c425aa9a..b733f4175a 100644 --- a/src/Unread.js +++ b/src/Unread.ts @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +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. @@ -14,9 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from "./MatrixClientPeg"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; + +import { MatrixClientPeg } from "./MatrixClientPeg"; import shouldHideEvent from './shouldHideEvent'; -import {haveTileForEvent} from "./components/views/rooms/EventTile"; +import { haveTileForEvent } from "./components/views/rooms/EventTile"; /** * Returns true iff this event arriving in a room should affect the room's @@ -25,28 +29,33 @@ import {haveTileForEvent} from "./components/views/rooms/EventTile"; * @param {Object} ev The event * @returns {boolean} True if the given event should affect the unread message count */ -export function eventTriggersUnreadCount(ev) { +export function eventTriggersUnreadCount(ev: MatrixEvent): boolean { if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { return false; - } else if (ev.getType() == 'm.room.member') { - return false; - } else if (ev.getType() == 'm.room.third_party_invite') { - return false; - } else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') { - return false; - } else if (ev.getType() == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { - return false; - } else if (ev.getType() == 'm.room.aliases' || ev.getType() == 'm.room.canonical_alias') { - return false; - } else if (ev.getType() == 'm.room.server_acl') { - return false; - } else if (ev.isRedacted()) { - return false; } + + switch (ev.getType()) { + case EventType.RoomMember: + case EventType.RoomThirdPartyInvite: + case EventType.CallAnswer: + case EventType.CallHangup: + case EventType.RoomAliases: + case EventType.RoomCanonicalAlias: + case EventType.RoomServerAcl: + return false; + + case EventType.RoomMessage: + if (ev.getContent().msgtype === MsgType.Notice) { + return false; + } + break; + } + + if (ev.isRedacted()) return false; return haveTileForEvent(ev); } -export function doesRoomHaveUnreadMessages(room) { +export function doesRoomHaveUnreadMessages(room: Room): boolean { const myUserId = MatrixClientPeg.get().getUserId(); // get the most recent read receipt sent by our account. diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx index 2a3e576e31..1cd5408210 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -57,6 +57,8 @@ export enum Modifiers { // Meta-modifier: isMac ? CMD : CONTROL export const CMD_OR_CTRL = isMac ? Modifiers.COMMAND : Modifiers.CONTROL; +// Meta-key representing the digits [0-9] often found at the top of standard keyboard layouts +export const DIGITS = "digits"; interface IKeybind { modifiers?: Modifiers[]; @@ -319,6 +321,7 @@ const alternateKeyName: Record = { [Key.SPACE]: _td("Space"), [Key.HOME]: _td("Home"), [Key.END]: _td("End"), + [DIGITS]: _td("[number]"), }; const keyIcon: Record = { [Key.ARROW_UP]: "↑", diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 5409825f45..7f3f5d2c01 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -15,8 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ReactElement} from 'react'; -import Room from 'matrix-js-sdk/src/models/room'; +import { ReactElement } from 'react'; +import { Room } from 'matrix-js-sdk/src/models/room'; + import CommandProvider from './CommandProvider'; import CommunityProvider from './CommunityProvider'; import DuckDuckGoProvider from './DuckDuckGoProvider'; @@ -24,7 +25,7 @@ import RoomProvider from './RoomProvider'; import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; import NotifProvider from './NotifProvider'; -import {timeout} from "../utils/promise"; +import { timeout } from "../utils/promise"; import AutocompleteProvider, {ICommand} from "./AutocompleteProvider"; import SettingsStore from "../settings/SettingsStore"; import SpaceProvider from "./SpaceProvider"; @@ -54,13 +55,14 @@ const PROVIDERS = [ EmojiProvider, NotifProvider, CommandProvider, - CommunityProvider, DuckDuckGoProvider, ]; // as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here if (SettingsStore.getValue("feature_spaces")) { PROVIDERS.push(SpaceProvider); +} else { + PROVIDERS.push(CommunityProvider); } // Providers will get rejected if they take longer than this. diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index 0bc7ead097..827f4aa885 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -15,7 +15,8 @@ limitations under the License. */ import React from 'react'; -import Room from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/models/room"; + import AutocompleteProvider from './AutocompleteProvider'; import { _t } from '../languageHandler'; import {MatrixClientPeg} from '../MatrixClientPeg'; diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index f784a57821..5240db6955 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -17,16 +17,16 @@ limitations under the License. */ import React from "react"; -import {uniqBy, sortBy} from "lodash"; -import Room from "matrix-js-sdk/src/models/room"; +import { uniqBy, sortBy } from "lodash"; +import { Room } from "matrix-js-sdk/src/models/room"; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; -import {MatrixClientPeg} from '../MatrixClientPeg'; +import { MatrixClientPeg } from '../MatrixClientPeg'; import QueryMatcher from './QueryMatcher'; -import {PillCompletion} from './Components'; -import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; -import {ICompletion, ISelectionRange} from "./Autocompleter"; +import { PillCompletion } from './Components'; +import { makeRoomPermalink } from "../utils/permalinks/Permalinks"; +import { ICompletion, ISelectionRange } from "./Autocompleter"; import RoomAvatar from '../components/views/avatars/RoomAvatar'; import SettingsStore from "../settings/SettingsStore"; diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index 66f998b616..8650224fb3 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -15,9 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { HTMLAttributes } from "react"; -interface IProps { +interface IProps extends HTMLAttributes { className?: string; onScroll?: () => void; onWheel?: () => void; @@ -52,14 +52,18 @@ export default class AutoHideScrollbar extends React.Component { } public render() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props; + return (
- { this.props.children } + { children }
); } } diff --git a/src/components/structures/GroupFilterPanel.js b/src/components/structures/GroupFilterPanel.js index 2ff91e4976..f1c28d588a 100644 --- a/src/components/structures/GroupFilterPanel.js +++ b/src/components/structures/GroupFilterPanel.js @@ -24,7 +24,6 @@ import * as sdk from '../../index'; import dis from '../../dispatcher/dispatcher'; import { _t } from '../../languageHandler'; -import { Droppable } from 'react-beautiful-dnd'; import classNames from 'classnames'; import MatrixClientContext from "../../contexts/MatrixClientContext"; import AutoHideScrollbar from "./AutoHideScrollbar"; @@ -83,7 +82,7 @@ class GroupFilterPanel extends React.Component { } }; - onMouseDown = e => { + onClick = e => { // only dispatch if its not a no-op if (this.state.selectedTags.length > 0) { dis.dispatch({action: 'deselect_tags'}); @@ -151,28 +150,15 @@ class GroupFilterPanel extends React.Component { return
- - { (provided, snapshot) => ( -
- { this.renderGlobalIcon() } - { tags } -
- {createButton} -
- { provided.placeholder } -
- ) } -
+
+ { this.renderGlobalIcon() } + { tags } +
+ { createButton } +
+
; } diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 51a3b287f0..25dcaeed39 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -185,21 +185,24 @@ export default class IndicatorScrollbar extends React.Component { }; render() { + // eslint-disable-next-line no-unused-vars + const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props; + const leftIndicatorStyle = {left: this.state.leftIndicatorOffset}; const rightIndicatorStyle = {right: this.state.rightIndicatorOffset}; - const leftOverflowIndicator = this.props.trackHorizontalOverflow + const leftOverflowIndicator = trackHorizontalOverflow ?
: null; - const rightOverflowIndicator = this.props.trackHorizontalOverflow + const rightOverflowIndicator = trackHorizontalOverflow ?
: null; return ( { leftOverflowIndicator } - { this.props.children } + { children } { rightOverflowIndicator } ); } diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 3091278e3a..e3d6b1ab9c 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -19,7 +19,6 @@ limitations under the License. import * as React from 'react'; import * as PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk/src/client'; -import { DragDropContext } from 'react-beautiful-dnd'; import {Key} from '../../Keyboard'; import PageTypes from '../../PageTypes'; @@ -30,8 +29,6 @@ import dis from '../../dispatcher/dispatcher'; import { IMatrixClientCreds } from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; -import TagOrderActions from '../../actions/TagOrderActions'; -import RoomListActions from '../../actions/RoomListActions'; import ResizeHandle from '../views/elements/ResizeHandle'; import {Resizer, CollapseDistributor} from '../../resizer'; import MatrixClientContext from "../../contexts/MatrixClientContext"; @@ -569,50 +566,6 @@ class LoggedInView extends React.Component { } }; - _onDragEnd = (result) => { - // Dragged to an invalid destination, not onto a droppable - if (!result.destination) { - return; - } - - const dest = result.destination.droppableId; - - if (dest === 'tag-panel-droppable') { - // Could be "GroupTile +groupId:domain" - const draggableId = result.draggableId.split(' ').pop(); - - // Dispatch synchronously so that the GroupFilterPanel receives an - // optimistic update from GroupFilterOrderStore before the previous - // state is shown. - dis.dispatch(TagOrderActions.moveTag( - this._matrixClient, - draggableId, - result.destination.index, - ), true); - } else if (dest.startsWith('room-sub-list-droppable_')) { - this._onRoomTileEndDrag(result); - } - }; - - _onRoomTileEndDrag = (result) => { - let newTag = result.destination.droppableId.split('_')[1]; - let prevTag = result.source.droppableId.split('_')[1]; - if (newTag === 'undefined') newTag = undefined; - if (prevTag === 'undefined') prevTag = undefined; - - const roomId = result.draggableId.split('_')[1]; - - const oldIndex = result.source.index; - const newIndex = result.destination.index; - - dis.dispatch(RoomListActions.tagRoom( - this._matrixClient, - this._matrixClient.getRoom(roomId), - prevTag, newTag, - oldIndex, newIndex, - ), true); - }; - render() { const RoomView = sdk.getComponent('structures.RoomView'); const UserView = sdk.getComponent('structures.UserView'); @@ -679,17 +632,15 @@ class LoggedInView extends React.Component { aria-hidden={this.props.hideToSRUsers} > - -
- { SettingsStore.getValue("feature_spaces") ? : null } - - - { pageElement } -
-
+
+ { SettingsStore.getValue("feature_spaces") ? : null } + + + { pageElement } +
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 0af2d3d635..27d628fecf 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1461,7 +1461,7 @@ export default class MatrixChat extends React.PureComponent { }); const dft = new DecryptionFailureTracker((total, errorCode) => { - Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total); + Analytics.trackEvent('E2E', 'Decryption failure', errorCode, String(total)); CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total }); }, (errorCode) => { // Map JS-SDK error codes to tracker codes for aggregation diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 1fab6c4348..d0a2fbff41 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -82,8 +82,7 @@ export default class MyGroups extends React.Component {

{ _t( - "To set up a filter, drag a community avatar over to the filter panel on " + - "the far left hand side of the screen. You can click on an avatar in the " + + "You can click on an avatar in the " + "filter panel at any time to see only the rooms and people associated " + "with that community.", ) } diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 1e0605f263..62944a9d98 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -337,11 +337,10 @@ export default class RoomDirectory extends React.Component { } private onRoomClicked = (room: IRoom, ev: ButtonEvent) => { + // If room was shift-clicked, remove it from the room directory if (ev.shiftKey && !this.state.selectedCommunityId) { ev.preventDefault(); this.removeFromDirectory(room); - } else { - this.showRoom(room); } }; @@ -568,11 +567,11 @@ export default class RoomDirectory extends React.Component { let avatarUrl = null; if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32); + // We use onMouseDown instead of onClick, so that we can avoid text getting selected return [ -

this.onRoomClicked(room, ev)} - // cancel onMouseDown otherwise shift-clicking highlights text - onMouseDown={(ev) => {ev.preventDefault();}} +
this.onRoomClicked(room, ev)} className="mx_RoomDirectory_roomAvatar" > { url={avatarUrl} />
, -
this.onRoomClicked(room, ev)} - // cancel onMouseDown otherwise shift-clicking highlights text - onMouseDown={(ev) => {ev.preventDefault();}} +
this.onRoomClicked(room, ev)} className="mx_RoomDirectory_roomDescription" > -
{ name }
  -
{ ev.stopPropagation(); } } +
this.onRoomClicked(room, ev)} + > + { name } +
  +
this.onRoomClicked(room, ev)} dangerouslySetInnerHTML={{ __html: topic }} /> -
{ getDisplayAliasForRoom(room) }
+
this.onRoomClicked(room, ev)} + > + { getDisplayAliasForRoom(room) } +
, -
this.onRoomClicked(room, ev)} - // cancel onMouseDown otherwise shift-clicking highlights text - onMouseDown={(ev) => {ev.preventDefault();}} +
this.onRoomClicked(room, ev)} className="mx_RoomDirectory_roomMemberCount" > { room.num_joined_members }
, -
this.onRoomClicked(room, ev)} +
this.onRoomClicked(room, ev)} // cancel onMouseDown otherwise shift-clicking highlights text - onMouseDown={(ev) => {ev.preventDefault();}} className="mx_RoomDirectory_preview" > - {previewButton} + { previewButton }
, -
this.onRoomClicked(room, ev)} - // cancel onMouseDown otherwise shift-clicking highlights text - onMouseDown={(ev) => {ev.preventDefault();}} +
this.onRoomClicked(room, ev)} className="mx_RoomDirectory_join" > - {joinOrViewButton} + { joinOrViewButton }
, ]; } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 1e3adcb518..885851e8e6 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -23,7 +23,7 @@ limitations under the License. import React, { createRef } from 'react'; import classNames from 'classnames'; -import { Room } from "matrix-js-sdk/src/models/room"; +import { IRecommendedVersion, NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { SearchResult } from "matrix-js-sdk/src/models/search-result"; import { EventSubscription } from "fbemitter"; @@ -60,7 +60,7 @@ import ScrollPanel from "./ScrollPanel"; import TimelinePanel from "./TimelinePanel"; import ErrorBoundary from "../views/elements/ErrorBoundary"; import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; -import SearchBar from "../views/rooms/SearchBar"; +import SearchBar, { SearchScope } from "../views/rooms/SearchBar"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; import AuxPanel from "../views/rooms/AuxPanel"; import RoomHeader from "../views/rooms/RoomHeader"; @@ -139,7 +139,7 @@ export interface IState { draggingFile: boolean; searching: boolean; searchTerm?: string; - searchScope?: "All" | "Room"; + searchScope?: SearchScope; searchResults?: XOR<{}, { count: number; highlights: string[]; @@ -172,11 +172,7 @@ export interface IState { // We load this later by asking the js-sdk to suggest a version for us. // This object is the result of Room#getRecommendedVersion() - upgradeRecommendation?: { - version: string; - needsUpgrade: boolean; - urgent: boolean; - }; + upgradeRecommendation?: IRecommendedVersion; canReact: boolean; canReply: boolean; layout: Layout; @@ -1267,7 +1263,7 @@ export default class RoomView extends React.Component { }); } - private onSearch = (term: string, scope) => { + private onSearch = (term: string, scope: SearchScope) => { this.setState({ searchTerm: term, searchScope: scope, @@ -1288,7 +1284,7 @@ export default class RoomView extends React.Component { this.searchId = new Date().getTime(); let roomId; - if (scope === "Room") roomId = this.state.room.roomId; + if (scope === SearchScope.Room) roomId = this.state.room.roomId; debuglog("sending search request"); const searchPromise = eventSearch(term, roomId); @@ -2058,7 +2054,7 @@ export default class RoomView extends React.Component { if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) { const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton'); jumpToBottom = ( 0} + highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0} numUnreadMessages={this.state.numUnreadMessages} onScrollToBottomClick={this.jumpToLiveTimeline} roomId={this.state.roomId} diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index acbde0b097..4292b60f41 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -14,34 +14,34 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {ReactNode, useMemo, useState} from "react"; -import {Room} from "matrix-js-sdk/src/models/room"; -import {MatrixClient} from "matrix-js-sdk/src/client"; -import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; +import React, { ReactNode, useMemo, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import classNames from "classnames"; -import {sortBy} from "lodash"; +import { sortBy } from "lodash"; -import {MatrixClientPeg} from "../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; import dis from "../../dispatcher/dispatcher"; -import {_t} from "../../languageHandler"; -import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton"; +import { _t } from "../../languageHandler"; +import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; import BaseDialog from "../views/dialogs/BaseDialog"; import Spinner from "../views/elements/Spinner"; import SearchBox from "./SearchBox"; import RoomAvatar from "../views/avatars/RoomAvatar"; import RoomName from "../views/elements/RoomName"; -import {useAsyncMemo} from "../../hooks/useAsyncMemo"; -import {EnhancedMap} from "../../utils/maps"; +import { useAsyncMemo } from "../../hooks/useAsyncMemo"; +import { EnhancedMap } from "../../utils/maps"; import StyledCheckbox from "../views/elements/StyledCheckbox"; import AutoHideScrollbar from "./AutoHideScrollbar"; import BaseAvatar from "../views/avatars/BaseAvatar"; -import {mediaFromMxc} from "../../customisations/Media"; +import { mediaFromMxc } from "../../customisations/Media"; import InfoTooltip from "../views/elements/InfoTooltip"; import TextWithTooltip from "../views/elements/TextWithTooltip"; -import {useStateToggle} from "../../hooks/useStateToggle"; -import {getOrder} from "../../stores/SpaceStore"; +import { useStateToggle } from "../../hooks/useStateToggle"; +import { getChildOrder } from "../../stores/SpaceStore"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; -import {linkifyElement} from "../../HtmlUtils"; +import { linkifyElement } from "../../HtmlUtils"; interface IHierarchyProps { space: Room; @@ -286,7 +286,7 @@ export const HierarchyLevel = ({ const children = Array.from(relations.get(spaceId)?.values() || []); const sortedChildren = sortBy(children, ev => { // XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting - return getOrder(ev.content.order, null, ev.state_key); + return getChildOrder(ev.content.order, null, ev.state_key); }); const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { const roomId = ev.state_key; diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 6949c14636..f98f8c88a1 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -17,16 +17,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useContext, useEffect, useState} from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import classNames from 'classnames'; +import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; + import * as AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; import RoomContext from "../../../contexts/RoomContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; -import {toPx} from "../../../utils/units"; -import {ResizeMethod} from "../../../Avatar"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import { toPx } from "../../../utils/units"; import { _t } from '../../../languageHandler'; interface IProps { diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx index 3734ba9504..13dbbfec09 100644 --- a/src/components/views/avatars/GroupAvatar.tsx +++ b/src/components/views/avatars/GroupAvatar.tsx @@ -15,10 +15,11 @@ limitations under the License. */ import React from 'react'; +import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; + import BaseAvatar from './BaseAvatar'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../../customisations/Media"; -import {ResizeMethod} from "../../../Avatar"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../../customisations/Media"; export interface IProps { groupId?: string; diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 7488616fb6..81b75cb450 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -17,13 +17,12 @@ limitations under the License. import React from 'react'; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; - +import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; import dis from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import BaseAvatar from "./BaseAvatar"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; -import { ResizeMethod } from "../../../Avatar"; import { omit } from "lodash"; interface IProps extends Omit, "name" | "idName" | "url"> { diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 4693d907ba..bd820509c5 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -13,17 +13,17 @@ 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, {ComponentProps} from 'react'; -import Room from 'matrix-js-sdk/src/models/room'; +import React, { ComponentProps } from 'react'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; import BaseAvatar from './BaseAvatar'; import ImageView from '../elements/ImageView'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import * as Avatar from '../../../Avatar'; -import {ResizeMethod} from "../../../Avatar"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../../customisations/Media"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../../customisations/Media"; interface IProps extends Omit, "name" | "idName" | "url" | "onClick"> { // Room may be left unset here, but if it is, diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx index 821c448f4f..aa4fe49f63 100644 --- a/src/components/views/beta/BetaCard.tsx +++ b/src/components/views/beta/BetaCard.tsx @@ -25,6 +25,7 @@ import TextWithTooltip from "../elements/TextWithTooltip"; import Modal from "../../../Modal"; import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog"; import SdkConfig from "../../../SdkConfig"; +import SettingsFlag from "../elements/SettingsFlag"; interface IProps { title?: string; @@ -66,7 +67,7 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => { const info = SettingsStore.getBetaInfo(featureId); if (!info) return null; // Beta is invalid/disabled - const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading } = info; + const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading, extraSettings } = info; const value = SettingsStore.getValue(featureId); let feedbackButton; @@ -82,26 +83,33 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => { } return
-
-

- { titleOverride || _t(title) } - -

- { _t(caption) } +
- { feedbackButton } - SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)} - kind={feedbackButton ? "primary_outline" : "primary"} - > - { value ? _t("Leave the beta") : _t("Join the beta") } - +

+ { titleOverride || _t(title) } + +

+ { _t(caption) } +
+ { feedbackButton } + SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)} + kind={feedbackButton ? "primary_outline" : "primary"} + > + { value ? _t("Leave the beta") : _t("Join the beta") } + +
+ { disclaimer &&
+ { disclaimer(value) } +
}
- { disclaimer &&
- { disclaimer(value) } -
} +
- + { extraSettings &&
+ { extraSettings.map(key => ( + + )) } +
}
; }; diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index 8dea62690c..4e381643ba 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -23,45 +23,70 @@ import TagOrderActions from '../../../actions/TagOrderActions'; import {MenuItem} from "../../structures/ContextMenu"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore"; @replaceableComponent("views.context_menus.TagTileContextMenu") export default class TagTileContextMenu extends React.Component { static propTypes = { tag: PropTypes.string.isRequired, + index: PropTypes.number.isRequired, /* callback called when the menu is dismissed */ onFinished: PropTypes.func.isRequired, }; static contextType = MatrixClientContext; - constructor() { - super(); - - this._onViewCommunityClick = this._onViewCommunityClick.bind(this); - this._onRemoveClick = this._onRemoveClick.bind(this); - } - - _onViewCommunityClick() { + _onViewCommunityClick = () => { dis.dispatch({ action: 'view_group', group_id: this.props.tag, }); this.props.onFinished(); - } + }; - _onRemoveClick() { + _onRemoveClick = () => { dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag)); this.props.onFinished(); - } + }; + + _onMoveUp = () => { + dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1)); + this.props.onFinished(); + }; + + _onMoveDown = () => { + dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index + 1)); + this.props.onFinished(); + }; render() { + let moveUp; + let moveDown; + if (this.props.index > 0) { + moveUp = ( + + { _t("Move up") } + + ); + } + if (this.props.index < (GroupFilterOrderStore.getOrderedTags() || []).length - 1) { + moveDown = ( + + { _t("Move down") } + + ); + } + return
{ _t('View Community') } + { (moveUp || moveDown) ?
: null } + { moveUp } + { moveDown }
- { _t('Hide') } + { _t("Unpin") }
; } diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 1c2dab4bfc..b8ff803627 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -44,7 +44,12 @@ const BetaFeedbackDialog: React.FC = ({featureId, onFinished}) => { const sendFeedback = async (ok: boolean) => { if (!ok) return onFinished(false); - submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact); + const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => { + o[k] = SettingsStore.getValue(k); + return o; + }, {}); + + submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact, extraData); onFinished(true); Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, { diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index a83f3f177c..6fbed6fc8b 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -14,30 +14,31 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useMemo, useState, useEffect} from "react"; +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 { 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 { RoomMember } from "matrix-js-sdk/src/models/room-member"; -import {_t} from "../../../languageHandler"; +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 { 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 { avatarUrlForUser } from "../../../Avatar"; import EventTile from "../rooms/EventTile"; import SearchBox from "../../structures/SearchBox"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; -import {Alignment} from '../elements/Tooltip'; +import { Alignment } from '../elements/Tooltip'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; +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 { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; +import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import QueryMatcher from "../../../autocomplete/QueryMatcher"; const AVATAR_SIZE = 30; @@ -171,7 +172,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr ); }, getMxcAvatarUrl: () => profileInfo.avatar_url, - }; + } as RoomMember; const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase(); diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 1a664951c5..303f17c342 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -108,7 +108,10 @@ export default class RoomSettingsDialog extends React.Component { ROOM_ADVANCED_TAB, _td("Advanced"), "mx_RoomSettingsDialog_warningIcon", - , + this.props.onFinished(true)} + />, )); } diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx index a135b6bc16..5e0cd96740 100644 --- a/src/components/views/dialogs/SpaceSettingsDialog.tsx +++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx @@ -14,24 +14,27 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useState} from 'react'; -import {Room} from "matrix-js-sdk/src/models/room"; -import {MatrixClient} from "matrix-js-sdk/src/client"; -import {EventType} from "matrix-js-sdk/src/@types/event"; +import React, { useMemo } from 'react'; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixClient } from "matrix-js-sdk/src/client"; -import {_t} from '../../../languageHandler'; -import {IDialogProps} from "./IDialogProps"; +import { _t, _td } from '../../../languageHandler'; +import { IDialogProps } from "./IDialogProps"; import BaseDialog from "./BaseDialog"; -import DevtoolsDialog from "./DevtoolsDialog"; -import SpaceBasicSettings from '../spaces/SpaceBasicSettings'; -import {getTopic} from "../elements/RoomTopic"; -import {avatarUrlForRoom} from "../../../Avatar"; -import ToggleSwitch from "../elements/ToggleSwitch"; -import AccessibleButton from "../elements/AccessibleButton"; -import Modal from "../../../Modal"; import defaultDispatcher from "../../../dispatcher/dispatcher"; -import {useDispatcher} from "../../../hooks/useDispatcher"; -import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; +import { useDispatcher } from "../../../hooks/useDispatcher"; +import TabbedView, { Tab } from "../../structures/TabbedView"; +import SpaceSettingsGeneralTab from '../spaces/SpaceSettingsGeneralTab'; +import SpaceSettingsVisibilityTab from "../spaces/SpaceSettingsVisibilityTab"; +import SettingsStore from "../../../settings/SettingsStore"; +import { UIFeature } from "../../../settings/UIFeature"; +import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab"; + +export enum SpaceSettingsTab { + General = "SPACE_GENERAL_TAB", + Visibility = "SPACE_VISIBILITY_TAB", + Advanced = "SPACE_ADVANCED_TAB", +} interface IProps extends IDialogProps { matrixClient: MatrixClient; @@ -45,63 +48,30 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin } }); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(""); - - const userId = cli.getUserId(); - - const [newAvatar, setNewAvatar] = useState(null); // undefined means to remove avatar - const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId); - const avatarChanged = newAvatar !== null; - - const [name, setName] = useState(space.name); - const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId); - const nameChanged = name !== space.name; - - const currentTopic = getTopic(space); - const [topic, setTopic] = useState(currentTopic); - const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId); - const topicChanged = topic !== currentTopic; - - const currentJoinRule = space.getJoinRule(); - const [joinRule, setJoinRule] = useState(currentJoinRule); - const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId); - const joinRuleChanged = joinRule !== currentJoinRule; - - const onSave = async () => { - setBusy(true); - const promises = []; - - if (avatarChanged) { - if (newAvatar) { - promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, { - url: await cli.uploadContent(newAvatar), - }, "")); - } else { - promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, "")); - } - } - - if (nameChanged) { - promises.push(cli.setRoomName(space.roomId, name)); - } - - if (topicChanged) { - promises.push(cli.setRoomTopic(space.roomId, topic)); - } - - if (joinRuleChanged) { - promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, "")); - } - - const results = await Promise.allSettled(promises); - setBusy(false); - const failures = results.filter(r => r.status === "rejected"); - if (failures.length > 0) { - console.error("Failed to save space settings: ", failures); - setError(_t("Failed to save space settings.")); - } - }; + const tabs = useMemo(() => { + return [ + new Tab( + SpaceSettingsTab.General, + _td("General"), + "mx_SpaceSettingsDialog_generalIcon", + , + ), + new Tab( + SpaceSettingsTab.Visibility, + _td("Visibility"), + "mx_SpaceSettingsDialog_visibilityIcon", + , + ), + SettingsStore.getValue(UIFeature.AdvancedSettings) + ? new Tab( + SpaceSettingsTab.Advanced, + _td("Advanced"), + "mx_RoomSettingsDialog_warningIcon", + , + ) + : null, + ].filter(Boolean); + }, [cli, space, onFinished]); return = ({ matrixClient: cli, space, onFin onFinished={onFinished} fixedWidth={false} > -
-
{ _t("Edit settings relating to your space.") }
- - { error &&
{ error }
} - - onFinished(false)} /> - - - -
- { _t("Make this space private") } - setJoinRule(checked ? "invite" : "public")} - disabled={!canSetJoinRule} - aria-label={_t("Make this space private")} - /> -
- - { - defaultDispatcher.dispatch({ - action: "leave_room", - room_id: space.roomId, - }); - }} - > - { _t("Leave Space") } - - -
- Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}> - { _t("View dev tools") } - - - { _t("Cancel") } - - - { busy ? _t("Saving...") : _t("Save Changes") } - -
+
+
; }; export default SpaceSettingsDialog; - diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index e634057a21..05bcca24b2 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -62,6 +62,8 @@ export default function AccessibleButton({ disabled, inputRef, className, + onKeyDown, + onKeyUp, ...restProps }: IProps) { const newProps: IAccessibleButtonProps = restProps; @@ -83,6 +85,8 @@ export default function AccessibleButton({ if (e.key === Key.SPACE) { e.stopPropagation(); e.preventDefault(); + } else { + onKeyDown?.(e); } }; newProps.onKeyUp = (e) => { @@ -94,6 +98,8 @@ export default function AccessibleButton({ if (e.key === Key.ENTER) { e.stopPropagation(); e.preventDefault(); + } else { + onKeyUp?.(e); } }; } diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js index 67572d4508..2e88d37882 100644 --- a/src/components/views/elements/DNDTagTile.js +++ b/src/components/views/elements/DNDTagTile.js @@ -18,7 +18,6 @@ limitations under the License. import TagTile from './TagTile'; import React from 'react'; -import { Draggable } from 'react-beautiful-dnd'; import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu"; import * as sdk from '../../../index'; @@ -31,32 +30,17 @@ export default function DNDTagTile(props) { const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu'); contextMenu = ( - + ); } - return
- - {(provided, snapshot) => ( -
- -
- )} -
+ return <> + {contextMenu} -
; + ; } diff --git a/src/components/views/elements/DesktopBuildsNotice.tsx b/src/components/views/elements/DesktopBuildsNotice.tsx index fd1c7848aa..426554f31e 100644 --- a/src/components/views/elements/DesktopBuildsNotice.tsx +++ b/src/components/views/elements/DesktopBuildsNotice.tsx @@ -14,10 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; import EventIndexPeg from "../../../indexing/EventIndexPeg"; import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; -import React from "react"; +import dis from "../../../dispatcher/dispatcher"; +import { Action } from "../../../dispatcher/actions"; +import { UserTab } from "../dialogs/UserSettingsDialog"; + export enum WarningKind { Files, @@ -33,6 +37,22 @@ export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) { if (!isRoomEncrypted) return null; if (EventIndexPeg.get()) return null; + if (EventIndexPeg.error) { + return <> + {_t("Message search initialisation failed, check your settings for more information", {}, { + a: sub => ( { + evt.preventDefault(); + dis.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Security, + }); + }}> + {sub} + ), + })} + ; + } + const {desktopBuilds, brand} = SdkConfig.get(); let text = null; diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.tsx similarity index 54% rename from src/components/views/elements/EditableItemList.js rename to src/components/views/elements/EditableItemList.tsx index d8ec5af278..89e2e1b8a0 100644 --- a/src/components/views/elements/EditableItemList.js +++ b/src/components/views/elements/EditableItemList.tsx @@ -1,5 +1,5 @@ /* -Copyright 2017, 2019 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,48 +14,48 @@ 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 React from "react"; + +import { _t } from '../../../languageHandler'; import Field from "./Field"; import AccessibleButton from "./AccessibleButton"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; -export class EditableItem extends React.Component { - static propTypes = { - index: PropTypes.number, - value: PropTypes.string, - onRemove: PropTypes.func, +interface IItemProps { + index?: number; + value?: string; + onRemove?(index: number): void; +} + +interface IItemState { + verifyRemove: boolean; +} + +export class EditableItem extends React.Component { + public state = { + verifyRemove: false, }; - constructor() { - super(); - - this.state = { - verifyRemove: false, - }; - } - - _onRemove = (e) => { + private onRemove = (e) => { e.stopPropagation(); e.preventDefault(); - this.setState({verifyRemove: true}); + this.setState({ verifyRemove: true }); }; - _onDontRemove = (e) => { + private onDontRemove = (e) => { e.stopPropagation(); e.preventDefault(); - this.setState({verifyRemove: false}); + this.setState({ verifyRemove: false }); }; - _onActuallyRemove = (e) => { + private onActuallyRemove = (e) => { e.stopPropagation(); e.preventDefault(); if (this.props.onRemove) this.props.onRemove(this.props.index); - this.setState({verifyRemove: false}); + this.setState({ verifyRemove: false }); }; render() { @@ -66,14 +66,14 @@ export class EditableItem extends React.Component { {_t("Are you sure?")} {_t("Yes")} @@ -85,59 +85,68 @@ export class EditableItem extends React.Component { return (
-
+
{this.props.value}
); } } +interface IProps { + id: string; + items: string[]; + itemsLabel?: string; + noItemsLabel?: string; + placeholder?: string; + newItem?: string; + canEdit?: boolean; + canRemove?: boolean; + suggestionsListId?: string; + onItemAdded?(item: string): void; + onItemRemoved?(index: number): void; + onNewItemChanged?(item: string): void; +} + @replaceableComponent("views.elements.EditableItemList") -export default class EditableItemList extends React.Component { - static propTypes = { - id: PropTypes.string.isRequired, - items: PropTypes.arrayOf(PropTypes.string).isRequired, - itemsLabel: PropTypes.string, - noItemsLabel: PropTypes.string, - placeholder: PropTypes.string, - newItem: PropTypes.string, - - onItemAdded: PropTypes.func, - onItemRemoved: PropTypes.func, - onNewItemChanged: PropTypes.func, - - canEdit: PropTypes.bool, - canRemove: PropTypes.bool, - }; - - _onItemAdded = (e) => { +export default class EditableItemList

extends React.PureComponent { + protected onItemAdded = (e) => { e.stopPropagation(); e.preventDefault(); if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem); }; - _onItemRemoved = (index) => { + protected onItemRemoved = (index) => { if (this.props.onItemRemoved) this.props.onItemRemoved(index); }; - _onNewItemChanged = (e) => { + protected onNewItemChanged = (e) => { if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value); }; - _renderNewItemField() { + protected renderNewItemField() { return (

- - - {_t("Add")} + + + { _t("Add") } ); @@ -153,19 +162,21 @@ export default class EditableItemList extends React.Component { key={item} index={index} value={item} - onRemove={this._onItemRemoved} + onRemove={this.onItemRemoved} />; }); const editableItemsSection = this.props.canRemove ? editableItems :
    {editableItems}
; const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel; - return (
-
- { label } + return ( +
+
+ { label } +
+ { editableItemsSection } + { this.props.canEdit ? this.renderNewItemField() :
}
- { editableItemsSection } - { this.props.canEdit ? this._renderNewItemField() :
} -
); + ); } } diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index cf3b7a6e61..332f3ac333 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -17,13 +17,14 @@ limitations under the License. import React from 'react'; import classnames from 'classnames'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; import * as Avatar from '../../../Avatar'; import EventTile from '../rooms/EventTile'; import SettingsStore from "../../../settings/SettingsStore"; -import {Layout} from "../../../settings/Layout"; -import {UIFeature} from "../../../settings/UIFeature"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { Layout } from "../../../settings/Layout"; +import { UIFeature } from "../../../settings/UIFeature"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { /** @@ -101,7 +102,8 @@ export default class EventTilePreview extends React.Component { // Fake it more event.sender = { - name: this.props.displayName, + name: this.props.displayName || this.props.userId, + rawDisplayName: this.props.displayName, userId: this.props.userId, getAvatarUrl: (..._) => { return Avatar.avatarUrlForUser( @@ -110,7 +112,7 @@ export default class EventTilePreview extends React.Component { ); }, getMxcAvatarUrl: () => this.props.avatarUrl, - }; + } as RoomMember; return event; } diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 59d9a11596..1373c2df0e 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -29,6 +29,11 @@ function getId() { return `${BASE_ID}_${count++}`; } +export interface IValidateOpts { + focused?: boolean; + allowEmpty?: boolean; +} + interface IProps { // The field's ID, which binds the input and label together. Immutable. id?: string; @@ -180,7 +185,7 @@ export default class Field extends React.PureComponent { } }; - public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) { + public async validate({ focused, allowEmpty = true }: IValidateOpts) { if (!this.props.onValidate) { return; } diff --git a/src/components/views/elements/FormButton.js b/src/components/views/elements/FormButton.js deleted file mode 100644 index f6b4c986f5..0000000000 --- a/src/components/views/elements/FormButton.js +++ /dev/null @@ -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. -*/ - -import React from 'react'; -import AccessibleButton from "./AccessibleButton"; - -export default function FormButton(props) { - const {className, label, kind, ...restProps} = props; - const newClassName = (className || "") + " mx_FormButton"; - const allProps = Object.assign({}, restProps, - {className: newClassName, kind: kind || "primary", children: [label]}); - return React.createElement(AccessibleButton, allProps); -} - -FormButton.propTypes = AccessibleButton.propTypes; diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.tsx similarity index 63% rename from src/components/views/elements/LabelledToggleSwitch.js rename to src/components/views/elements/LabelledToggleSwitch.tsx index ef60eeed7b..d97b698fd8 100644 --- a/src/components/views/elements/LabelledToggleSwitch.js +++ b/src/components/views/elements/LabelledToggleSwitch.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +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,38 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from "prop-types"; +import React from "react"; + import ToggleSwitch from "./ToggleSwitch"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + // The value for the toggle switch + value: boolean; + // The translated label for the switch + label: string; + // Whether or not to disable the toggle switch + disabled?: boolean; + // True to put the toggle in front of the label + // Default false. + toggleInFront?: boolean; + // Additional class names to append to the switch. Optional. + className?: string; + // The function to call when the value changes + onChange(checked: boolean): void; +} + @replaceableComponent("views.elements.LabelledToggleSwitch") -export default class LabelledToggleSwitch extends React.Component { - static propTypes = { - // The value for the toggle switch - value: PropTypes.bool.isRequired, - - // The function to call when the value changes - onChange: PropTypes.func.isRequired, - - // The translated label for the switch - label: PropTypes.string.isRequired, - - // Whether or not to disable the toggle switch - disabled: PropTypes.bool, - - // True to put the toggle in front of the label - // Default false. - toggleInFront: PropTypes.bool, - - // Additional class names to append to the switch. Optional. - className: PropTypes.string, - }; - +export default class LabelledToggleSwitch extends React.PureComponent { render() { // This is a minimal version of a SettingsFlag - let firstPart = {this.props.label}; + let firstPart = { this.props.label }; let secondPart = { + private fieldRef = createRef(); - constructor(props) { - super(props); - this.state = {isValid: true}; + constructor(props, context) { + super(props, context); + + this.state = { + isValid: true, + }; } - _asFullAlias(localpart) { + private asFullAlias(localpart: string): string { return `#${localpart}:${this.props.domain}`; } render() { - const Field = sdk.getComponent('views.elements.Field'); const poundSign = (#); const aliasPostfix = ":" + this.props.domain; const domain = ({aliasPostfix}); const maxlength = 255 - this.props.domain.length - 2; // 2 for # and : return ( this._fieldRef = ref} - onValidate={this._onValidate} - placeholder={_t("e.g. my-room")} - onChange={this._onChange} + ref={this.fieldRef} + onValidate={this.onValidate} + placeholder={this.props.placeholder || _t("e.g. my-room")} + onChange={this.onChange} value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)} maxLength={maxlength} /> ); } - _onChange = (ev) => { + private onChange = (ev) => { if (this.props.onChange) { - this.props.onChange(this._asFullAlias(ev.target.value)); + this.props.onChange(this.asFullAlias(ev.target.value)); } }; - _onValidate = async (fieldState) => { - const result = await this._validationRules(fieldState); + private onValidate = async (fieldState) => { + const result = await this.validationRules(fieldState); this.setState({isValid: result.valid}); return result; }; - _validationRules = withValidation({ + private validationRules = withValidation({ rules: [ { key: "safeLocalpart", @@ -81,7 +92,7 @@ export default class RoomAliasField extends React.PureComponent { if (!value) { return true; } - const fullAlias = this._asFullAlias(value); + const fullAlias = this.asFullAlias(value); // XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668 return !value.includes("#") && !value.includes(":") && !value.includes(",") && encodeURI(fullAlias) === fullAlias; @@ -90,7 +101,7 @@ export default class RoomAliasField extends React.PureComponent { }, { key: "required", test: async ({ value, allowEmpty }) => allowEmpty || !!value, - invalid: () => _t("Please provide a room address"), + invalid: () => _t("Please provide an address"), }, { key: "taken", final: true, @@ -100,7 +111,7 @@ export default class RoomAliasField extends React.PureComponent { } const client = MatrixClientPeg.get(); try { - await client.getRoomIdForAlias(this._asFullAlias(value)); + await client.getRoomIdForAlias(this.asFullAlias(value)); // we got a room id, so the alias is taken return false; } catch (err) { @@ -116,15 +127,15 @@ export default class RoomAliasField extends React.PureComponent { ], }); - get isValid() { + public get isValid() { return this.state.isValid; } - validate(options) { - return this._fieldRef.validate(options); + public validate(options: IValidateOpts) { + return this.fieldRef.current?.validate(options); } - focus() { - this._fieldRef.focus(); + public focus() { + this.fieldRef.current?.focus(); } } diff --git a/src/components/views/elements/RoomName.tsx b/src/components/views/elements/RoomName.tsx index 9178155d19..cdd83aedc2 100644 --- a/src/components/views/elements/RoomName.tsx +++ b/src/components/views/elements/RoomName.tsx @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {useEffect, useState} from "react"; -import {Room} from "matrix-js-sdk/src/models/room"; +import React, { useEffect, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; interface IProps { room: Room; @@ -34,7 +34,7 @@ const RoomName = ({ room, children }: IProps): JSX.Element => { }, [room]); if (children) return children(name); - return name || ""; + return <>{ name || "" }; }; export default RoomName; diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index 4f885ab47d..24a21e1a33 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -77,9 +77,10 @@ export default class SettingsFlag extends React.Component { public render() { const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level); - let label = this.props.label; - if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level); - else label = _t(label); + const label = this.props.label + ? _t(this.props.label) + : SettingsStore.getDisplayName(this.props.name, this.props.level); + const description = SettingsStore.getDescription(this.props.name); if (this.props.useCheckbox) { return { disabled={this.props.disabled || !canChange} aria-label={label} /> + { description &&
+ { description } +
}
); } diff --git a/src/components/views/elements/StyledRadioGroup.tsx b/src/components/views/elements/StyledRadioGroup.tsx index 6b9e992f92..744b6f2059 100644 --- a/src/components/views/elements/StyledRadioGroup.tsx +++ b/src/components/views/elements/StyledRadioGroup.tsx @@ -34,10 +34,19 @@ interface IProps { definitions: IDefinition[]; value?: T; // if not provided no options will be selected outlined?: boolean; + disabled?: boolean; onChange(newValue: T): void; } -function StyledRadioGroup({name, definitions, value, className, outlined, onChange}: IProps) { +function StyledRadioGroup({ + name, + definitions, + value, + className, + outlined, + disabled, + onChange, +}: IProps) { const _onChange = e => { onChange(e.target.value); }; @@ -50,12 +59,12 @@ function StyledRadioGroup({name, definitions, value, className checked={d.checked !== undefined ? d.checked : d.value === value} name={name} value={d.value} - disabled={d.disabled} + disabled={disabled || d.disabled} outlined={outlined} > - {d.label} + { d.label } - {d.description} + { d.description ? { d.description } : null } )} ; } diff --git a/src/components/views/groups/GroupPublicityToggle.js b/src/components/views/groups/GroupPublicityToggle.js index c06d827550..6bef141cb8 100644 --- a/src/components/views/groups/GroupPublicityToggle.js +++ b/src/components/views/groups/GroupPublicityToggle.js @@ -66,9 +66,7 @@ export default class GroupPublicityToggle extends React.Component { render() { const GroupTile = sdk.getComponent('groups.GroupTile'); return
- + diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js index 42a977fb79..dd8366bbe0 100644 --- a/src/components/views/groups/GroupTile.js +++ b/src/components/views/groups/GroupTile.js @@ -16,15 +16,15 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import { Draggable, Droppable } from 'react-beautiful-dnd'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import FlairStore from '../../../stores/FlairStore'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {mediaFromMxc} from "../../../customisations/Media"; - -function nop() {} +import { _t } from "../../../languageHandler"; +import TagOrderActions from "../../../actions/TagOrderActions"; +import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore"; @replaceableComponent("views.groups.GroupTile") class GroupTile extends React.Component { @@ -34,7 +34,6 @@ class GroupTile extends React.Component { showDescription: PropTypes.bool, // Height of the group avatar in pixels avatarHeight: PropTypes.number, - draggable: PropTypes.bool, }; static contextType = MatrixClientContext; @@ -42,7 +41,6 @@ class GroupTile extends React.Component { static defaultProps = { showDescription: true, avatarHeight: 50, - draggable: true, }; state = { @@ -57,7 +55,7 @@ class GroupTile extends React.Component { }); } - onMouseDown = e => { + onClick = e => { e.preventDefault(); dis.dispatch({ action: 'view_group', @@ -65,6 +63,18 @@ class GroupTile extends React.Component { }); }; + onPinClick = e => { + e.preventDefault(); + e.stopPropagation(); + dis.dispatch(TagOrderActions.moveTag(this.context, this.props.groupId, 0)); + }; + + onUnpinClick = e => { + e.preventDefault(); + e.stopPropagation(); + dis.dispatch(TagOrderActions.removeTag(this.context, this.props.groupId)); + }; + render() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -78,7 +88,7 @@ class GroupTile extends React.Component { ? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight) : null; - let avatarElement = ( + const avatarElement = (
); - if (this.props.draggable) { - const avatarClone = avatarElement; - avatarElement = ( - - { (droppableProvided, droppableSnapshot) => ( -
- - { (provided, snapshot) => ( -
-
- {avatarClone} -
- { /* Instead of a blank placeholder, use a copy of the avatar itself. */ } - { provided.placeholder ? avatarClone :
} -
- ) } - -
- ) } - - ); - } - // XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273 - // instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6156 - return + return { avatarElement }
{ name }
{ descElement }
{ this.props.groupId }
+ { !(GroupFilterOrderStore.getOrderedTags() || []).includes(this.props.groupId) + ? + { _t("Pin") } + + : + { _t("Unpin") } + + }
; } diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.tsx similarity index 76% rename from src/components/views/messages/MKeyVerificationRequest.js rename to src/components/views/messages/MKeyVerificationRequest.tsx index 988606a766..69467cfa50 100644 --- a/src/components/views/messages/MKeyVerificationRequest.js +++ b/src/components/views/messages/MKeyVerificationRequest.tsx @@ -15,41 +15,40 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixEvent } from 'matrix-js-sdk/src'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import {getNameForEventRoom, userLabelForEventRoom} +import { getNameForEventRoom, userLabelForEventRoom } from '../../../utils/KeyVerificationStateObserver'; import dis from "../../../dispatcher/dispatcher"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {Action} from "../../../dispatcher/actions"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { Action } from "../../../dispatcher/actions"; import EventTileBubble from "./EventTileBubble"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +interface IProps { + mxEvent: MatrixEvent +} @replaceableComponent("views.messages.MKeyVerificationRequest") -export default class MKeyVerificationRequest extends React.Component { - constructor(props) { - super(props); - this.state = {}; - } - - componentDidMount() { +export default class MKeyVerificationRequest extends React.Component { + public componentDidMount() { const request = this.props.mxEvent.verificationRequest; if (request) { - request.on("change", this._onRequestChanged); + request.on("change", this.onRequestChanged); } } - componentWillUnmount() { + public componentWillUnmount() { const request = this.props.mxEvent.verificationRequest; if (request) { - request.off("change", this._onRequestChanged); + request.off("change", this.onRequestChanged); } } - _openRequest = () => { - const {verificationRequest} = this.props.mxEvent; + private openRequest = () => { + const { verificationRequest } = this.props.mxEvent; const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId); dis.dispatch({ action: Action.SetRightPanelPhase, @@ -58,15 +57,15 @@ export default class MKeyVerificationRequest extends React.Component { }); }; - _onRequestChanged = () => { + private onRequestChanged = () => { this.forceUpdate(); }; - _onAcceptClicked = async () => { + private onAcceptClicked = async () => { const request = this.props.mxEvent.verificationRequest; if (request) { try { - this._openRequest(); + this.openRequest(); await request.accept(); } catch (err) { console.error(err.message); @@ -74,7 +73,7 @@ export default class MKeyVerificationRequest extends React.Component { } }; - _onRejectClicked = async () => { + private onRejectClicked = async () => { const request = this.props.mxEvent.verificationRequest; if (request) { try { @@ -85,7 +84,7 @@ export default class MKeyVerificationRequest extends React.Component { } }; - _acceptedLabel(userId) { + private acceptedLabel(userId: string) { const client = MatrixClientPeg.get(); const myUserId = client.getUserId(); if (userId === myUserId) { @@ -95,7 +94,7 @@ export default class MKeyVerificationRequest extends React.Component { } } - _cancelledLabel(userId) { + private cancelledLabel(userId: string) { const client = MatrixClientPeg.get(); const myUserId = client.getUserId(); const {cancellationCode} = this.props.mxEvent.verificationRequest; @@ -115,9 +114,8 @@ export default class MKeyVerificationRequest extends React.Component { } } - render() { + public render() { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - const FormButton = sdk.getComponent("elements.FormButton"); const {mxEvent} = this.props; const request = mxEvent.verificationRequest; @@ -134,11 +132,11 @@ export default class MKeyVerificationRequest extends React.Component { let stateLabel; const accepted = request.ready || request.started || request.done; if (accepted) { - stateLabel = ( - {this._acceptedLabel(request.receivingUserId)} + stateLabel = ( + {this.acceptedLabel(request.receivingUserId)} ); } else if (request.cancelled) { - stateLabel = this._cancelledLabel(request.cancellingUserId); + stateLabel = this.cancelledLabel(request.cancellingUserId); } else if (request.accepting) { stateLabel = _t("Accepting …"); } else if (request.declining) { @@ -153,8 +151,12 @@ export default class MKeyVerificationRequest extends React.Component { subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId()); if (request.canAccept) { stateNode = (
- - + + {_t("Decline")} + + + {_t("Accept")} +
); } } else { // request sent by us @@ -174,8 +176,3 @@ export default class MKeyVerificationRequest extends React.Component { return null; } } - -MKeyVerificationRequest.propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, -}; diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index c237a4ade6..3a26427246 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -14,28 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useEffect, useState} from "react"; +import React, { useCallback, useEffect, useState } from "react"; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { User } from "matrix-js-sdk/src/models/user"; +import { PHASE_REQUESTED, PHASE_UNSENT } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import EncryptionInfo from "./EncryptionInfo"; import VerificationPanel from "./VerificationPanel"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import {ensureDMExists} from "../../../createRoom"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { ensureDMExists } from "../../../createRoom"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; import Modal from "../../../Modal"; -import {PHASE_REQUESTED, PHASE_UNSENT} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import * as sdk from "../../../index"; -import {_t} from "../../../languageHandler"; -import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; -import {Action} from "../../../dispatcher/actions"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; +import { Action } from "../../../dispatcher/actions"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; // cancellation codes which constitute a key mismatch const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"]; interface IProps { - member: RoomMember; + member: RoomMember | User; onClose: () => void; verificationRequest: VerificationRequest; verificationRequestPromise: Promise; diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index a72731522f..1131c02dbf 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -16,7 +16,6 @@ limitations under the License. import React, {useCallback, useContext, useEffect, useState} from "react"; import { Room } from "matrix-js-sdk/src/models/room"; -import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventType } from 'matrix-js-sdk/src/@types/event'; @@ -28,6 +27,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter"; import PinningUtils from "../../../utils/PinningUtils"; import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; import PinnedEventTile from "../rooms/PinnedEventTile"; +import { useRoomState } from "../../../hooks/useRoomState"; interface IProps { room: Room; @@ -75,24 +75,6 @@ export const useReadPinnedEvents = (room: Room): Set => { return readPinnedEvents; }; -const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => { - const [value, setValue] = useState(room ? mapper(room.currentState) : undefined); - - const update = useCallback(() => { - if (!room) return; - setValue(mapper(room.currentState)); - }, [room, mapper]); - - useEventEmitter(room?.currentState, "RoomState.events", update); - useEffect(() => { - update(); - return () => { - setValue(undefined); - }; - }, [update]); - return value; -}; - const PinnedMessagesCard = ({ room, onClose }: IProps) => { const cli = useContext(MatrixClientContext); const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli)); diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 03954bad56..e22b2d5ff4 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -503,19 +503,15 @@ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) => return member.powerLevel < levelToSend; }; +const getPowerLevels = room => room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {}; + export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => { - const [powerLevels, setPowerLevels] = useState({}); + const [powerLevels, setPowerLevels] = useState(getPowerLevels(room)); const update = useCallback((ev?: MatrixEvent) => { if (!room) return; if (ev && ev.getType() !== EventType.RoomPowerLevels) return; - - const event = room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); - if (event) { - setPowerLevels(event.getContent()); - } else { - setPowerLevels({}); - } + setPowerLevels(getPowerLevels(room)); }, [room]); useEventEmitter(cli, "RoomState.events", update); @@ -1594,7 +1590,7 @@ const UserInfo: React.FC = ({ content = ( @@ -1605,7 +1601,7 @@ const UserInfo: React.FC = ({ content = ( } - member={member} + member={member as User | RoomMember} onClose={onEncryptionPanelClose} isRoomEncrypted={isRoomEncrypted} /> diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx index edfe0e3483..d3f2ba8cbf 100644 --- a/src/components/views/right_panel/VerificationPanel.tsx +++ b/src/components/views/right_panel/VerificationPanel.tsx @@ -195,14 +195,7 @@ export default class VerificationPanel extends React.PureComponent

{description}

- - + onClick={this.onReciprocateYesClick} + > + { _t("Yes") } +
; } else { diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.tsx similarity index 71% rename from src/components/views/room_settings/AliasSettings.js rename to src/components/views/room_settings/AliasSettings.tsx index 80e0099ab3..59c4bf2c0c 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2018, 2019 New Vector Ltd +Copyright 2016 - 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,59 +14,60 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React, { ChangeEvent, createRef } from "react"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + import EditableItemList from "../elements/EditableItemList"; -import React, {createRef} from 'react'; -import PropTypes from 'prop-types'; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import * as sdk from "../../../index"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from '../../../languageHandler'; import Field from "../elements/Field"; +import Spinner from "../elements/Spinner"; import ErrorDialog from "../dialogs/ErrorDialog"; import AccessibleButton from "../elements/AccessibleButton"; import Modal from "../../../Modal"; import RoomPublishSetting from "./RoomPublishSetting"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import RoomAliasField from "../elements/RoomAliasField"; -class EditableAliasesList extends EditableItemList { - constructor(props) { - super(props); +interface IEditableAliasesListProps { + domain?: string; +} - this._aliasField = createRef(); - } +class EditableAliasesList extends EditableItemList { + private aliasField = createRef(); - _onAliasAdded = async () => { - await this._aliasField.current.validate({ allowEmpty: false }); + private onAliasAdded = async () => { + await this.aliasField.current.validate({ allowEmpty: false }); - if (this._aliasField.current.isValid) { + if (this.aliasField.current.isValid) { if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem); return; } - this._aliasField.current.focus(); - this._aliasField.current.validate({ allowEmpty: false, focused: true }); + this.aliasField.current.focus(); + this.aliasField.current.validate({ allowEmpty: false, focused: true }); }; - _renderNewItemField() { + protected renderNewItemField() { // if we don't need the RoomAliasField, - // we don't need to overriden version of _renderNewItemField + // we don't need to overriden version of renderNewItemField if (!this.props.domain) { - return super._renderNewItemField(); + return super.renderNewItemField(); } - const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField'); - const onChange = (alias) => this._onNewItemChanged({target: {value: alias}}); + const onChange = (alias) => this.onNewItemChanged({target: {value: alias}}); return (
- + { _t("Add") } @@ -75,19 +75,30 @@ class EditableAliasesList extends EditableItemList { } } -@replaceableComponent("views.room_settings.AliasSettings") -export default class AliasSettings extends React.Component { - static propTypes = { - roomId: PropTypes.string.isRequired, - canSetCanonicalAlias: PropTypes.bool.isRequired, - canSetAliases: PropTypes.bool.isRequired, - canonicalAliasEvent: PropTypes.object, // MatrixEvent - }; +interface IProps { + roomId: string; + canSetCanonicalAlias: boolean; + canSetAliases: boolean; + canonicalAliasEvent?: MatrixEvent; + hidePublishSetting?: boolean; +} +interface IState { + altAliases: string[]; + localAliases: string[]; + canonicalAlias?: string; + updatingCanonicalAlias: boolean; + localAliasesLoading: boolean; + detailsOpen: boolean; + newAlias?: string; + newAltAlias?: string; +} + +@replaceableComponent("views.room_settings.AliasSettings") +export default class AliasSettings extends React.Component { static defaultProps = { canSetAliases: false, canSetCanonicalAlias: false, - aliasEvents: [], }; constructor(props) { @@ -122,7 +133,7 @@ export default class AliasSettings extends React.Component { } } - async loadLocalAliases() { + private async loadLocalAliases() { this.setState({ localAliasesLoading: true }); try { const cli = MatrixClientPeg.get(); @@ -134,12 +145,16 @@ export default class AliasSettings extends React.Component { } } this.setState({ localAliases }); + + if (localAliases.length === 0) { + this.setState({ detailsOpen: true }); + } } finally { this.setState({ localAliasesLoading: false }); } } - changeCanonicalAlias(alias) { + private changeCanonicalAlias(alias: string) { if (!this.props.canSetCanonicalAlias) return; const oldAlias = this.state.canonicalAlias; @@ -170,7 +185,7 @@ export default class AliasSettings extends React.Component { }); } - changeAltAliases(altAliases) { + private changeAltAliases(altAliases: string[]) { if (!this.props.canSetCanonicalAlias) return; this.setState({ @@ -181,7 +196,7 @@ export default class AliasSettings extends React.Component { const eventContent = {}; if (this.state.canonicalAlias) { - eventContent.alias = this.state.canonicalAlias; + eventContent["alias"] = this.state.canonicalAlias; } if (altAliases) { eventContent["alt_aliases"] = altAliases; @@ -202,11 +217,11 @@ export default class AliasSettings extends React.Component { }); } - onNewAliasChanged = (value) => { - this.setState({newAlias: value}); + private onNewAliasChanged = (value: string) => { + this.setState({ newAlias: value }); }; - onLocalAliasAdded = (alias) => { + private onLocalAliasAdded = (alias: string) => { if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases const localDomain = MatrixClientPeg.get().getDomain(); @@ -232,7 +247,7 @@ export default class AliasSettings extends React.Component { }); }; - onLocalAliasDeleted = (index) => { + private onLocalAliasDeleted = (index: number) => { const alias = this.state.localAliases[index]; // TODO: In future, we should probably be making sure that the alias actually belongs // to this room. See https://github.com/vector-im/element-web/issues/7353 @@ -261,7 +276,7 @@ export default class AliasSettings extends React.Component { }); }; - onLocalAliasesToggled = (event) => { + private onLocalAliasesToggled = (event: ChangeEvent) => { // expanded if (event.target.open) { // if local aliases haven't been preloaded yet at component mount @@ -269,43 +284,45 @@ export default class AliasSettings extends React.Component { this.loadLocalAliases(); } } - this.setState({detailsOpen: event.target.open}); + this.setState({ detailsOpen: event.currentTarget.open }); }; - onCanonicalAliasChange = (event) => { + private onCanonicalAliasChange = (event: ChangeEvent) => { this.changeCanonicalAlias(event.target.value); }; - onNewAltAliasChanged = (value) => { - this.setState({newAltAlias: value}); + private onNewAltAliasChanged = (value: string) => { + this.setState({ newAltAlias: value }); } - onAltAliasAdded = (alias) => { + private onAltAliasAdded = (alias: string) => { const altAliases = this.state.altAliases.slice(); if (!altAliases.some(a => a.trim() === alias.trim())) { altAliases.push(alias.trim()); this.changeAltAliases(altAliases); - this.setState({newAltAlias: ""}); + this.setState({ newAltAlias: "" }); } } - onAltAliasDeleted = (index) => { + private onAltAliasDeleted = (index: number) => { const altAliases = this.state.altAliases.slice(); altAliases.splice(index, 1); this.changeAltAliases(altAliases); } - _getAliases() { - return this.state.altAliases.concat(this._getLocalNonAltAliases()); + private getAliases() { + return this.state.altAliases.concat(this.getLocalNonAltAliases()); } - _getLocalNonAltAliases() { + private getLocalNonAltAliases() { const {altAliases} = this.state; return this.state.localAliases.filter(alias => !altAliases.includes(alias)); } render() { - const localDomain = MatrixClientPeg.get().getDomain(); + const cli = MatrixClientPeg.get(); + const localDomain = cli.getDomain(); + const isSpaceRoom = cli.getRoom(this.props.roomId)?.isSpaceRoom(); let found = false; const canonicalValue = this.state.canonicalAlias || ""; @@ -320,7 +337,7 @@ export default class AliasSettings extends React.Component { > { - this._getAliases().map((alias, i) => { + this.getAliases().map((alias, i) => { if (alias === this.state.canonicalAlias) found = true; return (
; }; diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx index a0660318bc..c09043da24 100644 --- a/src/components/views/voip/IncomingCallBox.tsx +++ b/src/components/views/voip/IncomingCallBox.tsx @@ -23,7 +23,7 @@ import { _t } from '../../../languageHandler'; import { ActionPayload } from '../../../dispatcher/payloads'; import CallHandler, { AudioID } from '../../../CallHandler'; import RoomAvatar from '../avatars/RoomAvatar'; -import FormButton from '../elements/FormButton'; +import AccessibleButton from '../elements/AccessibleButton'; import { CallState } from 'matrix-js-sdk/src/webrtc/call'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; @@ -143,21 +143,22 @@ export default class IncomingCallBox extends React.Component { />
- + > + { _t("Decline") } +
- + > + { _t("Accept") } +
; } } - diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts index f9d957b60c..37e91fc54b 100644 --- a/src/customisations/Media.ts +++ b/src/customisations/Media.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import {MatrixClientPeg} from "../MatrixClientPeg"; -import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent"; -import {ResizeMethod} from "../Avatar"; -import {MatrixClient} from "matrix-js-sdk/src/client"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; + +import { MatrixClientPeg } from "../MatrixClientPeg"; +import { IMediaEventContent, IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent"; // Populate this class with the details of your customisations when copying it. diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 30cb753975..6438ecc0f2 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -164,4 +164,9 @@ export enum Action { * Inserts content into the active composer. Should be used with ComposerInsertPayload */ ComposerInsert = "composer_insert", + + /** + * Switches space. Should be used with SwitchSpacePayload. + */ + SwitchSpace = "switch_space", } diff --git a/src/dispatcher/payloads/SwitchSpacePayload.ts b/src/dispatcher/payloads/SwitchSpacePayload.ts new file mode 100644 index 0000000000..04eb744334 --- /dev/null +++ b/src/dispatcher/payloads/SwitchSpacePayload.ts @@ -0,0 +1,27 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +export interface SwitchSpacePayload extends ActionPayload { + action: Action.SwitchSpace; + + /** + * The number of the space to switch to, 1-indexed, 0 is Home. + */ + num: number; +} diff --git a/src/dispatcher/payloads/ViewUserPayload.ts b/src/dispatcher/payloads/ViewUserPayload.ts index c2838d0dbb..c4d73aea6a 100644 --- a/src/dispatcher/payloads/ViewUserPayload.ts +++ b/src/dispatcher/payloads/ViewUserPayload.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { User } from "matrix-js-sdk/src/models/user"; import { ActionPayload } from "../payloads"; import { Action } from "../actions"; @@ -25,5 +26,5 @@ export interface ViewUserPayload extends ActionPayload { * The member to view. May be null or falsy to indicate that no member * should be shown (hide whichever relevant components). */ - member?: RoomMember; + member?: RoomMember | User; } diff --git a/src/hooks/useRoomState.ts b/src/hooks/useRoomState.ts new file mode 100644 index 0000000000..e778acf8a9 --- /dev/null +++ b/src/hooks/useRoomState.ts @@ -0,0 +1,46 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useCallback, useEffect, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomState } from "matrix-js-sdk/src/models/room-state"; + +import { useEventEmitter } from "./useEventEmitter"; + +type Mapper = (roomState: RoomState) => T; +const defaultMapper: Mapper = (roomState: RoomState) => roomState; + +// Hook to simplify watching Matrix Room state +export const useRoomState = ( + room: Room, + mapper: Mapper = defaultMapper as Mapper, +): T => { + const [value, setValue] = useState(room ? mapper(room.currentState) : undefined); + + const update = useCallback(() => { + if (!room) return; + setValue(mapper(room.currentState)); + }, [room, mapper]); + + useEventEmitter(room?.currentState, "RoomState.events", update); + useEffect(() => { + update(); + return () => { + setValue(undefined); + }; + }, [update]); + return value; +}; diff --git a/src/hooks/useStateToggle.ts b/src/hooks/useStateToggle.ts index b50a923234..33701c4f16 100644 --- a/src/hooks/useStateToggle.ts +++ b/src/hooks/useStateToggle.ts @@ -18,7 +18,7 @@ import {Dispatch, SetStateAction, useState} from "react"; // Hook to simplify toggling of a boolean state value // Returns value, method to toggle boolean value and method to set the boolean value -export const useStateToggle = (initialValue: boolean): [boolean, () => void, Dispatch>] => { +export const useStateToggle = (initialValue = false): [boolean, () => void, Dispatch>] => { const [value, setValue] = useState(initialValue); const toggleValue = () => { setValue(!value); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7a25ebfb7a..f0f8cd343b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -804,6 +804,10 @@ "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.", "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.", "Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.", + "Show all rooms in Home": "Show all rooms in Home", + "Show people in spaces": "Show people in spaces", + "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.", + "Show notification badges for People in Spaces": "Show notification badges for People in Spaces", "Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode", "Send and receive voice messages": "Send and receive voice messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages", @@ -1029,17 +1033,42 @@ "Your private space": "Your private space", "Add some details to help people recognise it.": "Add some details to help people recognise it.", "You can change these anytime.": "You can change these anytime.", + "e.g. my-space": "e.g. my-space", + "Address": "Address", "Creating...": "Creating...", "Create": "Create", + "All rooms": "All rooms", + "Home": "Home", "Expand space panel": "Expand space panel", "Collapse space panel": "Collapse space panel", - "All rooms": "All rooms", "Click to copy": "Click to copy", "Copied!": "Copied!", "Failed to copy": "Failed to copy", "Share invite link": "Share invite link", "Invite people": "Invite people", "Invite with email or username": "Invite with email or username", + "Failed to save space settings.": "Failed to save space settings.", + "General": "General", + "Edit settings relating to your space.": "Edit settings relating to your space.", + "Saving...": "Saving...", + "Save Changes": "Save Changes", + "Leave Space": "Leave Space", + "Failed to update the visibility of this space": "Failed to update the visibility of this space", + "Failed to update the guest access of this space": "Failed to update the guest access of this space", + "Failed to update the history visibility of this space": "Failed to update the history visibility of this space", + "Hide advanced": "Hide advanced", + "Enable guest access": "Enable guest access", + "Guests can join a space without having an account.": "Guests can join a space without having an account.", + "This may be useful for public spaces.": "This may be useful for public spaces.", + "Show advanced": "Show advanced", + "Visibility": "Visibility", + "Decide who can view and join %(spaceName)s.": "Decide who can view and join %(spaceName)s.", + "anyone with the link can view and join": "anyone with the link can view and join", + "Invite only": "Invite only", + "only invited people can view and join": "only invited people can view and join", + "Preview Space": "Preview Space", + "Allow people to preview your space before they join.": "Allow people to preview your space before they join.", + "Recommended for public spaces.": "Recommended for public spaces.", "Settings": "Settings", "Leave space": "Leave space", "Create new room": "Create new room", @@ -1048,6 +1077,8 @@ "Manage & explore rooms": "Manage & explore rooms", "Explore rooms": "Explore rooms", "Space options": "Space options", + "Expand": "Expand", + "Collapse": "Collapse", "Remove": "Remove", "This bridge was provisioned by .": "This bridge was provisioned by .", "This bridge is managed by .": "This bridge is managed by .", @@ -1234,8 +1265,6 @@ "Custom theme URL": "Custom theme URL", "Add theme": "Add theme", "Theme": "Theme", - "Hide advanced": "Hide advanced", - "Show advanced": "Show advanced", "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.", "Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout", "Customise your appearance": "Customise your appearance", @@ -1256,7 +1285,6 @@ "Deactivate Account": "Deactivate Account", "Deactivate account": "Deactivate account", "Discovery": "Discovery", - "General": "General", "Legal": "Legal", "Credits": "Credits", "For help with using %(brand)s, click here.": "For help with using %(brand)s, click here.", @@ -1362,6 +1390,7 @@ "Upgrade this room to the recommended room version": "Upgrade this room to the recommended room version", "this room": "this room", "View older messages in %(roomName)s.": "View older messages in %(roomName)s.", + "Space information": "Space information", "Room information": "Room information", "Internal room ID:": "Internal room ID:", "Room version": "Room version", @@ -1688,14 +1717,18 @@ "Error removing address": "Error removing address", "Main address": "Main address", "not specified": "not specified", + "This space has no local addresses": "This space has no local addresses", "This room has no local addresses": "This room has no local addresses", "Local address": "Local address", "Published Addresses": "Published Addresses", - "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.", + "Published addresses can be used by anyone on any server to join your space.": "Published addresses can be used by anyone on any server to join your space.", + "Published addresses can be used by anyone on any server to join your room.": "Published addresses can be used by anyone on any server to join your room.", + "To publish an address, it needs to be set as a local address first.": "To publish an address, it needs to be set as a local address first.", "Other published addresses:": "Other published addresses:", "No other published addresses yet, add one below": "No other published addresses yet, add one below", "New published address (e.g. #alias:server)": "New published address (e.g. #alias:server)", "Local Addresses": "Local Addresses", + "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)", "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", "Show more": "Show more", "Error updating flair": "Error updating flair", @@ -1942,6 +1975,7 @@ "Error loading Widget": "Error loading Widget", "Error - Mixed content": "Error - Mixed content", "Popout widget": "Popout widget", + "Message search initialisation failed, check your settings for more information": "Message search initialisation failed, check your settings for more information", "Use the Desktop app to see all encrypted files": "Use the Desktop app to see all encrypted files", "Use the Desktop app to search encrypted messages": "Use the Desktop app to search encrypted messages", "This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files", @@ -2029,7 +2063,7 @@ "Room address": "Room address", "e.g. my-room": "e.g. my-room", "Some characters not allowed": "Some characters not allowed", - "Please provide a room address": "Please provide a room address", + "Please provide an address": "Please provide an address", "This address is available to use": "This address is available to use", "This address is already in use": "This address is already in use", "Server Options": "Server Options", @@ -2039,7 +2073,6 @@ "Continue with %(provider)s": "Continue with %(provider)s", "Sign in with single sign-on": "Sign in with single sign-on", "And %(count)s more...|other": "And %(count)s more...", - "Home": "Home", "Enter a server name": "Enter a server name", "Looks good": "Looks good", "You are not allowed to view this server's rooms list": "You are not allowed to view this server's rooms list", @@ -2407,14 +2440,8 @@ "Share Room Message": "Share Room Message", "Link to selected message": "Link to selected message", "Command Help": "Command Help", - "Failed to save space settings.": "Failed to save space settings.", "Space settings": "Space settings", - "Edit settings relating to your space.": "Edit settings relating to your space.", - "Make this space private": "Make this space private", - "Leave Space": "Leave Space", - "View dev tools": "View dev tools", - "Saving...": "Saving...", - "Save Changes": "Save Changes", + "Settings - %(spaceName)s": "Settings - %(spaceName)s", "To help us prevent this in future, please send us logs.": "To help us prevent this in future, please send us logs.", "Missing session data": "Missing session data", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.", @@ -2515,6 +2542,8 @@ "Update status": "Update status", "Set status": "Set status", "Set a new status...": "Set a new status...", + "Move up": "Move up", + "Move down": "Move down", "View Community": "View Community", "Unable to start audio streaming.": "Unable to start audio streaming.", "Failed to start livestream": "Failed to start livestream", @@ -2661,7 +2690,7 @@ "%(count)s messages deleted.|one": "%(count)s message deleted.", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!", - "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", + "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", "Error whilst fetching joined communities": "Error whilst fetching joined communities", "Create a new community": "Create a new community", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", @@ -3016,5 +3045,6 @@ "Esc": "Esc", "Enter": "Enter", "Space": "Space", - "End": "End" + "End": "End", + "[number]": "[number]" } diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index 16950dc008..cc129fb6b9 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -112,7 +112,7 @@ export interface IVariables { [key: string]: SubstitutionValue; } -type Tags = Record; +export type Tags = Record; export type TranslatedString = string | React.ReactNode; diff --git a/src/mjolnir/BanList.ts b/src/mjolnir/BanList.ts index 21cd5d4cf7..89eec89500 100644 --- a/src/mjolnir/BanList.ts +++ b/src/mjolnir/BanList.ts @@ -92,7 +92,7 @@ export class BanList { if (!room) return; for (const eventType of ALL_RULE_TYPES) { - const events = room.currentState.getStateEvents(eventType, undefined); + const events = room.currentState.getStateEvents(eventType); for (const ev of events) { if (!ev.getStateKey()) continue; diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index 08d8ccfd13..859fdf046a 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -263,7 +263,13 @@ function uint8ToString(buf: Buffer) { return out; } -export async function submitFeedback(endpoint: string, label: string, comment: string, canContact = false) { +export async function submitFeedback( + endpoint: string, + label: string, + comment: string, + canContact = false, + extraData: Record = {}, +) { let version = "UNKNOWN"; try { version = await PlatformPeg.get().getAppVersion(); @@ -279,6 +285,10 @@ export async function submitFeedback(endpoint: string, label: string, comment: s body.append("platform", PlatformPeg.get().getHumanReadableName()); body.append("user_id", MatrixClientPeg.get()?.getUserId()); + for (const k in extraData) { + body.append(k, extraData[k]); + } + await _submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {}); } diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 97f1beb979..6ed5e0c3d8 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -1,6 +1,6 @@ /* Copyright 2017 Travis Ralston -Copyright 2018, 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2018 - 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. @@ -94,6 +94,9 @@ export interface ISetting { [level: SettingLevel]: string; }; + // Optional description which will be shown as microCopy under SettingsFlags + description?: string; + // The supported levels are required. Preferably, use the preset arrays // at the top of this file to define this rather than a custom array. supportedLevels?: SettingLevel[]; @@ -127,6 +130,7 @@ export interface ISetting { image: string; // require(...) feedbackSubheading?: string; feedbackLabel?: string; + extraSettings?: string[]; }; } @@ -174,8 +178,33 @@ export const SETTINGS: {[setting: string]: ISetting} = { feedbackSubheading: _td("Your feedback will help make spaces better. " + "The more detail you can go into, the better."), feedbackLabel: "spaces-feedback", + extraSettings: [ + "feature_spaces.all_rooms", + "feature_spaces.space_member_dms", + "feature_spaces.space_dm_badges", + ], }, }, + "feature_spaces.all_rooms": { + displayName: _td("Show all rooms in Home"), + supportedLevels: LEVELS_FEATURE, + default: true, + controller: new ReloadOnChangeController(), + }, + "feature_spaces.space_member_dms": { + displayName: _td("Show people in spaces"), + description: _td("If disabled, you can still add Direct Messages to Personal Spaces. " + + "If enabled, you'll automatically see everyone who is a member of the Space."), + supportedLevels: LEVELS_FEATURE, + default: true, + controller: new ReloadOnChangeController(), + }, + "feature_spaces.space_dm_badges": { + displayName: _td("Show notification badges for People in Spaces"), + supportedLevels: LEVELS_FEATURE, + default: false, + controller: new ReloadOnChangeController(), + }, "feature_dnd": { isFeature: true, displayName: _td("Show options to enable 'Do not disturb' mode"), diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index e1e300e185..44f3d5d838 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -248,6 +248,16 @@ export default class SettingsStore { return _t(displayName as string); } + /** + * Gets the translated description for a given setting + * @param {string} settingName The setting to look up. + * @return {String} The description for the setting, or null if not found. + */ + public static getDescription(settingName: string) { + if (!SETTINGS[settingName]?.description) return null; + return _t(SETTINGS[settingName].description); + } + /** * Determines if a setting is also a feature. * @param {string} settingName The setting to look up. diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts index 023845c9ee..a6f4574a58 100644 --- a/src/stores/CommunityPrototypeStore.ts +++ b/src/stores/CommunityPrototypeStore.ts @@ -107,8 +107,9 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient { const pl = generalChat.currentState.getStateEvents("m.room.power_levels", ""); if (!pl) return this.isAdminOf(communityId); + const plContent = pl.getContent(); - const invitePl = isNullOrUndefined(pl.invite) ? 50 : Number(pl.invite); + const invitePl = isNullOrUndefined(plContent.invite) ? 50 : Number(plContent.invite); return invitePl <= myMember.powerLevel; } @@ -159,10 +160,16 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient { if (SettingsStore.getValue("feature_communities_v2_prototypes")) { const data = this.matrixClient.getAccountData("im.vector.group_info." + roomId); if (data && data.getContent()) { - return {displayName: data.getContent().name, avatarMxc: data.getContent().avatar_url}; + return { + displayName: data.getContent().name, + avatarMxc: data.getContent().avatar_url, + }; } } - return {displayName: room.name, avatarMxc: room.avatar_url}; + return { + displayName: room.name, + avatarMxc: room.getMxcAvatarUrl(), + }; } protected async onReady(): Promise { diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index cc3eafffcd..87978df471 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -276,7 +276,7 @@ class RoomViewStore extends Store { const address = this.state.roomAlias || this.state.roomId; const viaServers = this.state.viaServers || []; try { - await retry(() => cli.joinRoom(address, { + await retry(() => cli.joinRoom(address, { viaServers, ...payload.opts, }), NUM_JOIN_RETRY, (err) => { diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 40997d30a8..e498574467 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -14,36 +14,44 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ListIteratee, Many, sortBy, throttle} from "lodash"; -import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; -import {Room} from "matrix-js-sdk/src/models/room"; -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import { ListIteratee, Many, sortBy, throttle } from "lodash"; +import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import {AsyncStoreWithClient} from "./AsyncStoreWithClient"; +import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; -import {ActionPayload} from "../dispatcher/payloads"; +import { ActionPayload } from "../dispatcher/payloads"; import RoomListStore from "./room-list/RoomListStore"; import SettingsStore from "../settings/SettingsStore"; import DMRoomMap from "../utils/DMRoomMap"; -import {FetchRoomFn} from "./notifications/ListNotificationState"; -import {SpaceNotificationState} from "./notifications/SpaceNotificationState"; -import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore"; -import {DefaultTagID} from "./room-list/models"; -import {EnhancedMap, mapDiff} from "../utils/maps"; -import {setHasDiff} from "../utils/sets"; -import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; +import { FetchRoomFn } from "./notifications/ListNotificationState"; +import { SpaceNotificationState } from "./notifications/SpaceNotificationState"; +import { RoomNotificationStateStore } from "./notifications/RoomNotificationStateStore"; +import { DefaultTagID } from "./room-list/models"; +import { EnhancedMap, mapDiff } from "../utils/maps"; +import { setHasDiff } from "../utils/sets"; +import { ISpaceSummaryEvent, ISpaceSummaryRoom } from "../components/structures/SpaceRoomDirectory"; import RoomViewStore from "./RoomViewStore"; +import { Action } from "../dispatcher/actions"; +import { arrayHasDiff } from "../utils/arrays"; +import { objectDiff } from "../utils/objects"; +import { arrayHasOrderChange } from "../utils/arrays"; +import { reorderLexicographically } from "../utils/stringOrderField"; + +type SpaceKey = string | symbol; interface IState {} const ACTIVE_SPACE_LS_KEY = "mx_active_space"; +export const HOME_SPACE = Symbol("home-space"); export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces"); export const UPDATE_INVITED_SPACES = Symbol("invited-spaces"); export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); -// Space Room ID will be emitted when a Space's children change +// Space Room ID/HOME_SPACE will be emitted when a Space's children change export interface ISuggestedRoom extends ISpaceSummaryRoom { viaServers: string[]; @@ -51,7 +59,8 @@ export interface ISuggestedRoom extends ISpaceSummaryRoom { const MAX_SUGGESTED_ROOMS = 20; -const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "ALL_ROOMS"}`; +const homeSpaceKey = SettingsStore.getValue("feature_spaces.all_rooms") ? "ALL_ROOMS" : "HOME_SPACE"; +const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || homeSpaceKey}`; const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] return arr.reduce((result, room: Room) => { @@ -60,18 +69,18 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, }, [[], []]); }; -// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` -export const getOrder = (order: string, creationTs: number, roomId: string): Array>> => { - let validatedOrder: string = null; - - if (typeof order === "string" && Array.from(order).every((c: string) => { +const validOrder = (order: string): string | undefined => { + if (typeof order === "string" && order.length <= 50 && Array.from(order).every((c: string) => { const charCode = c.charCodeAt(0); return charCode >= 0x20 && charCode <= 0x7E; })) { - validatedOrder = order; + return order; } +}; - return [validatedOrder, creationTs, roomId]; +// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` +export const getChildOrder = (order: string, creationTs: number, roomId: string): Array>> => { + return [validOrder(order), creationTs, roomId]; } const getRoomFn: FetchRoomFn = (room: Room) => { @@ -85,16 +94,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // The spaces representing the roots of the various tree-like hierarchies private rootSpaces: Room[] = []; + // The list of rooms not present in any currently joined spaces + private orphanedRooms = new Set(); // Map from room ID to set of spaces which list it as a child private parentMap = new EnhancedMap>(); - // Map from spaceId to SpaceNotificationState instance representing that space - private notificationStateMap = new Map(); + // Map from SpaceKey to SpaceNotificationState instance representing that space + private notificationStateMap = new Map(); // Map from space key to Set of room IDs that should be shown as part of that space's filter - private spaceFilteredRooms = new Map>(); - // The space currently selected in the Space Panel - if null then All Rooms is selected + private spaceFilteredRooms = new Map>(); + // The space currently selected in the Space Panel - if null then Home is selected private _activeSpace?: Room = null; private _suggestedRooms: ISuggestedRoom[] = []; private _invitedSpaces = new Set(); + private spaceOrderLocalEchoMap = new Map(); public get invitedSpaces(): Room[] { return Array.from(this._invitedSpaces); @@ -133,7 +145,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // if the space being selected is an invite then always view that invite // else if the last viewed room in this space is joined then view that // else view space home or home depending on what is being clicked on - if (space?.getMyMembership !== "invite" && + if (space?.getMyMembership() !== "invite" && this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join" ) { defaultDispatcher.dispatch({ @@ -214,7 +226,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const roomId = ev.getStateKey(); const childRoom = this.matrixClient?.getRoom(roomId); const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs(); - return getOrder(ev.getContent().order, createTs, roomId); + return getChildOrder(ev.getContent().order, createTs, roomId); }).map(ev => { return this.matrixClient.getRoom(ev.getStateKey()); }).filter(room => { @@ -251,10 +263,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } public getSpaceFilteredRoomIds = (space: Room | null): Set => { - if (!space) { + if (!space && SettingsStore.getValue("feature_spaces.all_rooms")) { return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId)); } - return this.spaceFilteredRooms.get(space.roomId) || new Set(); + return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); }; private rebuild = throttle(() => { @@ -285,7 +297,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); }); - const [rootSpaces] = partitionSpacesAndRooms(Array.from(unseenChildren)); + const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren)); // somewhat algorithm to handle full-cycles const detachedNodes = new Set(spaces); @@ -326,7 +338,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // rootSpaces.push(space); // }); - this.rootSpaces = rootSpaces; + this.orphanedRooms = new Set(orphanedRooms.map(r => r.roomId)); + this.rootSpaces = this.sortRootSpaces(rootSpaces); this.parentMap = backrefs; // if the currently selected space no longer exists, remove its selection @@ -338,14 +351,34 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); // build initial state of invited spaces as we would have missed the emitted events about the room at launch - this._invitedSpaces = new Set(invitedSpaces); + this._invitedSpaces = new Set(this.sortRootSpaces(invitedSpaces)); this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); }, 100, {trailing: true, leading: true}); - onSpaceUpdate = () => { + private onSpaceUpdate = () => { this.rebuild(); } + private showInHomeSpace = (room: Room) => { + if (SettingsStore.getValue("feature_spaces.all_rooms")) return true; + if (room.isSpaceRoom()) return false; + return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space + || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space + || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites + }; + + // Update a given room due to its tag changing (e.g DM-ness or Fav-ness) + // This can only change whether it shows up in the HOME_SPACE or not + private onRoomUpdate = (room: Room) => { + if (this.showInHomeSpace(room)) { + this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId); + this.emit(HOME_SPACE); + } else if (!this.orphanedRooms.has(room.roomId)) { + this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId); + this.emit(HOME_SPACE); + } + }; + private onSpaceMembersChange = (ev: MatrixEvent) => { // skip this update if we do not have a DM with this user if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return; @@ -359,6 +392,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const oldFilteredRooms = this.spaceFilteredRooms; this.spaceFilteredRooms = new Map(); + if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + // put all room invites in the Home Space + const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite"); + this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId))); + + visibleRooms.forEach(room => { + if (this.showInHomeSpace(room)) { + this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId); + } + }); + } + this.rootSpaces.forEach(s => { // traverse each space tree in DFS to build up the supersets as you go up, // reusing results from like subtrees. @@ -374,13 +419,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const roomIds = new Set(childRooms.map(r => r.roomId)); const space = this.matrixClient?.getRoom(spaceId); - // Add relevant DMs - space?.getMembers().forEach(member => { - if (member.membership !== "join" && member.membership !== "invite") return; - DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => { - roomIds.add(roomId); + if (SettingsStore.getValue("feature_spaces.space_member_dms")) { + // Add relevant DMs + space?.getMembers().forEach(member => { + if (member.membership !== "join" && member.membership !== "invite") return; + DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => { + roomIds.add(roomId); + }); }); - }); + } const newPath = new Set(parentPath).add(spaceId); childSpaces.forEach(childSpace => { @@ -406,6 +453,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Update NotificationStates this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => { if (roomIds.has(room.roomId)) { + if (s !== HOME_SPACE && SettingsStore.getValue("feature_spaces.space_dm_badges")) return true; + return !DMRoomMap.shared().getUserIdForRoomId(room.roomId) || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); } @@ -423,8 +472,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient { parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(roomId)); } if (!parent) { - const parents = Array.from(this.parentMap.get(roomId) || []); - parent = parents.find(p => this.matrixClient.getRoom(p)); + const parentIds = Array.from(this.parentMap.get(roomId) || []); + for (const parentId of parentIds) { + const room = this.matrixClient.getRoom(parentId); + if (room) { + parent = room; + break; + } + } } // don't trigger a context switch when we are switching a space to match the chosen room @@ -472,6 +527,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } }; + private notifyIfOrderChanged(): void { + const rootSpaces = this.sortRootSpaces(this.rootSpaces); + if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) { + this.rootSpaces = rootSpaces; + this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); + } + } + private onRoomState = (ev: MatrixEvent) => { const room = this.matrixClient.getRoom(ev.getRoomId()); if (!room) return; @@ -489,6 +552,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // TODO confirm this after implementing parenting behaviour if (room.isSpaceRoom()) { this.onSpaceUpdate(); + } else if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + this.onRoomUpdate(room); } this.emit(room.roomId); break; @@ -501,8 +566,47 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } }; + private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => { + if (!room.isSpaceRoom()) return; + + if (ev.getType() === EventType.SpaceOrder) { + this.spaceOrderLocalEchoMap.delete(room.roomId); // clear any local echo + const order = ev.getContent()?.order; + const lastOrder = lastEv?.getContent()?.order; + if (order !== lastOrder) { + this.notifyIfOrderChanged(); + } + } else if (ev.getType() === EventType.Tag && !SettingsStore.getValue("feature_spaces.all_rooms")) { + // If the room was in favourites and now isn't or the opposite then update its position in the trees + const oldTags = lastEv?.getContent()?.tags || {}; + const newTags = ev.getContent()?.tags || {}; + if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) { + this.onRoomUpdate(room); + } + } + } + + private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => { + if (ev.getType() === EventType.Direct) { + const lastContent = lastEvent.getContent(); + const content = ev.getContent(); + + const diff = objectDiff>(lastContent, content); + // filter out keys which changed by reference only by checking whether the sets differ + const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k])); + // DM tag changes, refresh relevant rooms + new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => { + const room = this.matrixClient?.getRoom(roomId); + if (room) { + this.onRoomUpdate(room); + } + }); + } + }; + protected async reset() { this.rootSpaces = []; + this.orphanedRooms = new Set(); this.parentMap = new EnhancedMap(); this.notificationStateMap = new Map(); this.spaceFilteredRooms = new Map(); @@ -516,7 +620,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (this.matrixClient) { this.matrixClient.removeListener("Room", this.onRoom); this.matrixClient.removeListener("Room.myMembership", this.onRoom); + this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); this.matrixClient.removeListener("RoomState.events", this.onRoomState); + if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + this.matrixClient.removeListener("accountData", this.onAccountData); + } } await this.reset(); } @@ -525,7 +633,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (!SettingsStore.getValue("feature_spaces")) return; this.matrixClient.on("Room", this.onRoom); this.matrixClient.on("Room.myMembership", this.onRoom); + this.matrixClient.on("Room.accountData", this.onRoomAccountData); this.matrixClient.on("RoomState.events", this.onRoomState); + if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + this.matrixClient.on("accountData", this.onAccountData); + } await this.onSpaceUpdate(); // trigger an initial update @@ -550,7 +662,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Don't context switch when navigating to the space room // as it will cause you to end up in the wrong room this.setActiveSpace(room, false); - } else if (this.activeSpace && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) { + } else if ( + (!SettingsStore.getValue("feature_spaces.all_rooms") || this.activeSpace) && + !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId) + ) { this.switchToRelatedSpace(roomId); } @@ -565,10 +680,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.setActiveSpace(null, false); } break; + case Action.SwitchSpace: + if (payload.num === 0) { + this.setActiveSpace(null); + } else if (this.spacePanelSpaces.length >= payload.num) { + this.setActiveSpace(this.spacePanelSpaces[payload.num - 1]); + } } } - public getNotificationState(key: string): SpaceNotificationState { + public getNotificationState(key: SpaceKey): SpaceNotificationState { if (this.notificationStateMap.has(key)) { return this.notificationStateMap.get(key); } @@ -599,6 +720,38 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath)); } + + private getSpaceTagOrdering = (space: Room): string | undefined => { + if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId); + return validOrder(space.getAccountData(EventType.SpaceOrder)?.getContent()?.order); + }; + + private sortRootSpaces(spaces: Room[]): Room[] { + return sortBy(spaces, [this.getSpaceTagOrdering, "roomId"]); + } + + private async setRootSpaceOrder(space: Room, order: string): Promise { + this.spaceOrderLocalEchoMap.set(space.roomId, order); + try { + await this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order }); + } catch (e) { + console.warn("Failed to set root space order", e); + if (this.spaceOrderLocalEchoMap.get(space.roomId) === order) { + this.spaceOrderLocalEchoMap.delete(space.roomId); + } + } + } + + public moveRootSpace(fromIndex: number, toIndex: number): void { + const currentOrders = this.rootSpaces.map(this.getSpaceTagOrdering); + const changes = reorderLexicographically(currentOrders, fromIndex, toIndex); + + changes.forEach(({ index, order }) => { + this.setRootSpaceOrder(this.rootSpaces[index], order); + }); + + this.notifyIfOrderChanged(); + } } export default class SpaceStore { diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts index 0b1b78bc75..a1f7786578 100644 --- a/src/stores/room-list/SpaceWatcher.ts +++ b/src/stores/room-list/SpaceWatcher.ts @@ -19,6 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { RoomListStoreClass } from "./RoomListStore"; import { SpaceFilterCondition } from "./filters/SpaceFilterCondition"; import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore"; +import SettingsStore from "../../settings/SettingsStore"; /** * Watches for changes in spaces to manage the filter on the provided RoomListStore @@ -28,6 +29,11 @@ export class SpaceWatcher { private activeSpace: Room = SpaceStore.instance.activeSpace; constructor(private store: RoomListStoreClass) { + if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + this.filter = new SpaceFilterCondition(); + this.updateFilter(); + store.addFilter(this.filter); + } SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated); } @@ -35,7 +41,7 @@ export class SpaceWatcher { this.activeSpace = activeSpace; if (this.filter) { - if (activeSpace) { + if (activeSpace || !SettingsStore.getValue("feature_spaces.all_rooms")) { this.updateFilter(); } else { this.store.removeFilter(this.filter); @@ -49,9 +55,11 @@ export class SpaceWatcher { }; private updateFilter = () => { - SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { - this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); - }); + if (this.activeSpace) { + SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { + this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); + }); + } this.filter.updateSpace(this.activeSpace); }; } diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts index 6a06bee0d8..0e6965d843 100644 --- a/src/stores/room-list/filters/SpaceFilterCondition.ts +++ b/src/stores/room-list/filters/SpaceFilterCondition.ts @@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition"; import { IDestroyable } from "../../../utils/IDestroyable"; -import SpaceStore from "../../SpaceStore"; +import SpaceStore, { HOME_SPACE } from "../../SpaceStore"; import { setHasDiff } from "../../../utils/sets"; /** @@ -29,7 +29,7 @@ import { setHasDiff } from "../../../utils/sets"; * + All DMs */ export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable { - private roomIds = new Set(); + private roomIds = new Set(); private space: Room = null; public get kind(): FilterKind { @@ -55,12 +55,10 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi } }; - private getSpaceEventKey = (space: Room) => space.roomId; + private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE; public updateSpace(space: Room) { - if (this.space) { - SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate); - } + SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate); SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate); this.onStoreUpdate(); // initial update from the change to the space } diff --git a/src/utils/EditorStateTransfer.js b/src/utils/EditorStateTransfer.ts similarity index 52% rename from src/utils/EditorStateTransfer.js rename to src/utils/EditorStateTransfer.ts index c7782a9ea8..ba303f9b73 100644 --- a/src/utils/EditorStateTransfer.js +++ b/src/utils/EditorStateTransfer.ts @@ -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,36 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +import { SerializedPart } from "../editor/parts"; +import { Caret } from "../editor/caret"; + /** * Used while editing, to pass the event, and to preserve editor state * from one editor instance to another when remounting the editor * upon receiving the remote echo for an unsent event. */ export default class EditorStateTransfer { - constructor(event) { - this._event = event; - this._serializedParts = null; - this.caret = null; + private serializedParts: SerializedPart[] = null; + private caret: Caret = null; + + constructor(private readonly event: MatrixEvent) {} + + public setEditorState(caret: Caret, serializedParts: SerializedPart[]) { + this.caret = caret; + this.serializedParts = serializedParts; } - setEditorState(caret, serializedParts) { - this._caret = caret; - this._serializedParts = serializedParts; + public hasEditorState() { + return !!this.serializedParts; } - hasEditorState() { - return !!this._serializedParts; + public getSerializedParts() { + return this.serializedParts; } - getSerializedParts() { - return this._serializedParts; + public getCaret() { + return this.caret; } - getCaret() { - return this._caret; - } - - getEvent() { - return this._event; + public getEvent() { + return this.event; } } diff --git a/src/utils/ErrorUtils.js b/src/utils/ErrorUtils.tsx similarity index 84% rename from src/utils/ErrorUtils.js rename to src/utils/ErrorUtils.tsx index b5bd5b0af0..c39ee21f09 100644 --- a/src/utils/ErrorUtils.js +++ b/src/utils/ErrorUtils.tsx @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +Copyright 2018 - 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,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { _t, _td } from '../languageHandler'; +import React, { ReactNode } from "react"; +import { MatrixError } from "matrix-js-sdk/src/http-api"; + +import { _t, _td, Tags, TranslatedString } from '../languageHandler'; /** * Produce a translated error message for a @@ -30,7 +33,12 @@ import { _t, _td } from '../languageHandler'; * for any tags in the strings apart from 'a' * @returns {*} Translated string or react component */ -export function messageForResourceLimitError(limitType, adminContact, strings, extraTranslations) { +export function messageForResourceLimitError( + limitType: string, + adminContact: string, + strings: Record, + extraTranslations?: Tags, +): TranslatedString { let errString = strings[limitType]; if (errString === undefined) errString = strings['']; @@ -49,7 +57,7 @@ export function messageForResourceLimitError(limitType, adminContact, strings, e } } -export function messageForSyncError(err) { +export function messageForSyncError(err: MatrixError | Error): ReactNode { if (err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { const limitError = messageForResourceLimitError( err.data.limit_type, diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.ts similarity index 85% rename from src/utils/EventUtils.js rename to src/utils/EventUtils.ts index be21896417..3d9c60d9cd 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +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,9 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventStatus } from 'matrix-js-sdk/src/models/event'; -import {MatrixClientPeg} from '../MatrixClientPeg'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event'; + +import { MatrixClientPeg } from '../MatrixClientPeg'; import shouldHideEvent from "../shouldHideEvent"; + /** * Returns whether an event should allow actions like reply, reactions, edit, etc. * which effectively checks whether it's a regular message that has been sent and that we @@ -25,7 +28,7 @@ import shouldHideEvent from "../shouldHideEvent"; * @param {MatrixEvent} mxEvent The event to check * @returns {boolean} true if actionable */ -export function isContentActionable(mxEvent) { +export function isContentActionable(mxEvent: MatrixEvent): boolean { const { status: eventStatus } = mxEvent; // status is SENT before remote-echo, null after @@ -45,7 +48,7 @@ export function isContentActionable(mxEvent) { return false; } -export function canEditContent(mxEvent) { +export function canEditContent(mxEvent: MatrixEvent): boolean { if (mxEvent.status === EventStatus.CANCELLED || mxEvent.getType() !== "m.room.message" || mxEvent.isRedacted()) { return false; } @@ -56,7 +59,7 @@ export function canEditContent(mxEvent) { mxEvent.getSender() === MatrixClientPeg.get().getUserId(); } -export function canEditOwnEvent(mxEvent) { +export function canEditOwnEvent(mxEvent: MatrixEvent): boolean { // for now we only allow editing // your own events. So this just call through // In the future though, moderators will be able to @@ -67,7 +70,7 @@ export function canEditOwnEvent(mxEvent) { } const MAX_JUMP_DISTANCE = 100; -export function findEditableEvent(room, isForward, fromEventId = undefined) { +export function findEditableEvent(room: Room, isForward: boolean, fromEventId: string = undefined): MatrixEvent { const liveTimeline = room.getLiveTimeline(); const events = liveTimeline.getEvents().concat(room.getPendingEvents()); const maxIdx = events.length - 1; diff --git a/src/utils/IdentityServerUtils.js b/src/utils/IdentityServerUtils.ts similarity index 82% rename from src/utils/IdentityServerUtils.js rename to src/utils/IdentityServerUtils.ts index 5ece308954..2476adca19 100644 --- a/src/utils/IdentityServerUtils.js +++ b/src/utils/IdentityServerUtils.ts @@ -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. @@ -15,14 +15,15 @@ limitations under the License. */ import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types'; + import SdkConfig from '../SdkConfig'; import {MatrixClientPeg} from '../MatrixClientPeg'; -export function getDefaultIdentityServerUrl() { +export function getDefaultIdentityServerUrl(): string { return SdkConfig.get()['validated_server_config']['isUrl']; } -export function useDefaultIdentityServer() { +export function useDefaultIdentityServer(): void { const url = getDefaultIdentityServerUrl(); // Account data change will update localstorage, client, etc through dispatcher MatrixClientPeg.get().setAccountData("m.identity_server", { @@ -30,7 +31,7 @@ export function useDefaultIdentityServer() { }); } -export async function doesIdentityServerHaveTerms(fullUrl) { +export async function doesIdentityServerHaveTerms(fullUrl: string): Promise { let terms; try { terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); @@ -46,7 +47,7 @@ export async function doesIdentityServerHaveTerms(fullUrl) { return terms && terms["policies"] && (Object.keys(terms["policies"]).length > 0); } -export function doesAccountDataHaveIdentityServer() { +export function doesAccountDataHaveIdentityServer(): boolean { const event = MatrixClientPeg.get().getAccountData("m.identity_server"); return event && event.getContent() && event.getContent()['base_url']; } diff --git a/src/utils/MessageDiffUtils.js b/src/utils/MessageDiffUtils.tsx similarity index 84% rename from src/utils/MessageDiffUtils.js rename to src/utils/MessageDiffUtils.tsx index 7398173fdd..b5d5e31432 100644 --- a/src/utils/MessageDiffUtils.js +++ b/src/utils/MessageDiffUtils.tsx @@ -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,31 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import classNames from 'classnames'; -import DiffMatchPatch from 'diff-match-patch'; -import {DiffDOM} from "diff-dom"; -import { checkBlockNode, bodyToHtml } from "../HtmlUtils"; +import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch'; +import { Action, DiffDOM, IDiff } from "diff-dom"; +import { IContent } from "matrix-js-sdk/src/models/event"; + +import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils"; const decodeEntities = (function() { let textarea = null; - return function(string) { + return function(str: string): string { if (!textarea) { textarea = document.createElement("textarea"); } - textarea.innerHTML = string; + textarea.innerHTML = str; return textarea.value; }; })(); -function textToHtml(text) { +function textToHtml(text: string): string { const container = document.createElement("div"); container.textContent = text; return container.innerHTML; } -function getSanitizedHtmlBody(content) { - const opts = { +function getSanitizedHtmlBody(content: IContent): string { + const opts: IOptsReturnString = { stripReplyFallback: true, returnString: true, }; @@ -57,21 +59,21 @@ function getSanitizedHtmlBody(content) { } } -function wrapInsertion(child) { +function wrapInsertion(child: Node): HTMLElement { const wrapper = document.createElement(checkBlockNode(child) ? "div" : "span"); wrapper.className = "mx_EditHistoryMessage_insertion"; wrapper.appendChild(child); return wrapper; } -function wrapDeletion(child) { +function wrapDeletion(child: Node): HTMLElement { const wrapper = document.createElement(checkBlockNode(child) ? "div" : "span"); wrapper.className = "mx_EditHistoryMessage_deletion"; wrapper.appendChild(child); return wrapper; } -function findRefNodes(root, route, isAddition) { +function findRefNodes(root: Node, route: number[], isAddition = false) { let refNode = root; let refParentNode; const end = isAddition ? route.length - 1 : route.length; @@ -79,7 +81,7 @@ function findRefNodes(root, route, isAddition) { refParentNode = refNode; refNode = refNode.childNodes[route[i]]; } - return {refNode, refParentNode}; + return { refNode, refParentNode }; } function diffTreeToDOM(desc) { @@ -101,7 +103,7 @@ function diffTreeToDOM(desc) { } } -function insertBefore(parent, nextSibling, child) { +function insertBefore(parent: Node, nextSibling: Node | null, child: Node): void { if (nextSibling) { parent.insertBefore(child, nextSibling); } else { @@ -109,7 +111,7 @@ function insertBefore(parent, nextSibling, child) { } } -function isRouteOfNextSibling(route1, route2) { +function isRouteOfNextSibling(route1: number[], route2: number[]): boolean { // routes are arrays with indices, // to be interpreted as a path in the dom tree @@ -127,7 +129,7 @@ function isRouteOfNextSibling(route1, route2) { return route2[lastD1Idx] >= route1[lastD1Idx]; } -function adjustRoutes(diff, remainingDiffs) { +function adjustRoutes(diff: IDiff, remainingDiffs: IDiff[]): void { if (diff.action === "removeTextElement" || diff.action === "removeElement") { // as removed text is not removed from the html, but marked as deleted, // we need to readjust indices that assume the current node has been removed. @@ -140,14 +142,14 @@ function adjustRoutes(diff, remainingDiffs) { } } -function stringAsTextNode(string) { +function stringAsTextNode(string: string): Text { return document.createTextNode(decodeEntities(string)); } -function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) { +function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatch: DiffMatchPatch): void { const {refNode, refParentNode} = findRefNodes(originalRootNode, diff.route); switch (diff.action) { - case "replaceElement": { + case Action.ReplaceElement: { const container = document.createElement("span"); const delNode = wrapDeletion(diffTreeToDOM(diff.oldValue)); const insNode = wrapInsertion(diffTreeToDOM(diff.newValue)); @@ -156,22 +158,22 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) { refNode.parentNode.replaceChild(container, refNode); break; } - case "removeTextElement": { + case Action.RemoveTextElement: { const delNode = wrapDeletion(stringAsTextNode(diff.value)); refNode.parentNode.replaceChild(delNode, refNode); break; } - case "removeElement": { + case Action.RemoveElement: { const delNode = wrapDeletion(diffTreeToDOM(diff.element)); refNode.parentNode.replaceChild(delNode, refNode); break; } - case "modifyTextElement": { + case Action.ModifyTextElement: { const textDiffs = diffMathPatch.diff_main(diff.oldValue, diff.newValue); diffMathPatch.diff_cleanupSemantic(textDiffs); const container = document.createElement("span"); for (const [modifier, text] of textDiffs) { - let textDiffNode = stringAsTextNode(text); + let textDiffNode: Node = stringAsTextNode(text); if (modifier < 0) { textDiffNode = wrapDeletion(textDiffNode); } else if (modifier > 0) { @@ -182,12 +184,12 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) { refNode.parentNode.replaceChild(container, refNode); break; } - case "addElement": { + case Action.AddElement: { const insNode = wrapInsertion(diffTreeToDOM(diff.element)); insertBefore(refParentNode, refNode, insNode); break; } - case "addTextElement": { + case Action.AddTextElement: { // XXX: sometimes diffDOM says insert a newline when there shouldn't be one // but we must insert the node anyway so that we don't break the route child IDs. // See https://github.com/fiduswriter/diffDOM/issues/100 @@ -197,11 +199,11 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) { } // e.g. when changing a the href of a link, // show the link with old href as removed and with the new href as added - case "removeAttribute": - case "addAttribute": - case "modifyAttribute": { + case Action.RemoveAttribute: + case Action.AddAttribute: + case Action.ModifyAttribute: { const delNode = wrapDeletion(refNode.cloneNode(true)); - const updatedNode = refNode.cloneNode(true); + const updatedNode = refNode.cloneNode(true) as HTMLElement; if (diff.action === "addAttribute" || diff.action === "modifyAttribute") { updatedNode.setAttribute(diff.name, diff.newValue); } else { @@ -220,12 +222,12 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) { } } -function routeIsEqual(r1, r2) { +function routeIsEqual(r1: number[], r2: number[]): boolean { return r1.length === r2.length && !r1.some((e, i) => e !== r2[i]); } // workaround for https://github.com/fiduswriter/diffDOM/issues/90 -function filterCancelingOutDiffs(originalDiffActions) { +function filterCancelingOutDiffs(originalDiffActions: IDiff[]): IDiff[] { const diffActions = originalDiffActions.slice(); for (let i = 0; i < diffActions.length; ++i) { @@ -252,7 +254,7 @@ function filterCancelingOutDiffs(originalDiffActions) { * @param {object} editContent the content for the edit message * @return {object} a react element similar to what `bodyToHtml` returns */ -export function editBodyDiffToHtml(originalContent, editContent) { +export function editBodyDiffToHtml(originalContent: IContent, editContent: IContent): ReactNode { // wrap the body in a div, DiffDOM needs a root element const originalBody = `
${getSanitizedHtmlBody(originalContent)}
`; const editBody = `
${getSanitizedHtmlBody(editContent)}
`; diff --git a/src/utils/PinningUtils.js b/src/utils/PinningUtils.ts similarity index 89% rename from src/utils/PinningUtils.js rename to src/utils/PinningUtils.ts index 90d26cc988..ec1eeccf1f 100644 --- a/src/utils/PinningUtils.js +++ b/src/utils/PinningUtils.ts @@ -14,13 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + export default class PinningUtils { /** * Determines if the given event may be pinned. * @param {MatrixEvent} event The event to check. * @return {boolean} True if the event may be pinned, false otherwise. */ - static isPinnable(event) { + static isPinnable(event: MatrixEvent): boolean { if (!event) return false; if (event.getType() !== "m.room.message") return false; if (event.isRedacted()) return false; diff --git a/src/utils/Receipt.js b/src/utils/Receipt.ts similarity index 83% rename from src/utils/Receipt.js rename to src/utils/Receipt.ts index d88c67fb18..2a626decc4 100644 --- a/src/utils/Receipt.js +++ b/src/utils/Receipt.ts @@ -1,5 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd +Copyright 2016 - 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,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + /** * Given MatrixEvent containing receipts, return the first * read receipt from the given user ID, or null if no such @@ -23,7 +25,7 @@ limitations under the License. * @param {string} userId A user ID * @returns {Object} Read receipt */ -export function findReadReceiptFromUserId(receiptEvent, userId) { +export function findReadReceiptFromUserId(receiptEvent: MatrixEvent, userId: string): object | null { const receiptKeys = Object.keys(receiptEvent.getContent()); for (let i = 0; i < receiptKeys.length; ++i) { const rcpt = receiptEvent.getContent()[receiptKeys[i]]; diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.ts similarity index 62% rename from src/utils/ResizeNotifier.js rename to src/utils/ResizeNotifier.ts index 4d46d10f6c..8bb7f52e57 100644 --- a/src/utils/ResizeNotifier.js +++ b/src/utils/ResizeNotifier.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +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,59 +22,58 @@ limitations under the License. * Fires when the middle panel has been resized by a pixel. * @event module:utils~ResizeNotifier#"middlePanelResizedNoisy" */ + import { EventEmitter } from "events"; import { throttle } from "lodash"; export default class ResizeNotifier extends EventEmitter { - constructor() { - super(); - // with default options, will call fn once at first call, and then every x ms - // if there was another call in that timespan - this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200); - this._isResizing = false; - } + private _isResizing = false; - get isResizing() { + // with default options, will call fn once at first call, and then every x ms + // if there was another call in that timespan + private throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200); + + public get isResizing() { return this._isResizing; } - startResizing() { + public startResizing() { this._isResizing = true; this.emit("isResizing", true); } - stopResizing() { + public stopResizing() { this._isResizing = false; this.emit("isResizing", false); } - _noisyMiddlePanel() { + private noisyMiddlePanel() { this.emit("middlePanelResizedNoisy"); } - _updateMiddlePanel() { - this._throttledMiddlePanel(); - this._noisyMiddlePanel(); + private updateMiddlePanel() { + this.throttledMiddlePanel(); + this.noisyMiddlePanel(); } // can be called in quick succession - notifyLeftHandleResized() { + public notifyLeftHandleResized() { // don't emit event for own region - this._updateMiddlePanel(); + this.updateMiddlePanel(); } // can be called in quick succession - notifyRightHandleResized() { - this._updateMiddlePanel(); + public notifyRightHandleResized() { + this.updateMiddlePanel(); } - notifyTimelineHeightChanged() { - this._updateMiddlePanel(); + public notifyTimelineHeightChanged() { + this.updateMiddlePanel(); } // can be called in quick succession - notifyWindowResized() { - this._updateMiddlePanel(); + public notifyWindowResized() { + this.updateMiddlePanel(); } } diff --git a/src/utils/ShieldUtils.ts b/src/utils/ShieldUtils.ts index 5fe653fed0..c855b81bf8 100644 --- a/src/utils/ShieldUtils.ts +++ b/src/utils/ShieldUtils.ts @@ -1,30 +1,31 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; + import DMRoomMap from './DMRoomMap'; -/* For now, a cut-down type spec for the client */ -interface Client { - getUserId: () => string; - checkUserTrust: (userId: string) => { - isCrossSigningVerified: () => boolean - wasCrossSigningVerified: () => boolean - }; - getStoredDevicesForUser: (userId: string) => [{ deviceId: string }]; - checkDeviceTrust: (userId: string, deviceId: string) => { - isVerified: () => boolean - }; -} - -interface Room { - getEncryptionTargetMembers: () => Promise<[{userId: string}]>; - roomId: string; -} - export enum E2EStatus { Warning = "warning", Verified = "verified", Normal = "normal" } -export async function shieldStatusForRoom(client: Client, room: Room): Promise { +export async function shieldStatusForRoom(client: MatrixClient, room: Room): Promise { const members = (await room.getEncryptionTargetMembers()).map(({userId}) => userId); const inDMMap = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId); diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index 7ff0529363..926278a20a 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -16,19 +16,20 @@ limitations under the License. */ import * as url from "url"; +import { Capability, IWidget, IWidgetData, MatrixCapabilities } from "matrix-widget-api"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import {MatrixClientPeg} from '../MatrixClientPeg'; +import { MatrixClientPeg } from '../MatrixClientPeg'; import SdkConfig from "../SdkConfig"; import dis from '../dispatcher/dispatcher'; import WidgetEchoStore from '../stores/WidgetEchoStore'; import SettingsStore from "../settings/SettingsStore"; -import {IntegrationManagers} from "../integrations/IntegrationManagers"; -import {Room} from "matrix-js-sdk/src/models/room"; -import {WidgetType} from "../widgets/WidgetType"; -import {objectClone} from "./objects"; -import {_t} from "../languageHandler"; -import {Capability, IWidget, IWidgetData, MatrixCapabilities} from "matrix-widget-api"; -import {IApp} from "../stores/WidgetStore"; +import { IntegrationManagers } from "../integrations/IntegrationManagers"; +import { WidgetType } from "../widgets/WidgetType"; +import { objectClone } from "./objects"; +import { _t } from "../languageHandler"; +import { IApp } from "../stores/WidgetStore"; // How long we wait for the state event echo to come back from the server // before waitFor[Room/User]Widget rejects its promise @@ -377,9 +378,9 @@ export default class WidgetUtils { return widgets.filter(w => w.content && w.content.type === "m.integration_manager"); } - static getRoomWidgetsOfType(room: Room, type: WidgetType): IWidgetEvent[] { - const widgets = WidgetUtils.getRoomWidgets(room); - return (widgets || []).filter(w => { + static getRoomWidgetsOfType(room: Room, type: WidgetType): MatrixEvent[] { + const widgets = WidgetUtils.getRoomWidgets(room) || []; + return widgets.filter(w => { const content = w.getContent(); return content.url && type.matches(content.type); }); diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index e527f43c29..6524debfb7 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {percentageOf, percentageWithin} from "./numbers"; +import { percentageOf, percentageWithin } from "./numbers"; /** * Quickly resample an array to have less/more data points. If an input which is larger @@ -223,6 +223,21 @@ export function arrayMerge(...a: T[][]): T[] { }, new Set())); } +/** + * Moves a single element from fromIndex to toIndex. + * @param {array} list the list from which to construct the new list. + * @param {number} fromIndex the index of the element to move. + * @param {number} toIndex the index of where to put the element. + * @returns {array} A new array with the requested value moved. + */ +export function moveElement(list: T[], fromIndex: number, toIndex: number): T[] { + const result = Array.from(list); + const [removed] = result.splice(fromIndex, 1); + result.splice(toIndex, 0, removed); + + return result; +} + /** * Helper functions to perform LINQ-like queries on arrays. */ diff --git a/src/utils/createMatrixClient.js b/src/utils/createMatrixClient.ts similarity index 76% rename from src/utils/createMatrixClient.js rename to src/utils/createMatrixClient.ts index f5e196d846..caaf75616d 100644 --- a/src/utils/createMatrixClient.js +++ b/src/utils/createMatrixClient.ts @@ -1,5 +1,5 @@ /* -Copyright 2017 Vector Creations 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,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {createClient} from "matrix-js-sdk/src/matrix"; -import {IndexedDBCryptoStore} from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store"; -import {WebStorageSessionStore} from "matrix-js-sdk/src/store/session/webstorage"; -import {IndexedDBStore} from "matrix-js-sdk/src/store/indexeddb"; +import { createClient, ICreateClientOpts } from "matrix-js-sdk/src/matrix"; +import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store"; +import { WebStorageSessionStore } from "matrix-js-sdk/src/store/session/webstorage"; +import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb"; const localStorage = window.localStorage; @@ -41,8 +41,8 @@ try { * * @returns {MatrixClient} the newly-created MatrixClient */ -export default function createMatrixClient(opts) { - const storeOpts = { +export default function createMatrixClient(opts: ICreateClientOpts) { + const storeOpts: Partial = { useAuthorizationHeader: true, }; @@ -65,9 +65,10 @@ export default function createMatrixClient(opts) { ); } - opts = Object.assign(storeOpts, opts); - - return createClient(opts); + return createClient({ + ...storeOpts, + ...opts, + }); } createMatrixClient.indexedDbWorkerScript = null; diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts new file mode 100644 index 0000000000..da840792ee --- /dev/null +++ b/src/utils/stringOrderField.ts @@ -0,0 +1,148 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { alphabetPad, baseToString, stringToBase, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils"; + +import { moveElement } from "./arrays"; + +export function midPointsBetweenStrings( + a: string, + b: string, + count: number, + maxLen: number, + alphabet = DEFAULT_ALPHABET, +): string[] { + const padN = Math.min(Math.max(a.length, b.length), maxLen); + const padA = alphabetPad(a, padN, alphabet); + const padB = alphabetPad(b, padN, alphabet); + const baseA = stringToBase(padA, alphabet); + const baseB = stringToBase(padB, alphabet); + + if (baseB - baseA - BigInt(1) < count) { + if (padN < maxLen) { + // this recurses once at most due to the new limit of n+1 + return midPointsBetweenStrings( + alphabetPad(padA, padN + 1, alphabet), + alphabetPad(padB, padN + 1, alphabet), + count, + padN + 1, + alphabet, + ); + } + return []; + } + + const step = (baseB - baseA) / BigInt(count + 1); + const start = BigInt(baseA + step); + return Array(count).fill(undefined).map((_, i) => baseToString(start + (BigInt(i) * step), alphabet)); +} + +interface IEntry { + index: number; + order: string; +} + +export const reorderLexicographically = ( + orders: Array, + fromIndex: number, + toIndex: number, + maxLen = 50, +): IEntry[] => { + // sanity check inputs + if ( + fromIndex < 0 || toIndex < 0 || + fromIndex > orders.length || toIndex > orders.length || + fromIndex === toIndex + ) { + return []; + } + + // zip orders with their indices to simplify later index wrangling + const ordersWithIndices: IEntry[] = orders.map((order, index) => ({ index, order })); + // apply the fundamental order update to the zipped array + const newOrder = moveElement(ordersWithIndices, fromIndex, toIndex); + + // check if we have to fill undefined orders to complete placement + const orderToLeftUndefined = newOrder[toIndex - 1]?.order === undefined; + + let leftBoundIdx = toIndex; + let rightBoundIdx = toIndex; + + let canMoveLeft = true; + const nextBase = newOrder[toIndex + 1]?.order !== undefined + ? stringToBase(newOrder[toIndex + 1].order) + : BigInt(Number.MAX_VALUE); + + // check how far left we would have to mutate to fit in that direction + for (let i = toIndex - 1, j = 1; i >= 0; i--, j++) { + if (newOrder[i]?.order !== undefined && nextBase - stringToBase(newOrder[i].order) > j) break; + leftBoundIdx = i; + } + + // verify the left move would be sufficient + const firstOrderBase = newOrder[0].order === undefined ? undefined : stringToBase(newOrder[0].order); + const bigToIndex = BigInt(toIndex); + if (leftBoundIdx === 0 && + firstOrderBase !== undefined && + nextBase - firstOrderBase <= bigToIndex && + firstOrderBase <= bigToIndex + ) { + canMoveLeft = false; + } + + const canDisplaceRight = !orderToLeftUndefined; + let canMoveRight = canDisplaceRight; + if (canDisplaceRight) { + const prevBase = newOrder[toIndex - 1]?.order !== undefined + ? stringToBase(newOrder[toIndex - 1]?.order) + : BigInt(Number.MIN_VALUE); + + // check how far right we would have to mutate to fit in that direction + for (let i = toIndex + 1, j = 1; i < newOrder.length; i++, j++) { + if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break; + rightBoundIdx = i; + } + + // verify the right move would be sufficient + if (rightBoundIdx === newOrder.length - 1 && + (newOrder[rightBoundIdx] + ? stringToBase(newOrder[rightBoundIdx].order) + : BigInt(Number.MAX_VALUE)) - prevBase <= (rightBoundIdx - toIndex) + ) { + canMoveRight = false; + } + } + + // pick the cheaper direction + const leftDiff = canMoveLeft ? toIndex - leftBoundIdx : Number.MAX_SAFE_INTEGER; + const rightDiff = canMoveRight ? rightBoundIdx - toIndex : Number.MAX_SAFE_INTEGER; + if (orderToLeftUndefined || leftDiff < rightDiff) { + rightBoundIdx = toIndex; + } else { + leftBoundIdx = toIndex; + } + + const prevOrder = newOrder[leftBoundIdx - 1]?.order ?? ""; + const nextOrder = newOrder[rightBoundIdx + 1]?.order + ?? DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1).repeat(prevOrder.length || 1); + + const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx, maxLen); + + return changes.map((order, i) => ({ + index: newOrder[leftBoundIdx + i].index, + order, + })); +}; diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 8f0242eb30..d32970a278 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -42,7 +42,7 @@ import DMRoomMap from "../../../src/utils/DMRoomMap"; configure({ adapter: new Adapter() }); let client; -const room = new Matrix.Room(); +const room = new Matrix.Room("!roomId:server_name"); // wrap MessagePanel with a component which provides the MatrixClient in the context. class WrappedMessagePanel extends React.Component { diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index bfb8e1afd4..6aad6a90fd 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -6,7 +6,6 @@ import * as TestUtils from '../../../test-utils'; import {MatrixClientPeg} from '../../../../src/MatrixClientPeg'; import sdk from '../../../skinned-sdk'; -import { DragDropContext } from 'react-beautiful-dnd'; import dis from '../../../../src/dispatcher/dispatcher'; import DMRoomMap from '../../../../src/utils/DMRoomMap'; @@ -68,9 +67,7 @@ describe('RoomList', () => { const RoomList = sdk.getComponent('views.rooms.RoomList'); const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList); root = ReactDOM.render( - - {}} /> - , + {}} />, parentDiv, ); ReactTestUtils.findRenderedComponentWithType(root, RoomList); diff --git a/test/end-to-end-tests/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js index abd4488db2..654c461296 100644 --- a/test/end-to-end-tests/src/usecases/room-settings.js +++ b/test/end-to-end-tests/src/usecases/room-settings.js @@ -140,8 +140,6 @@ async function changeRoomSettings(session, settings) { if (settings.alias) { session.log.step(`sets alias to ${settings.alias}`); - const summary = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings summary"); - await summary.click(); const aliasField = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details input[type=text]"); await session.replaceInputText(aliasField, settings.alias.substring(1, settings.alias.lastIndexOf(":"))); const addButton = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details .mx_AccessibleButton"); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 01bd528b87..4cbd9f43c8 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -123,8 +123,15 @@ describe("SpaceStore", () => { jest.runAllTimers(); client.getVisibleRooms.mockReturnValue(rooms = []); getValue.mockImplementation(settingName => { - if (settingName === "feature_spaces") { - return true; + switch (settingName) { + case "feature_spaces": + return true; + case "feature_spaces.all_rooms": + return true; + case "feature_spaces.space_member_dms": + return true; + case "feature_spaces.space_dm_badges": + return false; } }); }); diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts new file mode 100644 index 0000000000..331627dfc0 --- /dev/null +++ b/test/utils/stringOrderField-test.ts @@ -0,0 +1,291 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { sortBy } from "lodash"; +import { averageBetweenStrings, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils"; + +import { midPointsBetweenStrings, reorderLexicographically } from "../../src/utils/stringOrderField"; + +const moveLexicographicallyTest = ( + orders: Array, + fromIndex: number, + toIndex: number, + expectedChanges: number, + maxLength?: number, +): void => { + const ops = reorderLexicographically(orders, fromIndex, toIndex, maxLength); + + const zipped: Array<[number, string | undefined]> = orders.map((o, i) => [i, o]); + ops.forEach(({ index, order }) => { + zipped[index][1] = order; + }); + + const newOrders = sortBy(zipped, i => i[1]); + expect(newOrders[toIndex][0]).toBe(fromIndex); + expect(ops).toHaveLength(expectedChanges); +}; + +describe("stringOrderField", () => { + describe("midPointsBetweenStrings", () => { + it("should work", () => { + expect(averageBetweenStrings("!!", "##")).toBe('""'); + const midpoints = ["a", ...midPointsBetweenStrings("a", "e", 3, 1), "e"].sort(); + expect(midpoints[0]).toBe("a"); + expect(midpoints[4]).toBe("e"); + expect(midPointsBetweenStrings(" ", "!'Tu:}", 1, 50)).toStrictEqual([" S:J\\~"]); + }); + + it("should return empty array when the request is not possible", () => { + expect(midPointsBetweenStrings("a", "e", 0, 1)).toStrictEqual([]); + expect(midPointsBetweenStrings("a", "e", 4, 1)).toStrictEqual([]); + }); + }); + + describe("reorderLexicographically", () => { + it("should work when moving left", () => { + moveLexicographicallyTest(["a", "c", "e", "g", "i"], 2, 1, 1); + }); + + it("should work when moving right", () => { + moveLexicographicallyTest(["a", "c", "e", "g", "i"], 1, 2, 1); + }); + + it("should work when all orders are undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, undefined], + 4, + 1, + 2, + ); + }); + + it("should work when moving to end and all orders are undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, undefined], + 1, + 4, + 5, + ); + }); + + it("should work when moving left and some orders are undefined", () => { + moveLexicographicallyTest( + ["a", "c", "e", undefined, undefined, undefined], + 5, + 2, + 1, + ); + + moveLexicographicallyTest( + ["a", "a", "e", undefined, undefined, undefined], + 5, + 1, + 2, + ); + }); + + it("should work moving to the start when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined], + 2, + 0, + 1, + ); + }); + + it("should work moving to the end when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined], + 1, + 3, + 4, + ); + }); + + it("should work moving left when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, undefined], + 4, + 1, + 2, + ); + }); + + it("should work moving right when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined], + 1, + 2, + 3, + ); + }); + + it("should work moving more right when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, /**/ undefined, undefined], + 1, + 4, + 5, + ); + }); + + it("should work moving left when right is undefined", () => { + moveLexicographicallyTest( + ["20", undefined, undefined, undefined, undefined, undefined], + 4, + 2, + 2, + ); + }); + + it("should work moving right when right is undefined", () => { + moveLexicographicallyTest( + ["50", undefined, undefined, undefined, undefined, /**/ undefined, undefined], + 1, + 4, + 4, + ); + }); + + it("should work moving left when right is defined", () => { + moveLexicographicallyTest( + ["10", "20", "30", "40", undefined, undefined], + 3, + 1, + 1, + ); + }); + + it("should work moving right when right is defined", () => { + moveLexicographicallyTest( + ["10", "20", "30", "40", "50", undefined], + 1, + 3, + 1, + ); + }); + + it("should work moving left when all is defined", () => { + moveLexicographicallyTest( + ["11", "13", "15", "17", "19"], + 2, + 1, + 1, + ); + }); + + it("should work moving right when all is defined", () => { + moveLexicographicallyTest( + ["11", "13", "15", "17", "19"], + 1, + 2, + 1, + ); + }); + + it("should work moving left into no left space", () => { + moveLexicographicallyTest( + ["11", "12", "13", "14", "19"], + 3, + 1, + 2, + 2, + ); + + moveLexicographicallyTest( + [ + DEFAULT_ALPHABET.charAt(0), + // Target + DEFAULT_ALPHABET.charAt(1), + DEFAULT_ALPHABET.charAt(2), + DEFAULT_ALPHABET.charAt(3), + DEFAULT_ALPHABET.charAt(4), + DEFAULT_ALPHABET.charAt(5), + ], + 5, + 1, + 5, + 1, + ); + }); + + it("should work moving right into no right space", () => { + moveLexicographicallyTest( + ["15", "16", "17", "18", "19"], + 1, + 3, + 3, + 2, + ); + + moveLexicographicallyTest( + [ + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1), + ], + 1, + 3, + 3, + 1, + ); + }); + + it("should work moving right into no left space", () => { + moveLexicographicallyTest( + ["11", "12", "13", "14", "15", "16", undefined], + 1, + 3, + 3, + ); + + moveLexicographicallyTest( + ["0", "1", "2", "3", "4", "5"], + 1, + 3, + 3, + 1, + ); + }); + + it("should work moving left into no right space", () => { + moveLexicographicallyTest( + ["15", "16", "17", "18", "19"], + 4, + 3, + 4, + 2, + ); + + moveLexicographicallyTest( + [ + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1), + ], + 4, + 3, + 4, + 1, + ); + }); + }); +}); + diff --git a/yarn.lock b/yarn.lock index 79d79db88f..7ebe0d2ba8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1017,13 +1017,20 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2": + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" + integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" @@ -1479,6 +1486,11 @@ resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8" integrity sha512-PRuFlBBkvdDOtxlIASzTmkEFar+S66Ek48NVVTWMUjtJAdn5vyMSN8y6IZIoIymGpR36q2nZbIYazBWyFxL+IQ== +"@types/diff-match-patch@^1.0.32": + version "1.0.32" + resolved "https://registry.yarnpkg.com/@types/diff-match-patch/-/diff-match-patch-1.0.32.tgz#d9c3b8c914aa8229485351db4865328337a3d09f" + integrity sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A== + "@types/events@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" @@ -1504,6 +1516,14 @@ dependencies: "@types/node" "*" +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -1620,12 +1640,29 @@ dependencies: "@types/node" "*" -"@types/react-dom@^16.9.10": - version "16.9.10" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.10.tgz#4485b0bec3d41f856181b717f45fd7831101156f" - integrity sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw== +"@types/react-beautiful-dnd@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4" + integrity sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg== dependencies: - "@types/react" "^16" + "@types/react" "*" + +"@types/react-dom@^17.0.2": + version "17.0.8" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.8.tgz#3180de6d79bf53762001ad854e3ce49f36dd71fc" + integrity sha512-0ohAiJAx1DAUEcY9UopnfwCE9sSMDGnY/oXjWMax6g3RpzmTt2GMyMVAXcbn0mo8XAff0SbQJl2/SBU+hjSZ1A== + dependencies: + "@types/react" "*" + +"@types/react-redux@^7.1.16": + version "7.1.16" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21" + integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" "@types/react-transition-group@^4.4.0": version "4.4.0" @@ -1634,12 +1671,13 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16", "@types/react@^16.14", "@types/react@^16.9": - version "16.14.2" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c" - integrity sha512-BzzcAlyDxXl2nANlabtT4thtvbbnhee8hMmH/CcJrISDBVcJS1iOsP1f0OAgSdGE0MsY9tqcrb9YoZcOFv9dbQ== +"@types/react@*", "@types/react@^17.0.2": + version "17.0.11" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451" + integrity sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA== dependencies: "@types/prop-types" "*" + "@types/scheduler" "*" csstype "^3.0.2" "@types/sanitize-html@^2.3.1": @@ -1649,6 +1687,11 @@ dependencies: htmlparser2 "^6.0.0" +"@types/scheduler@*": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" + integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -2121,14 +2164,6 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" -babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - bail@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" @@ -2647,11 +2682,6 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= -core-js@^2.4.0: - version "2.6.12" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" - integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== - core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2711,6 +2741,13 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-select@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286" @@ -4240,7 +4277,7 @@ highlight.js@^10.5.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.5.0.tgz#3f09fede6a865757378f2d9ebdcbc15ba268f98f" integrity sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw== -hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -4455,13 +4492,6 @@ internal-slot@^1.0.2: has "^1.0.3" side-channel "^1.0.2" -invariant@^2.2.2, invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -5599,11 +5629,6 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash-es@^4.2.1: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7" - integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA== - lodash.escape@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" @@ -5624,7 +5649,7 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5793,10 +5818,10 @@ mdurl@~1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= -memoize-one@^3.0.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17" - integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA== +memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== meow@^9.0.0: version "9.0.0" @@ -6442,11 +6467,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU= - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -6656,7 +6676,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2: +prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -6733,12 +6753,12 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== -raf-schd@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-2.1.2.tgz#ec622b5167f2912089f054dc03ebd5bcf33c8f62" - integrity sha512-Orl0IEvMtUCgPddgSxtxreK77UiQz4nPYJy9RggVzu4mKsZkQWiAaG1y9HlYWdvm9xtN348xRaT37qkvL/+A+g== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== -raf@^3.1.0, raf@^3.4.1: +raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== @@ -6765,21 +6785,18 @@ re-resizable@^6.9.0: dependencies: fast-memoize "^2.5.1" -react-beautiful-dnd@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81" - integrity sha512-d73RMu4QOFCyjUELLWFyY/EuclnfqulI9pECx+2gIuJvV0ycf1uR88o+1x0RSB9ILD70inHMzCBKNkWVbbt+vA== +react-beautiful-dnd@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d" + integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA== dependencies: - babel-runtime "^6.26.0" - invariant "^2.2.2" - memoize-one "^3.0.1" - prop-types "^15.6.0" - raf-schd "^2.1.0" - react-motion "^0.5.2" - react-redux "^5.0.6" - redux "^3.7.2" - redux-thunk "^2.2.0" - reselect "^3.0.1" + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" react-clientside-effect@^1.2.2: version "1.2.3" @@ -6814,7 +6831,7 @@ react-focus-lock@^2.5.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: +react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -6824,32 +6841,17 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== -react-lifecycles-compat@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-motion@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" - integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ== +react-redux@^7.2.0: + version "7.2.4" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" + integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA== dependencies: - performance-now "^0.2.0" - prop-types "^15.5.8" - raf "^3.1.0" - -react-redux@^5.0.6: - version "5.1.2" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57" - integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q== - dependencies: - "@babel/runtime" "^7.1.2" - hoist-non-react-statics "^3.3.0" - invariant "^2.2.4" - loose-envify "^1.1.0" - prop-types "^15.6.1" - react-is "^16.6.0" - react-lifecycles-compat "^3.0.0" + "@babel/runtime" "^7.12.1" + "@types/react-redux" "^7.1.16" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.13.1" react-shallow-renderer@^16.13.1: version "16.14.1" @@ -6978,20 +6980,12 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -redux-thunk@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" - integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== - -redux@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b" - integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A== +redux@^4.0.0, redux@^4.0.4: + version "4.1.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4" + integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== dependencies: - lodash "^4.2.1" - lodash-es "^4.2.1" - loose-envify "^1.1.0" - symbol-observable "^1.0.3" + "@babel/runtime" "^7.9.2" regenerate-unicode-properties@^8.2.0: version "8.2.0" @@ -7005,11 +6999,6 @@ regenerate@^1.4.0: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - regenerator-runtime@^0.13.4: version "0.13.7" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" @@ -7167,11 +7156,6 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -reselect@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" - integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc= - resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -7899,11 +7883,6 @@ svg-tags@^1.0.0: resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= -symbol-observable@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== - symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -7966,6 +7945,11 @@ through@^2.3.6: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tiny-invariant@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" + integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== + tmatch@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf" @@ -8281,6 +8265,11 @@ use-callback-ref@^1.2.1: resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5" integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg== +use-memo-one@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" + integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== + use-sidecar@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.4.tgz#38398c3723727f9f924bed2343dfa3db6aaaee46"