From 3c06e7f7a098aa78afe1b4ecc11459be0543a954 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 17 Nov 2021 08:19:30 -0700 Subject: [PATCH 01/82] Add option to change the size of images/videos in the timeline (#7017) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Šimon Brandner Co-authored-by: J. Ryan Stinnett Co-authored-by: Timo K --- res/css/_common.scss | 1 + res/css/_components.scss | 1 + res/css/views/messages/_MImageBody.scss | 8 +- res/css/views/messages/_MVideoBody.scss | 4 +- res/css/views/settings/_ImageSizePanel.scss | 47 +++++++++++ .../element-icons/settings/img-size-large.svg | 15 ++++ .../settings/img-size-normal.svg | 20 +++++ src/components/structures/FilePanel.tsx | 2 +- src/components/structures/MessagePanel.tsx | 2 +- .../structures/NotificationPanel.tsx | 2 +- src/components/structures/RoomView.tsx | 2 +- src/components/structures/ThreadPanel.tsx | 2 +- src/components/structures/ThreadView.tsx | 2 +- src/components/structures/TimelinePanel.tsx | 2 +- .../views/dialogs/ForwardDialog.tsx | 2 +- .../views/elements/EventListSummary.tsx | 2 +- .../views/elements/EventTilePreview.tsx | 2 +- .../views/elements/MemberEventListSummary.tsx | 2 +- src/components/views/elements/ReplyChain.tsx | 2 +- src/components/views/messages/MImageBody.tsx | 31 ++++++-- src/components/views/messages/MVideoBody.tsx | 46 ++++++++--- src/components/views/rooms/EventTile.tsx | 2 +- .../views/settings/FontScalingPanel.tsx | 2 +- .../views/settings/ImageSizePanel.tsx | 79 +++++++++++++++++++ .../views/settings/LayoutSwitcher.tsx | 2 +- .../tabs/user/AppearanceUserSettingsTab.tsx | 4 +- src/contexts/RoomContext.ts | 2 +- src/i18n/strings/en_EN.json | 2 + src/settings/Settings.tsx | 7 +- .../NewLayoutSwitcherController.ts | 2 +- src/settings/enums/ImageSize.ts | 33 ++++++++ src/settings/{ => enums}/Layout.ts | 0 .../handlers/DeviceSettingsHandler.ts | 2 +- src/utils/exportUtils/HtmlExport.tsx | 2 +- .../views/rooms/SendMessageComposer-test.tsx | 2 +- 35 files changed, 293 insertions(+), 45 deletions(-) create mode 100644 res/css/views/settings/_ImageSizePanel.scss create mode 100644 res/img/element-icons/settings/img-size-large.svg create mode 100644 res/img/element-icons/settings/img-size-normal.svg create mode 100644 src/components/views/settings/ImageSizePanel.tsx create mode 100644 src/settings/enums/ImageSize.ts rename src/settings/{ => enums}/Layout.ts (100%) diff --git a/res/css/_common.scss b/res/css/_common.scss index 3d8b6659b38..7c8f7326a4c 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -32,6 +32,7 @@ $slider-selection-dot-size: 2.4em; $container-border-width: 8px; +$timeline-image-boarder-radius: 8px; :root { font-size: 10px; diff --git a/res/css/_components.scss b/res/css/_components.scss index e02c4c823be..81b5e3be99e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -252,6 +252,7 @@ @import "./views/settings/_E2eAdvancedPanel.scss"; @import "./views/settings/_EmailAddresses.scss"; @import "./views/settings/_FontScalingPanel.scss"; +@import "./views/settings/_ImageSizePanel.scss"; @import "./views/settings/_IntegrationManager.scss"; @import "./views/settings/_JoinRuleSettings.scss"; @import "./views/settings/_LayoutSwitcher.scss"; diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 0981cbf113e..db06c11e21e 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +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. @@ -14,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -$timelineImageBorderRadius: 4px; +$timeline-image-boarder-radius: 8px; .mx_MImageBody_thumbnail--blurhash { position: absolute; @@ -24,7 +25,7 @@ $timelineImageBorderRadius: 4px; .mx_MImageBody_thumbnail { object-fit: contain; - border-radius: $timelineImageBorderRadius; + border-radius: $timeline-image-boarder-radius; display: flex; justify-content: center; @@ -32,9 +33,10 @@ $timelineImageBorderRadius: 4px; height: 100%; width: 100%; + // this is needed so that the Blurhash can get have rounded corners without beeing the correct size during loading. + overflow: hidden; .mx_Blurhash > canvas { animation: mx--anim-pulse 1.75s infinite cubic-bezier(.4, 0, .6, 1); - border-radius: $timelineImageBorderRadius; } .mx_no-image-placeholder { diff --git a/res/css/views/messages/_MVideoBody.scss b/res/css/views/messages/_MVideoBody.scss index ac3491bc8ff..b5fdaeabef4 100644 --- a/res/css/views/messages/_MVideoBody.scss +++ b/res/css/views/messages/_MVideoBody.scss @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +18,6 @@ span.mx_MVideoBody { video.mx_MVideoBody { max-width: 100%; height: auto; - border-radius: 4px; + border-radius: $timeline-image-boarder-radius; } } diff --git a/res/css/views/settings/_ImageSizePanel.scss b/res/css/views/settings/_ImageSizePanel.scss new file mode 100644 index 00000000000..3b0b982be2e --- /dev/null +++ b/res/css/views/settings/_ImageSizePanel.scss @@ -0,0 +1,47 @@ +/* +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. +*/ + +.mx_ImageSizePanel { + color: $primary-content; + + .mx_ImageSizePanel_radios { + display: flex; + margin-top: 16px; // move away from header a bit + + > label { + margin-right: 68px; // keep the boxes separate + cursor: pointer; + } + + .mx_ImageSizePanel_size { + background-color: $quinary-content; + mask-repeat: no-repeat; + mask-size: 221px; + mask-position: center; + width: 221px; + height: 148px; + margin-bottom: 14px; // move radio button away from bottom edge a bit + + &.mx_ImageSizePanel_sizeDefault { + mask: url("$(res)/img/element-icons/settings/img-size-normal.svg"); + } + + &.mx_ImageSizePanel_sizeLarge { + mask: url("$(res)/img/element-icons/settings/img-size-large.svg"); + } + } + } +} diff --git a/res/img/element-icons/settings/img-size-large.svg b/res/img/element-icons/settings/img-size-large.svg new file mode 100644 index 00000000000..749a5c7ecbf --- /dev/null +++ b/res/img/element-icons/settings/img-size-large.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/res/img/element-icons/settings/img-size-normal.svg b/res/img/element-icons/settings/img-size-normal.svg new file mode 100644 index 00000000000..96d8fd3fb4f --- /dev/null +++ b/res/img/element-icons/settings/img-size-normal.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index c57c2f7ecf2..9a354e4d217 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -36,7 +36,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier'; import TimelinePanel from "./TimelinePanel"; import Spinner from "../views/elements/Spinner"; import { TileShape } from '../views/rooms/EventTile'; -import { Layout } from "../../settings/Layout"; +import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext'; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 6a204775dc0..bb0ee29bb07 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -27,7 +27,7 @@ import { wantsDateSeparator } from '../../DateUtils'; import { MatrixClientPeg } from '../../MatrixClientPeg'; import SettingsStore from '../../settings/SettingsStore'; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; -import { Layout } from "../../settings/Layout"; +import { Layout } from "../../settings/enums/Layout"; import { _t } from "../../languageHandler"; import EventTile, { haveTileForEvent, IReadReceiptProps, TileShape } from "../views/rooms/EventTile"; import { hasText } from "../../TextForEvent"; diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 5d2c5900816..f56d7469f16 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -23,7 +23,7 @@ import { replaceableComponent } from "../../utils/replaceableComponent"; import TimelinePanel from "./TimelinePanel"; import Spinner from "../views/elements/Spinner"; import { TileShape } from "../views/rooms/EventTile"; -import { Layout } from "../../settings/Layout"; +import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 342a8c31f34..833d3d3bccb 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -44,7 +44,7 @@ import RoomViewStore from '../../stores/RoomViewStore'; import RoomScrollStateStore, { ScrollState } from '../../stores/RoomScrollStateStore'; import WidgetEchoStore from '../../stores/WidgetEchoStore'; import SettingsStore from "../../settings/SettingsStore"; -import { Layout } from "../../settings/Layout"; +import { Layout } from "../../settings/enums/Layout"; import AccessibleButton from "../views/elements/AccessibleButton"; import RightPanelStore from "../../stores/RightPanelStore"; import { haveTileForEvent } from "../views/rooms/EventTile"; diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 615b90a631a..f43b6409cdc 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -27,7 +27,7 @@ import { ContextMenuButton } from '../../accessibility/context_menu/ContextMenuB import ContextMenu, { ChevronFace, useContextMenu } from './ContextMenu'; import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext'; import TimelinePanel from './TimelinePanel'; -import { Layout } from '../../settings/Layout'; +import { Layout } from '../../settings/enums/Layout'; import { useEventEmitter } from '../../hooks/useEventEmitter'; import AccessibleButton from '../views/elements/AccessibleButton'; import { TileShape } from '../views/rooms/EventTile'; diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index c07741510ad..615f3eed2a9 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -27,7 +27,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier'; import { TileShape } from '../views/rooms/EventTile'; import MessageComposer from '../views/rooms/MessageComposer'; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; -import { Layout } from '../../settings/Layout'; +import { Layout } from '../../settings/enums/Layout'; import TimelinePanel from './TimelinePanel'; import dis from "../../dispatcher/dispatcher"; import { ActionPayload } from '../../dispatcher/payloads'; diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 495d3c438ff..aa5fda2f286 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -25,7 +25,7 @@ import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event'; import { SyncState } from 'matrix-js-sdk/src/sync.api'; import SettingsStore from "../../settings/SettingsStore"; -import { Layout } from "../../settings/Layout"; +import { Layout } from "../../settings/enums/Layout"; import { _t } from '../../languageHandler'; import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomContext from "../../contexts/RoomContext"; diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 93149021044..c3e1848b11b 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -25,7 +25,7 @@ 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 { Layout } from "../../../settings/enums/Layout"; import { IDialogProps } from "./IDialogProps"; import BaseDialog from "./BaseDialog"; import { avatarUrlForUser } from "../../../Avatar"; diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index cbb0e17b427..69ae1344cbe 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -22,7 +22,7 @@ import MemberAvatar from '../avatars/MemberAvatar'; import { _t } from '../../../languageHandler'; import { useStateToggle } from "../../../hooks/useStateToggle"; import AccessibleButton from "./AccessibleButton"; -import { Layout } from '../../../settings/Layout'; +import { Layout } from '../../../settings/enums/Layout'; interface IProps { // An array of member events to summarise diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index a7ebf40c3a8..5e7c2a725b7 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -22,7 +22,7 @@ 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 { Layout } from "../../../settings/enums/Layout"; import { UIFeature } from "../../../settings/UIFeature"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import Spinner from './Spinner'; diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index 4eb0177fef6..13dcb114f91 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -31,7 +31,7 @@ import { Action } from '../../../dispatcher/actions'; import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; import { jsxJoin } from '../../../utils/ReactUtils'; import { EventType } from 'matrix-js-sdk/src/@types/event'; -import { Layout } from '../../../settings/Layout'; +import { Layout } from '../../../settings/enums/Layout'; const onPinnedMessagesClick = (): void => { defaultDispatcher.dispatch({ diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index 481a8c870bf..64a8ff67015 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -23,7 +23,7 @@ import dis from '../../../dispatcher/dispatcher'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import SettingsStore from "../../../settings/SettingsStore"; -import { Layout } from "../../../settings/Layout"; +import { Layout } from "../../../settings/enums/Layout"; import escapeHtml from "escape-html"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { getUserNameColorClass } from "../../../utils/FormattingUtils"; diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 85821129a1e..d8f2f7c6559 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -35,6 +35,7 @@ import classNames from 'classnames'; import { CSSTransition, SwitchTransition } from 'react-transition-group'; import { logger } from "matrix-js-sdk/src/logger"; +import { ImageSize, suggestedSize as suggestedImageSize } from "../../../settings/enums/ImageSize"; interface IState { decryptedUrl?: string; @@ -58,6 +59,7 @@ export default class MImageBody extends React.Component { private unmounted = true; private image = createRef(); private timeout?: number; + private sizeWatcher: string; constructor(props: IBodyProps) { super(props); @@ -317,12 +319,17 @@ export default class MImageBody extends React.Component { } }, 150); } + + this.sizeWatcher = SettingsStore.watchSetting("Images.size", null, () => { + this.forceUpdate(); // we don't really have a reliable thing to update, so just update the whole thing + }); } componentWillUnmount() { this.unmounted = true; this.context.removeListener('sync', this.onClientSync); this.clearBlurhashTimeout(); + SettingsStore.unwatchSetting(this.sizeWatcher); } protected messageContent( @@ -367,11 +374,25 @@ export default class MImageBody extends React.Component { infoHeight = this.state.loadedImageDimensions.naturalHeight; } - // The maximum height of the thumbnail as it is rendered as an - const maxHeight = forcedHeight || Math.min((this.props.maxImageHeight || 600), infoHeight); - // The maximum width of the thumbnail, as dictated by its natural - // maximum height. - const maxWidth = infoWidth * maxHeight / infoHeight; + // The maximum size of the thumbnail as it is rendered as an + // check for any height constraints + const imageSize = SettingsStore.getValue("Images.size") as ImageSize; + const suggestedAndPossibleWidth = Math.min(suggestedImageSize(imageSize).w, infoWidth); + const aspectRatio = infoWidth / infoHeight; + + let maxWidth; + let maxHeight; + const maxHeightConstraint = forcedHeight || this.props.maxImageHeight || undefined; + if (maxHeightConstraint && maxHeightConstraint * aspectRatio < suggestedAndPossibleWidth) { + // width is dictated by the maximum height that was defined by the props or the function param `forcedHeight` + maxWidth = maxHeightConstraint * aspectRatio; + // there is no need to check for infoHeight here since this is done with `maxHeightConstraint * aspectRatio < suggestedAndPossibleWidth` + maxHeight = maxHeightConstraint; + } else { + // height is dictated by suggestedWidth (based on the Image.size setting) + maxWidth = suggestedAndPossibleWidth; + maxHeight = suggestedAndPossibleWidth / aspectRatio; + } let img = null; let placeholder = null; diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index b2e587e51ab..078886ba49c 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -28,6 +28,7 @@ import { IBodyProps } from "./IBodyProps"; import MFileBody from "./MFileBody"; import { logger } from "matrix-js-sdk/src/logger"; +import { ImageSize, suggestedSize as suggestedVideoSize } from "../../../settings/enums/ImageSize"; interface IState { decryptedUrl?: string; @@ -42,6 +43,7 @@ interface IState { @replaceableComponent("views.messages.MVideoBody") export default class MVideoBody extends React.PureComponent { private videoRef = React.createRef(); + private sizeWatcher: string; constructor(props) { super(props); @@ -57,7 +59,22 @@ export default class MVideoBody extends React.PureComponent }; } - thumbScale(fullWidth: number, fullHeight: number, thumbWidth = 480, thumbHeight = 360) { + private get suggestedDimensions(): { w: number, h: number } { + return suggestedVideoSize(SettingsStore.getValue("Images.size") as ImageSize); + } + + private thumbScale( + fullWidth: number, + fullHeight: number, + thumbWidth?: number, + thumbHeight?: number, + ): number { + if (!thumbWidth || !thumbHeight) { + const dims = this.suggestedDimensions; + thumbWidth = dims.w; + thumbHeight = dims.h; + } + if (!fullWidth || !fullHeight) { // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even // log this because it's spammy @@ -68,14 +85,8 @@ export default class MVideoBody extends React.PureComponent return 1; } const widthMulti = thumbWidth / fullWidth; - const heightMulti = thumbHeight / fullHeight; - if (widthMulti < heightMulti) { - // width is the dominant dimension so scaling will be fixed on that - return widthMulti; - } else { - // height is the dominant dimension so scaling will be fixed on that - return heightMulti; - } + // always scale the videos based on their width. + return widthMulti; } private getContentUrl(): string|null { @@ -152,12 +163,16 @@ export default class MVideoBody extends React.PureComponent } } - async componentDidMount() { - const autoplay = SettingsStore.getValue("autoplayVideo") as boolean; + public async componentDidMount() { + this.sizeWatcher = SettingsStore.watchSetting("Images.size", null, () => { + this.forceUpdate(); // we don't really have a reliable thing to update, so just update the whole thing + }); + this.loadBlurhash(); if (this.props.mediaEventHelper.media.isEncrypted && this.state.decryptedUrl === null) { try { + const autoplay = SettingsStore.getValue("autoplayVideo") as boolean; const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value; if (autoplay) { logger.log("Preloading video"); @@ -189,6 +204,10 @@ export default class MVideoBody extends React.PureComponent } } + public componentWillUnmount() { + SettingsStore.unwatchSetting(this.sizeWatcher); + } + private videoOnPlay = async () => { if (this.hasContentUrl() || this.state.fetchingData || this.state.error) { // We have the file, we are fetching the file, or there is an error. @@ -249,8 +268,9 @@ export default class MVideoBody extends React.PureComponent const contentUrl = this.getContentUrl(); const thumbUrl = this.getThumbUrl(); - let height = null; - let width = null; + const defaultDims = this.suggestedDimensions; + let height = defaultDims.h; + let width = defaultDims.w; let poster = null; let preload = "metadata"; if (content.info) { diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 0745a6b1919..0ed29d18b63 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -28,7 +28,7 @@ import { _t } from '../../../languageHandler'; import { hasText } from "../../../TextForEvent"; import * as sdk from "../../../index"; import dis from '../../../dispatcher/dispatcher'; -import { Layout } from "../../../settings/Layout"; +import { Layout } from "../../../settings/enums/Layout"; import { formatTime } from "../../../DateUtils"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { ALL_RULE_TYPES } from "../../../mjolnir/BanList"; diff --git a/src/components/views/settings/FontScalingPanel.tsx b/src/components/views/settings/FontScalingPanel.tsx index aabfc1c9a48..ab701731d20 100644 --- a/src/components/views/settings/FontScalingPanel.tsx +++ b/src/components/views/settings/FontScalingPanel.tsx @@ -22,7 +22,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import Slider from "../elements/Slider"; import { FontWatcher } from "../../../settings/watchers/FontWatcher"; import { IValidationResult, IFieldState } from '../elements/Validation'; -import { Layout } from "../../../settings/Layout"; +import { Layout } from "../../../settings/enums/Layout"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { SettingLevel } from "../../../settings/SettingLevel"; import { _t } from "../../../languageHandler"; diff --git a/src/components/views/settings/ImageSizePanel.tsx b/src/components/views/settings/ImageSizePanel.tsx new file mode 100644 index 00000000000..bacdd6144d3 --- /dev/null +++ b/src/components/views/settings/ImageSizePanel.tsx @@ -0,0 +1,79 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import SettingsStore from "../../../settings/SettingsStore"; +import StyledRadioButton from "../elements/StyledRadioButton"; +import { _t } from "../../../languageHandler"; +import { SettingLevel } from "../../../settings/SettingLevel"; +import { ImageSize } from "../../../settings/enums/ImageSize"; + +interface IProps { + // none +} + +interface IState { + size: ImageSize; +} + +export default class ImageSizePanel extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + size: SettingsStore.getValue("Images.size"), + }; + } + + private onSizeChange = (ev: React.ChangeEvent): void => { + const newSize = ev.target.value as ImageSize; + this.setState({ size: newSize }); + + // noinspection JSIgnoredPromiseFromCall + SettingsStore.setValue("Images.size", null, SettingLevel.ACCOUNT, newSize); + }; + + public render(): JSX.Element { + return ( +
+ + { _t("Image size in the timeline") } + + +
+
); } diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index 605df72f0dd..8329335831e 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -17,7 +17,7 @@ limitations under the License. import { createContext } from "react"; import { IRoomState } from "../components/structures/RoomView"; -import { Layout } from "../settings/Layout"; +import { Layout } from "../settings/enums/Layout"; export enum TimelineRenderingType { Room = "Room", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 752e4135fed..76e752fa7bb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1182,6 +1182,8 @@ "Size must be a number": "Size must be a number", "Custom font size can only be between %(min)s pt and %(max)s pt": "Custom font size can only be between %(min)s pt and %(max)s pt", "Use between %(min)s pt and %(max)s pt": "Use between %(min)s pt and %(max)s pt", + "Image size in the timeline": "Image size in the timeline", + "Large": "Large", "Connecting to integration manager...": "Connecting to integration manager...", "Cannot connect to integration manager": "Cannot connect to integration manager", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index c263317dc49..d83198fc56d 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -37,11 +37,12 @@ import { isMac } from '../Keyboard'; import UIFeatureController from "./controllers/UIFeatureController"; import { UIFeature } from "./UIFeature"; import { OrderedMultiController } from "./controllers/OrderedMultiController"; -import { Layout } from "./Layout"; +import { Layout } from "./enums/Layout"; import ReducedMotionController from './controllers/ReducedMotionController'; import IncompatibleController from "./controllers/IncompatibleController"; import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController'; import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController'; +import { ImageSize } from "./enums/ImageSize"; import { MetaSpace } from "../stores/spaces"; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times @@ -737,6 +738,10 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: Layout.Group, }, + "Images.size": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: ImageSize.Normal, + }, "showChatEffects": { supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM, displayName: _td("Show chat effects (animations when receiving e.g. confetti)"), diff --git a/src/settings/controllers/NewLayoutSwitcherController.ts b/src/settings/controllers/NewLayoutSwitcherController.ts index b1d6cac55e6..e2ea710e080 100644 --- a/src/settings/controllers/NewLayoutSwitcherController.ts +++ b/src/settings/controllers/NewLayoutSwitcherController.ts @@ -14,7 +14,7 @@ limitations under the License. import SettingController from "./SettingController"; import { SettingLevel } from "../SettingLevel"; import SettingsStore from "../SettingsStore"; -import { Layout } from "../Layout"; +import { Layout } from "../enums/Layout"; export default class NewLayoutSwitcherController extends SettingController { public onChange(level: SettingLevel, roomId: string, newValue: any) { diff --git a/src/settings/enums/ImageSize.ts b/src/settings/enums/ImageSize.ts new file mode 100644 index 00000000000..47f16ddc834 --- /dev/null +++ b/src/settings/enums/ImageSize.ts @@ -0,0 +1,33 @@ +/* +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. +*/ + +const SIZE_LARGE = { w: 480, h: 360 }; +const SIZE_NORMAL = { w: 324, h: 220 }; + +export enum ImageSize { + Normal = "normal", + Large = "large", +} + +export function suggestedSize(size: ImageSize): { w: number, h: number } { + switch (size) { + case ImageSize.Large: + return SIZE_LARGE; + case ImageSize.Normal: + default: + return SIZE_NORMAL; + } +} diff --git a/src/settings/Layout.ts b/src/settings/enums/Layout.ts similarity index 100% rename from src/settings/Layout.ts rename to src/settings/enums/Layout.ts diff --git a/src/settings/handlers/DeviceSettingsHandler.ts b/src/settings/handlers/DeviceSettingsHandler.ts index e57862a8243..f7a9fe9108d 100644 --- a/src/settings/handlers/DeviceSettingsHandler.ts +++ b/src/settings/handlers/DeviceSettingsHandler.ts @@ -20,7 +20,7 @@ import SettingsHandler from "./SettingsHandler"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import { SettingLevel } from "../SettingLevel"; import { CallbackFn, WatchManager } from "../WatchManager"; -import { Layout } from "../Layout"; +import { Layout } from "../enums/Layout"; /** * Gets and sets settings at the "device" level for the current device. diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index f7d3bb5e09a..7c8265fd32c 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -21,7 +21,7 @@ import { mediaFromMxc } from "../../customisations/Media"; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { renderToStaticMarkup } from "react-dom/server"; -import { Layout } from "../../settings/Layout"; +import { Layout } from "../../settings/enums/Layout"; import { shouldFormContinuation } from "../../components/structures/MessagePanel"; import { formatFullDateNoDayNoTime, wantsDateSeparator } from "../../DateUtils"; import { RoomPermalinkCreator } from "../permalinks/Permalinks"; diff --git a/test/components/views/rooms/SendMessageComposer-test.tsx b/test/components/views/rooms/SendMessageComposer-test.tsx index ec4894719e5..9c1a558a866 100644 --- a/test/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/components/views/rooms/SendMessageComposer-test.tsx @@ -35,7 +35,7 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import SpecPermalinkConstructor from "../../../../src/utils/permalinks/SpecPermalinkConstructor"; import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import DocumentOffset from '../../../../src/editor/offset'; -import { Layout } from '../../../../src/settings/Layout'; +import { Layout } from '../../../../src/settings/enums/Layout'; jest.mock("../../../../src/stores/RoomViewStore"); From 8b6778e1d3b4a2c0cf22d666a21a668bd5a9af66 Mon Sep 17 00:00:00 2001 From: James Salter Date: Thu, 18 Nov 2021 09:01:32 +1100 Subject: [PATCH 02/82] Pass advanced_disable_decide to stop posthog contacting its decide endpoint (#7149) --- src/PosthogAnalytics.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index bdc0814b5dc..f071a562414 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -160,6 +160,7 @@ export class PosthogAnalytics { capture_pageview: false, sanitize_properties: this.sanitizeProperties, respect_dnt: true, + advanced_disable_decide: true, }); this.enabled = true; } else { From e549438e2af0d4ff3ed03953de58960e9735442b Mon Sep 17 00:00:00 2001 From: James Salter Date: Thu, 18 Nov 2021 09:01:45 +1100 Subject: [PATCH 03/82] Add logging for a bunch of silently swallowed errors (#7148) --- src/BasePlatform.ts | 8 ++++++-- src/Lifecycle.ts | 12 +++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index f3b26ec5104..9aabe82be5c 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -317,7 +317,9 @@ export default abstract class BasePlatform { let data; try { data = await idbLoad("pickleKey", [userId, deviceId]); - } catch (e) {} + } catch (e) { + logger.error("idbLoad for pickleKey failed", e); + } if (!data) { return null; } @@ -396,6 +398,8 @@ export default abstract class BasePlatform { async destroyPickleKey(userId: string, deviceId: string): Promise { try { await idbDelete("pickleKey", [userId, deviceId]); - } catch (e) {} + } catch (e) { + logger.error("idbDelete failed in destroyPickleKey", e); + } } } diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index d53bd39c958..d179f9bed37 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -324,7 +324,9 @@ export async function getStoredSessionVars(): Promise { let accessToken; try { accessToken = await StorageManager.idbLoad("account", "mx_access_token"); - } catch (e) {} + } catch (e) { + logger.error("StorageManager.idbLoad failed for account:mx_access_token", e); + } if (!accessToken) { accessToken = localStorage.getItem("mx_access_token"); if (accessToken) { @@ -332,7 +334,9 @@ export async function getStoredSessionVars(): Promise { // try to migrate access token to IndexedDB if we can await StorageManager.idbSave("account", "mx_access_token", accessToken); localStorage.removeItem("mx_access_token"); - } catch (e) {} + } catch (e) { + logger.error("migration of access token to IndexedDB failed", e); + } } } // if we pre-date storing "mx_has_access_token", but we retrieved an access @@ -858,7 +862,9 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise Date: Thu, 18 Nov 2021 12:47:11 +0000 Subject: [PATCH 04/82] Chat Effects & Commands in thread context (#7138) --- src/SlashCommands.tsx | 32 +++++++++++++++++-- src/autocomplete/AutocompleteProvider.tsx | 14 +++++++- src/autocomplete/Autocompleter.ts | 5 +-- src/autocomplete/CommandProvider.tsx | 14 +++++--- src/autocomplete/CommunityProvider.tsx | 6 ++-- src/autocomplete/EmojiProvider.tsx | 6 ++-- src/autocomplete/NotifProvider.tsx | 8 ++--- src/autocomplete/QueryMatcher.ts | 2 ++ src/autocomplete/RoomProvider.tsx | 5 +-- src/autocomplete/UserProvider.tsx | 9 ++++-- src/components/structures/RoomView.tsx | 6 +++- src/components/views/rooms/Autocomplete.tsx | 6 ++-- .../views/rooms/SendMessageComposer.tsx | 7 +++- src/effects/utils.ts | 5 ++- src/stores/widgets/StopGapWidgetDriver.ts | 14 +++++--- 15 files changed, 108 insertions(+), 31 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index c8884cead42..f172e6ecc60 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -57,6 +57,7 @@ import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpD import { logger } from "matrix-js-sdk/src/logger"; import { shouldShowComponent } from "./customisations/helpers/UIComponents"; +import { TimelineRenderingType } from './contexts/RoomContext'; // XXX: workaround for /~https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -102,6 +103,7 @@ interface ICommandOpts { category: string; hideCompletionAfterSpace?: boolean; isEnabled?(): boolean; + renderingTypes?: TimelineRenderingType[]; } export class Command { @@ -112,7 +114,8 @@ export class Command { runFn: undefined | RunFn; category: string; hideCompletionAfterSpace: boolean; - _isEnabled?: () => boolean; + private _isEnabled?: () => boolean; + public renderingTypes?: TimelineRenderingType[]; constructor(opts: ICommandOpts) { this.command = opts.command; @@ -123,6 +126,7 @@ export class Command { this.category = opts.category || CommandCategories.other; this.hideCompletionAfterSpace = opts.hideCompletionAfterSpace || false; this._isEnabled = opts.isEnabled; + this.renderingTypes = opts.renderingTypes; } getCommand() { @@ -143,7 +147,7 @@ export class Command { return _t('Usage') + ': ' + this.getCommandWithArgs(); } - isEnabled() { + isEnabled(): boolean { return this._isEnabled ? this._isEnabled() : true; } } @@ -271,6 +275,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'nick', @@ -283,6 +288,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'myroomnick', @@ -302,6 +308,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'roomavatar', @@ -319,6 +326,7 @@ export const Commands = [ })); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'myroomavatar', @@ -345,6 +353,7 @@ export const Commands = [ })); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'myavatar', @@ -362,6 +371,7 @@ export const Commands = [ })); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'topic', @@ -387,6 +397,7 @@ export const Commands = [ return success(); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'roomname', @@ -399,6 +410,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'invite', @@ -462,6 +474,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'join', @@ -577,6 +590,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'part', @@ -620,6 +634,7 @@ export const Commands = [ return success(leaveRoomBehaviour(targetRoomId)); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'kick', @@ -635,6 +650,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'ban', @@ -650,6 +666,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'unban', @@ -666,6 +683,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'ignore', @@ -755,6 +773,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'deop', @@ -776,6 +795,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'devtools', @@ -838,6 +858,7 @@ export const Commands = [ } }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'verify', @@ -903,6 +924,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.advanced, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'discardsession', @@ -916,6 +938,7 @@ export const Commands = [ return success(); }, category: CommandCategories.advanced, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: "rainbow", @@ -1053,6 +1076,7 @@ export const Commands = [ call.setRemoteOnHold(true); return success(); }, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: "unholdcall", @@ -1066,6 +1090,7 @@ export const Commands = [ call.setRemoteOnHold(false); return success(); }, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: "converttodm", @@ -1075,6 +1100,7 @@ export const Commands = [ const room = MatrixClientPeg.get().getRoom(roomId); return success(guessAndSetDMRoom(room, true)); }, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: "converttoroom", @@ -1084,6 +1110,7 @@ export const Commands = [ const room = MatrixClientPeg.get().getRoom(roomId); return success(guessAndSetDMRoom(room, false)); }, + renderingTypes: [TimelineRenderingType.Room], }), // Command definitions for autocompletion ONLY: @@ -1117,6 +1144,7 @@ export const Commands = [ })()); }, category: CommandCategories.effects, + renderingTypes: [TimelineRenderingType.Room], }); }), ]; diff --git a/src/autocomplete/AutocompleteProvider.tsx b/src/autocomplete/AutocompleteProvider.tsx index 2d82a9f5910..14ade84bf35 100644 --- a/src/autocomplete/AutocompleteProvider.tsx +++ b/src/autocomplete/AutocompleteProvider.tsx @@ -17,6 +17,7 @@ limitations under the License. */ import React from 'react'; +import { TimelineRenderingType } from '../contexts/RoomContext'; import type { ICompletion, ISelectionRange } from './Autocompleter'; export interface ICommand { @@ -27,11 +28,19 @@ export interface ICommand { }; } +export interface IAutocompleteOptions { + commandRegex?: RegExp; + forcedCommandRegex?: RegExp; + renderingType?: TimelineRenderingType; +} + export default abstract class AutocompleteProvider { commandRegex: RegExp; forcedCommandRegex: RegExp; - protected constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) { + protected renderingType: TimelineRenderingType = TimelineRenderingType.Room; + + protected constructor({ commandRegex, forcedCommandRegex, renderingType }: IAutocompleteOptions) { if (commandRegex) { if (!commandRegex.global) { throw new Error('commandRegex must have global flag set'); @@ -44,6 +53,9 @@ export default abstract class AutocompleteProvider { } this.forcedCommandRegex = forcedCommandRegex; } + if (renderingType) { + this.renderingType = renderingType; + } } destroy() { diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 555429e75fc..aeee335e04d 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -28,6 +28,7 @@ import { timeout } from "../utils/promise"; import AutocompleteProvider, { ICommand } from "./AutocompleteProvider"; import SpaceProvider from "./SpaceProvider"; import SpaceStore from "../stores/spaces/SpaceStore"; +import { TimelineRenderingType } from '../contexts/RoomContext'; export interface ISelectionRange { beginning?: boolean; // whether the selection is in the first block of the editor or not @@ -75,10 +76,10 @@ export default class Autocompleter { room: Room; providers: AutocompleteProvider[]; - constructor(room: Room) { + constructor(room: Room, renderingType: TimelineRenderingType = TimelineRenderingType.Room) { this.room = room; this.providers = PROVIDERS.map((Prov) => { - return new Prov(room); + return new Prov(room, renderingType); }); } diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx index 143b7e4cdc6..13daadbcf63 100644 --- a/src/autocomplete/CommandProvider.tsx +++ b/src/autocomplete/CommandProvider.tsx @@ -24,17 +24,20 @@ import QueryMatcher from './QueryMatcher'; import { TextualCompletion } from './Components'; import { ICompletion, ISelectionRange } from "./Autocompleter"; import { Command, Commands, CommandMap } from '../SlashCommands'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { TimelineRenderingType } from '../contexts/RoomContext'; const COMMAND_RE = /(^\/\w*)(?: .*)?/g; export default class CommandProvider extends AutocompleteProvider { matcher: QueryMatcher; - constructor() { - super(COMMAND_RE); + constructor(room: Room, renderingType?: TimelineRenderingType) { + super({ commandRegex: COMMAND_RE, renderingType }); this.matcher = new QueryMatcher(Commands, { keys: ['command', 'args', 'description'], funcs: [({ aliases }) => aliases.join(" ")], // aliases + context: renderingType, }); } @@ -47,7 +50,7 @@ export default class CommandProvider extends AutocompleteProvider { const { command, range } = this.getCurrentCommand(query, selection); if (!command) return []; - let matches = []; + let matches: Command[] = []; // check if the full match differs from the first word (i.e. returns false if the command has args) if (command[0] !== command[1]) { // The input looks like a command with arguments, perform exact match @@ -68,7 +71,10 @@ export default class CommandProvider extends AutocompleteProvider { } } - return matches.filter(cmd => cmd.isEnabled()).map((result) => { + return matches.filter(cmd => { + const display = !cmd.renderingTypes || cmd.renderingTypes.includes(this.renderingType); + return cmd.isEnabled() && display; + }).map((result) => { let completion = result.getCommand() + ' '; const usedAlias = result.aliases.find(alias => `/${alias}` === command[1]); // If the command (or an alias) is the same as the one they entered, we don't want to discard their arguments diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index 4b42f4c64e8..3e967063abd 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -28,6 +28,8 @@ import { ICompletion, ISelectionRange } from "./Autocompleter"; import FlairStore from "../stores/FlairStore"; import { mediaFromMxc } from "../customisations/Media"; import BaseAvatar from '../components/views/avatars/BaseAvatar'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { TimelineRenderingType } from '../contexts/RoomContext'; const COMMUNITY_REGEX = /\B\+\S*/g; @@ -43,8 +45,8 @@ function score(query, space) { export default class CommunityProvider extends AutocompleteProvider { matcher: QueryMatcher; - constructor() { - super(COMMUNITY_REGEX); + constructor(room: Room, renderingType?: TimelineRenderingType) { + super({ commandRegex: COMMUNITY_REGEX, renderingType }); this.matcher = new QueryMatcher([], { keys: ['groupId', 'name', 'shortDescription'], }); diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 326651e037a..c43eba0bf17 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -28,6 +28,8 @@ import SettingsStore from "../settings/SettingsStore"; import { EMOJI, IEmoji } from '../emoji'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { TimelineRenderingType } from '../contexts/RoomContext'; const LIMIT = 20; @@ -64,8 +66,8 @@ export default class EmojiProvider extends AutocompleteProvider { matcher: QueryMatcher; nameMatcher: QueryMatcher; - constructor() { - super(EMOJI_REGEX); + constructor(room: Room, renderingType?: TimelineRenderingType) { + super({ commandRegex: EMOJI_REGEX, renderingType }); this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)], diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index aa4f1174dc9..52e235a1b1c 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -23,15 +23,13 @@ import { MatrixClientPeg } from '../MatrixClientPeg'; import { PillCompletion } from './Components'; import { ICompletion, ISelectionRange } from "./Autocompleter"; import RoomAvatar from '../components/views/avatars/RoomAvatar'; +import { TimelineRenderingType } from '../contexts/RoomContext'; const AT_ROOM_REGEX = /@\S*/g; export default class NotifProvider extends AutocompleteProvider { - room: Room; - - constructor(room) { - super(AT_ROOM_REGEX); - this.room = room; + constructor(public room: Room, renderingType?: TimelineRenderingType) { + super({ commandRegex: AT_ROOM_REGEX, renderingType }); } async getCompletions( diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 3948be301ce..bdfd94790b9 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -18,6 +18,7 @@ limitations under the License. import { at, uniq } from 'lodash'; import { removeHiddenChars } from "matrix-js-sdk/src/utils"; +import { TimelineRenderingType } from '../contexts/RoomContext'; interface IOptions { keys: Array; @@ -25,6 +26,7 @@ interface IOptions { shouldMatchWordsOnly?: boolean; // whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true fuzzy?: boolean; + context?: TimelineRenderingType; } /** diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index ced0e7ad177..1879c85fe3f 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -29,6 +29,7 @@ import { makeRoomPermalink } from "../utils/permalinks/Permalinks"; import { ICompletion, ISelectionRange } from "./Autocompleter"; import RoomAvatar from '../components/views/avatars/RoomAvatar'; import SpaceStore from "../stores/spaces/SpaceStore"; +import { TimelineRenderingType } from "../contexts/RoomContext"; const ROOM_REGEX = /\B#\S*/g; @@ -48,8 +49,8 @@ function matcherObject(room: Room, displayedAlias: string, matchName = "") { export default class RoomProvider extends AutocompleteProvider { protected matcher: QueryMatcher; - constructor() { - super(ROOM_REGEX); + constructor(room: Room, renderingType?: TimelineRenderingType) { + super({ commandRegex: ROOM_REGEX, renderingType }); this.matcher = new QueryMatcher([], { keys: ['displayedAlias', 'matchName'], }); diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 48854657de4..e4b285df84d 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -33,6 +33,7 @@ import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; import { makeUserPermalink } from "../utils/permalinks/Permalinks"; import { ICompletion, ISelectionRange } from "./Autocompleter"; import MemberAvatar from '../components/views/avatars/MemberAvatar'; +import { TimelineRenderingType } from '../contexts/RoomContext'; const USER_REGEX = /\B@\S*/g; @@ -50,8 +51,12 @@ export default class UserProvider extends AutocompleteProvider { users: RoomMember[]; room: Room; - constructor(room: Room) { - super(USER_REGEX, FORCED_USER_REGEX); + constructor(room: Room, renderingType?: TimelineRenderingType) { + super({ + commandRegex: USER_REGEX, + forcedCommandRegex: FORCED_USER_REGEX, + renderingType, + }); this.room = room; this.matcher = new QueryMatcher([], { keys: ['name'], diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 833d3d3bccb..98d55f8ae6e 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -957,7 +957,11 @@ export class RoomView extends React.Component { CHAT_EFFECTS.forEach(effect => { if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) { - dis.dispatch({ action: `effects.${effect.command}` }); + // For initial threads launch, chat effects are disabled + // see #19731 + if (!SettingsStore.getValue("feature_thread") || !ev.isThreadRelation) { + dis.dispatch({ action: `effects.${effect.command}` }); + } } }); }; diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 34909baef13..0991a230ab4 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -24,6 +24,7 @@ import { Room } from 'matrix-js-sdk/src/models/room'; import SettingsStore from "../../../settings/SettingsStore"; import Autocompleter from '../../../autocomplete/Autocompleter'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import RoomContext from '../../../contexts/RoomContext'; const MAX_PROVIDER_MATCHES = 20; @@ -57,11 +58,11 @@ export default class Autocomplete extends React.PureComponent { debounceCompletionsRequest: number; private containerRef = createRef(); + public static contextType = RoomContext; + constructor(props) { super(props); - this.autocompleter = new Autocompleter(props.room); - this.state = { // list of completionResults, each containing completions completions: [], @@ -82,6 +83,7 @@ export default class Autocomplete extends React.PureComponent { } componentDidMount() { + this.autocompleter = new Autocompleter(this.props.room, this.context.timelineRenderingType); this.applyNewProps(); } diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index ba8f99f47fe..3b1db6bb902 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -492,7 +492,12 @@ export class SendMessageComposer extends React.Component { if (containsEmoji(content, effect.emojis)) { - dis.dispatch({ action: `effects.${effect.command}` }); + // For initial threads launch, chat effects are disabled + // see #19731 + const isNotThread = this.props.relation?.rel_type !== RelationType.Thread; + if (!SettingsStore.getValue("feature_thread") || !isNotThread) { + dis.dispatch({ action: `effects.${effect.command}` }); + } } }); if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) { diff --git a/src/effects/utils.ts b/src/effects/utils.ts index 9f6d7a512e0..559c8e04bba 100644 --- a/src/effects/utils.ts +++ b/src/effects/utils.ts @@ -14,11 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ + +import { IContent } from "matrix-js-sdk/src/models/event"; + /** * Checks a message if it contains one of the provided emojis * @param {Object} content The message * @param {Array} emojis The list of emojis to check for */ -export const containsEmoji = (content: { msgtype: string, body: string }, emojis: Array): boolean => { +export const containsEmoji = (content: IContent, emojis: Array): boolean => { return emojis.some((emoji) => content.body && content.body.includes(emoji)); }; diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index da844b19235..d52d7d9a8ef 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -38,15 +38,16 @@ import WidgetCapabilitiesPromptDialog from "../../components/views/dialogs/Widge import { WidgetPermissionCustomisations } from "../../customisations/WidgetPermissions"; import { OIDCState, WidgetPermissionStore } from "./WidgetPermissionStore"; import { WidgetType } from "../../widgets/WidgetType"; -import { EventType } from "matrix-js-sdk/src/@types/event"; +import { EventType, RelationType } from "matrix-js-sdk/src/@types/event"; import { CHAT_EFFECTS } from "../../effects"; import { containsEmoji } from "../../effects/utils"; import dis from "../../dispatcher/dispatcher"; import { tryTransformPermalinkToLocalHref } from "../../utils/permalinks/Permalinks"; -import { IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { IContent, IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Room } from "matrix-js-sdk/src/models/room"; import { logger } from "matrix-js-sdk/src/logger"; +import SettingsStore from "../../settings/SettingsStore"; // TODO: Purge this from the universe @@ -141,7 +142,7 @@ export class StopGapWidgetDriver extends WidgetDriver { public async sendEvent( eventType: string, - content: any, + content: IContent, stateKey: string = null, targetRoomId: string = null, ): Promise { @@ -164,7 +165,12 @@ export class StopGapWidgetDriver extends WidgetDriver { if (eventType === EventType.RoomMessage) { CHAT_EFFECTS.forEach((effect) => { if (containsEmoji(content, effect.emojis)) { - dis.dispatch({ action: `effects.${effect.command}` }); + // For initial threads launch, chat effects are disabled + // see #19731 + const isNotThread = content["m.relates_to"].rel_type !== RelationType.Thread; + if (!SettingsStore.getValue("feature_thread") || !isNotThread) { + dis.dispatch({ action: `effects.${effect.command}` }); + } } }); } From 28d7a51bb9d7963222db9ce51949cced50fde322 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 18 Nov 2021 13:50:37 -0700 Subject: [PATCH 05/82] Fix improper splice when determining vias for tombstones (#7159) `splice` takes one element from the array whereas `slice` takes all after a given point. We want `slice` so we can pick up ipv6 addresses and port numbers. --- src/components/views/rooms/MessageComposer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index a9bf62f887b..bb6671e5b01 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -370,7 +370,7 @@ export default class MessageComposer extends React.Component { if (createEvent && createEvent.getId()) createEventId = createEvent.getId(); } - const viaServers = [this.state.tombstone.getSender().split(':').splice(1).join(':')]; + const viaServers = [this.state.tombstone.getSender().split(':').slice(1).join(':')]; dis.dispatch({ action: 'view_room', highlighted: true, From e4d645e36002ad98d39a671cc0b3f9a2b7a1a302 Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 19 Nov 2021 09:07:12 +0000 Subject: [PATCH 06/82] Hide MFileBody download link for threaded messages (#7127) Co-authored-by: Travis Ralston --- src/components/views/messages/MImageBody.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index d8f2f7c6559..543c3832830 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -35,6 +35,7 @@ import classNames from 'classnames'; import { CSSTransition, SwitchTransition } from 'react-transition-group'; import { logger } from "matrix-js-sdk/src/logger"; +import { TileShape } from '../rooms/EventTile'; import { ImageSize, suggestedSize as suggestedImageSize } from "../../../settings/enums/ImageSize"; interface IState { @@ -517,8 +518,14 @@ export default class MImageBody extends React.Component { // Overidden by MStickerBody protected getFileBody(): string | JSX.Element { if (this.props.forExport) return null; - // We only ever need the download bar if we're appearing outside of the timeline - if (this.props.tileShape) { + /* + * In the room timeline or the thread context we don't need the download + * link as the message action bar will fullfil that + */ + const hasMessageActionBar = !this.props.tileShape + || this.props.tileShape === TileShape.Thread + || this.props.tileShape === TileShape.ThreadPanel; + if (!hasMessageActionBar) { return ; } } From c457da4c6b60853defeb1d8958ea23352551d4e2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Nov 2021 10:07:55 +0000 Subject: [PATCH 07/82] Room Context Menu should respond to tag changes (#7154) --- src/components/views/context_menus/RoomContextMenu.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx index f0aa5ab1333..38cbdd69e4f 100644 --- a/src/components/views/context_menus/RoomContextMenu.tsx +++ b/src/components/views/context_menus/RoomContextMenu.tsx @@ -28,7 +28,7 @@ import { _t } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { ButtonEvent } from "../elements/AccessibleButton"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; -import RoomListStore from "../../../stores/room-list/RoomListStore"; +import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import dis from "../../../dispatcher/dispatcher"; import RoomListActions from "../../../actions/RoomListActions"; import { Key } from "../../../Keyboard"; @@ -43,6 +43,7 @@ import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRight import { Action } from "../../../dispatcher/actions"; import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; import { ROOM_NOTIFICATIONS_TAB } from "../dialogs/RoomSettingsDialog"; +import { useEventEmitterState } from "../../../hooks/useEventEmitter"; interface IProps extends IContextMenuProps { room: Room; @@ -50,7 +51,11 @@ interface IProps extends IContextMenuProps { const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => { const cli = useContext(MatrixClientContext); - const roomTags = RoomListStore.instance.getTagsForRoom(room); + const roomTags = useEventEmitterState( + RoomListStore.instance, + LISTS_UPDATE_EVENT, + () => RoomListStore.instance.getTagsForRoom(room), + ); let leaveOption: JSX.Element; if (roomTags.includes(DefaultTagID.Archived)) { From c2ab59381682dde79e6a6266da7f69ca91d00456 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Nov 2021 10:08:57 +0000 Subject: [PATCH 08/82] Prevent custom power levels from breaking roles & permissions tab (#7160) --- .../views/settings/tabs/room/RolesRoomSettingsTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 3e2c68de3a6..fc09748870f 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -414,9 +414,9 @@ export default class RolesRoomSettingsTab extends React.Component { } const eventPowerSelectors = Object.keys(eventsLevels).map((eventType, i) => { - if (isSpaceRoom && plEventsToShow[eventType].hideForSpace) { + if (isSpaceRoom && plEventsToShow[eventType]?.hideForSpace) { return null; - } else if (!isSpaceRoom && plEventsToShow[eventType].hideForRoom) { + } else if (!isSpaceRoom && plEventsToShow[eventType]?.hideForRoom) { return null; } From 52e391a92a13284238a0f997bb8dec95f8e2e303 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Nov 2021 10:17:05 +0000 Subject: [PATCH 09/82] Show space members when not invited even if summary didn't fail (#7153) --- src/components/structures/SpaceRoomView.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index becd05b9c99..da9834d2adb 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -128,6 +128,7 @@ const useMyRoomMembership = (room: Room) => { }; const SpaceInfo = ({ space }: { space: Room }) => { + // summary will begin as undefined whilst loading and go null if it fails to load. const summary = useAsyncMemo(async () => { if (space.getMyMembership() !== "invite") return; try { @@ -156,7 +157,7 @@ const SpaceInfo = ({ space }: { space: Room }) => { memberSection = { _t("%(count)s members", { count: summary.num_joined_members }) } ; - } else if (summary === null) { + } else if (summary !== undefined) { // summary is not still loading memberSection = { (count) => count > 0 ? ( Date: Fri, 19 Nov 2021 10:49:34 +0000 Subject: [PATCH 10/82] Disable op/deop commands where user has no permissions (#7161) --- src/SlashCommands.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index f172e6ecc60..1ceaf910aef 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -19,6 +19,7 @@ limitations under the License. import * as React from 'react'; import { User } from "matrix-js-sdk/src/models/user"; +import { EventType } from "matrix-js-sdk/src/@types/event"; import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers'; import { MatrixClientPeg } from './MatrixClientPeg'; @@ -58,6 +59,7 @@ import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpD import { logger } from "matrix-js-sdk/src/logger"; import { shouldShowComponent } from "./customisations/helpers/UIComponents"; import { TimelineRenderingType } from './contexts/RoomContext'; +import RoomViewStore from "./stores/RoomViewStore"; // XXX: workaround for /~https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -748,6 +750,11 @@ export const Commands = [ command: 'op', args: ' []', description: _td('Define the power level of a user'), + isEnabled(): boolean { + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(RoomViewStore.getRoomId()); + return room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId()); + }, runFn: function(roomId, args) { if (args) { const matches = args.match(/^(\S+?)( +(-?\d+))?$/); @@ -779,6 +786,11 @@ export const Commands = [ command: 'deop', args: '', description: _td('Deops user with given id'), + isEnabled(): boolean { + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(RoomViewStore.getRoomId()); + return room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId()); + }, runFn: function(roomId, args) { if (args) { const matches = args.match(/^(\S+)$/); From bd09fc22312be9fd7df79275d1037609ac08317f Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Fri, 19 Nov 2021 11:57:35 +0100 Subject: [PATCH 11/82] Remove chevrons from RoomSummaryCard_Button (#7137) Co-authored-by: J. Ryan Stinnett --- res/css/views/right_panel/_BaseCard.scss | 17 +---------------- res/css/views/right_panel/_RoomSummaryCard.scss | 11 ++++++----- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/res/css/views/right_panel/_BaseCard.scss b/res/css/views/right_panel/_BaseCard.scss index bed4c68142c..0ef7db404df 100644 --- a/res/css/views/right_panel/_BaseCard.scss +++ b/res/css/views/right_panel/_BaseCard.scss @@ -99,7 +99,7 @@ limitations under the License. } .mx_BaseCard_Button { - padding: 10px 32px 10px 12px; + padding: 10px 10px 10px 12px; margin: 0; position: relative; font-size: $font-13px; @@ -120,21 +120,6 @@ limitations under the License. background-color: rgba(141, 151, 165, 0.1); } - &::after { - content: ''; - position: absolute; - top: 10px; - right: 6px; - height: 20px; - width: 20px; - mask-repeat: no-repeat; - mask-position: center; - background-color: $icon-button-color; - transform: rotate(270deg); - mask-size: 20px; - mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); - } - &.mx_AccessibleButton_disabled { padding-right: 12px; &::after { diff --git a/res/css/views/right_panel/_RoomSummaryCard.scss b/res/css/views/right_panel/_RoomSummaryCard.scss index bb3638c475c..9c93a885c5f 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.scss +++ b/res/css/views/right_panel/_RoomSummaryCard.scss @@ -170,21 +170,22 @@ limitations under the License. } .mx_RoomSummaryCard_app_pinToggle { - right: 24px; + right: 8px; &::before { mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); } } .mx_RoomSummaryCard_app_maximise { - right: 48px; + right: 32px; //24 + 8 + &::before { mask-size: 14px; mask-image: url("$(res)/img/element-icons/maximise-expand.svg"); } } .mx_RoomSummaryCard_app_minimise { - right: 48px; + right: 32px; //24 + 8 &::before { mask-size: 14px; mask-image: url("$(res)/img/element-icons/minimise-collapse.svg"); @@ -193,14 +194,14 @@ limitations under the License. } .mx_RoomSummaryCard_app_options { - right: 48px; + right: 32px; //24 + 8 display: none; &::before { mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); } &.mx_RoomSummaryCard_maximised_widget { - right: 72px; + right: 56px; //2*24 + 8 } } From a2c662272ec6bb4f4b167b850d0e6c67305d8ee9 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 19 Nov 2021 14:01:07 +0000 Subject: [PATCH 12/82] When we only show one compose button, show upload (#7165) --- src/components/views/rooms/MessageComposer.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index bb6671e5b01..98c2bc3f287 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -472,6 +472,7 @@ export default class MessageComposer extends React.Component { }; private renderButtons(menuPosition): JSX.Element | JSX.Element[] { + let uploadButtonIndex = 0; const buttons: JSX.Element[] = []; if (!this.state.haveRecording) { if (SettingsStore.getValue("feature_polls")) { @@ -479,6 +480,7 @@ export default class MessageComposer extends React.Component { , ); } + uploadButtonIndex = buttons.length; buttons.push( { }); return <> - { buttons[0] } + { buttons[uploadButtonIndex] } Date: Fri, 19 Nov 2021 14:01:30 +0000 Subject: [PATCH 13/82] Limit the length of poll questions and answers to 340 characters (#7166) --- src/components/views/elements/PollCreateDialog.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/elements/PollCreateDialog.tsx b/src/components/views/elements/PollCreateDialog.tsx index 8869286afa3..7df62090ab6 100644 --- a/src/components/views/elements/PollCreateDialog.tsx +++ b/src/components/views/elements/PollCreateDialog.tsx @@ -37,6 +37,8 @@ interface IState extends IScrollableBaseState { const MIN_OPTIONS = 2; const MAX_OPTIONS = 20; const DEFAULT_NUM_OPTIONS = 2; +const MAX_QUESTION_LENGTH = 340; +const MAX_OPTION_LENGTH = 340; export default class PollCreateDialog extends ScrollableBaseModal { private addOptionRef = createRef(); @@ -111,6 +113,7 @@ export default class PollCreateDialog extends ScrollableBaseModal{ _t("What is your poll question or topic?") }
this.onOptionChange(i, e)} From 21c142c19d7cfa03fd9e901b5df74cce93ef6087 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 19 Nov 2021 14:19:53 +0000 Subject: [PATCH 14/82] Make the Add option button look disabled when it is (#7164) --- res/css/views/dialogs/_PollCreateDialog.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/dialogs/_PollCreateDialog.scss b/res/css/views/dialogs/_PollCreateDialog.scss index 0b082a98835..d873ec44ef2 100644 --- a/res/css/views/dialogs/_PollCreateDialog.scss +++ b/res/css/views/dialogs/_PollCreateDialog.scss @@ -67,4 +67,8 @@ limitations under the License. padding: 0; margin-bottom: 40px; // arbitrary to create scrollable area under the poll } + + .mx_AccessibleButton_disabled { + opacity: 0.4; + } } From ddbfebbaa0e17827e2517eccb1f6d2dbe287c579 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Nov 2021 17:15:15 +0000 Subject: [PATCH 15/82] Fix automatic composer focus, regressed by threads work (#7167) --- src/components/structures/LoggedInView.tsx | 6 ++--- .../views/emojipicker/ReactionPicker.tsx | 4 +-- .../views/rooms/SendMessageComposer.tsx | 4 +-- src/dispatcher/actions.ts | 4 +-- .../payloads/FocusComposerPayload.ts | 25 +++++++++++++++++++ 5 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 src/dispatcher/payloads/FocusComposerPayload.ts diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 88756a334bf..69fe5c63160 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as React from 'react'; +import React, { ClipboardEvent } from 'react'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; @@ -415,9 +415,9 @@ class LoggedInView extends React.Component { }); }; - private onPaste = (ev) => { + private onPaste = (ev: ClipboardEvent) => { let canReceiveInput = false; - let element = ev.target; + let element = ev.currentTarget; // test for all parents because the target can be a child of a contenteditable element while (!canReceiveInput && element) { canReceiveInput = canElementReceiveInput(element); diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index e129b45c9a9..1f748796818 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -94,7 +94,7 @@ class ReactionPicker extends React.Component { this.props.mxEvent.getRoomId(), myReactions[reaction], ); - dis.dispatch({ action: Action.FocusAComposer }); + dis.fire(Action.FocusAComposer); // Tell the emoji picker not to bump this in the more frequently used list. return false; } else { @@ -106,7 +106,7 @@ class ReactionPicker extends React.Component { }, }); dis.dispatch({ action: "message_sent" }); - dis.dispatch({ action: Action.FocusAComposer }); + dis.fire(Action.FocusAComposer); return true; } }; diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 3b1db6bb902..e6ec92cffbe 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -56,7 +56,7 @@ import ErrorDialog from "../dialogs/ErrorDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; import { ActionPayload } from "../../../dispatcher/payloads"; import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics"; -import RoomContext from '../../../contexts/RoomContext'; +import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext'; import DocumentPosition from "../../../editor/position"; import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload"; @@ -597,7 +597,7 @@ export class SendMessageComposer extends React.Component Date: Fri, 19 Nov 2021 17:35:11 +0000 Subject: [PATCH 16/82] Iterate type definitions (#7168) --- src/RoomNotifs.ts | 18 ++++++++---------- .../views/dialogs/IncomingSasDialog.tsx | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index 5abee9a6add..50d75ea91e6 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -18,7 +18,7 @@ limitations under the License. import { MatrixClientPeg } from './MatrixClientPeg'; import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor'; import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; -import { IAnnotatedPushRule, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules"; +import { ConditionKind, IPushRule, PushRuleActionName, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules"; export enum RoomNotifState { AllMessagesLoud = 'all_messages_loud', @@ -205,14 +205,12 @@ function setRoomNotifsStateUnmuted(roomId: string, newState: RoomNotifState): Pr return Promise.all(promises); } -function findOverrideMuteRule(roomId: string): IAnnotatedPushRule { +function findOverrideMuteRule(roomId: string): IPushRule { const cli = MatrixClientPeg.get(); - if (!cli.pushRules || - !cli.pushRules['global'] || - !cli.pushRules['global'].override) { + if (!cli?.pushRules?.global?.override) { return null; } - for (const rule of cli.pushRules['global'].override) { + for (const rule of cli.pushRules.global.override) { if (isRuleForRoom(roomId, rule)) { if (isMuteRule(rule) && rule.enabled) { return rule; @@ -222,14 +220,14 @@ function findOverrideMuteRule(roomId: string): IAnnotatedPushRule { return null; } -function isRuleForRoom(roomId: string, rule: IAnnotatedPushRule): boolean { +function isRuleForRoom(roomId: string, rule: IPushRule): boolean { if (rule.conditions.length !== 1) { return false; } const cond = rule.conditions[0]; - return (cond.kind === 'event_match' && cond.key === 'room_id' && cond.pattern === roomId); + return (cond.kind === ConditionKind.EventMatch && cond.key === 'room_id' && cond.pattern === roomId); } -function isMuteRule(rule: IAnnotatedPushRule): boolean { - return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify'); +function isMuteRule(rule: IPushRule): boolean { + return (rule.actions.length === 1 && rule.actions[0] === PushRuleActionName.DontNotify); } diff --git a/src/components/views/dialogs/IncomingSasDialog.tsx b/src/components/views/dialogs/IncomingSasDialog.tsx index da766f495cd..9b372c2b389 100644 --- a/src/components/views/dialogs/IncomingSasDialog.tsx +++ b/src/components/views/dialogs/IncomingSasDialog.tsx @@ -39,7 +39,7 @@ const PHASE_VERIFIED = 3; const PHASE_CANCELLED = 4; interface IProps extends IDialogProps { - verifier: VerificationBase; // TODO types + verifier: VerificationBase; } interface IState { From b6b7511fd938117479411f703098e7cd4ff7b61f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 21 Nov 2021 14:16:12 +0000 Subject: [PATCH 17/82] Restore 'large' thumbnails to be 800x600 (#7172) In order to restore the original behaviour for those who want it, redefine "large" thumbnails to be fit within 800x600px. This means that timeline screenshots end up typically not being downscaled (which is important given screenshots are our easiest way of copying chunks of history between rooms currently :/). It also means that photos end up nice and big, which was a deliberate aesthetic choice for the app in the past, which some users will want to retain. --- src/settings/enums/ImageSize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/enums/ImageSize.ts b/src/settings/enums/ImageSize.ts index 47f16ddc834..3d8dba62e95 100644 --- a/src/settings/enums/ImageSize.ts +++ b/src/settings/enums/ImageSize.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -const SIZE_LARGE = { w: 480, h: 360 }; +const SIZE_LARGE = { w: 800, h: 600 }; const SIZE_NORMAL = { w: 324, h: 220 }; export enum ImageSize { From e0927f3fc8744e5a3c052bc80b8143896993a63d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 21 Nov 2021 18:56:25 +0100 Subject: [PATCH 18/82] s/boarder/border/g (#7174) --- res/css/_common.scss | 2 +- res/css/views/messages/_MImageBody.scss | 4 ++-- res/css/views/messages/_MVideoBody.scss | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 7c8f7326a4c..4c782d00fb3 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -32,7 +32,7 @@ $slider-selection-dot-size: 2.4em; $container-border-width: 8px; -$timeline-image-boarder-radius: 8px; +$timeline-image-border-radius: 8px; :root { font-size: 10px; diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index db06c11e21e..e155afcbf39 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -$timeline-image-boarder-radius: 8px; +$timeline-image-border-radius: 8px; .mx_MImageBody_thumbnail--blurhash { position: absolute; @@ -25,7 +25,7 @@ $timeline-image-boarder-radius: 8px; .mx_MImageBody_thumbnail { object-fit: contain; - border-radius: $timeline-image-boarder-radius; + border-radius: $timeline-image-border-radius; display: flex; justify-content: center; diff --git a/res/css/views/messages/_MVideoBody.scss b/res/css/views/messages/_MVideoBody.scss index b5fdaeabef4..ac4db004d78 100644 --- a/res/css/views/messages/_MVideoBody.scss +++ b/res/css/views/messages/_MVideoBody.scss @@ -18,6 +18,6 @@ span.mx_MVideoBody { video.mx_MVideoBody { max-width: 100%; height: auto; - border-radius: $timeline-image-boarder-radius; + border-radius: $timeline-image-border-radius; } } From 9e7b407bf7ec7e6d5eab666716228816e4b9c391 Mon Sep 17 00:00:00 2001 From: Germain Date: Mon, 22 Nov 2021 13:02:48 +0000 Subject: [PATCH 19/82] Add spacing between right panel and main timeline (#7176) --- res/css/structures/_MainSplit.scss | 1 + res/css/views/rooms/_AppsDrawer.scss | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss index 8c30abe97da..6dfbb14d1a3 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -25,6 +25,7 @@ limitations under the License. .mx_MainSplit > .mx_RightPanel_ResizeWrapper { // no padding on the left. The spacing is taken care of by the main split content. padding: 5px 5px 5px 0px; + margin-left: 8px; height: calc(100vh - 51px); // height of .mx_RoomHeader.light-panel &:hover .mx_RightPanel_ResizeHandle { diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index b69c1f84297..201f5811bb9 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -18,7 +18,7 @@ limitations under the License. $MiniAppTileHeight: 200px; .mx_AppsDrawer { - margin: 5px; + margin: 5px 5px 5px 13px; position: relative; display: flex; flex-direction: column; From a5f2f4df0acded6be3b57642ed72b7a83304d37e Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 22 Nov 2021 14:27:30 +0000 Subject: [PATCH 20/82] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e879543bed6..7d1ad31e437 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "bin": { "reskindex": "scripts/reskindex.js" }, - "main": "./lib/index.ts", + "main": "./src/index.ts", "matrix_src_main": "./src/index.ts", "matrix_lib_main": "./lib/index.ts", "matrix_lib_typings": "./lib/index.d.ts", @@ -221,6 +221,5 @@ "coverageReporters": [ "text" ] - }, - "typings": "./lib/index.d.ts" + } } From ea25f74714ef8e603ccf1e1a7b302671a570213b Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 22 Nov 2021 14:28:13 +0000 Subject: [PATCH 21/82] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7d1ad31e437..93867da9677 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "15.1.1", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^0.1.0-beta.17", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 27132cbecdf..03df1b3484c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5869,10 +5869,9 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@15.1.1: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "15.1.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-15.1.1.tgz#791aefb2c68e1e8d398beee3eedcb6c23b63315d" - integrity sha512-tKZQLFwYMQk2G4nmLNR0cDDXcfDkdzJGyg2S15HooB2aI7/Ev9PhCczkw+xzwhJusxzfa8j+yx52KjMXUb81WQ== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/55b489b17a519bc0d557c47ac9fe7ce54690b602" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 76581871862b452e78e5d583df6b55621d7d55b6 Mon Sep 17 00:00:00 2001 From: Germain Date: Mon, 22 Nov 2021 17:09:16 +0000 Subject: [PATCH 22/82] Fix composer focus after pasting (#7181) --- src/components/structures/LoggedInView.tsx | 23 ++++++++----------- .../views/messages/MessageActionBar.tsx | 4 ++++ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 69fe5c63160..f5ad0919132 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -76,11 +76,8 @@ import LegacyCommunityPreview from "./LegacyCommunityPreview"; // NB. this is just for server notices rather than pinned messages in general. const MAX_PINNED_NOTICES_PER_ROOM = 2; -function canElementReceiveInput(el) { - return el.tagName === "INPUT" || - el.tagName === "TEXTAREA" || - el.tagName === "SELECT" || - !!el.getAttribute("contenteditable"); +function getInputableElement(el: HTMLElement): HTMLElement | null { + return el.closest("input, textarea, select, [contenteditable=true]"); } interface IProps { @@ -416,14 +413,12 @@ class LoggedInView extends React.Component { }; private onPaste = (ev: ClipboardEvent) => { - let canReceiveInput = false; - let element = ev.currentTarget; - // test for all parents because the target can be a child of a contenteditable element - while (!canReceiveInput && element) { - canReceiveInput = canElementReceiveInput(element); - element = element.parentElement; - } - if (!canReceiveInput) { + const element = ev.target as HTMLElement; + const inputableElement = getInputableElement(element); + + if (inputableElement) { + inputableElement.focus(); + } else { // refocusing during a paste event will make the // paste end up in the newly focused element, // so dispatch synchronously before paste happens @@ -580,7 +575,7 @@ class LoggedInView extends React.Component { // If the user is entering a printable character outside of an input field // redirect it to the composer for them. - if (!isClickShortcut && isPrintable && !canElementReceiveInput(ev.target)) { + if (!isClickShortcut && isPrintable && !getInputableElement(ev.target as HTMLElement)) { // synchronous dispatch so we focus before key generates input dis.fire(Action.FocusSendMessageComposer, true); ev.stopPropagation(); diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index e88ae7ad6b8..1831e2da12f 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -196,6 +196,10 @@ export default class MessageActionBar extends React.PureComponent { dispatchShowThreadEvent(this.props.mxEvent); + dis.dispatch({ + action: Action.FocusSendMessageComposer, + context: TimelineRenderingType.Thread, + }); }; private onEditClick = (ev: React.MouseEvent): void => { From 925136d016ecd48bbbef8087944131544d1b55fb Mon Sep 17 00:00:00 2001 From: Germain Date: Mon, 22 Nov 2021 17:17:05 +0000 Subject: [PATCH 23/82] Disallow KeyVerificationRequest as thread root (#7182) --- src/Notifier.ts | 13 +++++++------ .../views/messages/MessageActionBar.tsx | 19 +++++++++++++++++-- src/components/views/rooms/EventTile.tsx | 4 ++-- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Notifier.ts b/src/Notifier.ts index 1acd028b084..c03655f2096 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -39,6 +39,7 @@ import { mediaFromMxc } from "./customisations/Media"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import { logger } from "matrix-js-sdk/src/logger"; +import { MsgType } from "matrix-js-sdk/src/@types/event"; /* * Dispatches: @@ -55,8 +56,8 @@ Override both the content body and the TextForEvent handler for specific msgtype This is useful when the content body contains fallback text that would explain that the client can't handle a particular type of tile. */ -const typehandlers = { - "m.key.verification.request": (event) => { +const msgTypeHandlers = { + [MsgType.KeyVerificationRequest]: (event) => { const name = (event.sender || {}).name; return _t("%(name)s is requesting verification", { name }); }, @@ -71,8 +72,8 @@ export const Notifier = { pendingEncryptedEventIds: [], notificationMessageForEvent: function(ev: MatrixEvent): string { - if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) { - return typehandlers[ev.getContent().msgtype](ev); + if (msgTypeHandlers.hasOwnProperty(ev.getContent().msgtype)) { + return msgTypeHandlers[ev.getContent().msgtype](ev); } return TextForEvent.textForEvent(ev); }, @@ -97,7 +98,7 @@ export const Notifier = { title = room.name; // notificationMessageForEvent includes sender, // but we already have the sender here - if (ev.getContent().body && !typehandlers.hasOwnProperty(ev.getContent().msgtype)) { + if (ev.getContent().body && !msgTypeHandlers.hasOwnProperty(ev.getContent().msgtype)) { msg = ev.getContent().body; } } else if (ev.getType() === 'm.room.member') { @@ -108,7 +109,7 @@ export const Notifier = { title = ev.sender.name + " (" + room.name + ")"; // notificationMessageForEvent includes sender, // but we've just out sender in the title - if (ev.getContent().body && !typehandlers.hasOwnProperty(ev.getContent().msgtype)) { + if (ev.getContent().body && !msgTypeHandlers.hasOwnProperty(ev.getContent().msgtype)) { msg = ev.getContent().body; } } diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 1831e2da12f..2b0faa210a5 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -42,6 +42,7 @@ import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import ReplyChain from '../elements/ReplyChain'; import { dispatchShowThreadEvent } from '../../../dispatcher/dispatch-actions/threads'; import ReactionPicker from "../emojipicker/ReactionPicker"; +import { MsgType } from 'matrix-js-sdk/src/@types/event'; interface IOptionsButtonProps { mxEvent: MatrixEvent; @@ -210,6 +211,21 @@ export default class MessageActionBar extends React.PureComponent - { (SettingsStore.getValue("feature_thread") - && this.context.timelineRenderingType !== TimelineRenderingType.Thread) && ( + { (this.showReplyInThreadAction) && (