From b9b237fc9a531eb3d42d1203198bcb03848c86c9 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 8 May 2021 19:48:34 -0400 Subject: [PATCH 001/115] Replace forwarding UI with dialog Replaces the old forwarding UI with a dialog based on designs from https://github.com/vector-im/element-web/issues/14690. Signed-off-by: Robin Townsend --- res/css/_components.scss | 1 + res/css/views/dialogs/_ForwardDialog.scss | 115 ++++++++++ .../views/context_menus/MessageContextMenu.js | 8 +- .../views/dialogs/ForwardDialog.tsx | 213 ++++++++++++++++++ src/i18n/strings/en_EN.json | 5 + 5 files changed, 338 insertions(+), 4 deletions(-) create mode 100644 res/css/views/dialogs/_ForwardDialog.scss create mode 100644 src/components/views/dialogs/ForwardDialog.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index ec9592f3a1..ebb025d880 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -74,6 +74,7 @@ @import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_FeedbackDialog.scss"; +@import "./views/dialogs/_ForwardDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_HostSignupDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss new file mode 100644 index 0000000000..a3aa08d04f --- /dev/null +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -0,0 +1,115 @@ +/* +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_ForwardDialog { + width: 520px; + color: $primary-fg-color; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + height: 80vh; + + .mx_ForwardDialog_preview { + max-height: 30%; + flex-shrink: 0; + overflow: scroll; + + div { + pointer-events: none; + } + + .mx_EventTile_msgOption { + display: none; + } + } + + .mx_ForwardList { + display: contents; + + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + } + + .mx_ForwardList_content { + flex-grow: 1; + } + + .mx_ForwardList_noResults { + display: block; + margin-top: 24px; + } + + .mx_ForwardList_section { + &:not(:first-child) { + margin-top: 24px; + } + + > h3 { + margin: 0; + color: $secondary-fg-color; + font-size: $font-12px; + font-weight: $font-semi-bold; + line-height: $font-15px; + } + + .mx_ForwardList_entry { + display: flex; + margin-top: 12px; + + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_ForwardList_entry_name { + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-right: 12px; + } + + .mx_AccessibleButton { + &.mx_ForwardList_sending, &.mx_ForwardList_sent { + &::before { + content: ''; + display: inline-block; + background-color: $button-primary-bg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 14px; + width: 14px; + height: 14px; + margin-right: 5px; + } + } + + &.mx_ForwardList_sending::before { + mask-image: url('$(res)/img/element-icons/circle-sending.svg'); + } + + &.mx_ForwardList_sent::before { + mask-image: url('$(res)/img/element-icons/circle-sent.svg'); + } + } + } + } + } +} diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 35efd12c9c..e069bba975 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -154,11 +154,11 @@ export default class MessageContextMenu extends React.Component { }; onForwardClick = () => { - if (this.props.onCloseDialog) this.props.onCloseDialog(); - dis.dispatch({ - action: 'forward_event', + const ForwardDialog = sdk.getComponent("dialogs.ForwardDialog"); + Modal.createTrackedDialog('Forward Message', '', ForwardDialog, { + cli: MatrixClientPeg.get(), event: this.props.mxEvent, - }); + }, 'mx_Dialog_forwardmessage'); this.closeMenu(); }; diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx new file mode 100644 index 0000000000..ce38a783b3 --- /dev/null +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -0,0 +1,213 @@ +/* +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, {useMemo, useState, useEffect} from "react"; +import classnames from "classnames"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {Room} from "matrix-js-sdk/src/models/room"; +import {MatrixClient} from "matrix-js-sdk/src/client"; + +import {_t} from "../../../languageHandler"; +import SettingsStore from "../../../settings/SettingsStore"; +import {UIFeature} from "../../../settings/UIFeature"; +import {Layout} from "../../../settings/Layout"; +import {IDialogProps} from "./IDialogProps"; +import BaseDialog from "./BaseDialog"; +import {avatarUrlForUser} from "../../../Avatar"; +import EventTile from "../rooms/EventTile"; +import SearchBox from "../../structures/SearchBox"; +import RoomAvatar from "../avatars/RoomAvatar"; +import AccessibleButton from "../elements/AccessibleButton"; +import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; + +const AVATAR_SIZE = 30; + +interface IProps extends IDialogProps { + cli: MatrixClient; + event: MatrixEvent; +} + +interface IEntryProps { + room: Room; + // Callback to forward the message to this room + send(): Promise; +} + +enum SendState { + CanSend, + Sending, + Sent, + Failed, +} + +const Entry: React.FC = ({ room, send }) => { + const [sendState, setSendState] = useState(SendState.CanSend); + + let label; + let className; + if (sendState === SendState.CanSend) { + label = _t("Send"); + className = "mx_ForwardList_canSend"; + } else if (sendState === SendState.Sending) { + label = _t("Sending…"); + className = "mx_ForwardList_sending"; + } else if (sendState === SendState.Sent) { + label = _t("Sent"); + className = "mx_ForwardList_sent"; + } else { + label = _t("Failed to send"); + className = "mx_ForwardList_sendFailed"; + } + + return
+ + { room.name } + { + setSendState(SendState.Sending); + try { + await send(); + setSendState(SendState.Sent); + } catch (e) { + setSendState(SendState.Failed); + } + }} + disabled={sendState !== SendState.CanSend} + > + { label } + +
; +}; + +const ForwardDialog: React.FC = ({ cli, event, onFinished }) => { + const userId = cli.getUserId(); + const [profileInfo, setProfileInfo] = useState({}); + useEffect(() => { + cli.getProfileInfo(userId).then(info => setProfileInfo(info)); + }, [cli, userId]); + + // For the message preview we fake the sender as ourselves + const mockEvent = new MatrixEvent({ + type: "m.room.message", + sender: userId, + content: event.getContent(), + unsigned: { + age: 97, + }, + event_id: "$9999999999999999999999999999999999999999999", + room_id: "!999999999999999999:example.org", + }); + mockEvent.sender = { + name: profileInfo.displayname || userId, + userId: userId, + getAvatarUrl: (..._) => { + return avatarUrlForUser( + { avatarUrl: profileInfo.avatar_url }, + AVATAR_SIZE, AVATAR_SIZE, "crop", + ); + }, + getMxcAvatarUrl: () => profileInfo.avatar_url, + }; + + const visibleRooms = useMemo(() => sortRooms( + cli.getVisibleRooms().filter( + room => room.getMyMembership() === "join" && + !(SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()), + ), + ), [cli]); + + const [query, setQuery] = useState(""); + const lcQuery = query.toLowerCase(); + + const [rooms, dms] = visibleRooms.reduce((arr, room) => { + if (room.name.toLowerCase().includes(lcQuery)) { + if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + arr[1].push(room); + } else { + arr[0].push(room); + } + } + return arr; + }, [[], []]); + + const previewLayout = SettingsStore.getValue("layout"); + + return +
+ +
+
+

{ _t("Forward to") }

+ + + { rooms.length > 0 ? ( +
+

{ _t("Rooms") }

+ { rooms.map(room => { + return cli.sendEvent(room.roomId, event.getType(), event.getContent())} + />; + }) } +
+ ) : undefined } + + { dms.length > 0 ? ( +
+

{ _t("Direct Messages") }

+ { dms.map(room => { + return cli.sendEvent(room.roomId, event.getType(), event.getContent())} + />; + }) } +
+ ) : null } + + { rooms.length + dms.length < 1 ? + { _t("No results") } + : undefined } +
+
+
; +}; + +export default ForwardDialog; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dcad970300..ed49fb6b44 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2187,6 +2187,11 @@ "Report a bug": "Report a bug", "Please view existing bugs on Github first. No match? Start a new one.": "Please view existing bugs on Github first. No match? Start a new one.", "Send feedback": "Send feedback", + "Sending…": "Sending…", + "Sent": "Sent", + "Forward message": "Forward message", + "Forward to": "Forward to", + "Filter your rooms and DMs": "Filter your rooms and DMs", "Confirm abort of host creation": "Confirm abort of host creation", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.", "Abort": "Abort", From 74925b2c6d1f7de459bb6f35bb8cf472f476ccc3 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 8 May 2021 19:51:51 -0400 Subject: [PATCH 002/115] Test ForwardDialog Signed-off-by: Robin Townsend --- .../views/dialogs/ForwardDialog-test.js | 124 ++++++++++++++++++ test/test-utils.js | 3 +- 2 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 test/components/views/dialogs/ForwardDialog-test.js diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js new file mode 100644 index 0000000000..cabcbb0325 --- /dev/null +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -0,0 +1,124 @@ +/* +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 "../../../skinned-sdk"; + +import React from "react"; +import {configure, mount} from "enzyme"; +import Adapter from "enzyme-adapter-react-16"; +import {act} from "react-dom/test-utils"; + +import * as TestUtils from "../../../test-utils"; +import {MatrixClientPeg} from "../../../../src/MatrixClientPeg"; +import DMRoomMap from "../../../../src/utils/DMRoomMap"; +import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog"; + +configure({ adapter: new Adapter() }); + +describe("ForwardDialog", () => { + let client; + let wrapper; + + const message = TestUtils.mkMessage({ + room: "!111111111111111111:example.org", + user: "@alice:example.org", + msg: "Hello world!", + event: true, + }); + + beforeEach(async () => { + TestUtils.stubClient(); + DMRoomMap.makeShared(); + client = MatrixClientPeg.get(); + client.getVisibleRooms = jest.fn().mockReturnValue( + ["a", "A", "b"].map(name => TestUtils.mkStubRoom(name, name)), + ); + client.getUserId = jest.fn().mockReturnValue("@bob:example.org"); + + await act(async () => { + wrapper = mount( + , + ); + // Wait one tick for our profile data to load so the state update happens within act + await new Promise(resolve => setImmediate(resolve)); + }); + }); + + it("shows a preview with us as the sender", () => { + const previewBody = wrapper.find(".mx_EventTile_body"); + expect(previewBody.text()).toBe("Hello world!"); + + // We would just test SenderProfile for the user ID, but it's stubbed + const previewAvatar = wrapper.find(".mx_EventTile_avatar .mx_BaseAvatar_image"); + expect(previewAvatar.prop("title")).toBe("@bob:example.org"); + }); + + it("filters the rooms", () => { + const roomsBefore = wrapper.find(".mx_ForwardList_entry"); + expect(roomsBefore).toHaveLength(3); + + const searchInput = wrapper.find(".mx_SearchBox input"); + searchInput.instance().value = "a"; + searchInput.simulate("change"); + + const roomsAfter = wrapper.find(".mx_ForwardList_entry"); + expect(roomsAfter).toHaveLength(2); + }); + + it("tracks message sending progress across multiple rooms", async () => { + // Make sendEvent require manual resolution so we can see the sending state + let finishSend; + let cancelSend; + client.sendEvent = jest.fn(() => new Promise((resolve, reject) => { + finishSend = resolve; + cancelSend = reject; + })); + + const firstRoom = wrapper.find(".mx_ForwardList_entry").first(); + expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Send"); + + act(() => { + firstRoom.find(".mx_AccessibleButton").simulate("click"); + }); + expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Sending…"); + + await act(async () => { + cancelSend(); + // Wait one tick for the button to realize the send failed + await new Promise(resolve => setImmediate(resolve)); + }); + expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Failed to send"); + + const secondRoom = wrapper.find(".mx_ForwardList_entry").at(1); + expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Send"); + + act(() => { + secondRoom.find(".mx_AccessibleButton").simulate("click"); + }); + expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Sending…"); + + await act(async () => { + finishSend(); + // Wait one tick for the button to realize the send succeeded + await new Promise(resolve => setImmediate(resolve)); + }); + expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Sent"); + }); +}); diff --git a/test/test-utils.js b/test/test-utils.js index 6dc02463a5..985e9f804b 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -218,7 +218,7 @@ export function mkMessage(opts) { return mkEvent(opts); } -export function mkStubRoom(roomId = null) { +export function mkStubRoom(roomId = null, name) { const stubTimeline = { getEvents: () => [] }; return { roomId, @@ -254,6 +254,7 @@ export function mkStubRoom(roomId = null) { on: jest.fn(), removeListener: jest.fn(), getDMInviter: jest.fn(), + name, getAvatarUrl: () => 'mxc://avatar.url/room.png', getMxcAvatarUrl: () => 'mxc://avatar.url/room.png', isSpaceRoom: jest.fn(() => false), From 7fa81766db34449534d265cf7c2e60069049dd5b Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 8 May 2021 20:07:51 -0400 Subject: [PATCH 003/115] Remove old forwarding code This has been replaced by ForwardDialog. Signed-off-by: Robin Townsend --- src/components/structures/RoomView.tsx | 30 +---------- src/components/views/rooms/ForwardMessage.js | 53 ------------------- src/components/views/rooms/RoomHeader.js | 9 ---- .../views/rooms/SimpleRoomHeader.js | 20 ------- src/i18n/strings/en_EN.json | 1 - src/stores/RoomViewStore.tsx | 21 -------- 6 files changed, 2 insertions(+), 132 deletions(-) delete mode 100644 src/components/views/rooms/ForwardMessage.js diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 58a87e6641..d16247e4ec 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -60,7 +60,6 @@ import ScrollPanel from "./ScrollPanel"; import TimelinePanel from "./TimelinePanel"; import ErrorBoundary from "../views/elements/ErrorBoundary"; import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; -import ForwardMessage from "../views/rooms/ForwardMessage"; import SearchBar from "../views/rooms/SearchBar"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel"; @@ -136,7 +135,6 @@ export interface IState { // Whether to highlight the event scrolled to isInitialEventHighlighted?: boolean; replyToEvent?: MatrixEvent; - forwardingEvent?: MatrixEvent; numUnreadMessages: number; draggingFile: boolean; searching: boolean; @@ -324,7 +322,6 @@ export default class RoomView extends React.Component { initialEventId: RoomViewStore.getInitialEventId(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), replyToEvent: RoomViewStore.getQuotingEvent(), - forwardingEvent: RoomViewStore.getForwardingEvent(), // we should only peek once we have a ready client shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId), @@ -1394,18 +1391,6 @@ export default class RoomView extends React.Component { dis.dispatch({ action: "open_room_settings" }); }; - private onCancelClick = () => { - console.log("updateTint from onCancelClick"); - this.updateTint(); - if (this.state.forwardingEvent) { - dis.dispatch({ - action: 'forward_event', - event: null, - }); - } - dis.fire(Action.FocusComposer); - }; - private onAppsClick = () => { dis.dispatch({ action: "appsDrawer", @@ -1856,11 +1841,7 @@ export default class RoomView extends React.Component { let aux = null; let previewBar; - let hideCancel = false; - if (this.state.forwardingEvent) { - aux = ; - } else if (this.state.searching) { - hideCancel = true; // has own cancel + if (this.state.searching) { aux = { />; } else if (showRoomUpgradeBar) { aux = ; - hideCancel = true; } else if (this.state.showingPinned) { - hideCancel = true; // has own cancel aux = ; } else if (myMembership !== "join") { // We do have a room object for this room, but we're not currently in it. @@ -1881,7 +1860,6 @@ export default class RoomView extends React.Component { inviterName = this.props.oobData.inviterName; } const invitedEmail = this.props.threepidInvite?.toEmail; - hideCancel = true; previewBar = ( { hideMessagePanel = true; } - const shouldHighlight = this.state.isInitialEventHighlighted; let highlightedEventId = null; - if (this.state.forwardingEvent) { - highlightedEventId = this.state.forwardingEvent.getId(); - } else if (shouldHighlight) { + if (this.state.isInitialEventHighlighted) { highlightedEventId = this.state.initialEventId; } @@ -2091,7 +2066,6 @@ export default class RoomView extends React.Component { onSearchClick={this.onSearchClick} onSettingsClick={this.onSettingsClick} onPinnedClick={this.onPinnedClick} - onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null} onForgetClick={(myMembership === "leave") ? this.onForgetClick : null} onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null} e2eStatus={this.state.e2eStatus} diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js deleted file mode 100644 index dd894c0dcf..0000000000 --- a/src/components/views/rooms/ForwardMessage.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - Copyright 2017 Michael Telatynski - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; -import {Key} from '../../../Keyboard'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; - -@replaceableComponent("views.rooms.ForwardMessage") -export default class ForwardMessage extends React.Component { - static propTypes = { - onCancelClick: PropTypes.func.isRequired, - }; - - componentDidMount() { - document.addEventListener('keydown', this._onKeyDown); - } - - componentWillUnmount() { - document.removeEventListener('keydown', this._onKeyDown); - } - - _onKeyDown = ev => { - switch (ev.key) { - case Key.ESCAPE: - this.props.onCancelClick(); - break; - } - }; - - render() { - return ( -
-

{ _t('Please select the destination room for this message') }

-
- ); - } -} diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index f856f7f6ef..cd4f89f964 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import RateLimitedFunc from '../../../ratelimitedfunc'; -import {CancelButton} from './SimpleRoomHeader'; import SettingsStore from "../../../settings/SettingsStore"; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import E2EIcon from './E2EIcon'; @@ -44,7 +43,6 @@ export default class RoomHeader extends React.Component { onPinnedClick: PropTypes.func, onSearchClick: PropTypes.func, onLeaveClick: PropTypes.func, - onCancelClick: PropTypes.func, e2eStatus: PropTypes.string, onAppsClick: PropTypes.func, appsShown: PropTypes.bool, @@ -54,7 +52,6 @@ export default class RoomHeader extends React.Component { static defaultProps = { editing: false, inRoom: false, - onCancelClick: null, }; componentDidMount() { @@ -120,13 +117,8 @@ export default class RoomHeader extends React.Component { render() { let searchStatus = null; - let cancelButton = null; let pinnedEventsButton = null; - if (this.props.onCancelClick) { - cancelButton = ; - } - // don't display the search count until the search completes and // gives us a valid (possibly zero) searchCount. if (this.props.searchInfo && @@ -265,7 +257,6 @@ export default class RoomHeader extends React.Component {
{ e2eIcon }
{ name } { topicElement } - { cancelButton } { rightRow } diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index b2a66f6670..02c1e1e8a1 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -16,23 +16,9 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import AccessibleButton from '../elements/AccessibleButton'; import * as sdk from '../../../index'; -import { _t } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -// cancel button which is shared between room header and simple room header -export function CancelButton(props) { - const {onClick} = props; - - return ( - - {_t("Cancel")} - - ); -} - /* * A stripped-down room header used for things like the user settings * and room directory. @@ -41,18 +27,13 @@ export function CancelButton(props) { export default class SimpleRoomHeader extends React.Component { static propTypes = { title: PropTypes.string, - onCancelClick: PropTypes.func, // `src` to a TintableSvg. Optional. icon: PropTypes.string, }; render() { - let cancelButton; let icon; - if (this.props.onCancelClick) { - cancelButton = ; - } if (this.props.icon) { const TintableSvg = sdk.getComponent('elements.TintableSvg'); icon = { icon } { this.props.title } - { cancelButton } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ed49fb6b44..6f2a4e8aee 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1458,7 +1458,6 @@ "Encrypting your message...": "Encrypting your message...", "Your message was sent": "Your message was sent", "Failed to send": "Failed to send", - "Please select the destination room for this message": "Please select the destination room for this message", "Scroll to most recent messages": "Scroll to most recent messages", "Close preview": "Close preview", "and %(count)s others...|other": "and %(count)s others...", diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index fe2e0a66b2..37162c8ae7 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -53,8 +53,6 @@ const INITIAL_STATE = { // Any error that has occurred during loading roomLoadError: null, - forwardingEvent: null, - quotingEvent: null, replyingToEvent: null, @@ -149,11 +147,6 @@ class RoomViewStore extends Store { case 'on_logged_out': this.reset(); break; - case 'forward_event': - this.setState({ - forwardingEvent: payload.event, - }); - break; case 'reply_to_event': // If currently viewed room does not match the room in which we wish to reply then change rooms // this can happen when performing a search across all rooms @@ -186,7 +179,6 @@ class RoomViewStore extends Store { roomAlias: payload.room_alias, initialEventId: payload.event_id, isInitialEventHighlighted: payload.highlighted, - forwardingEvent: null, roomLoading: false, roomLoadError: null, // should peek by default @@ -206,14 +198,6 @@ class RoomViewStore extends Store { newState.replyingToEvent = payload.replyingToEvent; } - if (this.state.forwardingEvent) { - dis.dispatch({ - action: 'send_event', - room_id: newState.roomId, - event: this.state.forwardingEvent, - }); - } - this.setState(newState); if (payload.auto_join) { @@ -419,11 +403,6 @@ class RoomViewStore extends Store { return this.state.joinError; } - // The mxEvent if one is about to be forwarded - public getForwardingEvent() { - return this.state.forwardingEvent; - } - // The mxEvent if one is currently being replied to/quoted public getQuotingEvent() { return this.state.replyingToEvent; From 219c983d191d9172e3c68aa11b1765f91ac5c6cd Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 9 May 2021 10:58:44 -0400 Subject: [PATCH 004/115] Use import instead of sdk.getComponent Signed-off-by: Robin Townsend --- src/components/views/context_menus/MessageContextMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index e069bba975..fc2c366b07 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -31,6 +31,7 @@ import { isContentActionable } from '../../../utils/EventUtils'; import {MenuItem} from "../../structures/ContextMenu"; import {EventType} from "matrix-js-sdk/src/@types/event"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import ForwardDialog from "../dialogs/ForwardDialog"; export function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; @@ -154,7 +155,6 @@ export default class MessageContextMenu extends React.Component { }; onForwardClick = () => { - const ForwardDialog = sdk.getComponent("dialogs.ForwardDialog"); Modal.createTrackedDialog('Forward Message', '', ForwardDialog, { cli: MatrixClientPeg.get(), event: this.props.mxEvent, From c96888c9cba48dfb1d318b2cfb89cd699c6a8529 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 00:38:01 -0400 Subject: [PATCH 005/115] Make ForwardDialog more readable Signed-off-by: Robin Townsend --- .../views/dialogs/ForwardDialog.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index ce38a783b3..b1f558dfef 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -39,6 +39,7 @@ const AVATAR_SIZE = 30; interface IProps extends IDialogProps { cli: MatrixClient; + // The event to forward event: MatrixEvent; } @@ -116,7 +117,7 @@ const ForwardDialog: React.FC = ({ cli, event, onFinished }) => { }); mockEvent.sender = { name: profileInfo.displayname || userId, - userId: userId, + userId, getAvatarUrl: (..._) => { return avatarUrlForUser( { avatarUrl: profileInfo.avatar_url }, @@ -179,28 +180,28 @@ const ForwardDialog: React.FC = ({ cli, event, onFinished }) => { { rooms.length > 0 ? (

{ _t("Rooms") }

- { rooms.map(room => { - return + cli.sendEvent(room.roomId, event.getType(), event.getContent())} - />; - }) } + />, + ) }
) : undefined } { dms.length > 0 ? (

{ _t("Direct Messages") }

- { dms.map(room => { - return + cli.sendEvent(room.roomId, event.getType(), event.getContent())} - />; - }) } + />, + ) }
- ) : null } + ) : undefined } { rooms.length + dms.length < 1 ? { _t("No results") } From 100efb1a90c92ac6a8771787cd2564f218507550 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 00:40:54 -0400 Subject: [PATCH 006/115] Fix ForwardDialog crashing when rendering reply Signed-off-by: Robin Townsend --- src/components/views/context_menus/MessageContextMenu.js | 1 + src/components/views/dialogs/ForwardDialog.tsx | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index fc2c366b07..002542bf88 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -158,6 +158,7 @@ export default class MessageContextMenu extends React.Component { Modal.createTrackedDialog('Forward Message', '', ForwardDialog, { cli: MatrixClientPeg.get(), event: this.props.mxEvent, + permalinkCreator: this.props.permalinkCreator, }, 'mx_Dialog_forwardmessage'); this.closeMenu(); }; diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index b1f558dfef..5cb5d68745 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -33,6 +33,7 @@ import RoomAvatar from "../avatars/RoomAvatar"; import AccessibleButton from "../elements/AccessibleButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import DMRoomMap from "../../../utils/DMRoomMap"; +import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; const AVATAR_SIZE = 30; @@ -41,6 +42,9 @@ interface IProps extends IDialogProps { cli: MatrixClient; // The event to forward event: MatrixEvent; + // We need a permalink creator for the source room to pass through to EventTile + // in case the event is a reply (even though the user can't get at the link) + permalinkCreator: RoomPermalinkCreator; } interface IEntryProps { @@ -97,7 +101,7 @@ const Entry: React.FC = ({ room, send }) => { ; }; -const ForwardDialog: React.FC = ({ cli, event, onFinished }) => { +const ForwardDialog: React.FC = ({ cli, event, permalinkCreator, onFinished }) => { const userId = cli.getUserId(); const [profileInfo, setProfileInfo] = useState({}); useEffect(() => { @@ -113,7 +117,7 @@ const ForwardDialog: React.FC = ({ cli, event, onFinished }) => { age: 97, }, event_id: "$9999999999999999999999999999999999999999999", - room_id: "!999999999999999999:example.org", + room_id: event.getRoomId(), }); mockEvent.sender = { name: profileInfo.displayname || userId, @@ -165,6 +169,7 @@ const ForwardDialog: React.FC = ({ cli, event, onFinished }) => { mxEvent={mockEvent} layout={previewLayout} enableFlair={SettingsStore.getValue(UIFeature.Flair)} + permalinkCreator={permalinkCreator} />
From eb07f1fb86de32944b8a0375beec89abdeef63d0 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 00:54:00 -0400 Subject: [PATCH 007/115] Test that ForwardDialog can render replies Previously ForwardDialog was not giving its EventTile message preview the information it needed to render a ReplyThread. This was a bit tricky to fix since we were pulling a fake event out of thin air, so this ensures it doesn't regress. Signed-off-by: Robin Townsend --- .../views/dialogs/ForwardDialog-test.js | 64 ++++++++++++++----- test/test-utils.js | 1 + 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index cabcbb0325..af325ab8f0 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -24,44 +24,51 @@ import {act} from "react-dom/test-utils"; import * as TestUtils from "../../../test-utils"; import {MatrixClientPeg} from "../../../../src/MatrixClientPeg"; import DMRoomMap from "../../../../src/utils/DMRoomMap"; +import {RoomPermalinkCreator} from "../../../../src/utils/permalinks/Permalinks"; import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog"; configure({ adapter: new Adapter() }); describe("ForwardDialog", () => { - let client; - let wrapper; - - const message = TestUtils.mkMessage({ - room: "!111111111111111111:example.org", + const sourceRoom = "!111111111111111111:example.org"; + const defaultMessage = TestUtils.mkMessage({ + room: sourceRoom, user: "@alice:example.org", msg: "Hello world!", event: true, }); + const defaultRooms = ["a", "A", "b"].map(name => TestUtils.mkStubRoom(name, name)); - beforeEach(async () => { - TestUtils.stubClient(); - DMRoomMap.makeShared(); - client = MatrixClientPeg.get(); - client.getVisibleRooms = jest.fn().mockReturnValue( - ["a", "A", "b"].map(name => TestUtils.mkStubRoom(name, name)), - ); - client.getUserId = jest.fn().mockReturnValue("@bob:example.org"); + const mountForwardDialog = async (message = defaultMessage, rooms = defaultRooms) => { + const client = MatrixClientPeg.get(); + client.getVisibleRooms = jest.fn().mockReturnValue(rooms); + let wrapper; await act(async () => { wrapper = mount( , ); // Wait one tick for our profile data to load so the state update happens within act await new Promise(resolve => setImmediate(resolve)); }); + + return wrapper; + }; + + beforeEach(() => { + TestUtils.stubClient(); + DMRoomMap.makeShared(); + MatrixClientPeg.get().getUserId = jest.fn().mockReturnValue("@bob:example.org"); }); - it("shows a preview with us as the sender", () => { + it("shows a preview with us as the sender", async () => { + const wrapper = await mountForwardDialog(); + const previewBody = wrapper.find(".mx_EventTile_body"); expect(previewBody.text()).toBe("Hello world!"); @@ -70,7 +77,9 @@ describe("ForwardDialog", () => { expect(previewAvatar.prop("title")).toBe("@bob:example.org"); }); - it("filters the rooms", () => { + it("filters the rooms", async () => { + const wrapper = await mountForwardDialog(); + const roomsBefore = wrapper.find(".mx_ForwardList_entry"); expect(roomsBefore).toHaveLength(3); @@ -83,10 +92,12 @@ describe("ForwardDialog", () => { }); it("tracks message sending progress across multiple rooms", async () => { + const wrapper = await mountForwardDialog(); + // Make sendEvent require manual resolution so we can see the sending state let finishSend; let cancelSend; - client.sendEvent = jest.fn(() => new Promise((resolve, reject) => { + MatrixClientPeg.get().sendEvent = jest.fn(() => new Promise((resolve, reject) => { finishSend = resolve; cancelSend = reject; })); @@ -121,4 +132,25 @@ describe("ForwardDialog", () => { }); expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Sent"); }); + + it("can render replies", async () => { + const replyMessage = TestUtils.mkEvent({ + type: "m.room.message", + room: "!111111111111111111:example.org", + user: "@alice:example.org", + content: { + msgtype: "m.text", + body: "> <@bob:example.org> Hi Alice!\n\nHi Bob!", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$2222222222222222222222222222222222222222222", + }, + }, + }, + event: true, + }); + + const wrapper = await mountForwardDialog(replyMessage); + expect(wrapper.find("ReplyThread")).toBeTruthy(); + }); }); diff --git a/test/test-utils.js b/test/test-utils.js index 985e9f804b..8c9c62844a 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -236,6 +236,7 @@ export function mkStubRoom(roomId = null, name) { getPendingEvents: () => [], getLiveTimeline: () => stubTimeline, getUnfilteredTimelineSet: () => null, + findEventById: () => null, getAccountData: () => null, hasMembershipState: () => null, getVersion: () => '1', From 35cf0e1c7efbd4f627e8fcc00cd8f8b96daf1b8e Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 01:02:12 -0400 Subject: [PATCH 008/115] Find components by name rather than class in ForwardDialog test It makes things shorter and more readable! Signed-off-by: Robin Townsend --- .../views/dialogs/ForwardDialog-test.js | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index af325ab8f0..f181157d09 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -80,15 +80,13 @@ describe("ForwardDialog", () => { it("filters the rooms", async () => { const wrapper = await mountForwardDialog(); - const roomsBefore = wrapper.find(".mx_ForwardList_entry"); - expect(roomsBefore).toHaveLength(3); + expect(wrapper.find("Entry")).toHaveLength(3); - const searchInput = wrapper.find(".mx_SearchBox input"); + const searchInput = wrapper.find("SearchBox input"); searchInput.instance().value = "a"; searchInput.simulate("change"); - const roomsAfter = wrapper.find(".mx_ForwardList_entry"); - expect(roomsAfter).toHaveLength(2); + expect(wrapper.find("Entry")).toHaveLength(2); }); it("tracks message sending progress across multiple rooms", async () => { @@ -102,35 +100,31 @@ describe("ForwardDialog", () => { cancelSend = reject; })); - const firstRoom = wrapper.find(".mx_ForwardList_entry").first(); - expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Send"); + const firstButton = wrapper.find("Entry AccessibleButton").first(); + expect(firstButton.text()).toBe("Send"); - act(() => { - firstRoom.find(".mx_AccessibleButton").simulate("click"); - }); - expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Sending…"); + act(() => { firstButton.simulate("click"); }); + expect(firstButton.text()).toBe("Sending…"); await act(async () => { cancelSend(); // Wait one tick for the button to realize the send failed await new Promise(resolve => setImmediate(resolve)); }); - expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Failed to send"); + expect(firstButton.text()).toBe("Failed to send"); - const secondRoom = wrapper.find(".mx_ForwardList_entry").at(1); - expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Send"); + const secondButton = wrapper.find("Entry AccessibleButton").at(1); + expect(secondButton.text()).toBe("Send"); - act(() => { - secondRoom.find(".mx_AccessibleButton").simulate("click"); - }); - expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Sending…"); + act(() => { secondButton.simulate("click"); }); + expect(secondButton.text()).toBe("Sending…"); await act(async () => { finishSend(); // Wait one tick for the button to realize the send succeeded await new Promise(resolve => setImmediate(resolve)); }); - expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Sent"); + expect(secondButton.text()).toBe("Sent"); }); it("can render replies", async () => { From 09ba74a8514fd907e758f1af24c795fa78ce8643 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 01:04:25 -0400 Subject: [PATCH 009/115] Disable forward buttons for rooms without send permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …and add a tooltip to explain why they can't accept forwarded messages. It was chosen to disable the buttons rather than hide the entries from the list, since hiding them without explanation could cause confusion. Signed-off-by: Robin Townsend --- .../views/dialogs/ForwardDialog.tsx | 76 ++++++++++++------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 5cb5d68745..fd10a88d93 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -31,6 +31,7 @@ import EventTile from "../rooms/EventTile"; import SearchBox from "../../structures/SearchBox"; import RoomAvatar from "../avatars/RoomAvatar"; import AccessibleButton from "../elements/AccessibleButton"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import DMRoomMap from "../../../utils/DMRoomMap"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; @@ -63,41 +64,58 @@ enum SendState { const Entry: React.FC = ({ room, send }) => { const [sendState, setSendState] = useState(SendState.CanSend); - let label; - let className; - if (sendState === SendState.CanSend) { - label = _t("Send"); - className = "mx_ForwardList_canSend"; - } else if (sendState === SendState.Sending) { - label = _t("Sending…"); - className = "mx_ForwardList_sending"; - } else if (sendState === SendState.Sent) { - label = _t("Sent"); - className = "mx_ForwardList_sent"; + let button; + if (room.maySendMessage()) { + let label; + let className; + if (sendState === SendState.CanSend) { + label = _t("Send"); + className = "mx_ForwardList_canSend"; + } else if (sendState === SendState.Sending) { + label = _t("Sending…"); + className = "mx_ForwardList_sending"; + } else if (sendState === SendState.Sent) { + label = _t("Sent"); + className = "mx_ForwardList_sent"; + } else { + label = _t("Failed to send"); + className = "mx_ForwardList_sendFailed"; + } + + button = + { + setSendState(SendState.Sending); + try { + await send(); + setSendState(SendState.Sent); + } catch (e) { + setSendState(SendState.Failed); + } + }} + disabled={sendState !== SendState.CanSend} + > + { label } + ; } else { - label = _t("Failed to send"); - className = "mx_ForwardList_sendFailed"; + button = + {}} + disabled={true} + title={_t("You do not have permission to post to this room")} + > + { _t("Send") } + ; } return
{ room.name } - { - setSendState(SendState.Sending); - try { - await send(); - setSendState(SendState.Sent); - } catch (e) { - setSendState(SendState.Failed); - } - }} - disabled={sendState !== SendState.CanSend} - > - { label } - + { button }
; }; From eb779cd3d8db279e9cf91134b1aa5bbf4326662c Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 01:07:42 -0400 Subject: [PATCH 010/115] Test that forward buttons are disabled for rooms without permission Signed-off-by: Robin Townsend --- test/components/views/dialogs/ForwardDialog-test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index f181157d09..706d47acdc 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -147,4 +147,17 @@ describe("ForwardDialog", () => { const wrapper = await mountForwardDialog(replyMessage); expect(wrapper.find("ReplyThread")).toBeTruthy(); }); + + it("disables buttons for rooms without send permissions", async () => { + const readOnlyRoom = TestUtils.mkStubRoom("a", "a"); + readOnlyRoom.maySendMessage = jest.fn().mockReturnValue(false); + const rooms = [readOnlyRoom, TestUtils.mkStubRoom("b", "b")]; + + const wrapper = await mountForwardDialog(undefined, rooms); + + const firstButton = wrapper.find("Entry AccessibleButton").first(); + expect(firstButton.prop("disabled")).toBe(true); + const secondButton = wrapper.find("Entry AccessibleButton").last(); + expect(secondButton.prop("disabled")).toBe(false); + }); }); From 5c10e1e5744e7c8b753304e4994c172f4ba493c3 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 01:16:37 -0400 Subject: [PATCH 011/115] Fix lints Signed-off-by: Robin Townsend --- test/components/views/dialogs/ForwardDialog-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index 706d47acdc..2f062a84cc 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -133,8 +133,8 @@ describe("ForwardDialog", () => { room: "!111111111111111111:example.org", user: "@alice:example.org", content: { - msgtype: "m.text", - body: "> <@bob:example.org> Hi Alice!\n\nHi Bob!", + "msgtype": "m.text", + "body": "> <@bob:example.org> Hi Alice!\n\nHi Bob!", "m.relates_to": { "m.in_reply_to": { event_id: "$2222222222222222222222222222222222222222222", From bfba2b0b6f09b7d470a6274d07053d0d854f7cd0 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 11:16:51 -0400 Subject: [PATCH 012/115] Push ForwardDialog scrollbar into the gutter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …to give more space between it and the buttons. Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index a3aa08d04f..a447b93cae 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -48,6 +48,8 @@ limitations under the License. .mx_ForwardList_content { flex-grow: 1; + // Push the scrollbar into the gutter + margin-right: -6px; } .mx_ForwardList_noResults { @@ -56,6 +58,8 @@ limitations under the License. } .mx_ForwardList_section { + margin-right: 6px; + &:not(:first-child) { margin-top: 24px; } From 503301aa89e3a8378d9e48c95b37ec19ab4d5e89 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 13:00:06 -0400 Subject: [PATCH 013/115] Make rooms in ForwardDialog clickable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …so that you can jump to a room easily once you've forwarded a message there. Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 29 ++++++---- .../views/dialogs/ForwardDialog.tsx | 53 ++++++++++++------- .../views/dialogs/ForwardDialog-test.js | 10 ++-- 3 files changed, 58 insertions(+), 34 deletions(-) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index a447b93cae..30a4cfcacf 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -74,23 +74,30 @@ limitations under the License. .mx_ForwardList_entry { display: flex; + justify-content: space-between; margin-top: 12px; - .mx_BaseAvatar { + .mx_ForwardList_roomButton { + display: flex; margin-right: 12px; - } - - .mx_ForwardList_entry_name { - font-size: $font-15px; - line-height: 30px; flex-grow: 1; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin-right: 12px; + min-width: 0; + + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_ForwardList_entry_name { + font-size: $font-15px; + line-height: 30px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-right: 12px; + } } - .mx_AccessibleButton { + .mx_ForwardList_sendButton { &.mx_ForwardList_sending, &.mx_ForwardList_sent { &::before { content: ''; diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index fd10a88d93..ed6973cd4a 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -21,6 +21,7 @@ import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClient} from "matrix-js-sdk/src/client"; import {_t} from "../../../languageHandler"; +import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; import {UIFeature} from "../../../settings/UIFeature"; import {Layout} from "../../../settings/Layout"; @@ -50,8 +51,9 @@ interface IProps extends IDialogProps { interface IEntryProps { room: Room; - // Callback to forward the message to this room - send(): Promise; + event: MatrixEvent; + cli: MatrixClient; + onFinished(success: boolean): void; } enum SendState { @@ -61,9 +63,26 @@ enum SendState { Failed, } -const Entry: React.FC = ({ room, send }) => { +const Entry: React.FC = ({ room, event, cli, onFinished }) => { const [sendState, setSendState] = useState(SendState.CanSend); + const jumpToRoom = () => { + dis.dispatch({ + action: "view_room", + room_id: room.roomId, + }); + onFinished(true); + }; + const send = async () => { + setSendState(SendState.Sending); + try { + await cli.sendEvent(room.roomId, event.getType(), event.getContent()); + setSendState(SendState.Sent); + } catch (e) { + setSendState(SendState.Failed); + } + }; + let button; if (room.maySendMessage()) { let label; @@ -85,16 +104,8 @@ const Entry: React.FC = ({ room, send }) => { button = { - setSendState(SendState.Sending); - try { - await send(); - setSendState(SendState.Sent); - } catch (e) { - setSendState(SendState.Failed); - } - }} + className={`mx_ForwardList_sendButton ${className}`} + onClick={send} disabled={sendState !== SendState.CanSend} > { label } @@ -103,7 +114,7 @@ const Entry: React.FC = ({ room, send }) => { button = {}} disabled={true} title={_t("You do not have permission to post to this room")} @@ -113,8 +124,10 @@ const Entry: React.FC = ({ room, send }) => { } return
- - { room.name } + + + { room.name } + { button }
; }; @@ -207,7 +220,9 @@ const ForwardDialog: React.FC = ({ cli, event, permalinkCreator, onFinis cli.sendEvent(room.roomId, event.getType(), event.getContent())} + event={event} + cli={cli} + onFinished={onFinished} />, ) }
@@ -220,7 +235,9 @@ const ForwardDialog: React.FC = ({ cli, event, permalinkCreator, onFinis cli.sendEvent(room.roomId, event.getType(), event.getContent())} + event={event} + cli={cli} + onFinished={onFinished} />, ) } diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index 2f062a84cc..331ee9d131 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -100,7 +100,7 @@ describe("ForwardDialog", () => { cancelSend = reject; })); - const firstButton = wrapper.find("Entry AccessibleButton").first(); + const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first(); expect(firstButton.text()).toBe("Send"); act(() => { firstButton.simulate("click"); }); @@ -113,8 +113,8 @@ describe("ForwardDialog", () => { }); expect(firstButton.text()).toBe("Failed to send"); - const secondButton = wrapper.find("Entry AccessibleButton").at(1); - expect(secondButton.text()).toBe("Send"); + const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").at(1); + expect(secondButton.render().text()).toBe("Send"); act(() => { secondButton.simulate("click"); }); expect(secondButton.text()).toBe("Sending…"); @@ -155,9 +155,9 @@ describe("ForwardDialog", () => { const wrapper = await mountForwardDialog(undefined, rooms); - const firstButton = wrapper.find("Entry AccessibleButton").first(); + const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first(); expect(firstButton.prop("disabled")).toBe(true); - const secondButton = wrapper.find("Entry AccessibleButton").last(); + const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").last(); expect(secondButton.prop("disabled")).toBe(false); }); }); From 7efbd2d930cc93dffdabe10f84bc320f58cf23c5 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 13:57:47 -0400 Subject: [PATCH 014/115] Hide unencrypted badge from ForwardDialog preview Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 30a4cfcacf..9bd0bd2879 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -35,6 +35,12 @@ limitations under the License. .mx_EventTile_msgOption { display: none; } + + // When forwarding messages from encrypted rooms, EventTile will complain + // that our preview is unencrypted, which doesn't actually matter + .mx_EventTile_e2eIcon_unencrypted { + display: none; + } } .mx_ForwardList { From e798b36f1db8ea73c81fcc1626e1af31014c0800 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 16 May 2021 08:39:22 -0400 Subject: [PATCH 015/115] Decorate forward dialog room avatars Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 2 +- src/components/views/dialogs/ForwardDialog.tsx | 4 ++-- test/test-utils.js | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 9bd0bd2879..2a72ea4ffb 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -89,7 +89,7 @@ limitations under the License. flex-grow: 1; min-width: 0; - .mx_BaseAvatar { + .mx_DecoratedRoomAvatar { margin-right: 12px; } diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index ed6973cd4a..1ef4841851 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -30,7 +30,7 @@ import BaseDialog from "./BaseDialog"; import {avatarUrlForUser} from "../../../Avatar"; import EventTile from "../rooms/EventTile"; import SearchBox from "../../structures/SearchBox"; -import RoomAvatar from "../avatars/RoomAvatar"; +import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import AccessibleButton from "../elements/AccessibleButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; @@ -125,7 +125,7 @@ const Entry: React.FC = ({ room, event, cli, onFinished }) => { return
- + { room.name } { button } diff --git a/test/test-utils.js b/test/test-utils.js index 8c9c62844a..a2ccf84728 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -253,6 +253,7 @@ export function mkStubRoom(roomId = null, name) { tags: {}, setBlacklistUnverifiedDevices: jest.fn(), on: jest.fn(), + off: jest.fn(), removeListener: jest.fn(), getDMInviter: jest.fn(), name, From 83224dc7b66b6d3b51d6d58568759611726b0d04 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 19 May 2021 13:32:27 -0400 Subject: [PATCH 016/115] Ensure forward list room decorations are aligned Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 2a72ea4ffb..6fcd7d881a 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -82,6 +82,7 @@ limitations under the License. display: flex; justify-content: space-between; margin-top: 12px; + height: 32px; .mx_ForwardList_roomButton { display: flex; From 6cb6c7f3d0c538ac5e0742c806849d33ec685631 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 19 May 2021 13:33:48 -0400 Subject: [PATCH 017/115] Combine forward dialog room and DM lists Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 2 +- .../views/dialogs/ForwardDialog.tsx | 46 ++++--------------- 2 files changed, 9 insertions(+), 39 deletions(-) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 6fcd7d881a..47ab219150 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -63,7 +63,7 @@ limitations under the License. margin-top: 24px; } - .mx_ForwardList_section { + .mx_ForwardList_results { margin-right: 6px; &:not(:first-child) { diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 1ef4841851..b7a176cfce 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -34,7 +34,6 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import AccessibleButton from "../elements/AccessibleButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import DMRoomMap from "../../../utils/DMRoomMap"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; @@ -162,26 +161,15 @@ const ForwardDialog: React.FC = ({ cli, event, permalinkCreator, onFinis getMxcAvatarUrl: () => profileInfo.avatar_url, }; - const visibleRooms = useMemo(() => sortRooms( + const [query, setQuery] = useState(""); + const lcQuery = query.toLowerCase(); + + const rooms = useMemo(() => sortRooms( cli.getVisibleRooms().filter( room => room.getMyMembership() === "join" && !(SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()), ), - ), [cli]); - - const [query, setQuery] = useState(""); - const lcQuery = query.toLowerCase(); - - const [rooms, dms] = visibleRooms.reduce((arr, room) => { - if (room.name.toLowerCase().includes(lcQuery)) { - if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { - arr[1].push(room); - } else { - arr[0].push(room); - } - } - return arr; - }, [[], []]); + ), [cli]).filter(room => room.name.toLowerCase().includes(lcQuery)); const previewLayout = SettingsStore.getValue("layout"); @@ -214,8 +202,7 @@ const ForwardDialog: React.FC = ({ cli, event, permalinkCreator, onFinis /> { rooms.length > 0 ? ( -
-

{ _t("Rooms") }

+
{ rooms.map(room => = ({ cli, event, permalinkCreator, onFinis />, ) }
- ) : undefined } - - { dms.length > 0 ? ( -
-

{ _t("Direct Messages") }

- { dms.map(room => - , - ) } -
- ) : undefined } - - { rooms.length + dms.length < 1 ? + ) : { _t("No results") } - : undefined } + }
; From 7a045021514788f67c52bf6324e9bf16fceb42cc Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 21 May 2021 12:41:29 -0400 Subject: [PATCH 018/115] Iterate on forward dialog design feedback Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 80 ++++++++++------ .../views/dialogs/ForwardDialog.tsx | 93 +++++++++++-------- .../elements/AccessibleTooltipButton.tsx | 6 +- src/i18n/strings/en_EN.json | 6 +- .../views/dialogs/ForwardDialog-test.js | 12 +-- 5 files changed, 116 insertions(+), 81 deletions(-) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 47ab219150..1eb37e09da 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -23,7 +23,15 @@ limitations under the License. min-height: 0; height: 80vh; - .mx_ForwardDialog_preview { + > h3 { + margin: 0 0 6px; + color: $secondary-fg-color; + font-size: $font-12px; + font-weight: $font-semi-bold; + line-height: $font-15px; + } + + > .mx_ForwardDialog_preview { max-height: 30%; flex-shrink: 0; overflow: scroll; @@ -43,7 +51,14 @@ limitations under the License. } } - .mx_ForwardList { + > hr { + width: 100%; + border: none; + border-top: 1px solid $input-border-color; + margin: 12px 0; + } + + > .mx_ForwardList { display: contents; .mx_SearchBox { @@ -54,8 +69,6 @@ limitations under the License. .mx_ForwardList_content { flex-grow: 1; - // Push the scrollbar into the gutter - margin-right: -6px; } .mx_ForwardList_noResults { @@ -64,30 +77,24 @@ limitations under the License. } .mx_ForwardList_results { - margin-right: 6px; - &:not(:first-child) { margin-top: 24px; } - > h3 { - margin: 0; - color: $secondary-fg-color; - font-size: $font-12px; - font-weight: $font-semi-bold; - line-height: $font-15px; - } - .mx_ForwardList_entry { display: flex; justify-content: space-between; - margin-top: 12px; height: 32px; + padding: 6px; + border-radius: 8px; + + &:hover { + background-color: $groupFilterPanel-bg-color; + } .mx_ForwardList_roomButton { display: flex; margin-right: 12px; - flex-grow: 1; min-width: 0; .mx_DecoratedRoomAvatar { @@ -105,26 +112,39 @@ limitations under the License. } .mx_ForwardList_sendButton { - &.mx_ForwardList_sending, &.mx_ForwardList_sent { - &::before { - content: ''; - display: inline-block; - background-color: $button-primary-bg-color; - mask-position: center; - mask-repeat: no-repeat; - mask-size: 14px; - width: 14px; - height: 14px; - margin-right: 5px; - } + position: relative; + + &:not(.mx_ForwardList_canSend) .mx_ForwardList_sendLabel { + // Hide the "Send" label while preserving button size + visibility: hidden; } - &.mx_ForwardList_sending::before { + .mx_ForwardList_sendIcon, .mx_NotificationBadge { + position: absolute; + } + + .mx_NotificationBadge { + opacity: 0.4; + } + + &.mx_ForwardList_sending .mx_ForwardList_sendIcon { + background-color: $button-primary-bg-color; mask-image: url('$(res)/img/element-icons/circle-sending.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: 14px; + width: 14px; + height: 14px; } - &.mx_ForwardList_sent::before { + &.mx_ForwardList_sent .mx_ForwardList_sendIcon { + background-color: $button-primary-bg-color; mask-image: url('$(res)/img/element-icons/circle-sent.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: 14px; + width: 14px; + height: 14px; } } } diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index b7a176cfce..3448e636d9 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -31,9 +31,11 @@ import {avatarUrlForUser} from "../../../Avatar"; import EventTile from "../rooms/EventTile"; import SearchBox from "../../structures/SearchBox"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; -import AccessibleButton from "../elements/AccessibleButton"; +import {Alignment} from '../elements/Tooltip'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; +import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; +import NotificationBadge from "../rooms/NotificationBadge"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; @@ -82,52 +84,60 @@ const Entry: React.FC = ({ room, event, cli, onFinished }) => { } }; - let button; - if (room.maySendMessage()) { - let label; - let className; - if (sendState === SendState.CanSend) { - label = _t("Send"); - className = "mx_ForwardList_canSend"; - } else if (sendState === SendState.Sending) { - label = _t("Sending…"); - className = "mx_ForwardList_sending"; - } else if (sendState === SendState.Sent) { - label = _t("Sent"); - className = "mx_ForwardList_sent"; + let className; + let disabled = false; + let title; + let icon; + if (sendState === SendState.CanSend) { + className = "mx_ForwardList_canSend"; + if (room.maySendMessage()) { + title = _t("Send"); } else { - label = _t("Failed to send"); - className = "mx_ForwardList_sendFailed"; + disabled = true; + title = _t("You do not have permission to do this"); } - - button = - - { label } - ; + } else if (sendState === SendState.Sending) { + className = "mx_ForwardList_sending"; + disabled = true; + title = _t("Sending…"); + icon =
; + } else if (sendState === SendState.Sent) { + className = "mx_ForwardList_sent"; + disabled = true; + title = _t("Sent"); + icon =
; } else { - button = - {}} - disabled={true} - title={_t("You do not have permission to post to this room")} - > - { _t("Send") } - ; + className = "mx_ForwardList_sendFailed"; + disabled = true; + title = _t("Failed to send"); + icon = ; } return
- + { room.name } - - { button } + + +
{ _t("Send") }
+ { icon } +
; }; @@ -180,6 +190,7 @@ const ForwardDialog: React.FC = ({ cli, event, permalinkCreator, onFinis onFinished={onFinished} fixedWidth={false} > +

{ _t("Message preview") }

= ({ cli, event, permalinkCreator, onFinis permalinkCreator={permalinkCreator} />
+
-

{ _t("Forward to") }

{ @@ -28,6 +28,7 @@ interface ITooltipProps extends React.ComponentProps { tooltipClassName?: string; forceHide?: boolean; yOffset?: number; + alignment?: Alignment; } interface IState { @@ -66,13 +67,14 @@ export default class AccessibleTooltipButton extends React.PureComponent :
; return ( debug logs to help us track down the problem.": "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.", "Report a bug": "Report a bug", "Please view existing bugs on Github first. No match? Start a new one.": "Please view existing bugs on Github first. No match? Start a new one.", + "You do not have permission to do this": "You do not have permission to do this", "Sending…": "Sending…", "Sent": "Sent", + "Open link": "Open link", "Forward message": "Forward message", - "Forward to": "Forward to", - "Filter your rooms and DMs": "Filter your rooms and DMs", + "Message preview": "Message preview", + "Search for rooms or people": "Search for rooms or people", "Confirm abort of host creation": "Confirm abort of host creation", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.", "Abort": "Abort", diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index 331ee9d131..ce62b4fa7c 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -101,30 +101,30 @@ describe("ForwardDialog", () => { })); const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first(); - expect(firstButton.text()).toBe("Send"); + expect(firstButton.render().is(".mx_ForwardList_canSend")).toBe(true); act(() => { firstButton.simulate("click"); }); - expect(firstButton.text()).toBe("Sending…"); + expect(firstButton.render().is(".mx_ForwardList_sending")).toBe(true); await act(async () => { cancelSend(); // Wait one tick for the button to realize the send failed await new Promise(resolve => setImmediate(resolve)); }); - expect(firstButton.text()).toBe("Failed to send"); + expect(firstButton.render().is(".mx_ForwardList_sendFailed")).toBe(true); const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").at(1); - expect(secondButton.render().text()).toBe("Send"); + expect(secondButton.render().is(".mx_ForwardList_canSend")).toBe(true); act(() => { secondButton.simulate("click"); }); - expect(secondButton.text()).toBe("Sending…"); + expect(secondButton.render().is(".mx_ForwardList_sending")).toBe(true); await act(async () => { finishSend(); // Wait one tick for the button to realize the send succeeded await new Promise(resolve => setImmediate(resolve)); }); - expect(secondButton.text()).toBe("Sent"); + expect(secondButton.render().is(".mx_ForwardList_sent")).toBe(true); }); it("can render replies", async () => { From cd460a2555026fe0ceaf8bb53d31af7ad6405b64 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 21 May 2021 12:59:13 -0400 Subject: [PATCH 019/115] Adjust forward dialog copy Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 3448e636d9..ff08ce43df 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -94,7 +94,7 @@ const Entry: React.FC = ({ room, event, cli, onFinished }) => { title = _t("Send"); } else { disabled = true; - title = _t("You do not have permission to do this"); + title = _t("You don't have permission to do this"); } } else if (sendState === SendState.Sending) { className = "mx_ForwardList_sending"; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ebe5f54f61..ceb48852c5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2202,7 +2202,7 @@ "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.", "Report a bug": "Report a bug", "Please view existing bugs on Github first. No match? Start a new one.": "Please view existing bugs on Github first. No match? Start a new one.", - "You do not have permission to do this": "You do not have permission to do this", + "You don't have permission to do this": "You don't have permission to do this", "Sending…": "Sending…", "Sent": "Sent", "Open link": "Open link", From 400917623ce24f37b4eb12fda5769531f7c635c5 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 24 May 2021 08:33:28 -0400 Subject: [PATCH 020/115] Make myself the copyright holder for forward dialog code Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 2 +- src/components/views/dialogs/ForwardDialog.tsx | 2 +- test/components/views/dialogs/ForwardDialog-test.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 1eb37e09da..593fe12531 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 Robin Townsend. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index ff08ce43df..66b4b49997 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 Robin Townsend. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index ce62b4fa7c..fddba1d5ae 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 Robin Townsend. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 6ced61b7096ed31cd00ada688495235fa61b9df8 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 24 May 2021 08:34:03 -0400 Subject: [PATCH 021/115] Use camelCase Signed-off-by: Robin Townsend --- src/components/views/context_menus/MessageContextMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 5494b0a32f..4120f7be9e 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -161,7 +161,7 @@ export default class MessageContextMenu extends React.Component { cli: MatrixClientPeg.get(), event: this.props.mxEvent, permalinkCreator: this.props.permalinkCreator, - }, 'mx_Dialog_forwardmessage'); + }, 'mx_Dialog_forwardMessage'); this.closeMenu(); }; From 121ed5eba996f41ce78c8b789278d196443e53b1 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 24 May 2021 08:51:04 -0400 Subject: [PATCH 022/115] Pass Matrix client around as matrixClient Signed-off-by: Robin Townsend --- .../views/context_menus/MessageContextMenu.js | 2 +- src/components/views/dialogs/ForwardDialog.tsx | 10 +++++----- test/components/views/dialogs/ForwardDialog-test.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 4120f7be9e..08fc5844ec 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -158,7 +158,7 @@ export default class MessageContextMenu extends React.Component { onForwardClick = () => { Modal.createTrackedDialog('Forward Message', '', ForwardDialog, { - cli: MatrixClientPeg.get(), + matrixClient: MatrixClientPeg.get(), event: this.props.mxEvent, permalinkCreator: this.props.permalinkCreator, }, 'mx_Dialog_forwardMessage'); diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 66b4b49997..3087ce51a1 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -42,7 +42,7 @@ import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/Recent const AVATAR_SIZE = 30; interface IProps extends IDialogProps { - cli: MatrixClient; + matrixClient: MatrixClient; // The event to forward event: MatrixEvent; // We need a permalink creator for the source room to pass through to EventTile @@ -53,7 +53,7 @@ interface IProps extends IDialogProps { interface IEntryProps { room: Room; event: MatrixEvent; - cli: MatrixClient; + matrixClient: MatrixClient; onFinished(success: boolean): void; } @@ -64,7 +64,7 @@ enum SendState { Failed, } -const Entry: React.FC = ({ room, event, cli, onFinished }) => { +const Entry: React.FC = ({ room, event, matrixClient: cli, onFinished }) => { const [sendState, setSendState] = useState(SendState.CanSend); const jumpToRoom = () => { @@ -141,7 +141,7 @@ const Entry: React.FC = ({ room, event, cli, onFinished }) => {
; }; -const ForwardDialog: React.FC = ({ cli, event, permalinkCreator, onFinished }) => { +const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => { const userId = cli.getUserId(); const [profileInfo, setProfileInfo] = useState({}); useEffect(() => { @@ -219,7 +219,7 @@ const ForwardDialog: React.FC = ({ cli, event, permalinkCreator, onFinis key={room.roomId} room={room} event={event} - cli={cli} + matrixClient={cli} onFinished={onFinished} />, ) } diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index fddba1d5ae..004954c713 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -47,7 +47,7 @@ describe("ForwardDialog", () => { await act(async () => { wrapper = mount( Date: Mon, 24 May 2021 08:55:08 -0400 Subject: [PATCH 023/115] Give forward dialog send buttons an accessible label Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 6 +++--- src/i18n/strings/en_EN.json | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 3087ce51a1..59475b1b51 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -99,13 +99,13 @@ const Entry: React.FC = ({ room, event, matrixClient: cli, onFinish } else if (sendState === SendState.Sending) { className = "mx_ForwardList_sending"; disabled = true; - title = _t("Sending…"); - icon =
; + title = _t("Sending"); + icon =
; } else if (sendState === SendState.Sent) { className = "mx_ForwardList_sent"; disabled = true; title = _t("Sent"); - icon =
; + icon =
; } else { className = "mx_ForwardList_sendFailed"; disabled = true; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c2c6aa9e6d..817765ca69 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2203,7 +2203,7 @@ "Report a bug": "Report a bug", "Please view existing bugs on Github first. No match? Start a new one.": "Please view existing bugs on Github first. No match? Start a new one.", "You don't have permission to do this": "You don't have permission to do this", - "Sending…": "Sending…", + "Sending": "Sending", "Sent": "Sent", "Open link": "Open link", "Forward message": "Forward message", @@ -2668,7 +2668,6 @@ "Some of your messages have not been sent": "Some of your messages have not been sent", "Delete all": "Delete all", "Retry all": "Retry all", - "Sending": "Sending", "You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete", "Connectivity to the server has been lost.": "Connectivity to the server has been lost.", "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", From 170b11d130e28b75285baa0a807b41f833435831 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 12:13:16 +0100 Subject: [PATCH 024/115] Convert RightPanel to Typescript --- .../{RightPanel.js => RightPanel.tsx} | 126 +++++++++--------- src/components/views/right_panel/UserInfo.tsx | 20 ++- 2 files changed, 70 insertions(+), 76 deletions(-) rename src/components/structures/{RightPanel.js => RightPanel.tsx} (79%) diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.tsx similarity index 79% rename from src/components/structures/RightPanel.js rename to src/components/structures/RightPanel.tsx index d8c763eabd..15fa94e035 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.tsx @@ -1,6 +1,6 @@ /* Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2015 - 2020 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,13 +16,14 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import {Room} from "matrix-js-sdk/src/models/room"; +import {User} from "matrix-js-sdk/src/models/user"; +import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import * as sdk from '../../index'; import dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; -import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GroupStore from '../../stores/GroupStore'; import { RightPanelPhases, @@ -36,50 +37,70 @@ import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import WidgetCard from "../views/right_panel/WidgetCard"; import {replaceableComponent} from "../../utils/replaceableComponent"; import SettingsStore from "../../settings/SettingsStore"; +import {ActionPayload} from "../../dispatcher/payloads"; +import MemberList from "../views/rooms/MemberList"; +import GroupMemberList from "../views/groups/GroupMemberList"; +import GroupRoomList from "../views/groups/GroupRoomList"; +import GroupRoomInfo from "../views/groups/GroupRoomInfo"; +import UserInfo from "../views/right_panel/UserInfo"; +import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo"; +import FilePanel from "./FilePanel"; +import NotificationPanel from "./NotificationPanel"; +import ResizeNotifier from "../../utils/ResizeNotifier"; + +interface IProps { + room?: Room; // if showing panels for a given room, this is set + groupId?: string; // if showing panels for a given group, this is set + user?: User; // used if we know the user ahead of opening the panel + resizeNotifier: ResizeNotifier; +} + +interface IState { + phase: RightPanelPhases; + isUserPrivilegedInGroup?: boolean; + member?: RoomMember; + verificationRequest?: VerificationRequest; + verificationRequestPromise?: Promise; + space?: Room; + widgetId?: string; + groupRoomId?: string; + groupId?: string; + event: MatrixEvent; +} @replaceableComponent("structures.RightPanel") -export default class RightPanel extends React.Component { - static get propTypes() { - return { - room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set - groupId: PropTypes.string, // if showing panels for a given group, this is set - user: PropTypes.object, // used if we know the user ahead of opening the panel - }; - } - +export default class RightPanel extends React.Component { static contextType = MatrixClientContext; + private readonly delayedUpdate: RateLimitedFunc; + private dispatcherRef: string; + constructor(props, context) { super(props, context); this.state = { ...RightPanelStore.getSharedInstance().roomPanelPhaseParams, - phase: this._getPhaseFromProps(), + phase: this.getPhaseFromProps(), isUserPrivilegedInGroup: null, - member: this._getUserForPanel(), + member: this.getUserForPanel(), }; - this.onAction = this.onAction.bind(this); - this.onRoomStateMember = this.onRoomStateMember.bind(this); - this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this); - this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this); - this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this); - this._delayedUpdate = new RateLimitedFunc(() => { + this.delayedUpdate = new RateLimitedFunc(() => { this.forceUpdate(); }, 500); } - // Helper function to split out the logic for _getPhaseFromProps() and the constructor + // Helper function to split out the logic for getPhaseFromProps() and the constructor // as both are called at the same time in the constructor. - _getUserForPanel() { + private getUserForPanel() { if (this.state && this.state.member) return this.state.member; const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams; return this.props.user || lastParams['member']; } // gets the current phase from the props and also maybe the store - _getPhaseFromProps() { + private getPhaseFromProps() { const rps = RightPanelStore.getSharedInstance(); - const userForPanel = this._getUserForPanel(); + const userForPanel = this.getUserForPanel(); if (this.props.groupId) { if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) { dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList}); @@ -118,7 +139,7 @@ export default class RightPanel extends React.Component { this.dispatcherRef = dis.register(this.onAction); const cli = this.context; cli.on("RoomState.members", this.onRoomStateMember); - this._initGroupStore(this.props.groupId); + this.initGroupStore(this.props.groupId); } componentWillUnmount() { @@ -126,61 +147,47 @@ export default class RightPanel extends React.Component { if (this.context) { this.context.removeListener("RoomState.members", this.onRoomStateMember); } - this._unregisterGroupStore(this.props.groupId); + this.unregisterGroupStore(); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase if (newProps.groupId !== this.props.groupId) { - this._unregisterGroupStore(this.props.groupId); - this._initGroupStore(newProps.groupId); + this.unregisterGroupStore(); + this.initGroupStore(newProps.groupId); } } - _initGroupStore(groupId) { + private initGroupStore(groupId: string) { if (!groupId) return; GroupStore.registerListener(groupId, this.onGroupStoreUpdated); } - _unregisterGroupStore() { + private unregisterGroupStore() { GroupStore.unregisterListener(this.onGroupStoreUpdated); } - onGroupStoreUpdated() { + private onGroupStoreUpdated = () => { this.setState({ isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId), }); - } + }; - onInviteToGroupButtonClick() { - showGroupInviteDialog(this.props.groupId).then(() => { - this.setState({ - phase: RightPanelPhases.GroupMemberList, - }); - }); - } - - onAddRoomToGroupButtonClick() { - showGroupAddRoomDialog(this.props.groupId).then(() => { - this.forceUpdate(); - }); - } - - onRoomStateMember(ev, state, member) { + private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => { if (!this.props.room || member.roomId !== this.props.room.roomId) { return; } // redraw the badge on the membership list if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) { - this._delayedUpdate(); + this.delayedUpdate(); } else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId && member.userId === this.state.member.userId) { // refresh the member info (e.g. new power level) - this._delayedUpdate(); + this.delayedUpdate(); } - } + }; - onAction(payload) { + private onAction = (payload: ActionPayload) => { if (payload.action === Action.AfterRightPanelPhaseChange) { this.setState({ phase: payload.phase, @@ -194,9 +201,9 @@ export default class RightPanel extends React.Component { space: payload.space, }); } - } + }; - onClose = () => { + private onClose = () => { // XXX: There are three different ways of 'closing' this panel depending on what state // things are in... this knows far more than it should do about the state of the rest // of the app and is generally a bit silly. @@ -224,16 +231,6 @@ export default class RightPanel extends React.Component { }; render() { - const MemberList = sdk.getComponent('rooms.MemberList'); - const UserInfo = sdk.getComponent('right_panel.UserInfo'); - const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo'); - const NotificationPanel = sdk.getComponent('structures.NotificationPanel'); - const FilePanel = sdk.getComponent('structures.FilePanel'); - - const GroupMemberList = sdk.getComponent('groups.GroupMemberList'); - const GroupRoomList = sdk.getComponent('groups.GroupRoomList'); - const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo'); - let panel =
; const roomId = this.props.room ? this.props.room.roomId : undefined; @@ -285,6 +282,7 @@ export default class RightPanel extends React.Component { user={this.state.member} groupId={this.props.groupId} key={this.state.member.userId} + phase={this.state.phase} onClose={this.onClose} />; break; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 9798b282f6..9ed2943bbb 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -25,6 +25,7 @@ import {User} from 'matrix-js-sdk/src/models/user'; import {Room} from 'matrix-js-sdk/src/models/room'; import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; import {MatrixEvent} from 'matrix-js-sdk/src/models/event'; +import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import dis from '../../../dispatcher/dispatcher'; import Modal from '../../../Modal'; @@ -1529,21 +1530,16 @@ interface IProps { user: Member; groupId?: string; room?: Room; - phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo; + phase: RightPanelPhases.RoomMemberInfo + | RightPanelPhases.GroupMemberInfo + | RightPanelPhases.SpaceMemberInfo + | RightPanelPhases.EncryptionPanel; onClose(): void; + verificationRequest?: VerificationRequest; + verificationRequestPromise?: Promise; } -interface IPropsWithEncryptionPanel extends React.ComponentProps { - user: Member; - groupId: void; - room: Room; - phase: RightPanelPhases.EncryptionPanel; - onClose(): void; -} - -type Props = IProps | IPropsWithEncryptionPanel; - -const UserInfo: React.FC = ({ +const UserInfo: React.FC = ({ user, groupId, room, From 152c178ea9786fa071df680598600c4ba34fa18d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 12:15:37 +0100 Subject: [PATCH 025/115] Convert NotificationPanel to Typescript --- ...ficationPanel.js => NotificationPanel.tsx} | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) rename src/components/structures/{NotificationPanel.js => NotificationPanel.tsx} (76%) diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.tsx similarity index 76% rename from src/components/structures/NotificationPanel.js rename to src/components/structures/NotificationPanel.tsx index 41aafc8b13..7e1566521d 100644 --- a/src/components/structures/NotificationPanel.js +++ b/src/components/structures/NotificationPanel.tsx @@ -1,7 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2016, 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. @@ -16,29 +14,25 @@ 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 { _t } from '../../languageHandler'; import {MatrixClientPeg} from "../../MatrixClientPeg"; -import * as sdk from "../../index"; import BaseCard from "../views/right_panel/BaseCard"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import TimelinePanel from "./TimelinePanel"; +import Spinner from "../views/elements/Spinner"; + +interface IProps { + onClose(): void; +} /* * Component which shows the global notification list using a TimelinePanel */ @replaceableComponent("structures.NotificationPanel") -class NotificationPanel extends React.Component { - static propTypes = { - onClose: PropTypes.func.isRequired, - }; - +class NotificationPanel extends React.Component { render() { - // wrap a TimelinePanel with the jump-to-event bits turned off. - const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); - const Loader = sdk.getComponent("elements.Spinner"); - const emptyState = (

{_t('You’re all caught up')}

{_t('You have no visible notifications.')}

@@ -47,6 +41,7 @@ class NotificationPanel extends React.Component { let content; const timelineSet = MatrixClientPeg.get().getNotifTimelineSet(); if (timelineSet) { + // wrap a TimelinePanel with the jump-to-event bits turned off. content = ( ; + content = ; } return From 13427aaf0778a8a09d574d99ea33388586ee9a79 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 13:02:09 +0100 Subject: [PATCH 026/115] Add a pulse animation to the pinned messages indicator and move it --- res/css/views/rooms/_RoomHeader.scss | 41 ++++++++++++++++++------ res/img/element-icons/room/pin.svg | 10 +++--- src/components/views/rooms/RoomHeader.js | 11 +------ 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 387d1588a3..a102dfcdd7 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -281,18 +281,39 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/pin.svg'); } -.mx_RoomHeader_pinsIndicator { - position: absolute; - right: 0; - bottom: 4px; - width: 8px; - height: 8px; - border-radius: 8px; - background-color: $pinned-color; -} +$dot-size: 8px; +$pulse-color: $pinned-unread-color; .mx_RoomHeader_pinsIndicatorUnread { - background-color: $pinned-unread-color; + position: absolute; + right: 0; + top: 0; + margin: 4px; + width: $dot-size; + height: $dot-size; + border-radius: 50%; + transform: scale(1); + background: rgba($pulse-color, 1); + box-shadow: 0 0 0 0 rgba($pulse-color, 1); + animation: mx_RoomHeader_indicator_pulse 2s infinite; + animation-iteration-count: 1; +} + +@keyframes mx_RoomHeader_indicator_pulse { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba($pulse-color, 0.7); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba($pulse-color, 0); + } + + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba($pulse-color, 0); + } } @media only screen and (max-width: 480px) { diff --git a/res/img/element-icons/room/pin.svg b/res/img/element-icons/room/pin.svg index 16941b329b..2448fc61c5 100644 --- a/res/img/element-icons/room/pin.svg +++ b/res/img/element-icons/room/pin.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 6d3b50c10d..7eeb5875ef 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -110,13 +110,6 @@ export default class RoomHeader extends React.Component { return true; } - _hasPins() { - const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", ''); - if (!currentPinEvent) return false; - - return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0); - } - render() { let searchStatus = null; let cancelButton = null; @@ -184,9 +177,7 @@ export default class RoomHeader extends React.Component { if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) { let pinsIndicator = null; if (this._hasUnreadPins()) { - pinsIndicator = (
); - } else if (this._hasPins()) { - pinsIndicator = (
); + pinsIndicator = (
); } pinnedEventsButton = From 96928e5d319653352ad8a5ae56840cfb5dae1e02 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 13:17:14 +0100 Subject: [PATCH 027/115] Header Buttons switch to a fragment from an array of nodes --- .../views/right_panel/GroupHeaderButtons.tsx | 14 ++++++++------ src/components/views/right_panel/HeaderButtons.tsx | 2 +- .../views/right_panel/RoomHeaderButtons.tsx | 10 ++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/views/right_panel/GroupHeaderButtons.tsx b/src/components/views/right_panel/GroupHeaderButtons.tsx index f006975b08..5ae5709d44 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.tsx +++ b/src/components/views/right_panel/GroupHeaderButtons.tsx @@ -84,19 +84,21 @@ export default class GroupHeaderButtons extends HeaderButtons { }; renderButtons() { - return [ - + , - + , - ]; + /> + ; } } diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 2144292679..6b8445e9b9 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -95,7 +95,7 @@ export default abstract class HeaderButtons extends React.Component diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 0571622e64..2a7a9e191b 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -81,23 +81,21 @@ export default class RoomHeaderButtons extends HeaderButtons { }; public renderButtons() { - return [ + return <> , + /> , - ]; + /> + ; } } From 02d11b8926494b16f897e49f97076af8461c1278 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 13:53:46 +0100 Subject: [PATCH 028/115] Extend HeaderButton and HeaderButtons to be more generic --- .../views/right_panel/HeaderButton.tsx | 26 ++++++++----------- .../views/right_panel/HeaderButtons.tsx | 4 +-- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/components/views/right_panel/HeaderButton.tsx b/src/components/views/right_panel/HeaderButton.tsx index 2bc360e380..8451f69fd3 100644 --- a/src/components/views/right_panel/HeaderButton.tsx +++ b/src/components/views/right_panel/HeaderButton.tsx @@ -29,8 +29,6 @@ interface IProps { isHighlighted: boolean; // click handler onClick: () => void; - // The badge to display above the icon - badge?: React.ReactNode; // The parameters to track the click event analytics: Parameters; @@ -40,31 +38,29 @@ interface IProps { title: string; } -// TODO: replace this, the composer buttons and the right panel buttons with a unified -// representation +// TODO: replace this, the composer buttons and the right panel buttons with a unified representation @replaceableComponent("views.right_panel.HeaderButton") export default class HeaderButton extends React.Component { - constructor(props: IProps) { - super(props); - this.onClick = this.onClick.bind(this); - } - - private onClick() { + private onClick = () => { Analytics.trackEvent(...this.props.analytics); this.props.onClick(); - } + }; public render() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const {isHighlighted, onClick, analytics, name, title, ...props} = this.props; + const classes = classNames({ mx_RightPanel_headerButton: true, - mx_RightPanel_headerButton_highlight: this.props.isHighlighted, - [`mx_RightPanel_${this.props.name}`]: true, + mx_RightPanel_headerButton_highlight: isHighlighted, + [`mx_RightPanel_${name}`]: true, }); return ; diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 6b8445e9b9..281d7edd8b 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -43,11 +43,11 @@ interface IState { interface IProps {} @replaceableComponent("views.right_panel.HeaderButtons") -export default abstract class HeaderButtons extends React.Component { +export default abstract class HeaderButtons

extends React.Component { private storeToken: EventSubscription; private dispatcherRef: string; - constructor(props: IProps, kind: HeaderKind) { + constructor(props: IProps & P, kind: HeaderKind) { super(props); const rps = RightPanelStore.getSharedInstance(); From a6ca8f797dd851552608b5ea0c8355d34bb9d25c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 15:34:44 +0100 Subject: [PATCH 029/115] Fix useAsyncMemo out-of-order resolutions --- src/hooks/useAsyncMemo.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/hooks/useAsyncMemo.ts b/src/hooks/useAsyncMemo.ts index 38c70de259..eda44b1ae4 100644 --- a/src/hooks/useAsyncMemo.ts +++ b/src/hooks/useAsyncMemo.ts @@ -21,7 +21,15 @@ type Fn = () => Promise; export const useAsyncMemo = (fn: Fn, deps: DependencyList, initialValue?: T): T => { const [value, setValue] = useState(initialValue); useEffect(() => { - fn().then(setValue); + let discard = false; + fn().then(v => { + if (!discard) { + setValue(v); + } + }); + return () => { + discard = true; + }; }, deps); // eslint-disable-line react-hooks/exhaustive-deps return value; }; From 4fa6d3599b2b1902f5faa78cf9a5f130397de1ae Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 15:44:39 +0100 Subject: [PATCH 030/115] Convert PinnedEventTile to Typescript --- ...PinnedEventTile.js => PinnedEventTile.tsx} | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) rename src/components/views/rooms/{PinnedEventTile.js => PinnedEventTile.tsx} (76%) diff --git a/src/components/views/rooms/PinnedEventTile.js b/src/components/views/rooms/PinnedEventTile.tsx similarity index 76% rename from src/components/views/rooms/PinnedEventTile.js rename to src/components/views/rooms/PinnedEventTile.tsx index 78cf422cc6..482fd6ab17 100644 --- a/src/components/views/rooms/PinnedEventTile.js +++ b/src/components/views/rooms/PinnedEventTile.tsx @@ -1,5 +1,6 @@ /* Copyright 2017 Travis Ralston +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. @@ -15,7 +16,9 @@ limitations under the License. */ import React from "react"; -import PropTypes from 'prop-types'; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + import {MatrixClientPeg} from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; import AccessibleButton from "../elements/AccessibleButton"; @@ -25,15 +28,15 @@ import { _t } from '../../../languageHandler'; import {formatFullDate} from '../../../DateUtils'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.rooms.PinnedEventTile") -export default class PinnedEventTile extends React.Component { - static propTypes = { - mxRoom: PropTypes.object.isRequired, - mxEvent: PropTypes.object.isRequired, - onUnpinned: PropTypes.func, - }; +interface IProps { + mxRoom: Room; + mxEvent: MatrixEvent; + onUnpinned?(): void; +} - onTileClicked = () => { +@replaceableComponent("views.rooms.PinnedEventTile") +export default class PinnedEventTile extends React.Component { + private onTileClicked = () => { dis.dispatch({ action: 'view_room', event_id: this.props.mxEvent.getId(), @@ -42,7 +45,7 @@ export default class PinnedEventTile extends React.Component { }); }; - onUnpinClicked = () => { + private onUnpinClicked = () => { const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", ""); if (!pinnedEvents || !pinnedEvents.getContent().pinned) { // Nothing to do: already unpinned @@ -60,7 +63,7 @@ export default class PinnedEventTile extends React.Component { } }; - _canUnpin() { + private canUnpin() { return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get()); } @@ -71,10 +74,16 @@ export default class PinnedEventTile extends React.Component { const avatarSize = 40; let unpinButton = null; - if (this._canUnpin()) { + if (this.canUnpin()) { unpinButton = ( - {_t('Unpin + {_t('Unpin ); } @@ -82,14 +91,22 @@ export default class PinnedEventTile extends React.Component { return (

- + { _t("Jump to message") } { unpinButton }
- + { senderProfile ? senderProfile.name : sender } From 59f4c728c9a9b2b6072ae26fb1e5f0953fe17fd0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 16:10:44 +0100 Subject: [PATCH 031/115] Initial cut of Pinned event card in the right panel --- res/css/_components.scss | 2 +- res/css/structures/_RightPanel.scss | 42 +++ .../right_panel/_PinnedMessagesCard.scss | 241 ++++++++++++++++++ res/css/views/rooms/_PinnedEventsPanel.scss | 37 --- res/css/views/rooms/_RoomHeader.scss | 39 --- src/components/structures/RightPanel.tsx | 11 +- src/components/structures/RoomView.tsx | 15 -- .../views/right_panel/PinnedMessagesCard.tsx | 145 +++++++++++ .../views/right_panel/RoomHeaderButtons.tsx | 46 +++- src/components/views/right_panel/UserInfo.tsx | 3 - src/components/views/rooms/RoomHeader.js | 49 +--- src/settings/Settings.tsx | 4 - src/stores/RightPanelStorePhases.ts | 1 + 13 files changed, 483 insertions(+), 152 deletions(-) create mode 100644 res/css/views/right_panel/_PinnedMessagesCard.scss delete mode 100644 res/css/views/rooms/_PinnedEventsPanel.scss create mode 100644 src/components/views/right_panel/PinnedMessagesCard.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index c8985cbb51..418b8f51c9 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -179,6 +179,7 @@ @import "./views/messages/_common_CryptoEvent.scss"; @import "./views/right_panel/_BaseCard.scss"; @import "./views/right_panel/_EncryptionInfo.scss"; +@import "./views/right_panel/_PinnedMessagesCard.scss"; @import "./views/right_panel/_RoomSummaryCard.scss"; @import "./views/right_panel/_UserInfo.scss"; @import "./views/right_panel/_VerificationPanel.scss"; @@ -203,7 +204,6 @@ @import "./views/rooms/_NewRoomIntro.scss"; @import "./views/rooms/_NotificationBadge.scss"; @import "./views/rooms/_PinnedEventTile.scss"; -@import "./views/rooms/_PinnedEventsPanel.scss"; @import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_ReplyPreview.scss"; @import "./views/rooms/_RoomBreadcrumbs.scss"; diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 5515fe4060..e24100a9bb 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -98,6 +98,48 @@ limitations under the License. mask-position: center; } +$dot-size: 8px; +$pulse-color: $pinned-unread-color; + +.mx_RightPanel_pinnedMessagesButton { + &::before { + mask-image: url('$(res)/img/element-icons/room/pin.svg'); + mask-position: center; + } + + .mx_RightPanel_pinnedMessagesButton_unreadIndicator { + position: absolute; + right: 0; + top: 0; + margin: 4px; + width: $dot-size; + height: $dot-size; + border-radius: 50%; + transform: scale(1); + background: rgba($pulse-color, 1); + box-shadow: 0 0 0 0 rgba($pulse-color, 1); + animation: mx_RightPanel_indicator_pulse 2s infinite; + animation-iteration-count: 1; + } +} + +@keyframes mx_RightPanel_indicator_pulse { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba($pulse-color, 0.7); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba($pulse-color, 0); + } + + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba($pulse-color, 0); + } +} + .mx_RightPanel_headerButton_highlight { &::before { background-color: $accent-color !important; diff --git a/res/css/views/right_panel/_PinnedMessagesCard.scss b/res/css/views/right_panel/_PinnedMessagesCard.scss new file mode 100644 index 0000000000..4cbc8b9abc --- /dev/null +++ b/res/css/views/right_panel/_PinnedMessagesCard.scss @@ -0,0 +1,241 @@ +/* +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_PinnedMessagesCard { + .mx_BaseCard_header { + text-align: center; + margin-top: 20px; + + h2 { + font-weight: $font-semi-bold; + font-size: $font-18px; + margin: 12px 0 4px; + } + + .mx_RoomSummaryCard_alias { + font-size: $font-13px; + color: $secondary-fg-color; + } + + h2, .mx_RoomSummaryCard_alias { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + white-space: pre-wrap; + } + + .mx_RoomSummaryCard_avatar { + display: inline-flex; + + .mx_RoomSummaryCard_e2ee { + display: inline-block; + position: relative; + width: 54px; + height: 54px; + border-radius: 50%; + background-color: #737d8c; + margin-top: -3px; // alignment + margin-left: -10px; // overlap + border: 3px solid $dark-panel-bg-color; + + &::before { + content: ''; + position: absolute; + top: 13px; + left: 13px; + height: 28px; + width: 28px; + mask-size: cover; + mask-repeat: no-repeat; + mask-position: center; + mask-image: url('$(res)/img/e2e/disabled.svg'); + background-color: #ffffff; + } + } + + .mx_RoomSummaryCard_e2ee_normal { + background-color: #424446; + &::before { + mask-image: url('$(res)/img/e2e/normal.svg'); + } + } + + .mx_RoomSummaryCard_e2ee_verified { + background-color: #0dbd8b; + &::before { + mask-image: url('$(res)/img/e2e/verified.svg'); + } + } + + .mx_RoomSummaryCard_e2ee_warning { + background-color: #ff4b55; + &::before { + mask-image: url('$(res)/img/e2e/warning.svg'); + } + } + } + } + + .mx_RoomSummaryCard_aboutGroup { + .mx_RoomSummaryCard_Button { + padding-left: 44px; + + &::before { + content: ''; + position: absolute; + top: 8px; + left: 10px; + height: 24px; + width: 24px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $icon-button-color; + } + } + } + + .mx_RoomSummaryCard_appsGroup { + .mx_RoomSummaryCard_Button { + // this button is special so we have to override some of the original styling + // as we will be applying it in its children + padding: 0; + height: auto; + color: $tertiary-fg-color; + + .mx_RoomSummaryCard_icon_app { + padding: 10px 48px 10px 12px; // based on typical mx_RoomSummaryCard_Button padding + text-overflow: ellipsis; + overflow: hidden; + + .mx_BaseAvatar_image { + vertical-align: top; + margin-right: 12px; + } + + span { + color: $primary-fg-color; + } + } + + .mx_RoomSummaryCard_app_pinToggle, + .mx_RoomSummaryCard_app_options { + position: absolute; + top: 0; + height: 100%; // to give bigger interactive zone + width: 24px; + padding: 12px 4px; + box-sizing: border-box; + min-width: 24px; // prevent flexbox crushing + + &:hover { + &::after { + content: ''; + position: absolute; + height: 24px; + width: 24px; + top: 8px; // equal to padding-top of parent + left: 0; + border-radius: 12px; + background-color: rgba(141, 151, 165, 0.1); + } + } + + &::before { + content: ''; + position: absolute; + height: 16px; + width: 16px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 16px; + background-color: $icon-button-color; + } + } + + .mx_RoomSummaryCard_app_pinToggle { + right: 24px; + + &::before { + mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); + } + } + + .mx_RoomSummaryCard_app_options { + right: 48px; + display: none; + + &::before { + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); + } + } + + &.mx_RoomSummaryCard_Button_pinned { + &::after { + opacity: 0.2; + } + + .mx_RoomSummaryCard_app_pinToggle::before { + background-color: $accent-color; + } + } + + &:hover { + .mx_RoomSummaryCard_icon_app { + padding-right: 72px; + } + + .mx_RoomSummaryCard_app_options { + display: unset; + } + } + + &::before { + content: unset; + } + + &::after { + top: 8px; // re-align based on the height change + pointer-events: none; // pass through to the real button + } + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + margin-top: 12px; + margin-bottom: 12px; + font-size: $font-13px; + font-weight: $font-semi-bold; + } +} + +.mx_RoomSummaryCard_icon_people::before { + mask-image: url("$(res)/img/element-icons/room/members.svg"); +} + +.mx_RoomSummaryCard_icon_files::before { + mask-image: url('$(res)/img/element-icons/room/files.svg'); +} + +.mx_RoomSummaryCard_icon_share::before { + mask-image: url('$(res)/img/element-icons/room/share.svg'); +} + +.mx_RoomSummaryCard_icon_settings::before { + mask-image: url('$(res)/img/element-icons/settings.svg'); +} diff --git a/res/css/views/rooms/_PinnedEventsPanel.scss b/res/css/views/rooms/_PinnedEventsPanel.scss deleted file mode 100644 index 663d5bdf6e..0000000000 --- a/res/css/views/rooms/_PinnedEventsPanel.scss +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2017 Travis Ralston - -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_PinnedEventsPanel { - border-top: 1px solid $primary-hairline-color; -} - -.mx_PinnedEventsPanel_body { - max-height: 300px; - overflow-y: auto; - padding-bottom: 15px; -} - -.mx_PinnedEventsPanel_header { - margin: 0; - padding-top: 8px; - padding-bottom: 15px; -} - -.mx_PinnedEventsPanel_cancel { - margin: 12px; - float: right; - display: inline-block; -} diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index a102dfcdd7..4142b0a2ef 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -277,45 +277,6 @@ limitations under the License. margin-top: 18px; } -.mx_RoomHeader_pinnedButton::before { - mask-image: url('$(res)/img/element-icons/room/pin.svg'); -} - -$dot-size: 8px; -$pulse-color: $pinned-unread-color; - -.mx_RoomHeader_pinsIndicatorUnread { - position: absolute; - right: 0; - top: 0; - margin: 4px; - width: $dot-size; - height: $dot-size; - border-radius: 50%; - transform: scale(1); - background: rgba($pulse-color, 1); - box-shadow: 0 0 0 0 rgba($pulse-color, 1); - animation: mx_RoomHeader_indicator_pulse 2s infinite; - animation-iteration-count: 1; -} - -@keyframes mx_RoomHeader_indicator_pulse { - 0% { - transform: scale(0.95); - box-shadow: 0 0 0 0 rgba($pulse-color, 0.7); - } - - 70% { - transform: scale(1); - box-shadow: 0 0 0 10px rgba($pulse-color, 0); - } - - 100% { - transform: scale(0.95); - box-shadow: 0 0 0 0 rgba($pulse-color, 0); - } -} - @media only screen and (max-width: 480px) { .mx_RoomHeader_wrapper { padding: 0; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 15fa94e035..e3237d98a6 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -26,9 +26,9 @@ import dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; import GroupStore from '../../stores/GroupStore'; import { - RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS, RIGHT_PANEL_SPACE_PHASES, + RightPanelPhases, } from "../../stores/RightPanelStorePhases"; import RightPanelStore from "../../stores/RightPanelStore"; import MatrixClientContext from "../../contexts/MatrixClientContext"; @@ -47,6 +47,7 @@ import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo"; import FilePanel from "./FilePanel"; import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; +import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; interface IProps { room?: Room; // if showing panels for a given room, this is set @@ -294,7 +295,13 @@ export default class RightPanel extends React.Component { break; case RightPanelPhases.NotificationPanel: - panel = ; + if (SettingsStore.getValue("feature_pinning")) { + panel = ; + } + break; + + case RightPanelPhases.PinnedMessages: + panel = ; break; case RightPanelPhases.FilePanel: diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index d822b6a839..e8ad1d7e45 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -155,7 +155,6 @@ export interface IState { canPeek: boolean; showApps: boolean; isPeeking: boolean; - showingPinned: boolean; showReadReceipts: boolean; showRightPanel: boolean; // error object, as from the matrix client/server API @@ -232,7 +231,6 @@ export default class RoomView extends React.Component { canPeek: false, showApps: false, isPeeking: false, - showingPinned: false, showReadReceipts: true, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, joining: false, @@ -327,7 +325,6 @@ export default class RoomView extends React.Component { forwardingEvent: RoomViewStore.getForwardingEvent(), // we should only peek once we have a ready client shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), - showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), wasContextSwitch: RoomViewStore.getWasContextSwitch(), }; @@ -1375,13 +1372,6 @@ export default class RoomView extends React.Component { return ret; } - private onPinnedClick = () => { - const nowShowingPinned = !this.state.showingPinned; - const roomId = this.state.room.roomId; - this.setState({showingPinned: nowShowingPinned, searching: false}); - SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned); - }; - private onCallPlaced = (type: PlaceCallType) => { dis.dispatch({ action: 'place_call', @@ -1498,7 +1488,6 @@ export default class RoomView extends React.Component { private onSearchClick = () => { this.setState({ searching: !this.state.searching, - showingPinned: false, }); }; @@ -1825,9 +1814,6 @@ export default class RoomView extends React.Component { } else if (showRoomUpgradeBar) { aux = ; hideCancel = true; - } else if (this.state.showingPinned) { - hideCancel = true; // has own cancel - aux = ; } else if (myMembership !== "join") { // We do have a room object for this room, but we're not currently in it. // We may have a 3rd party invite to it. @@ -2045,7 +2031,6 @@ export default class RoomView extends React.Component { inRoom={myMembership === 'join'} onSearchClick={this.onSearchClick} onSettingsClick={this.onSettingsClick} - onPinnedClick={this.onPinnedClick} onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null} onForgetClick={(myMembership === "leave") ? this.onForgetClick : null} onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null} diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx new file mode 100644 index 0000000000..46625b6e07 --- /dev/null +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -0,0 +1,145 @@ +/* +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, {useCallback, useContext, useEffect, useState} from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { EventType } from 'matrix-js-sdk/src/@types/event'; + +import { _t } from "../../../languageHandler"; +import BaseCard from "./BaseCard"; +import Spinner from "../elements/Spinner"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import PinningUtils from "../../../utils/PinningUtils"; +import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; +import PinnedEventTile from "../rooms/PinnedEventTile"; + +interface IProps { + room: Room; + onClose(): void; +} + +export const usePinnedEvents = (room: Room): string[] => { + const [pinnedEvents, setPinnedEvents] = useState([]); + + const update = useCallback((ev?: MatrixEvent) => { + if (!room) return; + if (ev && ev.getType() !== EventType.RoomPinnedEvents) return; + setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []); + }, [room]); + + useEventEmitter(room.currentState, "RoomState.events", update); + useEffect(() => { + update(); + return () => { + setPinnedEvents([]); + }; + }, [update]); + return pinnedEvents; +}; + +const ReadPinsEventId = "im.vector.room.read_pins"; +const ReadPinsNumIds = 10; + +export const useReadPinnedEvents = (room: Room): Set => { + const [readPinnedEvents, setReadPinnedEvents] = useState>(new Set()); + + const update = useCallback((ev?: MatrixEvent) => { + if (!room) return; + if (ev && ev.getType() !== ReadPinsEventId) return; + const readPins = room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids; + setReadPinnedEvents(new Set(readPins || [])); + }, [room]); + + useEventEmitter(room, "Room.accountData", update); + useEffect(() => { + update(); + return () => { + setReadPinnedEvents(new Set()); + }; + }, [update]); + return readPinnedEvents; +}; + +const PinnedMessagesCard = ({ room, onClose }: IProps) => { + const cli = useContext(MatrixClientContext); + const pinnedEventIds = usePinnedEvents(room); + const readPinnedEvents = useReadPinnedEvents(room); + + useEffect(() => { + const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id)); + if (newlyRead.length > 0) { + // Only keep the last N event IDs to avoid infinite growth + cli.setRoomAccountData(room.roomId, ReadPinsEventId, { + event_ids: [ + ...newlyRead.reverse(), + ...readPinnedEvents, + ].splice(0, ReadPinsNumIds), + }); + } + }, [cli, room.roomId, pinnedEventIds, readPinnedEvents]); + + const pinnedEvents = useAsyncMemo(() => { + const promises = pinnedEventIds.map(async eventId => { + const timelineSet = room.getUnfilteredTimelineSet(); + const localEvent = timelineSet?.getTimelineForEvent(eventId)?.getEvents().find(e => e.getId() === eventId); + if (localEvent) return localEvent; + + try { + const evJson = await cli.fetchRoomEvent(room.roomId, eventId); + const event = new MatrixEvent(evJson); + if (event.isEncrypted()) { + await cli.decryptEventIfNeeded(event); // TODO await? + } + if (event && PinningUtils.isPinnable(event)) { + return event; + } + } catch (err) { + console.error("Error looking up pinned event " + eventId + " in room " + room.roomId); + console.error(err); + } + return null; + }); + + return Promise.all(promises); + }, [cli, room, pinnedEventIds], null); + + let content; + if (!pinnedEvents) { + content = ; + } else if (pinnedEvents.length > 0) { + content = pinnedEvents.filter(Boolean).map(ev => ( + {}} + /> + )); + } else { + content =
+

{_t("You’re all caught up")}

+

{_t("You have no visible notifications.")}

+
; + } + + return + { content } + ; +}; + +export default PinnedMessagesCard; diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 2a7a9e191b..3d3f33be00 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -18,7 +18,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; + import {_t} from '../../../languageHandler'; import HeaderButton from './HeaderButton'; import HeaderButtons, {HeaderKind} from './HeaderButtons'; @@ -27,6 +29,8 @@ import {Action} from "../../../dispatcher/actions"; import {ActionPayload} from "../../../dispatcher/payloads"; import RightPanelStore from "../../../stores/RightPanelStore"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {useSettingValue} from "../../../hooks/useSettings"; +import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard'; const ROOM_INFO_PHASES = [ RightPanelPhases.RoomSummary, @@ -38,9 +42,35 @@ const ROOM_INFO_PHASES = [ RightPanelPhases.Room3pidMemberInfo, ]; +const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => { + const pinningEnabled = useSettingValue("feature_pinning"); + const pinnedEvents = usePinnedEvents(pinningEnabled && room); + const readPinnedEvents = useReadPinnedEvents(pinningEnabled && room); + if (!pinningEnabled) return null; + + let unreadIndicator; + if (pinnedEvents.some(id => !readPinnedEvents.has(id))) { + unreadIndicator =
; + } + + return + { unreadIndicator } + ; +}; + +interface IProps { + room?: Room; +} + @replaceableComponent("views.right_panel.RoomHeaderButtons") -export default class RoomHeaderButtons extends HeaderButtons { - constructor(props) { +export default class RoomHeaderButtons extends HeaderButtons { + constructor(props: IProps) { super(props, HeaderKind.Room); } @@ -80,8 +110,18 @@ export default class RoomHeaderButtons extends HeaderButtons { this.setPhase(RightPanelPhases.NotificationPanel); }; + private onPinnedMessagesClicked = () => { + // This toggles for us, if needed + this.setPhase(RightPanelPhases.PinnedMessages); + }; + public renderButtons() { return <> + { } else { setPowerLevels({}); } - return () => { - setPowerLevels({}); - }; }, [room]); useEventEmitter(cli, "RoomState.events", update); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 7eeb5875ef..217b3ea5c2 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -40,7 +40,6 @@ export default class RoomHeader extends React.Component { oobData: PropTypes.object, inRoom: PropTypes.bool, onSettingsClick: PropTypes.func, - onPinnedClick: PropTypes.func, onSearchClick: PropTypes.func, onLeaveClick: PropTypes.func, onCancelClick: PropTypes.func, @@ -59,14 +58,12 @@ export default class RoomHeader extends React.Component { componentDidMount() { const cli = MatrixClientPeg.get(); cli.on("RoomState.events", this._onRoomStateEvents); - cli.on("Room.accountData", this._onRoomAccountData); } componentWillUnmount() { const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener("RoomState.events", this._onRoomStateEvents); - cli.removeListener("Room.accountData", this._onRoomAccountData); } } @@ -79,41 +76,14 @@ export default class RoomHeader extends React.Component { this._rateLimitedUpdate(); }; - _onRoomAccountData = (event, room) => { - if (!this.props.room || room.roomId !== this.props.room.roomId) return; - if (event.getType() !== "im.vector.room.read_pins") return; - - this._rateLimitedUpdate(); - }; - _rateLimitedUpdate = new RateLimitedFunc(function() { /* eslint-disable babel/no-invalid-this */ this.forceUpdate(); }, 500); - _hasUnreadPins() { - const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", ''); - if (!currentPinEvent) return false; - if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) { - return false; // no pins == nothing to read - } - - const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins"); - if (readPinsEvent && readPinsEvent.getContent()) { - const readStateEvents = readPinsEvent.getContent().event_ids || []; - if (readStateEvents) { - return !readStateEvents.includes(currentPinEvent.getId()); - } - } - - // There's pins, and we haven't read any of them - return true; - } - render() { let searchStatus = null; let cancelButton = null; - let pinnedEventsButton = null; if (this.props.onCancelClick) { cancelButton = ; @@ -174,22 +144,6 @@ export default class RoomHeader extends React.Component { />; } - if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) { - let pinsIndicator = null; - if (this._hasUnreadPins()) { - pinsIndicator = (
); - } - - pinnedEventsButton = - - { pinsIndicator } - ; - } - let forgetButton; if (this.props.onForgetClick) { forgetButton = @@ -239,7 +193,6 @@ export default class RoomHeader extends React.Component {
{ videoCallButton } { voiceCallButton } - { pinnedEventsButton } { forgetButton } { appsButton } { searchButton } @@ -256,7 +209,7 @@ export default class RoomHeader extends React.Component { { topicElement } { cancelButton } { rightRow } - +
); diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 6ff14c16b5..155d039572 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -601,10 +601,6 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td('Enable widget screenshots on supported widgets'), default: false, }, - "PinnedEvents.isOpen": { - supportedLevels: [SettingLevel.ROOM_DEVICE], - default: false, - }, "promptBeforeInviteUnknownUsers": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Prompt before sending invites to potentially invalid matrix IDs'), diff --git a/src/stores/RightPanelStorePhases.ts b/src/stores/RightPanelStorePhases.ts index aea78c7460..0d96f92945 100644 --- a/src/stores/RightPanelStorePhases.ts +++ b/src/stores/RightPanelStorePhases.ts @@ -24,6 +24,7 @@ export enum RightPanelPhases { EncryptionPanel = 'EncryptionPanel', RoomSummary = 'RoomSummary', Widget = 'Widget', + PinnedMessages = "PinnedMessages", Room3pidMemberInfo = 'Room3pidMemberInfo', // Group stuff From c1f397dcf7bfaad0e894b4d949b94094108813b6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 16:20:23 +0100 Subject: [PATCH 032/115] delint --- src/components/structures/RoomView.tsx | 2 -- src/components/views/right_panel/HeaderButtons.tsx | 2 +- src/contexts/RoomContext.ts | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index e8ad1d7e45..482f4a45a7 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -54,7 +54,6 @@ import RoomContext from "../../contexts/RoomContext"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils'; import { Action } from "../../dispatcher/actions"; -import { SettingLevel } from "../../settings/SettingLevel"; import { IMatrixClientCreds } from "../../MatrixClientPeg"; import ScrollPanel from "./ScrollPanel"; import TimelinePanel from "./TimelinePanel"; @@ -63,7 +62,6 @@ import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; import ForwardMessage from "../views/rooms/ForwardMessage"; import SearchBar from "../views/rooms/SearchBar"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; -import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel"; import AuxPanel from "../views/rooms/AuxPanel"; import RoomHeader from "../views/rooms/RoomHeader"; import { XOR } from "../../@types/common"; diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 281d7edd8b..0c7d9ee299 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -43,7 +43,7 @@ interface IState { interface IProps {} @replaceableComponent("views.right_panel.HeaderButtons") -export default abstract class HeaderButtons

extends React.Component { +export default abstract class HeaderButtons

extends React.Component { private storeToken: EventSubscription; private dispatcherRef: string; diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index 30ff74b071..7f2c0bb157 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -31,7 +31,6 @@ const RoomContext = createContext({ canPeek: false, showApps: false, isPeeking: false, - showingPinned: false, showReadReceipts: true, showRightPanel: true, joining: false, From fd74a946e087b0ae42dbc74cbd4a30ee5386c895 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 17:24:43 +0100 Subject: [PATCH 033/115] add header --- .../views/right_panel/PinnedMessagesCard.tsx | 6 +- .../views/rooms/PinnedEventsPanel.js | 145 ------------------ src/i18n/strings/en_EN.json | 10 +- 3 files changed, 10 insertions(+), 151 deletions(-) delete mode 100644 src/components/views/rooms/PinnedEventsPanel.js diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index 46625b6e07..579b12c3cf 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -137,7 +137,11 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {

; } - return + return { _t("Pinned") }} + className="mx_NotificationPanel" + onClose={onClose} + > { content } ; }; diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js deleted file mode 100644 index 4b310dbbca..0000000000 --- a/src/components/views/rooms/PinnedEventsPanel.js +++ /dev/null @@ -1,145 +0,0 @@ -/* -Copyright 2017 Travis Ralston -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 PropTypes from 'prop-types'; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import AccessibleButton from "../elements/AccessibleButton"; -import PinnedEventTile from "./PinnedEventTile"; -import { _t } from '../../../languageHandler'; -import PinningUtils from "../../../utils/PinningUtils"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; - -@replaceableComponent("views.rooms.PinnedEventsPanel") -export default class PinnedEventsPanel extends React.Component { - static propTypes = { - // The Room from the js-sdk we're going to show pinned events for - room: PropTypes.object.isRequired, - - onCancelClick: PropTypes.func, - }; - - state = { - loading: true, - }; - - componentDidMount() { - this._updatePinnedMessages(); - MatrixClientPeg.get().on("RoomState.events", this._onStateEvent); - } - - componentWillUnmount() { - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent); - } - } - - _onStateEvent = ev => { - if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") { - this._updatePinnedMessages(); - } - }; - - _updatePinnedMessages = () => { - const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", ""); - if (!pinnedEvents || !pinnedEvents.getContent().pinned) { - this.setState({ loading: false, pinned: [] }); - } else { - const promises = []; - const cli = MatrixClientPeg.get(); - - pinnedEvents.getContent().pinned.map((eventId) => { - promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then( - (timeline) => { - const event = timeline.getEvents().find((e) => e.getId() === eventId); - return {eventId, timeline, event}; - }).catch((err) => { - console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId); - console.error(err); - return null; // return lack of context to avoid unhandled errors - })); - }); - - Promise.all(promises).then((contexts) => { - // Filter out the messages before we try to render them - const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event)); - - this.setState({ loading: false, pinned }); - }); - } - - this._updateReadState(); - }; - - _updateReadState() { - const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", ""); - if (!pinnedEvents) return; // nothing to read - - let readStateEvents = []; - const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins"); - if (readPinsEvent && readPinsEvent.getContent()) { - readStateEvents = readPinsEvent.getContent().event_ids || []; - } - - if (!readStateEvents.includes(pinnedEvents.getId())) { - readStateEvents.push(pinnedEvents.getId()); - - // Only keep the last 10 event IDs to avoid infinite growth - readStateEvents = readStateEvents.reverse().splice(0, 10).reverse(); - - MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", { - event_ids: readStateEvents, - }); - } - } - - _getPinnedTiles() { - if (this.state.pinned.length === 0) { - return (
{ _t("No pinned messages.") }
); - } - - return this.state.pinned.map((context) => { - return ( - - ); - }); - } - - render() { - let tiles =
{ _t("Loading...") }
; - if (this.state && !this.state.loading) { - tiles = this._getPinnedTiles(); - } - - return ( -
-
- - - -

{ _t("Pinned Messages") }

- { tiles } -
-
- ); - } -} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7ceb039822..0aebfc48ce 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1508,9 +1508,6 @@ "Invite to just this room": "Invite to just this room", "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "This is the start of .": "This is the start of .", - "No pinned messages.": "No pinned messages.", - "Loading...": "Loading...", - "Pinned Messages": "Pinned Messages", "Unpin Message": "Unpin Message", "Jump to message": "Jump to message", "%(duration)ss": "%(duration)ss", @@ -1718,6 +1715,10 @@ "The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to", "Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection", "Yours, or the other users’ session": "Yours, or the other users’ session", + "You’re all caught up": "You’re all caught up", + "You have no visible notifications.": "You have no visible notifications.", + "Pinned": "Pinned", + "Pinned Messages": "Pinned Messages", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "Unpin": "Unpin", @@ -1896,6 +1897,7 @@ "Add rooms to this community": "Add rooms to this community", "Filter community rooms": "Filter community rooms", "Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.", + "Loading...": "Loading...", "Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.", "You're not currently a member of any communities.": "You're not currently a member of any communities.", "Frequently Used": "Frequently Used", @@ -2626,8 +2628,6 @@ "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.", "Communities are changing to Spaces": "Communities are changing to Spaces", - "You’re all caught up": "You’re all caught up", - "You have no visible notifications.": "You have no visible notifications.", "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", "%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.", "The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.", From 27ad90760d117e6c8c203d04669c715d564a01e5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 13:51:17 +0100 Subject: [PATCH 034/115] Iterate pinned messages --- .../right_panel/_PinnedMessagesCard.scss | 222 +----------------- res/css/views/rooms/_PinnedEventTile.scss | 122 ++++++---- .../views/right_panel/PinnedMessagesCard.tsx | 61 +++-- .../views/right_panel/RoomHeaderButtons.tsx | 2 +- .../views/rooms/PinnedEventTile.tsx | 132 +++++------ src/i18n/strings/en_EN.json | 6 +- src/stores/RightPanelStorePhases.ts | 1 + 7 files changed, 185 insertions(+), 361 deletions(-) diff --git a/res/css/views/right_panel/_PinnedMessagesCard.scss b/res/css/views/right_panel/_PinnedMessagesCard.scss index 4cbc8b9abc..b6b8238bed 100644 --- a/res/css/views/right_panel/_PinnedMessagesCard.scss +++ b/res/css/views/right_panel/_PinnedMessagesCard.scss @@ -15,227 +15,21 @@ limitations under the License. */ .mx_PinnedMessagesCard { + padding-top: 0; + .mx_BaseCard_header { text-align: center; - margin-top: 20px; + margin-top: 0; + border-bottom: 1px solid $menu-border-color; - h2 { + > h2 { font-weight: $font-semi-bold; font-size: $font-18px; - margin: 12px 0 4px; + margin: 8px 0; } - .mx_RoomSummaryCard_alias { - font-size: $font-13px; - color: $secondary-fg-color; - } - - h2, .mx_RoomSummaryCard_alias { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; - white-space: pre-wrap; - } - - .mx_RoomSummaryCard_avatar { - display: inline-flex; - - .mx_RoomSummaryCard_e2ee { - display: inline-block; - position: relative; - width: 54px; - height: 54px; - border-radius: 50%; - background-color: #737d8c; - margin-top: -3px; // alignment - margin-left: -10px; // overlap - border: 3px solid $dark-panel-bg-color; - - &::before { - content: ''; - position: absolute; - top: 13px; - left: 13px; - height: 28px; - width: 28px; - mask-size: cover; - mask-repeat: no-repeat; - mask-position: center; - mask-image: url('$(res)/img/e2e/disabled.svg'); - background-color: #ffffff; - } - } - - .mx_RoomSummaryCard_e2ee_normal { - background-color: #424446; - &::before { - mask-image: url('$(res)/img/e2e/normal.svg'); - } - } - - .mx_RoomSummaryCard_e2ee_verified { - background-color: #0dbd8b; - &::before { - mask-image: url('$(res)/img/e2e/verified.svg'); - } - } - - .mx_RoomSummaryCard_e2ee_warning { - background-color: #ff4b55; - &::before { - mask-image: url('$(res)/img/e2e/warning.svg'); - } - } + .mx_BaseCard_close { + margin-right: 6px; } } - - .mx_RoomSummaryCard_aboutGroup { - .mx_RoomSummaryCard_Button { - padding-left: 44px; - - &::before { - content: ''; - position: absolute; - top: 8px; - left: 10px; - height: 24px; - width: 24px; - mask-repeat: no-repeat; - mask-position: center; - background-color: $icon-button-color; - } - } - } - - .mx_RoomSummaryCard_appsGroup { - .mx_RoomSummaryCard_Button { - // this button is special so we have to override some of the original styling - // as we will be applying it in its children - padding: 0; - height: auto; - color: $tertiary-fg-color; - - .mx_RoomSummaryCard_icon_app { - padding: 10px 48px 10px 12px; // based on typical mx_RoomSummaryCard_Button padding - text-overflow: ellipsis; - overflow: hidden; - - .mx_BaseAvatar_image { - vertical-align: top; - margin-right: 12px; - } - - span { - color: $primary-fg-color; - } - } - - .mx_RoomSummaryCard_app_pinToggle, - .mx_RoomSummaryCard_app_options { - position: absolute; - top: 0; - height: 100%; // to give bigger interactive zone - width: 24px; - padding: 12px 4px; - box-sizing: border-box; - min-width: 24px; // prevent flexbox crushing - - &:hover { - &::after { - content: ''; - position: absolute; - height: 24px; - width: 24px; - top: 8px; // equal to padding-top of parent - left: 0; - border-radius: 12px; - background-color: rgba(141, 151, 165, 0.1); - } - } - - &::before { - content: ''; - position: absolute; - height: 16px; - width: 16px; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 16px; - background-color: $icon-button-color; - } - } - - .mx_RoomSummaryCard_app_pinToggle { - right: 24px; - - &::before { - mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); - } - } - - .mx_RoomSummaryCard_app_options { - right: 48px; - display: none; - - &::before { - mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); - } - } - - &.mx_RoomSummaryCard_Button_pinned { - &::after { - opacity: 0.2; - } - - .mx_RoomSummaryCard_app_pinToggle::before { - background-color: $accent-color; - } - } - - &:hover { - .mx_RoomSummaryCard_icon_app { - padding-right: 72px; - } - - .mx_RoomSummaryCard_app_options { - display: unset; - } - } - - &::before { - content: unset; - } - - &::after { - top: 8px; // re-align based on the height change - pointer-events: none; // pass through to the real button - } - } - } - - .mx_AccessibleButton_kind_link { - padding: 0; - margin-top: 12px; - margin-bottom: 12px; - font-size: $font-13px; - font-weight: $font-semi-bold; - } -} - -.mx_RoomSummaryCard_icon_people::before { - mask-image: url("$(res)/img/element-icons/room/members.svg"); -} - -.mx_RoomSummaryCard_icon_files::before { - mask-image: url('$(res)/img/element-icons/room/files.svg'); -} - -.mx_RoomSummaryCard_icon_share::before { - mask-image: url('$(res)/img/element-icons/room/share.svg'); -} - -.mx_RoomSummaryCard_icon_settings::before { - mask-image: url('$(res)/img/element-icons/settings.svg'); } diff --git a/res/css/views/rooms/_PinnedEventTile.scss b/res/css/views/rooms/_PinnedEventTile.scss index 030a76674a..8e96f3caeb 100644 --- a/res/css/views/rooms/_PinnedEventTile.scss +++ b/res/css/views/rooms/_PinnedEventTile.scss @@ -16,62 +16,90 @@ limitations under the License. .mx_PinnedEventTile { min-height: 40px; - margin-bottom: 5px; width: 100%; - border-radius: 5px; // for the hover -} + padding: 0 4px 12px; -.mx_PinnedEventTile:hover { - background-color: $event-selected-color; -} + display: grid; + grid-template-areas: "avatar name remove" + "content content content" + "footer footer footer"; + grid-template-rows: max-content auto max-content; + grid-template-columns: 24px auto 24px; + grid-row-gap: 12px; + grid-column-gap: 8px; -.mx_PinnedEventTile .mx_PinnedEventTile_sender, -.mx_PinnedEventTile .mx_PinnedEventTile_timestamp { - color: #868686; - font-size: 0.8em; - vertical-align: top; - display: inline-block; - padding-bottom: 3px; -} + & + .mx_PinnedEventTile { + padding: 12px 4px; + border-top: 1px solid $menu-border-color; + } -.mx_PinnedEventTile .mx_PinnedEventTile_timestamp { - padding-left: 15px; - display: none; -} + .mx_PinnedEventTile_senderAvatar { + grid-area: avatar; + } -.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar { - float: left; - margin-right: 10px; -} + .mx_PinnedEventTile_sender { + grid-area: name; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-24px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } -.mx_PinnedEventTile_actions { - float: right; - margin-right: 10px; - display: none; -} + .mx_PinnedEventTile_unpinButton { + visibility: hidden; + grid-area: remove; + position: relative; + width: 24px; + height: 24px; + border-radius: 8px; -.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp { - display: inline-block; -} + &:hover { + background-color: $roomheader-addroom-bg-color; + } -.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions { - display: block; -} + &::before { + content: ""; + position: absolute; + //top: 0; + //left: 0; + height: inherit; + width: inherit; + background: $secondary-fg-color; + mask-position: center; + mask-size: 8px; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/image-view/close.svg'); + } + } -.mx_PinnedEventTile_unpinButton { - display: inline-block; - cursor: pointer; - margin-left: 10px; -} + .mx_PinnedEventTile_message { + grid-area: content; + } -.mx_PinnedEventTile_gotoButton { - display: inline-block; - font-size: 0.7em; // Smaller text to avoid conflicting with the layout -} + .mx_PinnedEventTile_footer { + grid-area: footer; + font-size: 10px; + line-height: 12px; -.mx_PinnedEventTile_message { - margin-left: 50px; - position: relative; - top: 0; - left: 0; + .mx_PinnedEventTile_timestamp { + font-size: inherit; + line-height: inherit; + color: $secondary-fg-color; + } + + .mx_AccessibleButton_kind_link { + padding: 0; + margin-left: 12px; + font-size: inherit; + line-height: inherit; + } + } + + &:hover { + .mx_PinnedEventTile_unpinButton { + visibility: visible; + } + } } diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index 579b12c3cf..48be8cd706 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -16,6 +16,7 @@ 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'; @@ -42,7 +43,7 @@ export const usePinnedEvents = (room: Room): string[] => { setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []); }, [room]); - useEventEmitter(room.currentState, "RoomState.events", update); + useEventEmitter(room?.currentState, "RoomState.events", update); useEffect(() => { update(); return () => { @@ -53,7 +54,6 @@ export const usePinnedEvents = (room: Room): string[] => { }; const ReadPinsEventId = "im.vector.room.read_pins"; -const ReadPinsNumIds = 10; export const useReadPinnedEvents = (room: Room): Set => { const [readPinnedEvents, setReadPinnedEvents] = useState>(new Set()); @@ -75,20 +75,36 @@ 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)); const pinnedEventIds = usePinnedEvents(room); const readPinnedEvents = useReadPinnedEvents(room); useEffect(() => { const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id)); if (newlyRead.length > 0) { - // Only keep the last N event IDs to avoid infinite growth + // clear out any read pinned events which no longer are pinned cli.setRoomAccountData(room.roomId, ReadPinsEventId, { - event_ids: [ - ...newlyRead.reverse(), - ...readPinnedEvents, - ].splice(0, ReadPinsNumIds), + event_ids: pinnedEventIds, }); } }, [cli, room.roomId, pinnedEventIds, readPinnedEvents]); @@ -122,24 +138,35 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => { if (!pinnedEvents) { content = ; } else if (pinnedEvents.length > 0) { - content = pinnedEvents.filter(Boolean).map(ev => ( - {}} - /> + let onUnpinClicked; + if (canUnpin) { + onUnpinClicked = async (event: MatrixEvent) => { + const pinnedEvents = room.currentState.getStateEvents(EventType.RoomPinnedEvents, ""); + if (pinnedEvents?.getContent()?.pinned) { + const pinned = pinnedEvents.getContent().pinned; + const index = pinned.indexOf(event.getId()); + if (index !== -1) { + pinned.splice(index, 1); + await cli.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned }, ""); + } + } + }; + } + + // show them in reverse, with latest pinned at the top + content = pinnedEvents.filter(Boolean).reverse().map(ev => ( + )); } else { - content =
+ content =

{_t("You’re all caught up")}

{_t("You have no visible notifications.")}

; } return { _t("Pinned") }} - className="mx_NotificationPanel" + header={

{ _t("Pinned messages") }

} + className="mx_PinnedMessagesCard" onClose={onClose} > { content } diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 3d3f33be00..7ff7207fb6 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -55,7 +55,7 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => { return { + static contextType = MatrixClientContext; + private onTileClicked = () => { dis.dispatch({ action: 'view_room', - event_id: this.props.mxEvent.getId(), + event_id: this.props.event.getId(), highlighted: true, - room_id: this.props.mxEvent.getRoomId(), + room_id: this.props.event.getRoomId(), }); }; - private onUnpinClicked = () => { - const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", ""); - if (!pinnedEvents || !pinnedEvents.getContent().pinned) { - // Nothing to do: already unpinned - if (this.props.onUnpinned) this.props.onUnpinned(); - } else { - const pinned = pinnedEvents.getContent().pinned; - const index = pinned.indexOf(this.props.mxEvent.getId()); - if (index !== -1) { - pinned.splice(index, 1); - MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '') - .then(() => { - if (this.props.onUnpinned) this.props.onUnpinned(); - }); - } else if (this.props.onUnpinned) this.props.onUnpinned(); - } - }; - - private canUnpin() { - return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get()); - } - render() { - const sender = this.props.mxEvent.getSender(); - // Get the latest sender profile rather than historical - const senderProfile = this.props.mxRoom.getMember(sender); - const avatarSize = 40; + const sender = this.props.event.getSender(); + const senderProfile = this.props.room.getMember(sender); let unpinButton = null; - if (this.canUnpin()) { + if (this.props.onUnpinClicked) { unpinButton = ( - - {_t('Unpin - + ); } - return ( -
-
- - { _t("Jump to message") } - - { unpinButton } -
+ return
+ - - - - - { senderProfile ? senderProfile.name : sender } - - - { formatFullDate(new Date(this.props.mxEvent.getTs())) } - -
- {}} // we need to give this, apparently - /> -
+ + { senderProfile?.name || sender } + + + { unpinButton } + +
+ {}} // we need to give this, apparently + />
- ); + +
+ + { formatDate(new Date(this.props.event.getTs())) } + + + + { _t("View message") } + +
+
; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0aebfc48ce..d5b37a7d2d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1509,7 +1509,7 @@ "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "This is the start of .": "This is the start of .", "Unpin Message": "Unpin Message", - "Jump to message": "Jump to message", + "View message": "View message", "%(duration)ss": "%(duration)ss", "%(duration)sm": "%(duration)sm", "%(duration)sh": "%(duration)sh", @@ -1717,8 +1717,7 @@ "Yours, or the other users’ session": "Yours, or the other users’ session", "You’re all caught up": "You’re all caught up", "You have no visible notifications.": "You have no visible notifications.", - "Pinned": "Pinned", - "Pinned Messages": "Pinned Messages", + "Pinned messages": "Pinned messages", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "Unpin": "Unpin", @@ -1952,7 +1951,6 @@ "Rotate Right": "Rotate Right", "Download": "Download", "Information": "Information", - "View message": "View message", "Language Dropdown": "Language Dropdown", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", diff --git a/src/stores/RightPanelStorePhases.ts b/src/stores/RightPanelStorePhases.ts index 0d96f92945..d62f6c6110 100644 --- a/src/stores/RightPanelStorePhases.ts +++ b/src/stores/RightPanelStorePhases.ts @@ -44,6 +44,7 @@ export enum RightPanelPhases { export const RIGHT_PANEL_PHASES_NO_ARGS = [ RightPanelPhases.RoomSummary, RightPanelPhases.NotificationPanel, + RightPanelPhases.PinnedMessages, RightPanelPhases.FilePanel, RightPanelPhases.RoomMemberList, RightPanelPhases.GroupMemberList, From 54d8953024b1026fd636f2bed4e45cacda12a360 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 14:00:53 +0100 Subject: [PATCH 035/115] delint --- res/css/views/rooms/_PinnedEventTile.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_PinnedEventTile.scss b/res/css/views/rooms/_PinnedEventTile.scss index 8e96f3caeb..15b3c16faa 100644 --- a/res/css/views/rooms/_PinnedEventTile.scss +++ b/res/css/views/rooms/_PinnedEventTile.scss @@ -20,9 +20,10 @@ limitations under the License. padding: 0 4px 12px; display: grid; - grid-template-areas: "avatar name remove" - "content content content" - "footer footer footer"; + grid-template-areas: + "avatar name remove" + "content content content" + "footer footer footer"; grid-template-rows: max-content auto max-content; grid-template-columns: 24px auto 24px; grid-row-gap: 12px; From 0758c09d9e9760d3a2bae3b562016ff73af82ec9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 14:06:12 +0100 Subject: [PATCH 036/115] i18n --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d5b37a7d2d..7c24364725 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1508,7 +1508,7 @@ "Invite to just this room": "Invite to just this room", "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "This is the start of .": "This is the start of .", - "Unpin Message": "Unpin Message", + "Unpin": "Unpin", "View message": "View message", "%(duration)ss": "%(duration)ss", "%(duration)sm": "%(duration)sm", @@ -1720,7 +1720,6 @@ "Pinned messages": "Pinned messages", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", - "Unpin": "Unpin", "Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel", "Options": "Options", "Set my room layout for everyone": "Set my room layout for everyone", @@ -2464,6 +2463,7 @@ "Unable to reject invite": "Unable to reject invite", "Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)", "Forward Message": "Forward Message", + "Unpin Message": "Unpin Message", "Pin Message": "Pin Message", "Unhide Preview": "Unhide Preview", "Share Permalink": "Share Permalink", From 1ff870927a425145d8676e0e87a97feadc313efa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 15:40:24 +0100 Subject: [PATCH 037/115] When pinning a message automatically mark it as read --- .../views/context_menus/MessageContextMenu.js | 37 +++++++++---------- .../views/right_panel/PinnedMessagesCard.tsx | 2 +- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 365f2ab1de..d9f21906fe 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -31,6 +31,7 @@ import { isContentActionable } from '../../../utils/EventUtils'; import {MenuItem} from "../../structures/ContextMenu"; import {EventType} from "matrix-js-sdk/src/@types/event"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; export function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; @@ -82,7 +83,7 @@ export default class MessageContextMenu extends React.Component { const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId) && this.props.mxEvent.getType() !== EventType.RoomServerAcl && this.props.mxEvent.getType() !== EventType.RoomEncryption; - let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli); + let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli); // HACK: Intentionally say we can't pin if the user doesn't want to use the functionality if (!SettingsStore.getValue("feature_pinning")) canPin = false; @@ -92,7 +93,7 @@ export default class MessageContextMenu extends React.Component { _isPinned() { const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); - const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', ''); + const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, ''); if (!pinnedEvent) return false; const content = pinnedEvent.getContent(); return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); @@ -165,25 +166,23 @@ export default class MessageContextMenu extends React.Component { }; onPinClick = () => { - MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '') - .catch((e) => { - // Intercept the Event Not Found error and fall through the promise chain with no event. - if (e.errcode === "M_NOT_FOUND") return null; - throw e; - }) - .then((event) => { - const eventIds = (event ? event.pinned : []) || []; - if (!eventIds.includes(this.props.mxEvent.getId())) { - // Not pinned - add - eventIds.push(this.props.mxEvent.getId()); - } else { - // Pinned - remove - eventIds.splice(eventIds.indexOf(this.props.mxEvent.getId()), 1); - } + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(this.props.mxEvent.getRoomId()); + const eventId = this.props.mxEvent.getId(); - const cli = MatrixClientPeg.get(); - cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, ''); + const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || []; + if (pinnedIds.includes(eventId)) { + pinnedIds.splice(pinnedIds.indexOf(eventId), 1); + } else { + pinnedIds.push(eventId); + cli.setRoomAccountData(room.roomId, ReadPinsEventId, { + event_ids: [ + ...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids, + eventId, + ], }); + } + cli.sendStateEvent(this.props.mxEvent.getRoomId(), EventType.RoomPinnedEvents, { pinned: pinnedIds }, ""); this.closeMenu(); }; diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index 48be8cd706..a3f1f2d9df 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -53,7 +53,7 @@ export const usePinnedEvents = (room: Room): string[] => { return pinnedEvents; }; -const ReadPinsEventId = "im.vector.room.read_pins"; +export const ReadPinsEventId = "im.vector.room.read_pins"; export const useReadPinnedEvents = (room: Room): Set => { const [readPinnedEvents, setReadPinnedEvents] = useState>(new Set()); From 53ebf3b8e31560ee71d744d203f653b3dd36b32c Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Sat, 29 May 2021 01:37:19 -0500 Subject: [PATCH 038/115] Don't include via when sharing room alias Signed-off-by: Aaron Raimist --- src/utils/permalinks/Permalinks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index d87c826cc2..cecd79dd53 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -149,7 +149,7 @@ export class RoomPermalinkCreator { // Prefer to use canonical alias for permalink if possible const alias = this.room.getCanonicalAlias(); if (alias) { - return getPermalinkConstructor().forRoom(alias, this._serverCandidates); + return getPermalinkConstructor().forRoom(alias); } } return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates); From ccdd2311f447111f4cf56725834ba08be78abc08 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Sat, 29 May 2021 01:38:41 -0500 Subject: [PATCH 039/115] Make "share this room" use aliases if possible Signed-off-by: Aaron Raimist --- src/utils/permalinks/Permalinks.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index cecd79dd53..2f268aaa0c 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -293,16 +293,16 @@ export function makeRoomPermalink(roomId: string): string { // If the roomId isn't actually a room ID, don't try to list the servers. // Aliases are already routable, and don't need extra information. - if (roomId[0] !== '!') return getPermalinkConstructor().forRoom(roomId, []); + if (roomId[0] !== '!') return getPermalinkConstructor().forShareableRoom(roomId, []); const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); if (!room) { - return getPermalinkConstructor().forRoom(roomId, []); + return getPermalinkConstructor().forShareableRoom(roomId, []); } const permalinkCreator = new RoomPermalinkCreator(room); permalinkCreator.load(); - return permalinkCreator.forRoom(); + return permalinkCreator.forShareableRoom(); } export function makeGroupPermalink(groupId: string): string { From 0c5e8ef381815725656c6619364fb54089a53137 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 1 Jun 2021 16:13:01 +0100 Subject: [PATCH 040/115] Upgrade matrix-js-sdk to 11.2.0-rc.1 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 13047b69cf..108a61cdaf 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "11.2.0-rc.1", "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 0ff235a660..1b07d4502b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5673,9 +5673,10 @@ 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@github:matrix-org/matrix-js-sdk#develop": - version "11.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/acb9bc8cc5234326a7583514a8e120a4ac42eedc" +matrix-js-sdk@11.2.0-rc.1: + version "11.2.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.2.0-rc.1.tgz#339259d892ce08195864c1130780dd0b3d996af3" + integrity sha512-ACFONqdRqiPIj7aWf7G4f/2d0S/hfouIH6tdkFGDbizbWJCmqXRJ8EN2gmu0F3A6GGDo0+k9cqaeMf3Y1KIM8w== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From d02f462df9a3cd0a336aea733c2992abcbc07b2d Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 1 Jun 2021 16:18:25 +0100 Subject: [PATCH 041/115] Prepare changelog for v3.23.0-rc.1 --- CHANGELOG.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d9afd51d..a3abd1f32b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,101 @@ +Changes in [3.23.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0-rc.1) (2021-06-01) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0...v3.23.0-rc.1) + + * Upgrade to JS SDK 11.2.0-rc.1 + * Translations update from Weblate + [\#6128](https://github.com/matrix-org/matrix-react-sdk/pull/6128) + * Fix all DMs wrongly appearing in room list when `m.direct` is changed + [\#6122](https://github.com/matrix-org/matrix-react-sdk/pull/6122) + * Update way of checking for registration disabled + [\#6123](https://github.com/matrix-org/matrix-react-sdk/pull/6123) + * Fix the ability to remove avatar from a space via settings + [\#6126](https://github.com/matrix-org/matrix-react-sdk/pull/6126) + * Switch to stable endpoint/fields for MSC2858 + [\#6125](https://github.com/matrix-org/matrix-react-sdk/pull/6125) + * Clear stored editor state when canceling editing using a shortcut + [\#6117](https://github.com/matrix-org/matrix-react-sdk/pull/6117) + * Respect newlines in space topics + [\#6124](https://github.com/matrix-org/matrix-react-sdk/pull/6124) + * Add url param `defaultUsername` to prefill the login username field + [\#5674](https://github.com/matrix-org/matrix-react-sdk/pull/5674) + * Bump ws from 7.4.2 to 7.4.6 + [\#6115](https://github.com/matrix-org/matrix-react-sdk/pull/6115) + * Sticky headers repositioning without layout trashing + [\#6110](https://github.com/matrix-org/matrix-react-sdk/pull/6110) + * Handle user_busy in voip calls + [\#6112](https://github.com/matrix-org/matrix-react-sdk/pull/6112) + * Avoid showing warning modals from the invite dialog after it unmounts + [\#6105](https://github.com/matrix-org/matrix-react-sdk/pull/6105) + * Fix misleading child counts in spaces + [\#6109](https://github.com/matrix-org/matrix-react-sdk/pull/6109) + * Close creation menu when expanding space panel via expand hierarchy + [\#6090](https://github.com/matrix-org/matrix-react-sdk/pull/6090) + * Prevent having duplicates in pending room state + [\#6108](https://github.com/matrix-org/matrix-react-sdk/pull/6108) + * Update reactions row on event decryption + [\#6106](https://github.com/matrix-org/matrix-react-sdk/pull/6106) + * Destroy playback instance on voice message unmount + [\#6101](https://github.com/matrix-org/matrix-react-sdk/pull/6101) + * Fix message preview not up to date + [\#6102](https://github.com/matrix-org/matrix-react-sdk/pull/6102) + * Convert some Flow typed files to TS (round 2) + [\#6076](https://github.com/matrix-org/matrix-react-sdk/pull/6076) + * Remove unused middlePanelResized event listener + [\#6086](https://github.com/matrix-org/matrix-react-sdk/pull/6086) + * Fix accessing currentState on an invalid joinedRoom + [\#6100](https://github.com/matrix-org/matrix-react-sdk/pull/6100) + * Remove Promise allSettled polyfill as js-sdk uses it directly + [\#6097](https://github.com/matrix-org/matrix-react-sdk/pull/6097) + * Prevent DecoratedRoomAvatar to update its state for the same value + [\#6099](https://github.com/matrix-org/matrix-react-sdk/pull/6099) + * Skip generatePreview if event is not part of the live timeline + [\#6098](https://github.com/matrix-org/matrix-react-sdk/pull/6098) + * fix sticky headers when results num get displayed + [\#6095](https://github.com/matrix-org/matrix-react-sdk/pull/6095) + * Improve addEventsToTimeline performance scoping WhoIsTypingTile::setState + [\#6094](https://github.com/matrix-org/matrix-react-sdk/pull/6094) + * Safeguards to prevent layout trashing for window dimensions + [\#6092](https://github.com/matrix-org/matrix-react-sdk/pull/6092) + * Use local room state to render space hierarchy if the room is known + [\#6089](https://github.com/matrix-org/matrix-react-sdk/pull/6089) + * Add spinner in UserMenu to list pending long running actions + [\#6085](https://github.com/matrix-org/matrix-react-sdk/pull/6085) + * Stop overscroll in Firefox Nightly for macOS + [\#6093](https://github.com/matrix-org/matrix-react-sdk/pull/6093) + * Move SettingsStore watchers/monitors over to ES6 maps for performance + [\#6063](https://github.com/matrix-org/matrix-react-sdk/pull/6063) + * Bump libolm version. + [\#6080](https://github.com/matrix-org/matrix-react-sdk/pull/6080) + * Improve styling of the message action bar + [\#6066](https://github.com/matrix-org/matrix-react-sdk/pull/6066) + * Improve explore rooms when no results are found + [\#6070](https://github.com/matrix-org/matrix-react-sdk/pull/6070) + * Remove logo spinner + [\#6078](https://github.com/matrix-org/matrix-react-sdk/pull/6078) + * Fix add reaction prompt showing even when user is not joined to room + [\#6073](https://github.com/matrix-org/matrix-react-sdk/pull/6073) + * Vectorize spinners + [\#5680](https://github.com/matrix-org/matrix-react-sdk/pull/5680) + * Fix handling of via servers for suggested rooms + [\#6077](https://github.com/matrix-org/matrix-react-sdk/pull/6077) + * Upgrade showChatEffects to room-level setting exposure + [\#6075](https://github.com/matrix-org/matrix-react-sdk/pull/6075) + * Delete RoomView dead code + [\#6071](https://github.com/matrix-org/matrix-react-sdk/pull/6071) + * Reduce noise in tests + [\#6074](https://github.com/matrix-org/matrix-react-sdk/pull/6074) + * Fix room name issues in right panel summary card + [\#6069](https://github.com/matrix-org/matrix-react-sdk/pull/6069) + * Cache normalized room name + [\#6072](https://github.com/matrix-org/matrix-react-sdk/pull/6072) + * Update MemberList to reflect changes for invite permission change + [\#6061](https://github.com/matrix-org/matrix-react-sdk/pull/6061) + * Delete RoomView dead code + [\#6065](https://github.com/matrix-org/matrix-react-sdk/pull/6065) + * Show subspace rooms count even if it is 0 for consistency + [\#6067](https://github.com/matrix-org/matrix-react-sdk/pull/6067) + Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0) From 300b0016d60fd3f3b0527e68f987df3731f2feb0 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 1 Jun 2021 16:18:26 +0100 Subject: [PATCH 042/115] v3.23.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 108a61cdaf..4f6a023383 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.22.0", + "version": "3.23.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -25,7 +25,7 @@ "bin": { "reskindex": "scripts/reskindex.js" }, - "main": "./src/index.js", + "main": "./lib/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -200,5 +200,6 @@ "coverageReporters": [ "text" ] - } + }, + "typings": "./lib/index.d.ts" } From b032422c6a536b856d7d105dd593a804b245c933 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 1 Jun 2021 17:37:31 -0400 Subject: [PATCH 043/115] Fix whitespace lints Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 59475b1b51..58759d7d42 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -184,13 +184,13 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr const previewLayout = SettingsStore.getValue("layout"); return -

{ _t("Message preview") }

+

{_t("Message preview")}

= ({ matrixClient: cli, event, permalinkCr
Date: Tue, 1 Jun 2021 17:56:46 -0400 Subject: [PATCH 044/115] Match forward dialog send failed indicator color with button Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 593fe12531..fc59259ca2 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -124,7 +124,8 @@ limitations under the License. } .mx_NotificationBadge { - opacity: 0.4; + // Match the failed to send indicator's color with the disabled button + background-color: $button-danger-disabled-fg-color; } &.mx_ForwardList_sending .mx_ForwardList_sendIcon { From c78167977a49421156f62d995cf5aab97801a758 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 1 Jun 2021 17:57:26 -0400 Subject: [PATCH 045/115] Remove unused class Signed-off-by: Robin Townsend --- src/components/views/context_menus/MessageContextMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 08fc5844ec..442470bb52 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -161,7 +161,7 @@ export default class MessageContextMenu extends React.Component { matrixClient: MatrixClientPeg.get(), event: this.props.mxEvent, permalinkCreator: this.props.permalinkCreator, - }, 'mx_Dialog_forwardMessage'); + }); this.closeMenu(); }; From 4ef69fcbf6cd5bb90261759eff4cd3e720a96f14 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 1 Jun 2021 18:09:51 -0400 Subject: [PATCH 046/115] Use settings hooks in forward dialog ...to dynamically watch for layout changes. Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 58759d7d42..2c4920da4b 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -22,7 +22,7 @@ import {MatrixClient} from "matrix-js-sdk/src/client"; import {_t} from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; -import SettingsStore from "../../../settings/SettingsStore"; +import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings"; import {UIFeature} from "../../../settings/UIFeature"; import {Layout} from "../../../settings/Layout"; import {IDialogProps} from "./IDialogProps"; @@ -174,14 +174,16 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase(); + const spacesEnabled = useFeatureEnabled("feature_spaces"); + const flairEnabled = useFeatureEnabled(UIFeature.Flair); + const previewLayout = useSettingValue("layout"); + const rooms = useMemo(() => sortRooms( cli.getVisibleRooms().filter( room => room.getMyMembership() === "join" && - !(SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()), + !(spacesEnabled && room.isSpaceRoom()), ), - ), [cli]).filter(room => room.name.toLowerCase().includes(lcQuery)); - - const previewLayout = SettingsStore.getValue("layout"); + ), [cli, spacesEnabled]).filter(room => room.name.toLowerCase().includes(lcQuery)); return = ({ matrixClient: cli, event, permalinkCr
From 59660df0cbe74dbe39ada90e4296d427e5375bbc Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 1 Jun 2021 20:17:20 -0400 Subject: [PATCH 047/115] Use a QueryMatcher for forward dialog filtering This also allows us to filter by room aliases. Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 15 ++++++++++++--- src/hooks/useSettings.ts | 4 ++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 2c4920da4b..cba292ea25 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -38,6 +38,7 @@ import {StaticNotificationState} from "../../../stores/notifications/StaticNotif import NotificationBadge from "../rooms/NotificationBadge"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; +import QueryMatcher from "../../../autocomplete/QueryMatcher"; const AVATAR_SIZE = 30; @@ -176,14 +177,22 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr const spacesEnabled = useFeatureEnabled("feature_spaces"); const flairEnabled = useFeatureEnabled(UIFeature.Flair); - const previewLayout = useSettingValue("layout"); + const previewLayout = useSettingValue("layout"); - const rooms = useMemo(() => sortRooms( + let rooms = useMemo(() => sortRooms( cli.getVisibleRooms().filter( room => room.getMyMembership() === "join" && !(spacesEnabled && room.isSpaceRoom()), ), - ), [cli, spacesEnabled]).filter(room => room.name.toLowerCase().includes(lcQuery)); + ), [cli, spacesEnabled]); + + if (lcQuery) { + rooms = new QueryMatcher(rooms, { + keys: ["name"], + funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)], + shouldMatchWordsOnly: false, + }).match(lcQuery); + } return (settingName: string, roomId: string = null, e }; // Hook to fetch whether a feature is enabled and dynamically update when that changes -export const useFeatureEnabled = (featureName: string, roomId: string = null) => { - const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId)); +export const useFeatureEnabled = (featureName: string, roomId: string = null): boolean => { + const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId)); useEffect(() => { const ref = SettingsStore.watchSetting(featureName, roomId, () => { From 992861a1cd821c2d7564fd644b4294220f54ff96 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 1 Jun 2021 20:36:28 -0400 Subject: [PATCH 048/115] Fix forward dialog tests Signed-off-by: Robin Townsend --- test/test-utils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test-utils.js b/test/test-utils.js index c82885b5f0..6053924103 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -265,6 +265,8 @@ export function mkStubRoom(roomId = null, name) { isSpaceRoom: jest.fn(() => false), getUnreadNotificationCount: jest.fn(() => 0), getEventReadUpTo: jest.fn(() => null), + getCanonicalAlias: jest.fn(), + getAltAliases: jest.fn().mockReturnValue([]), timeline: [], }; } From 5b2dacd99ec7dd09a7dc2fbc76c81d1f44f9dfd8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 21:36:28 -0600 Subject: [PATCH 049/115] Adapt for js-sdk MatrixClient conversion to TS For https://github.com/matrix-org/matrix-js-sdk/pull/1718 --- src/Searching.js | 10 +++++----- src/SecurityManager.ts | 2 +- src/components/views/dialogs/DevtoolsDialog.tsx | 2 +- src/components/views/settings/CrossSigningPanel.js | 4 ++-- src/components/views/settings/SecureBackupPanel.js | 4 ++-- src/indexing/EventIndex.js | 2 +- src/rageshake/submit-rageshake.ts | 6 +++--- src/stores/CommunityPrototypeStore.ts | 2 +- src/stores/SetupEncryptionStore.js | 2 +- test/test-utils.js | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index f65b8920b3..2b17aee054 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -66,7 +66,7 @@ async function serverSideSearchProcess(term, roomId = undefined) { highlights: [], }; - return client._processRoomEventsSearch(searchResult, result.response); + return client.processRoomEventsSearch(searchResult, result.response); } function compareEvents(a, b) { @@ -131,7 +131,7 @@ async function combinedSearch(searchTerm) { }, }; - const result = client._processRoomEventsSearch(emptyResult, response); + const result = client.processRoomEventsSearch(emptyResult, response); // Restore our encryption info so we can properly re-verify the events. restoreEncryptionInfo(result.results); @@ -185,7 +185,7 @@ async function localSearchProcess(searchTerm, roomId = undefined) { }, }; - const processedResult = MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response); + const processedResult = MatrixClientPeg.get().processRoomEventsSearch(emptyResult, response); // Restore our encryption info so we can properly re-verify the events. restoreEncryptionInfo(processedResult.results); @@ -210,7 +210,7 @@ async function localPagination(searchResult) { }, }; - const result = MatrixClientPeg.get()._processRoomEventsSearch(searchResult, response); + const result = MatrixClientPeg.get().processRoomEventsSearch(searchResult, response); // Restore our encryption info so we can properly re-verify the events. const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0)); @@ -520,7 +520,7 @@ async function combinedPagination(searchResult) { const oldResultCount = searchResult.results ? searchResult.results.length : 0; // Let the client process the combined result. - const result = client._processRoomEventsSearch(searchResult, response); + const result = client.processRoomEventsSearch(searchResult, response); // Restore our encryption info so we can properly re-verify the events. const newResultCount = result.results.length - oldResultCount; diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 203830d232..09c8d30614 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -271,7 +271,7 @@ async function onSecretRequested( } return key && encodeBase64(key); } else if (name === "m.megolm_backup.v1") { - const key = await client._crypto.getSessionBackupPrivateKey(); + const key = await client.crypto.getSessionBackupPrivateKey(); if (!key) { console.log( `session backup key requested by ${deviceId}, but not found in cache`, diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 0c37ea9599..fdbf6a36fc 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent { render() { const cli = this.context; const room = this.props.room; - const inRoomChannel = cli._crypto._inRoomVerificationRequests; + const inRoomChannel = cli.crypto._inRoomVerificationRequests; const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map(); return (
diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index c0f23cb906..0cd1a64ada 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent { async _getUpdatedStatus() { const cli = MatrixClientPeg.get(); const pkCache = cli.getCrossSigningCacheCallbacks(); - const crossSigning = cli._crypto._crossSigningInfo; - const secretStorage = cli._crypto._secretStorage; + const crossSigning = cli.crypto._crossSigningInfo; + const secretStorage = cli.crypto._secretStorage; const crossSigningPublicKeysOnDevice = crossSigning.getId(); const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage); const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master")); diff --git a/src/components/views/settings/SecureBackupPanel.js b/src/components/views/settings/SecureBackupPanel.js index 310114c8af..4f3eb0bdf6 100644 --- a/src/components/views/settings/SecureBackupPanel.js +++ b/src/components/views/settings/SecureBackupPanel.js @@ -131,10 +131,10 @@ export default class SecureBackupPanel extends React.PureComponent { async _getUpdatedDiagnostics() { const cli = MatrixClientPeg.get(); - const secretStorage = cli._crypto._secretStorage; + const secretStorage = cli.crypto._secretStorage; const backupKeyStored = !!(await cli.isKeyBackupKeyStored()); - const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey(); + const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey(); const backupKeyCached = !!(backupKeyFromCache); const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array; const secretStorageKeyInAccount = await secretStorage.hasKey(); diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index ed4418140b..33f2d594ae 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -453,7 +453,7 @@ export default class EventIndex extends EventEmitter { let res; try { - res = await client._createMessagesRequest( + res = await client.createMessagesRequest( checkpoint.roomId, checkpoint.token, this._eventsPerCrawl, checkpoint.direction); } catch (e) { diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index f46dd88fba..b2ad9fe6f6 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -92,8 +92,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { body.append('cross_signing_key', client.getCrossSigningId()); // add cross-signing status information - const crossSigning = client._crypto._crossSigningInfo; - const secretStorage = client._crypto._secretStorage; + const crossSigning = client.crypto._crossSigningInfo; + const secretStorage = client.crypto._secretStorage; body.append("cross_signing_ready", String(await client.isCrossSigningReady())); body.append("cross_signing_supported_by_hs", @@ -114,7 +114,7 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { body.append("secret_storage_key_in_account", String(!!(await secretStorage.hasKey()))); body.append("session_backup_key_in_secret_storage", String(!!(await client.isKeyBackupKeyStored()))); - const sessionBackupKeyFromCache = await client._crypto.getSessionBackupPrivateKey(); + const sessionBackupKeyFromCache = await client.crypto.getSessionBackupPrivateKey(); body.append("session_backup_key_cached", String(!!sessionBackupKeyFromCache)); body.append("session_backup_key_well_formed", String(sessionBackupKeyFromCache instanceof Uint8Array)); } diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts index 92e094c83b..023845c9ee 100644 --- a/src/stores/CommunityPrototypeStore.ts +++ b/src/stores/CommunityPrototypeStore.ts @@ -126,7 +126,7 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient { if (membership === EffectiveMembership.Invite) { try { const path = utils.encodeUri("/rooms/$roomId/group_info", {$roomId: room.roomId}); - const profile = await this.matrixClient._http.authedRequest( + const profile = await this.matrixClient.http.authedRequest( undefined, "GET", path, undefined, undefined, {prefix: "/_matrix/client/unstable/im.vector.custom"}); diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index 5f0054ff24..b768ae69df 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -196,7 +196,7 @@ export class SetupEncryptionStore extends EventEmitter { this.phase = PHASE_FINISHED; this.emit("update"); // async - ask other clients for keys, if necessary - MatrixClientPeg.get()._crypto.cancelAndResendAllOutgoingKeyRequests(); + MatrixClientPeg.get().crypto.cancelAndResendAllOutgoingKeyRequests(); } async _setActiveVerificationRequest(request) { diff --git a/test/test-utils.js b/test/test-utils.js index 4faf948178..b9014f4876 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -90,7 +90,7 @@ export function createTestClient() { }), // Used by various internal bits we aren't concerned with (yet) - _sessionStore: { + sessionStore: { store: { getItem: jest.fn(), }, From c9883f346c32d0c5d670b0efcc5702ec18f5ecad Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 22:21:04 -0600 Subject: [PATCH 050/115] Build pass 1 --- src/ContentMessages.tsx | 26 ++++++++++++------- src/Presence.ts | 2 +- src/Terms.ts | 2 +- src/components/structures/LoggedInView.tsx | 2 +- src/components/structures/MatrixChat.tsx | 2 +- .../structures/auth/Registration.tsx | 2 +- .../tabs/user/HelpUserSettingsTab.tsx | 2 +- 7 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 95b45cce4a..b21829ac63 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -40,6 +40,7 @@ import { UploadStartedPayload, } from "./dispatcher/payloads/UploadPayload"; import {IUpload} from "./models/IUpload"; +import { IImageInfo } from "matrix-js-sdk/src/@types/partials"; const MAX_WIDTH = 800; const MAX_HEIGHT = 600; @@ -208,12 +209,12 @@ function infoForImageFile(matrixClient, roomId, imageFile) { } let imageInfo; - return loadImageElement(imageFile).then(function(r) { + return loadImageElement(imageFile).then((r) => { return createThumbnail(r.img, r.width, r.height, thumbnailType); - }).then(function(result) { + }).then((result) => { imageInfo = result.info; return uploadFile(matrixClient, roomId, result.thumbnail); - }).then(function(result) { + }).then((result) => { imageInfo.thumbnail_url = result.url; imageInfo.thumbnail_file = result.file; return imageInfo; @@ -264,12 +265,12 @@ function infoForVideoFile(matrixClient, roomId, videoFile) { const thumbnailType = "image/jpeg"; let videoInfo; - return loadVideoElement(videoFile).then(function(video) { + return loadVideoElement(videoFile).then((video) => { return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType); - }).then(function(result) { + }).then((result) => { videoInfo = result.info; return uploadFile(matrixClient, roomId, result.thumbnail); - }).then(function(result) { + }).then((result) => { videoInfo.thumbnail_url = result.url; videoInfo.thumbnail_file = result.file; return videoInfo; @@ -308,7 +309,12 @@ function readFileAsArrayBuffer(file: File | Blob): Promise { * If the file is unencrypted then the object will have a "url" key. * If the file is encrypted then the object will have a "file" key. */ -function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blob, progressHandler?: any) { +function uploadFile( + matrixClient: MatrixClient, + roomId: string, + file: File | Blob, + progressHandler?: any, // TODO: Types +): Promise<{url?: string, file?: any}> { // TODO: Types let canceled = false; if (matrixClient.isRoomEncrypted(roomId)) { // If the room is encrypted then encrypt the file before uploading it. @@ -355,7 +361,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo // If the attachment isn't encrypted then include the URL directly. return {"url": url}; }); - promise1.abort = () => { + (promise1 as any).abort = () => { canceled = true; MatrixClientPeg.get().cancelUpload(basePromise); }; @@ -367,7 +373,7 @@ export default class ContentMessages { private inprogress: IUpload[] = []; private mediaConfig: IMediaConfig = null; - sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) { + sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) { const startTime = CountlyAnalytics.getTimestamp(); const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => { console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); @@ -441,7 +447,7 @@ export default class ContentMessages { let uploadAll = false; // Promise to complete before sending next file into room, used for synchronisation of file-sending // to match the order the files were specified in - let promBefore = Promise.resolve(); + let promBefore: Promise = Promise.resolve(); for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; if (!uploadAll) { diff --git a/src/Presence.ts b/src/Presence.ts index eb56c5714e..8f2e127cb4 100644 --- a/src/Presence.ts +++ b/src/Presence.ts @@ -98,7 +98,7 @@ class Presence { } try { - await MatrixClientPeg.get().setPresence(this.state); + await MatrixClientPeg.get().setPresence({presence: this.state}); console.info("Presence:", newState); } catch (err) { console.error("Failed to set presence:", err); diff --git a/src/Terms.ts b/src/Terms.ts index 1b1c152fdd..a6ea40a6e8 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -103,7 +103,7 @@ export async function startTermsFlow( // fetch the set of agreed policy URLs from account data const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms'); - let agreedUrlSet; + let agreedUrlSet: Set; if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) { agreedUrlSet = new Set(); } else { diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index ad5c759f0d..3091278e3a 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -358,7 +358,7 @@ class LoggedInView extends React.Component { const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM); for (const eventId of pinnedEventIds) { - const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); + const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId); const event = timeline.getEvents().find(ev => ev.getId() === eventId); if (event) events.push(event); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index d5e8e6a1f8..16da9321e2 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -378,7 +378,7 @@ export default class MatrixChat extends React.PureComponent { this.onLoggedIn(); } - const promisesList = [this.firstSyncPromise.promise]; + const promisesList: Promise[] = [this.firstSyncPromise.promise]; if (cryptoEnabled) { // wait for the client to finish downloading cross-signing keys for us so we // know whether or not we have keys set up on this account diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index e4515dd627..6feb1e34f7 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -61,7 +61,7 @@ interface IProps { is_url?: string; session_id: string; /* eslint-enable camelcase */ - }): void; + }): string; // registration shouldn't know or care how login is done. onLoginClick(): void; onServerConfigChange(config: ValidatedServerConfig): void; diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 3fa0be478c..74c28e35fc 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -227,7 +227,7 @@ export default class HelpUserSettingsTab extends React.Component const appVersion = this.state.appVersion || 'unknown'; - let olmVersion = MatrixClientPeg.get().olmVersion; + let olmVersion: string = MatrixClientPeg.get().olmVersion?.toString(); olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : ''; let updateButton = null; From 3dc6cfbf341dd40e2500399f5957de53630f0333 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 22:31:08 -0600 Subject: [PATCH 051/115] Undo olmVersion handling --- src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 74c28e35fc..3fa0be478c 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -227,7 +227,7 @@ export default class HelpUserSettingsTab extends React.Component const appVersion = this.state.appVersion || 'unknown'; - let olmVersion: string = MatrixClientPeg.get().olmVersion?.toString(); + let olmVersion = MatrixClientPeg.get().olmVersion; olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : ''; let updateButton = null; From 2c4fa73a4571ff3da3ae55e60ae8facc7748006e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 2 Jun 2021 17:39:13 +0100 Subject: [PATCH 052/115] Map phone number lookup results to their native rooms When dialing a phone number, also look to see if there's a corresponding native user for the resulting user, and if so, go to the native room for that user. --- src/CallHandler.tsx | 33 +++++++++- src/VoipUserMapper.ts | 6 +- src/components/views/voip/DialPadModal.tsx | 23 ++----- src/dispatcher/actions.ts | 8 +++ test/CallHandler-test.ts | 73 ++++++++++++++-------- 5 files changed, 95 insertions(+), 48 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index a05d3a25c8..ba0178fa59 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -521,7 +521,9 @@ export default class CallHandler extends EventEmitter { let newNativeAssertedIdentity = newAssertedIdentity; if (newAssertedIdentity) { const response = await this.sipNativeLookup(newAssertedIdentity); - if (response.length) newNativeAssertedIdentity = response[0].userid; + if (response.length && response[0].fields.lookup_success) { + newNativeAssertedIdentity = response[0].userid; + } } console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`); @@ -862,9 +864,38 @@ export default class CallHandler extends EventEmitter { }); break; } + case Action.DialNumber: + this.dialNumber(payload.number); + break; } } + private async dialNumber(number: string) { + const results = await this.pstnLookup(number); + if (!results || results.length === 0 || !results[0].userid) { + Modal.createTrackedDialog('', '', ErrorDialog, { + title: _t("Unable to look up phone number"), + description: _t("There was an error looking up the phone number"), + }); + return; + } + const userId = results[0].userid; + + // Now check to see if this is a virtual user, in which case we should find the + // native user + const nativeLookupResults = await this.sipNativeLookup(userId); + const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success; + const nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId; + console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId); + + const roomId = await ensureDMExists(MatrixClientPeg.get(), nativeUserId); + + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); + } + setActiveCallRoomId(activeCallRoomId: string) { logger.info("Setting call in room " + activeCallRoomId + " active"); diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index e5bed2e812..d576a5434c 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -33,7 +33,7 @@ export default class VoipUserMapper { private async userToVirtualUser(userId: string): Promise { const results = await CallHandler.sharedInstance().sipVirtualLookup(userId); - if (results.length === 0) return null; + if (results.length === 0 || !results[0].fields.lookup_success) return null; return results[0].userid; } @@ -82,14 +82,14 @@ export default class VoipUserMapper { return Boolean(claimedNativeRoomId); } - public async onNewInvitedRoom(invitedRoom: Room) { + public async onNewInvitedRoom(invitedRoom: Room): Promise { if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return; const inviterId = invitedRoom.getDMInviter(); console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId); if (result.length === 0) { - return true; + return; } if (result[0].fields.is_virtual) { diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx index cdd5bc6641..f3ee49e4ac 100644 --- a/src/components/views/voip/DialPadModal.tsx +++ b/src/components/views/voip/DialPadModal.tsx @@ -15,17 +15,13 @@ limitations under the License. */ import * as React from "react"; -import { ensureDMExists } from "../../../createRoom"; import { _t } from "../../../languageHandler"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import AccessibleButton from "../elements/AccessibleButton"; import Field from "../elements/Field"; import DialPad from './DialPad'; import dis from '../../../dispatcher/dispatcher'; -import Modal from "../../../Modal"; -import ErrorDialog from "../../views/dialogs/ErrorDialog"; -import CallHandler from "../../../CallHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { Action } from "../../../dispatcher/actions"; interface IProps { onFinished: (boolean) => void; @@ -67,21 +63,10 @@ export default class DialpadModal extends React.PureComponent { } onDialPress = async () => { - const results = await CallHandler.sharedInstance().pstnLookup(this.state.value); - if (!results || results.length === 0 || !results[0].userid) { - Modal.createTrackedDialog('', '', ErrorDialog, { - title: _t("Unable to look up phone number"), - description: _t("There was an error looking up the phone number"), - }); - } - const userId = results[0].userid; - - const roomId = await ensureDMExists(MatrixClientPeg.get(), userId); - dis.dispatch({ - action: 'view_room', - room_id: roomId, - }); + action: Action.DialNumber, + number: this.state.value, + }) this.props.onFinished(true); } diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 9fc0b54eea..073e82bef6 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -100,6 +100,14 @@ export enum Action { */ OpenDialPad = "open_dial_pad", + /** + * Dial the phone number in the payload + * payload: { + * number: , + * } + */ + DialNumber = "dial_number", + /** * Fired when CallHandler has checked for PSTN protocol support * payload: none diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index 1e3f92e788..a93a134133 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -23,8 +23,8 @@ import dis from '../src/dispatcher/dispatcher'; import { CallEvent, CallState } from 'matrix-js-sdk/src/webrtc/call'; import DMRoomMap from '../src/utils/DMRoomMap'; import EventEmitter from 'events'; -import { Action } from '../src/dispatcher/actions'; import SdkConfig from '../src/SdkConfig'; +import { ActionPayload } from '../src/dispatcher/payloads'; const REAL_ROOM_ID = '$room1:example.org'; const MAPPED_ROOM_ID = '$room2:example.org'; @@ -75,6 +75,18 @@ class FakeCall extends EventEmitter { } } +function untilDispatch(waitForAction: string): Promise { + let dispatchHandle; + return new Promise(resolve => { + dispatchHandle = dis.register(payload => { + if (payload.action === waitForAction) { + dis.unregister(dispatchHandle); + resolve(payload); + } + }); + }); +} + describe('CallHandler', () => { let dmRoomMap; let callHandler; @@ -94,6 +106,21 @@ describe('CallHandler', () => { callHandler = new CallHandler(); callHandler.start(); + const realRoom = mkStubDM(REAL_ROOM_ID, '@user1:example.org'); + const mappedRoom = mkStubDM(MAPPED_ROOM_ID, '@user2:example.org'); + const mappedRoom2 = mkStubDM(MAPPED_ROOM_ID_2, '@user3:example.org'); + + MatrixClientPeg.get().getRoom = roomId => { + switch (roomId) { + case REAL_ROOM_ID: + return realRoom; + case MAPPED_ROOM_ID: + return mappedRoom; + case MAPPED_ROOM_ID_2: + return mappedRoom2; + } + }; + dmRoomMap = { getUserIdForRoomId: roomId => { if (roomId === REAL_ROOM_ID) { @@ -134,38 +161,34 @@ describe('CallHandler', () => { SdkConfig.unset(); }); + it('should look up the correct user and open the room when a phone number is dialled', async () => { + MatrixClientPeg.get().getThirdpartyUser = jest.fn().mockResolvedValue([{ + userid: '@user2:example.org', + protocol: "im.vector.protocol.sip_native", + fields: { + is_native: true, + lookup_success: true, + }, + }]); + + dis.dispatch({ + action: 'dial_number', + number: '01818118181', + }, true); + + const viewRoomPayload = await untilDispatch('view_room'); + expect(viewRoomPayload.room_id).toEqual(MAPPED_ROOM_ID); + }); + it('should move calls between rooms when remote asserted identity changes', async () => { - const realRoom = mkStubDM(REAL_ROOM_ID, '@user1:example.org'); - const mappedRoom = mkStubDM(MAPPED_ROOM_ID, '@user2:example.org'); - const mappedRoom2 = mkStubDM(MAPPED_ROOM_ID_2, '@user3:example.org'); - - MatrixClientPeg.get().getRoom = roomId => { - switch (roomId) { - case REAL_ROOM_ID: - return realRoom; - case MAPPED_ROOM_ID: - return mappedRoom; - case MAPPED_ROOM_ID_2: - return mappedRoom2; - } - }; - dis.dispatch({ action: 'place_call', type: PlaceCallType.Voice, room_id: REAL_ROOM_ID, }, true); - let dispatchHandle; // wait for the call to be set up - await new Promise(resolve => { - dispatchHandle = dis.register(payload => { - if (payload.action === 'call_state') { - resolve(); - } - }); - }); - dis.unregister(dispatchHandle); + await untilDispatch('call_state'); // should start off in the actual room ID it's in at the protocol level expect(callHandler.getCallForRoom(REAL_ROOM_ID)).toBe(fakeCall); From 0aeddea30f2f54d7cb06ae0a1c58be52b654d75a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 2 Jun 2021 17:47:29 +0100 Subject: [PATCH 053/115] Only do native lookup if it's supported Also fix as bug where we were checking the wrong field to check for native/virtual support: oops. --- src/CallHandler.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index ba0178fa59..0d87451b5f 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -264,7 +264,7 @@ export default class CallHandler extends EventEmitter { } public getSupportsVirtualRooms() { - return this.supportsPstnProtocol; + return this.supportsSipNativeVirtual; } public pstnLookup(phoneNumber: string): Promise { @@ -883,10 +883,15 @@ export default class CallHandler extends EventEmitter { // Now check to see if this is a virtual user, in which case we should find the // native user - const nativeLookupResults = await this.sipNativeLookup(userId); - const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success; - const nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId; - console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId); + let nativeUserId; + if (this.getSupportsVirtualRooms()) { + const nativeLookupResults = await this.sipNativeLookup(userId); + const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success; + nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId; + console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId); + } else { + nativeUserId = userId; + } const roomId = await ensureDMExists(MatrixClientPeg.get(), nativeUserId); From 58652152c13039c485ccd4a72a92a7915698a6e2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 2 Jun 2021 17:52:26 +0100 Subject: [PATCH 054/115] Strings are now in a different place... --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3d6fcb8643..ada264d110 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -63,6 +63,8 @@ "Already in call": "Already in call", "You're already in a call with this person.": "You're already in a call with this person.", "You cannot place a call with yourself.": "You cannot place a call with yourself.", + "Unable to look up phone number": "Unable to look up phone number", + "There was an error looking up the phone number": "There was an error looking up the phone number", "Call in Progress": "Call in Progress", "A call is currently being placed!": "A call is currently being placed!", "Permission Required": "Permission Required", @@ -898,8 +900,6 @@ "Fill Screen": "Fill Screen", "Return to call": "Return to call", "%(name)s on hold": "%(name)s on hold", - "Unable to look up phone number": "Unable to look up phone number", - "There was an error looking up the phone number": "There was an error looking up the phone number", "Dial pad": "Dial pad", "Unknown caller": "Unknown caller", "Incoming voice call": "Incoming voice call", From 53fc47553906b662d6b27796b3bca6e44a04bfaa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:03:31 +0100 Subject: [PATCH 055/115] Update src/components/views/rooms/PinnedEventTile.tsx Co-authored-by: Travis Ralston --- src/components/views/rooms/PinnedEventTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/PinnedEventTile.tsx b/src/components/views/rooms/PinnedEventTile.tsx index 83eee7d81d..774dea70c8 100644 --- a/src/components/views/rooms/PinnedEventTile.tsx +++ b/src/components/views/rooms/PinnedEventTile.tsx @@ -40,7 +40,7 @@ const AVATAR_SIZE = 24; @replaceableComponent("views.rooms.PinnedEventTile") export default class PinnedEventTile extends React.Component { - static contextType = MatrixClientContext; + public static contextType = MatrixClientContext; private onTileClicked = () => { dis.dispatch({ From 42a3ace82af9fec69948e2816346c79b9c472f7f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:35:12 +0100 Subject: [PATCH 056/115] Iterate PR based on feedback --- src/components/structures/NotificationPanel.tsx | 4 +--- src/components/structures/RightPanel.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 7e1566521d..3bfadf7110 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -31,7 +31,7 @@ interface IProps { * Component which shows the global notification list using a TimelinePanel */ @replaceableComponent("structures.NotificationPanel") -class NotificationPanel extends React.Component { +export default class NotificationPanel extends React.PureComponent { render() { const emptyState = (

{_t('You’re all caught up')}

@@ -62,5 +62,3 @@ class NotificationPanel extends React.Component { ; } } - -export default NotificationPanel; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index e3237d98a6..353f50ad34 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -295,13 +295,13 @@ export default class RightPanel extends React.Component { break; case RightPanelPhases.NotificationPanel: - if (SettingsStore.getValue("feature_pinning")) { - panel = ; - } + panel = ; break; case RightPanelPhases.PinnedMessages: - panel = ; + if (SettingsStore.getValue("feature_pinning")) { + panel = ; + } break; case RightPanelPhases.FilePanel: From a34f8a29f4a58ce11ff8bbbf160ae292ac752ed0 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 3 Jun 2021 08:41:12 +0100 Subject: [PATCH 057/115] fix mx_Event containment rules and empty read avatar row --- res/css/structures/_RoomView.scss | 1 - src/components/views/rooms/EventTile.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index b019fe7e01..09e1d58617 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -222,7 +222,6 @@ limitations under the License. .mx_RoomView_MessageList li { clear: both; - contain: content; } li.mx_RoomView_myReadMarker_container { diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 67df5a84ba..7b652cbba2 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -631,7 +631,7 @@ export default class EventTile extends React.Component { // return early if there are no read receipts if (!this.props.readReceipts || this.props.readReceipts.length === 0) { - return (); + return null; } const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); From 83d223475bb98af6e08587c701de14b525f25de9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:41:22 +0100 Subject: [PATCH 058/115] delint imports --- .../structures/NotificationPanel.tsx | 4 +-- src/components/structures/RightPanel.tsx | 16 ++++----- src/components/structures/RoomView.tsx | 4 +-- .../views/context_menus/MessageContextMenu.js | 10 +++--- .../views/right_panel/GroupHeaderButtons.tsx | 12 +++---- .../views/right_panel/HeaderButton.tsx | 2 +- .../views/right_panel/HeaderButtons.tsx | 8 ++--- .../views/right_panel/RoomHeaderButtons.tsx | 14 ++++---- src/components/views/right_panel/UserInfo.tsx | 36 +++++++++---------- src/components/views/rooms/RoomHeader.js | 8 ++--- src/contexts/RoomContext.ts | 4 +-- src/hooks/useAsyncMemo.ts | 2 +- 12 files changed, 60 insertions(+), 60 deletions(-) diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 3bfadf7110..b4f13f6b37 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -17,9 +17,9 @@ limitations under the License. import React from "react"; import { _t } from '../../languageHandler'; -import {MatrixClientPeg} from "../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; import BaseCard from "../views/right_panel/BaseCard"; -import {replaceableComponent} from "../../utils/replaceableComponent"; +import { replaceableComponent } from "../../utils/replaceableComponent"; import TimelinePanel from "./TimelinePanel"; import Spinner from "../views/elements/Spinner"; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 353f50ad34..294865fe08 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -16,11 +16,11 @@ limitations under the License. */ import React from 'react'; -import {Room} from "matrix-js-sdk/src/models/room"; -import {User} from "matrix-js-sdk/src/models/user"; -import {RoomMember} from "matrix-js-sdk/src/models/room-member"; -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; -import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { User } from "matrix-js-sdk/src/models/user"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; @@ -32,12 +32,12 @@ import { } from "../../stores/RightPanelStorePhases"; import RightPanelStore from "../../stores/RightPanelStore"; import MatrixClientContext from "../../contexts/MatrixClientContext"; -import {Action} from "../../dispatcher/actions"; +import { Action } from "../../dispatcher/actions"; import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import WidgetCard from "../views/right_panel/WidgetCard"; -import {replaceableComponent} from "../../utils/replaceableComponent"; +import { replaceableComponent } from "../../utils/replaceableComponent"; import SettingsStore from "../../settings/SettingsStore"; -import {ActionPayload} from "../../dispatcher/payloads"; +import { ActionPayload } from "../../dispatcher/payloads"; import MemberList from "../views/rooms/MemberList"; import GroupMemberList from "../views/groups/GroupMemberList"; import GroupRoomList from "../views/groups/GroupRoomList"; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 482f4a45a7..fdfc5c2ada 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -46,7 +46,7 @@ import RoomViewStore from '../../stores/RoomViewStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; import WidgetEchoStore from '../../stores/WidgetEchoStore'; import SettingsStore from "../../settings/SettingsStore"; -import {Layout} from "../../settings/Layout"; +import { Layout } from "../../settings/Layout"; import AccessibleButton from "../views/elements/AccessibleButton"; import RightPanelStore from "../../stores/RightPanelStore"; import { haveTileForEvent } from "../views/rooms/EventTile"; @@ -80,7 +80,7 @@ import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager'; import { objectHasDiff } from "../../utils/objects"; import SpaceRoomView from "./SpaceRoomView"; import { IOpts } from "../../createRoom"; -import {replaceableComponent} from "../../utils/replaceableComponent"; +import { replaceableComponent } from "../../utils/replaceableComponent"; const DEBUG = false; let debuglog = function(msg: string) {}; diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index d9f21906fe..594b98b1f5 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -17,9 +17,9 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {EventStatus} from 'matrix-js-sdk/src/models/event'; +import { EventStatus } from 'matrix-js-sdk/src/models/event'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -28,9 +28,9 @@ import Resend from '../../../Resend'; import SettingsStore from '../../../settings/SettingsStore'; import { isUrlPermitted } from '../../../HtmlUtils'; import { isContentActionable } from '../../../utils/EventUtils'; -import {MenuItem} from "../../structures/ContextMenu"; -import {EventType} from "matrix-js-sdk/src/@types/event"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { MenuItem } from "../../structures/ContextMenu"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; export function canCancel(eventStatus) { diff --git a/src/components/views/right_panel/GroupHeaderButtons.tsx b/src/components/views/right_panel/GroupHeaderButtons.tsx index 5ae5709d44..3c93cf6470 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.tsx +++ b/src/components/views/right_panel/GroupHeaderButtons.tsx @@ -21,12 +21,12 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; -import HeaderButtons, {HeaderKind} from './HeaderButtons'; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {Action} from "../../../dispatcher/actions"; -import {ActionPayload} from "../../../dispatcher/payloads"; -import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import HeaderButtons, { HeaderKind } from './HeaderButtons'; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { Action } from "../../../dispatcher/actions"; +import { ActionPayload } from "../../../dispatcher/payloads"; +import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; const GROUP_PHASES = [ RightPanelPhases.GroupMemberInfo, diff --git a/src/components/views/right_panel/HeaderButton.tsx b/src/components/views/right_panel/HeaderButton.tsx index 8451f69fd3..cdf4f44e06 100644 --- a/src/components/views/right_panel/HeaderButton.tsx +++ b/src/components/views/right_panel/HeaderButton.tsx @@ -22,7 +22,7 @@ import React from 'react'; import classNames from 'classnames'; import Analytics from '../../../Analytics'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { // Whether this button is highlighted diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 0c7d9ee299..6d44a081d9 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -21,14 +21,14 @@ limitations under the License. import React from 'react'; import dis from '../../../dispatcher/dispatcher'; import RightPanelStore from "../../../stores/RightPanelStore"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {Action} from '../../../dispatcher/actions'; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { Action } from '../../../dispatcher/actions'; import { SetRightPanelPhasePayload, SetRightPanelPhaseRefireParams, } from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; -import {EventSubscription} from "fbemitter"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import type { EventSubscription } from "fbemitter"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; export enum HeaderKind { Room = "room", diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 7ff7207fb6..54e18e4529 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -21,15 +21,15 @@ limitations under the License. import React from "react"; import { Room } from "matrix-js-sdk/src/models/room"; -import {_t} from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; -import HeaderButtons, {HeaderKind} from './HeaderButtons'; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {Action} from "../../../dispatcher/actions"; -import {ActionPayload} from "../../../dispatcher/payloads"; +import HeaderButtons, { HeaderKind } from './HeaderButtons'; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { Action } from "../../../dispatcher/actions"; +import { ActionPayload } from "../../../dispatcher/payloads"; import RightPanelStore from "../../../stores/RightPanelStore"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {useSettingValue} from "../../../hooks/useSettings"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { useSettingValue } from "../../../hooks/useSettings"; import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard'; const ROOM_INFO_PHASES = [ diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 5a40b60111..cdbca59db9 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -17,19 +17,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; -import {MatrixClient} from 'matrix-js-sdk/src/client'; -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 {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; -import {MatrixEvent} from 'matrix-js-sdk/src/models/event'; -import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { MatrixClient } from 'matrix-js-sdk/src/client'; +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 { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import dis from '../../../dispatcher/dispatcher'; import Modal from '../../../Modal'; -import {_t} from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import AccessibleButton from '../elements/AccessibleButton'; @@ -40,18 +40,18 @@ import MultiInviter from "../../../utils/MultiInviter"; import GroupStore from "../../../stores/GroupStore"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; -import {textualPowerLevel} from '../../../Roles'; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import { textualPowerLevel } from '../../../Roles'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; import EncryptionPanel from "./EncryptionPanel"; -import {useAsyncMemo} from '../../../hooks/useAsyncMemo'; -import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification'; -import {Action} from "../../../dispatcher/actions"; +import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; +import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification'; +import { Action } from "../../../dispatcher/actions"; import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog"; -import {useIsEncrypted} from "../../../hooks/useIsEncrypted"; +import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; -import {E2EStatus} from "../../../utils/ShieldUtils"; +import { E2EStatus } from "../../../utils/ShieldUtils"; import ImageView from "../elements/ImageView"; import Spinner from "../elements/Spinner"; import PowerSelector from "../elements/PowerSelector"; @@ -66,7 +66,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; -import {mediaFromMxc} from "../../../customisations/Media"; +import { mediaFromMxc } from "../../../customisations/Media"; export interface IDevice { deviceId: string; diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 217b3ea5c2..cb6bb0afca 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -19,10 +19,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import RateLimitedFunc from '../../../ratelimitedfunc'; -import {CancelButton} from './SimpleRoomHeader'; +import { CancelButton } from './SimpleRoomHeader'; import SettingsStore from "../../../settings/SettingsStore"; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import E2EIcon from './E2EIcon'; @@ -30,8 +30,8 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import RoomTopic from "../elements/RoomTopic"; import RoomName from "../elements/RoomName"; -import {PlaceCallType} from "../../../CallHandler"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { PlaceCallType } from "../../../CallHandler"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.rooms.RoomHeader") export default class RoomHeader extends React.Component { diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index 7f2c0bb157..e925f8624b 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -16,8 +16,8 @@ limitations under the License. import { createContext } from "react"; -import {IState} from "../components/structures/RoomView"; -import {Layout} from "../settings/Layout"; +import { IState } from "../components/structures/RoomView"; +import { Layout } from "../settings/Layout"; const RoomContext = createContext({ roomLoading: true, diff --git a/src/hooks/useAsyncMemo.ts b/src/hooks/useAsyncMemo.ts index eda44b1ae4..1776ec1d36 100644 --- a/src/hooks/useAsyncMemo.ts +++ b/src/hooks/useAsyncMemo.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {useState, useEffect, DependencyList} from 'react'; +import { useState, useEffect, DependencyList } from 'react'; type Fn = () => Promise; From 8ef95a62378a8fca563aa094ee897da06e05c438 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 3 Jun 2021 14:38:13 +0100 Subject: [PATCH 059/115] Interface dispatcher payload & use constant in test --- src/components/views/voip/DialPadModal.tsx | 6 +++-- src/dispatcher/actions.ts | 4 +--- src/dispatcher/payloads/DialNumberPayload.ts | 23 ++++++++++++++++++++ test/CallHandler-test.ts | 4 +++- 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 src/dispatcher/payloads/DialNumberPayload.ts diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx index f3ee49e4ac..8c0af5e81a 100644 --- a/src/components/views/voip/DialPadModal.tsx +++ b/src/components/views/voip/DialPadModal.tsx @@ -21,6 +21,7 @@ import Field from "../elements/Field"; import DialPad from './DialPad'; import dis from '../../../dispatcher/dispatcher'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload"; import { Action } from "../../../dispatcher/actions"; interface IProps { @@ -63,10 +64,11 @@ export default class DialpadModal extends React.PureComponent { } onDialPress = async () => { - dis.dispatch({ + const payload: DialNumberPayload = { action: Action.DialNumber, number: this.state.value, - }) + }; + dis.dispatch(payload); this.props.onFinished(true); } diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 073e82bef6..300eed2b98 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -102,9 +102,7 @@ export enum Action { /** * Dial the phone number in the payload - * payload: { - * number: , - * } + * payload: DialNumberPayload */ DialNumber = "dial_number", diff --git a/src/dispatcher/payloads/DialNumberPayload.ts b/src/dispatcher/payloads/DialNumberPayload.ts new file mode 100644 index 0000000000..1b591b9f6b --- /dev/null +++ b/src/dispatcher/payloads/DialNumberPayload.ts @@ -0,0 +1,23 @@ +/* +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 DialNumberPayload extends ActionPayload { + action: Action.DialNumber; + number: string; +} diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index a93a134133..12316ac01c 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -25,6 +25,8 @@ import DMRoomMap from '../src/utils/DMRoomMap'; import EventEmitter from 'events'; import SdkConfig from '../src/SdkConfig'; import { ActionPayload } from '../src/dispatcher/payloads'; +import { Actions } from '../src/notifications/types'; +import { Action } from '../src/dispatcher/actions'; const REAL_ROOM_ID = '$room1:example.org'; const MAPPED_ROOM_ID = '$room2:example.org'; @@ -172,7 +174,7 @@ describe('CallHandler', () => { }]); dis.dispatch({ - action: 'dial_number', + action: Action.DialNumber, number: '01818118181', }, true); From 5ef78f43a42005ed09f3f1bc02eb55170d8c0487 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 3 Jun 2021 16:26:20 +0100 Subject: [PATCH 060/115] fix containment rule to keep height when resizing vertically --- res/css/views/rooms/_RoomTile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 421fbfe39b..03146e0325 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -19,7 +19,7 @@ limitations under the License. margin-bottom: 4px; padding: 4px; - contain: strict; + contain: content; // Not strict as it will break when resizing a sublist vertically height: 40px; box-sizing: border-box; From 5d0d81e79acd06ba9b024e312bfb888844f99a9e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 19:37:26 +0100 Subject: [PATCH 061/115] not sure how I butchered this merge conflict resolution this much. --- src/components/structures/RoomView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 4390d21783..5ffc2fd0da 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -79,8 +79,8 @@ import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutS import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager'; import { objectHasDiff } from "../../utils/objects"; import SpaceRoomView from "./SpaceRoomView"; -import { IOpts } from "../../createRoom -import { replaceableComponent } from "../../utils/replaceableComponent +import { IOpts } from "../../createRoom"; +import { replaceableComponent } from "../../utils/replaceableComponent"; import { omit } from 'lodash'; import UIStore from "../../stores/UIStore"; From 48bd6583ed3e1431a995074dcb50ca47146f61a0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 4 Jun 2021 11:34:54 +0100 Subject: [PATCH 062/115] Fix unpinning of pinned messages --- src/components/views/right_panel/PinnedMessagesCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index a3f1f2d9df..55809ee02b 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -155,7 +155,7 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => { // show them in reverse, with latest pinned at the top content = pinnedEvents.filter(Boolean).reverse().map(ev => ( - + onUnpinClicked(ev)} /> )); } else { content =
From 93f41ce0ab484172a76f0e07e4036eace2fe9bce Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 4 Jun 2021 11:35:17 +0100 Subject: [PATCH 063/115] Actually finish the empty state for the pinned messages card --- .../right_panel/_PinnedMessagesCard.scss | 55 +++++++++++++++++++ .../views/right_panel/PinnedMessagesCard.tsx | 17 +++++- src/i18n/strings/en_EN.json | 6 +- 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/res/css/views/right_panel/_PinnedMessagesCard.scss b/res/css/views/right_panel/_PinnedMessagesCard.scss index b6b8238bed..785aee09ca 100644 --- a/res/css/views/right_panel/_PinnedMessagesCard.scss +++ b/res/css/views/right_panel/_PinnedMessagesCard.scss @@ -32,4 +32,59 @@ limitations under the License. margin-right: 6px; } } + + .mx_PinnedMessagesCard_empty { + display: flex; + height: 100%; + + > div { + height: max-content; + text-align: center; + margin: auto 40px; + + .mx_PinnedMessagesCard_MessageActionBar { + pointer-events: none; + display: flex; + height: 32px; + line-height: $font-24px; + border-radius: 8px; + background: $primary-bg-color; + border: 1px solid $input-border-color; + padding: 1px; + width: max-content; + margin: 0 auto; + box-sizing: border-box; + + .mx_MessageActionBar_maskButton { + display: inline-block; + position: relative; + } + + .mx_MessageActionBar_optionsButton { + background: $roomlist-button-bg-color; + border-radius: 6px; + z-index: 1; + + &::after { + background-color: $primary-fg-color; + } + } + } + + > h2 { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + margin-top: 24px; + margin-bottom: 20px; + } + + > span { + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + } + } + } } diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index 55809ee02b..3a4dc9cc92 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -158,9 +158,20 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => { onUnpinClicked(ev)} /> )); } else { - content =
-

{_t("You’re all caught up")}

-

{_t("You have no visible notifications.")}

+ content =
+
+
+
+
+
+
+ +

{ _t("Nothing pinned, yet") }

+ { _t("If you have permissions, open the menu on any message and select " + + "Pin to stick them here.", {}, { + b: sub => { sub }, + }) } +
; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9e85ea28c8..302c8709fa 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1717,8 +1717,8 @@ "The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to", "Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection", "Yours, or the other users’ session": "Yours, or the other users’ session", - "You’re all caught up": "You’re all caught up", - "You have no visible notifications.": "You have no visible notifications.", + "Nothing pinned, yet": "Nothing pinned, yet", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "If you have permissions, open the menu on any message and select Pin to stick them here.", "Pinned messages": "Pinned messages", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", @@ -2628,6 +2628,8 @@ "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.", "Communities are changing to Spaces": "Communities are changing to Spaces", + "You’re all caught up": "You’re all caught up", + "You have no visible notifications.": "You have no visible notifications.", "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", "%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.", "The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.", From ab96d5f8af9d4b5afa01f355d4ec7e68d41239bb Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 4 Jun 2021 11:54:58 +0100 Subject: [PATCH 064/115] Repair event status position in timeline https://github.com/matrix-org/matrix-react-sdk/pull/6079 caused a regression in the event status indicator. The `mx_EventTile_msgOption` container was folded into the avatars code path, but the event status is a special case of this, so it now needs to also have this container to preserve its positioning. Fixes https://github.com/vector-im/element-web/issues/17552 --- src/components/views/rooms/EventTile.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index d3057fed62..8cec067c39 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1340,11 +1340,15 @@ class SentReceipt extends React.PureComponent; } - return - - {nonCssBadge} - {tooltip} - - ; + return ( +
+ + + {nonCssBadge} + {tooltip} + + +
+ ); } } From 31604c13c04f1a0738038725718889ca9d47ca0a Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 4 Jun 2021 16:52:50 +0100 Subject: [PATCH 065/115] Lint the typescript tests Turns out we hadn't told eslint to lint .ts in tests/ Also fix all the lint errors, including removing a use of assert that had randomly crept in. --- .eslintrc.js | 2 +- test/CallHandler-test.ts | 1 - test/KeyBindingsManager-test.ts | 98 ++++++++++++++++----------------- test/stores/SpaceStore-test.ts | 2 +- 4 files changed, 50 insertions(+), 53 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 9ae51f9bc5..c759eae9e3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,7 +18,7 @@ module.exports = { }, overrides: [{ - "files": ["src/**/*.{ts,tsx}"], + "files": ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"], "extends": ["matrix-org/ts"], "rules": { // We're okay being explicit at the moment diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index 12316ac01c..4c3dd25fb4 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -25,7 +25,6 @@ import DMRoomMap from '../src/utils/DMRoomMap'; import EventEmitter from 'events'; import SdkConfig from '../src/SdkConfig'; import { ActionPayload } from '../src/dispatcher/payloads'; -import { Actions } from '../src/notifications/types'; import { Action } from '../src/dispatcher/actions'; const REAL_ROOM_ID = '$room1:example.org'; diff --git a/test/KeyBindingsManager-test.ts b/test/KeyBindingsManager-test.ts index 41614b61fa..694efac7b5 100644 --- a/test/KeyBindingsManager-test.ts +++ b/test/KeyBindingsManager-test.ts @@ -15,7 +15,6 @@ limitations under the License. */ import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager'; -const assert = require('assert'); function mockKeyEvent(key: string, modifiers?: { ctrlKey?: boolean, @@ -28,7 +27,7 @@ function mockKeyEvent(key: string, modifiers?: { ctrlKey: modifiers?.ctrlKey ?? false, altKey: modifiers?.altKey ?? false, shiftKey: modifiers?.shiftKey ?? false, - metaKey: modifiers?.metaKey ?? false + metaKey: modifiers?.metaKey ?? false, } as KeyboardEvent; } @@ -37,9 +36,8 @@ describe('KeyBindingsManager', () => { const combo1: KeyCombo = { key: 'k', }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo1, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n'), combo1, false), false); - + expect(isKeyComboMatch(mockKeyEvent('k'), combo1, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n'), combo1, false)).toBe(false); }); it('should match key + modifier key combo', () => { @@ -47,38 +45,38 @@ describe('KeyBindingsManager', () => { key: 'k', ctrlKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k'), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false)).toBe(false); const combo2: KeyCombo = { key: 'k', metaKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo2, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k'), combo2, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false)).toBe(false); const combo3: KeyCombo = { key: 'k', altKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo3, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k'), combo3, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false)).toBe(false); const combo4: KeyCombo = { key: 'k', shiftKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo4, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k'), combo4, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false)).toBe(false); }); it('should match key + multiple modifiers key combo', () => { @@ -87,11 +85,11 @@ describe('KeyBindingsManager', () => { ctrlKey: true, altKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo, - false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo, + false)).toBe(false); const combo2: KeyCombo = { key: 'k', @@ -99,13 +97,13 @@ describe('KeyBindingsManager', () => { shiftKey: true, altKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2, - false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2, - false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', - { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2, + false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2, + false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', + { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false)).toBe(false); const combo3: KeyCombo = { key: 'k', @@ -114,12 +112,12 @@ describe('KeyBindingsManager', () => { altKey: true, metaKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', - { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', - { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', - { ctrlKey: true, shiftKey: true, altKey: true }), combo3, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', + { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', + { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', + { ctrlKey: true, shiftKey: true, altKey: true }), combo3, false)).toBe(false); }); it('should match ctrlOrMeta key combo', () => { @@ -128,13 +126,13 @@ describe('KeyBindingsManager', () => { ctrlOrCmd: true, }; // PC: - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false); // MAC: - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true), false); + expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true)).toBe(false); }); it('should match advanced ctrlOrMeta key combo', () => { @@ -144,10 +142,10 @@ describe('KeyBindingsManager', () => { altKey: true, }; // PC: - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false)).toBe(false); // MAC: - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true), false); + expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true)).toBe(false); }); }); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 20c48c29db..01bd528b87 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -21,7 +21,7 @@ import "../skinned-sdk"; // Must be first for skinning to work import SpaceStore, { UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, - UPDATE_TOP_LEVEL_SPACES + UPDATE_TOP_LEVEL_SPACES, } from "../../src/stores/SpaceStore"; import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils"; import { mkEvent, mkStubRoom, stubClient } from "../test-utils"; From 40d2f8228f4d2a3f7f386d320c42ae1970932ee4 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 01:16:52 -0400 Subject: [PATCH 066/115] Fix watching settings An accidental variable shadowing was preventing setting watcher callbacks from being fired. Signed-off-by: Robin Townsend --- src/settings/WatchManager.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/settings/WatchManager.ts b/src/settings/WatchManager.ts index 56f911f180..744d75b136 100644 --- a/src/settings/WatchManager.ts +++ b/src/settings/WatchManager.ts @@ -63,8 +63,7 @@ export class WatchManager { if (!inRoomId) { // Fire updates to all the individual room watchers too, as they probably care about the change higher up. - const callbacks = Array.from(roomWatchers.values()).flat(1); - callbacks.push(...callbacks); + callbacks.push(...Array.from(roomWatchers.values()).flat(1)); } else if (roomWatchers.has(IRRELEVANT_ROOM)) { callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM)); } From bbd5fab7b5345f3147f8392907fb114129b6c74e Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 11:15:06 -0400 Subject: [PATCH 067/115] Fix type check As TypeScript points out, you can only set an id on HTML elements, not arbitrary components. Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index cba292ea25..355c92240e 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -214,7 +214,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr />

-
+
= ({ matrixClient: cli, event, permalinkCr autoComplete={true} autoFocus={true} /> - + { rooms.length > 0 ? (
{ rooms.map(room => From b06da16a8567e74b90ef74b860e23f6c04399d89 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 13:20:27 -0400 Subject: [PATCH 068/115] Fix jumping to bottom without a highlighted event Signed-off-by: Robin Townsend --- src/components/structures/RoomView.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 5ffc2fd0da..135057dbb1 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1526,10 +1526,19 @@ export default class RoomView extends React.Component { // jump down to the bottom of this room, where new events are arriving private jumpToLiveTimeline = () => { - dis.dispatch({ - action: 'view_room', - room_id: this.state.room.roomId, - }); + if (this.state.initialEventId && this.state.isInitialEventHighlighted) { + // If we were viewing a highlighted event, firing view_room without + // an event will take care of both clearing the URL fragment and + // jumping to the bottom + dis.dispatch({ + action: 'view_room', + room_id: this.state.room.roomId, + }); + } else { + // Otherwise we have to jump manually + this.messagePanel.jumpToLiveTimeline(); + dis.fire(Action.FocusComposer); + } }; // jump up to wherever our read marker is From 17edfec3aa5adf594dec54672c3d8c4332c91a8c Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 21:08:44 -0400 Subject: [PATCH 069/115] Make it easier to pan images in the lightbox Previously, if you were dragging an image and your cursor outpaced the edge of the image as it was moving, panning would abruptly stop. This moves a few of the lightbox event listeners one level up to the image wrapper to ensure that all drag movements are detected, even if they don't end over the image's current position. Signed-off-by: Robin Townsend --- res/css/views/elements/_ImageView.scss | 6 +++--- src/components/views/elements/ImageView.tsx | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 71035dadc3..da23957b36 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -22,6 +22,7 @@ limitations under the License. } .mx_ImageView_image_wrapper { + pointer-events: initial; display: flex; justify-content: center; align-items: center; @@ -30,7 +31,6 @@ limitations under the License. } .mx_ImageView_image { - pointer-events: all; flex-shrink: 0; } @@ -43,7 +43,7 @@ limitations under the License. } .mx_ImageView_info_wrapper { - pointer-events: all; + pointer-events: initial; padding-left: 32px; display: flex; flex-direction: row; @@ -63,7 +63,7 @@ limitations under the License. .mx_ImageView_toolbar { padding-right: 16px; - pointer-events: all; + pointer-events: initial; display: flex; align-items: center; } diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index df73e1a8cb..d2f09917ad 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -471,7 +471,12 @@ export default class ImageView extends React.Component {
+ ref={this.imageWrapper} + onMouseDown={this.props.onFinished} + onMouseMove={this.onMoving} + onMouseUp={this.onEndMoving} + onMouseLeave={this.onEndMoving} + > { className="mx_ImageView_image" draggable={true} onMouseDown={this.onStartMoving} - onMouseMove={this.onMoving} - onMouseUp={this.onEndMoving} - onMouseLeave={this.onEndMoving} />
From e891d18fa264491fe81fbbcd893fa5c9fdc1d51a Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 21:41:28 -0400 Subject: [PATCH 070/115] Add my email to my copyright notices Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 2 +- src/components/views/dialogs/ForwardDialog.tsx | 2 +- test/components/views/dialogs/ForwardDialog-test.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index fc59259ca2..7422d39626 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -1,5 +1,5 @@ /* -Copyright 2021 Robin Townsend. +Copyright 2021 Robin Townsend Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 355c92240e..e97958973b 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021 Robin Townsend. +Copyright 2021 Robin Townsend Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index 004954c713..c50fb073bf 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -1,5 +1,5 @@ /* -Copyright 2021 Robin Townsend. +Copyright 2021 Robin Townsend Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From ea2120bdfdb6b75b25973fb89e63df5dc2a9b72b Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 6 Jun 2021 01:55:01 -0400 Subject: [PATCH 071/115] Fix timestamps 7f835908468dc10848e3e9dcb071f08c7f8132b9 changed timestamps to be hidden at the DOM level, not the CSS level. We can keep that approach, we just need to ensure they still get shown at the right times. Signed-off-by: Robin Townsend --- res/css/views/rooms/_EventTile.scss | 21 +------------------ src/components/structures/MessagePanel.js | 10 +-------- .../views/dialogs/MessageEditHistoryDialog.js | 2 +- src/components/views/elements/ReplyThread.js | 6 +++++- src/components/views/rooms/EventTile.tsx | 6 +++++- src/components/views/rooms/ReplyPreview.js | 2 +- .../views/rooms/SearchResultTile.js | 2 ++ 7 files changed, 16 insertions(+), 33 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 51d9e1cc9d..c8b4138f27 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -85,12 +85,11 @@ $left-gutter: 64px; } .mx_EventTile_isEditing .mx_MessageTimestamp { - visibility: hidden !important; + visibility: hidden; } .mx_EventTile .mx_MessageTimestamp { display: block; - visibility: hidden; white-space: nowrap; left: 0px; text-align: center; @@ -142,29 +141,11 @@ $left-gutter: 64px; line-height: 57px !important; } -.mx_MessagePanel_alwaysShowTimestamps .mx_MessageTimestamp { - visibility: visible; -} - .mx_EventTile_selected > div > a > .mx_MessageTimestamp { left: 3px; width: auto; } -// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) -// The first set is to handle the 'group layout' (default) and the second for the IRC layout -.mx_EventTile_last > div > a > .mx_MessageTimestamp, -.mx_EventTile:hover > div > a > .mx_MessageTimestamp, -.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp, -.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp, -.mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp, -.mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp, -.mx_IRCLayout .mx_ReplyThread .mx_EventTile > a > .mx_MessageTimestamp, -.mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp, -.mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp { - visibility: visible; -} - .mx_EventTile:hover .mx_MessageActionBar, .mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar, [data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar, diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 6709fef814..e64d3770d4 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -19,7 +19,6 @@ limitations under the License. import React, {createRef} from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; import shouldHideEvent from '../../shouldHideEvent'; import {wantsDateSeparator} from '../../DateUtils'; import * as sdk from '../../index'; @@ -854,13 +853,6 @@ export default class MessagePanel extends React.Component { const style = this.props.hidden ? { display: 'none' } : {}; - const className = classNames( - this.props.className, - { - "mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps, - }, - ); - let whoIsTyping; if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) { whoIsTyping = ( -
    {this._renderEdits()}
+
    {this._renderEdits()}
); } const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index bbced5328f..81ed360b17 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -46,6 +46,8 @@ export default class ReplyThread extends React.Component { permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, // Specifies which layout to use. layout: LayoutPropType, + // Whether to always show a timestamp + alwaysShowTimestamps: PropTypes.bool, }; static contextType = MatrixClientContext; @@ -212,7 +214,7 @@ export default class ReplyThread extends React.Component { }; } - static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) { + static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, alwaysShowTimestamps) { if (!ReplyThread.getParentEventId(parentEv)) { return null; } @@ -222,6 +224,7 @@ export default class ReplyThread extends React.Component { ref={ref} permalinkCreator={permalinkCreator} layout={layout} + alwaysShowTimestamps={alwaysShowTimestamps} />; } @@ -386,6 +389,7 @@ export default class ReplyThread extends React.Component { isRedacted={ev.isRedacted()} isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} layout={this.props.layout} + alwaysShowTimestamps={this.props.alwaysShowTimestamps} enableFlair={SettingsStore.getValue(UIFeature.Flair)} replacingEventId={ev.replacingEventId()} /> diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 8cec067c39..e1e3b04f36 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -975,7 +975,8 @@ export default class EventTile extends React.Component { onFocusChange={this.onActionBarFocusChange} /> : undefined; - const showTimestamp = this.props.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.state.hover); + const showTimestamp = this.props.mxEvent.getTs() && + (this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused); const timestamp = showTimestamp ? : null; @@ -1108,6 +1109,8 @@ export default class EventTile extends React.Component { this.props.onHeightChanged, this.props.permalinkCreator, this.replyThread, + null, + this.props.alwaysShowTimestamps || this.state.hover, ); } return ( @@ -1139,6 +1142,7 @@ export default class EventTile extends React.Component { this.props.permalinkCreator, this.replyThread, this.props.layout, + this.props.alwaysShowTimestamps || this.state.hover, ); // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index d7f8c74d73..5835eb9f36 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -89,7 +89,7 @@ export default class ReplyPreview extends React.Component {
]; + const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps"); const timeline = result.context.getTimeline(); for (let j = 0; j < timeline.length; j++) { @@ -67,6 +68,7 @@ export default class SearchResultTile extends React.Component { highlightLink={this.props.resultLink} onHeightChanged={this.props.onHeightChanged} isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} + alwaysShowTimestamps={alwaysShowTimestamps} enableFlair={SettingsStore.getValue(UIFeature.Flair)} /> )); From 1c8e34114318e472d7deee6ecc8dd99e2e6baa7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 6 Jun 2021 08:09:41 +0200 Subject: [PATCH 072/115] Reset all translation vars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not doing this would result in jumps because everything would get out of sync Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index df73e1a8cb..1b1decd6d5 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -283,6 +283,10 @@ export default class ImageView extends React.Component { translationX: 0, translationY: 0, }); + this.lastX = 0; + this.lastY = 0; + this.initX = 0; + this.initY = 0; } this.setState({moving: false}); }; From c0964b69b73af9de4759b2f45d97caf0d6c98b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 6 Jun 2021 08:14:43 +0200 Subject: [PATCH 073/115] Remove redundant vars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 1b1decd6d5..d6256f7dd2 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -95,8 +95,6 @@ export default class ImageView extends React.Component { private initX = 0; private initY = 0; - private lastX = 0; - private lastY = 0; private previousX = 0; private previousY = 0; @@ -253,8 +251,8 @@ export default class ImageView extends React.Component { this.setState({moving: true}); this.previousX = this.state.translationX; this.previousY = this.state.translationY; - this.initX = ev.pageX - this.lastX; - this.initY = ev.pageY - this.lastY; + this.initX = ev.pageX - this.state.translationX; + this.initY = ev.pageY - this.state.translationY; }; private onMoving = (ev: React.MouseEvent) => { @@ -263,11 +261,9 @@ export default class ImageView extends React.Component { if (!this.state.moving) return; - this.lastX = ev.pageX - this.initX; - this.lastY = ev.pageY - this.initY; this.setState({ - translationX: this.lastX, - translationY: this.lastY, + translationX: ev.pageX - this.initX, + translationY: ev.pageY - this.initY, }); }; @@ -283,8 +279,6 @@ export default class ImageView extends React.Component { translationX: 0, translationY: 0, }); - this.lastX = 0; - this.lastY = 0; this.initX = 0; this.initY = 0; } From 28be581af18615ec24c4a664744ccae3a110696f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 6 Jun 2021 08:51:07 +0200 Subject: [PATCH 074/115] Take image rotation into account when zooming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 31 +++++++++++++-------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index d6256f7dd2..d4870a569b 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -97,29 +97,36 @@ export default class ImageView extends React.Component { private initY = 0; private previousX = 0; private previousY = 0; + private rotation = 0; componentDidMount() { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); // We want to recalculate zoom whenever the window's size changes - window.addEventListener("resize", this.calculateZoom); + window.addEventListener("resize", this.calculateZoomAndRotate); // After the image loads for the first time we want to calculate the zoom - this.image.current.addEventListener("load", this.calculateZoom); + this.image.current.addEventListener("load", this.calculateZoomAndRotate); } componentWillUnmount() { this.focusLock.current.removeEventListener('wheel', this.onWheel); - window.removeEventListener("resize", this.calculateZoom); - this.image.current.removeEventListener("load", this.calculateZoom); + window.removeEventListener("resize", this.calculateZoomAndRotate); + this.image.current.removeEventListener("load", this.calculateZoomAndRotate); } - private calculateZoom = () => { + private calculateZoomAndRotate = () => { const image = this.image.current; const imageWrapper = this.imageWrapper.current; - const zoomX = imageWrapper.clientWidth / image.naturalWidth; - const zoomY = imageWrapper.clientHeight / image.naturalHeight; + const landscape = this.rotation % 180 === 0; + + // If the image is rotated take it into account + const width = landscape ? image.naturalWidth : image.naturalHeight; + const height = landscape ? image.naturalHeight : image.naturalWidth; + + const zoomX = imageWrapper.clientWidth / width; + const zoomY = imageWrapper.clientHeight / height; // If the image is smaller in both dimensions set its the zoom to 1 to // display it in its original size @@ -128,6 +135,7 @@ export default class ImageView extends React.Component { zoom: 1, minZoom: 1, maxZoom: 1, + rotation: this.rotation, }); return; } @@ -140,6 +148,7 @@ export default class ImageView extends React.Component { this.setState({ minZoom: minZoom, maxZoom: 1, + rotation: this.rotation, }); } @@ -190,14 +199,14 @@ export default class ImageView extends React.Component { private onRotateCounterClockwiseClick = () => { const cur = this.state.rotation; - const rotationDegrees = cur - 90; - this.setState({ rotation: rotationDegrees }); + this.rotation = cur - 90; + this.calculateZoomAndRotate(); }; private onRotateClockwiseClick = () => { const cur = this.state.rotation; - const rotationDegrees = cur + 90; - this.setState({ rotation: rotationDegrees }); + this.rotation = cur + 90; + this.calculateZoomAndRotate(); }; private onDownloadClick = () => { From c3fdd733570550fcd48473834009bf6bafafeaac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 6 Jun 2021 08:56:12 +0200 Subject: [PATCH 075/115] Avoid multiple setState() calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index d4870a569b..c4ec3497e4 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -144,11 +144,14 @@ export default class ImageView extends React.Component { // image by default const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; - if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom}); + // If zoom is smaller than minZoom don't go beneath that value + const zoom = (this.state.zoom <= this.state.minZoom) ? minZoom : this.state.zoom; + this.setState({ minZoom: minZoom, maxZoom: 1, rotation: this.rotation, + zoom: zoom, }); } From ba0f6766caefb700324a0a4441e0427689b8faf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 6 Jun 2021 09:02:30 +0200 Subject: [PATCH 076/115] Update curly braces styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 131 ++++++++++---------- 1 file changed, 66 insertions(+), 65 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index c4ec3497e4..948e005978 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -19,20 +19,20 @@ limitations under the License. import React, { createRef } from 'react'; import { _t } from '../../../languageHandler'; import AccessibleTooltipButton from "./AccessibleTooltipButton"; -import {Key} from "../../../Keyboard"; +import { Key } from "../../../Keyboard"; import FocusLock from "react-focus-lock"; import MemberAvatar from "../avatars/MemberAvatar"; -import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; +import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import MessageContextMenu from "../context_menus/MessageContextMenu"; -import {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu'; +import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu'; import MessageTimestamp from "../messages/MessageTimestamp"; import SettingsStore from "../../../settings/SettingsStore"; -import {formatFullDate} from "../../../DateUtils"; +import { formatFullDate } from "../../../DateUtils"; import dis from '../../../dispatcher/dispatcher'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks" -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; -import {normalizeWheelEvent} from "../../../utils/Mouse"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks" +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { normalizeWheelEvent } from "../../../utils/Mouse"; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; @@ -167,7 +167,7 @@ export default class ImageView extends React.Component { return; } if (newZoom >= this.state.maxZoom) { - this.setState({zoom: this.state.maxZoom}); + this.setState({ zoom: this.state.maxZoom }); return; } @@ -256,11 +256,11 @@ export default class ImageView extends React.Component { // Zoom in if we are completely zoomed out if (this.state.zoom === this.state.minZoom) { - this.setState({zoom: this.state.maxZoom}); + this.setState({ zoom: this.state.maxZoom }); return; } - this.setState({moving: true}); + this.setState({ moving: true }); this.previousX = this.state.translationX; this.previousY = this.state.translationY; this.initX = ev.pageX - this.state.translationX; @@ -294,7 +294,7 @@ export default class ImageView extends React.Component { this.initX = 0; this.initY = 0; } - this.setState({moving: false}); + this.setState({ moving: false }); }; private renderContextMenu() { @@ -302,14 +302,14 @@ export default class ImageView extends React.Component { if (this.state.contextMenuDisplayed) { contextMenu = ( ); @@ -347,10 +347,10 @@ export default class ImageView extends React.Component { const style = { cursor: cursor, transition: this.state.moving ? null : "transform 200ms ease 0s", - transform: `translateX(${translatePixelsX}) - translateY(${translatePixelsY}) - scale(${zoom}) - rotate(${rotationDegrees})`, + transform: `translateX(${ translatePixelsX }) + translateY(${ translatePixelsY }) + scale(${ zoom }) + rotate(${ rotationDegrees })`, }; let info; @@ -365,37 +365,38 @@ export default class ImageView extends React.Component { const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); const sender = (
- {senderName} + { senderName }
); const messageTimestamp = ( ); const avatar = ( ); info = (
- {avatar} + { avatar }
- {sender} - {messageTimestamp} + { sender } + { messageTimestamp }
); @@ -413,10 +414,10 @@ export default class ImageView extends React.Component { contextMenuButton = ( ); } @@ -427,14 +428,14 @@ export default class ImageView extends React.Component { zoomOutButton = ( + title={ _t("Zoom out") } + onClick={ this.onZoomOutClick }> ); zoomInButton = ( ); @@ -442,57 +443,57 @@ export default class ImageView extends React.Component { return (
- {info} + { info }
+ title={ _t("Rotate Right") } + onClick={ this.onRotateClockwiseClick }> - {zoomOutButton} - {zoomInButton} + { zoomOutButton } + { zoomInButton } - {contextMenuButton} + { contextMenuButton } - {this.renderContextMenu()} + { this.renderContextMenu() }
+ ref={ this.imageWrapper }>
From 31d308a1fbdd94176a8a3ea43e1e20f5b41590db Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 09:22:47 +0100 Subject: [PATCH 077/115] Fix Stickerpicker context menu --- .../views/context_menus/WidgetContextMenu.tsx | 46 ++++++++++++------- src/components/views/elements/AppTile.js | 2 + src/components/views/rooms/Stickerpicker.js | 6 +-- src/stores/widgets/WidgetLayoutStore.ts | 2 +- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 623fe04f2f..068bfd6497 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -40,6 +40,8 @@ interface IProps extends React.ComponentProps { showUnpin?: boolean; // override delete handler onDeleteClick?(): void; + // override edit handler + onEditClick?(): void; } const WidgetContextMenu: React.FC = ({ @@ -47,6 +49,7 @@ const WidgetContextMenu: React.FC = ({ app, userWidget, onDeleteClick, + onEditClick, showUnpin, ...props }) => { @@ -89,12 +92,16 @@ const WidgetContextMenu: React.FC = ({ let editButton; if (canModify && WidgetUtils.isManagedByManager(app)) { - const onEditClick = () => { - WidgetUtils.editWidget(room, app); + const _onEditClick = () => { + if (onEditClick) { + onEditClick(); + } else { + WidgetUtils.editWidget(room, app); + } onFinished(); }; - editButton = ; + editButton = ; } let snapshotButton; @@ -116,24 +123,29 @@ const WidgetContextMenu: React.FC = ({ let deleteButton; if (onDeleteClick || canModify) { - const onDeleteClickDefault = () => { - // Show delete confirmation dialog - Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, { - title: _t("Delete Widget"), - description: _t( - "Deleting a widget removes it for all users in this room." + - " Are you sure you want to delete this widget?"), - button: _t("Delete widget"), - onFinished: (confirmed) => { - if (!confirmed) return; - WidgetUtils.setRoomWidget(roomId, app.id); - }, - }); + const _onDeleteClick = () => { + if (onDeleteClick) { + onDeleteClick(); + } else { + // Show delete confirmation dialog + Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, { + title: _t("Delete Widget"), + description: _t( + "Deleting a widget removes it for all users in this room." + + " Are you sure you want to delete this widget?"), + button: _t("Delete widget"), + onFinished: (confirmed) => { + if (!confirmed) return; + WidgetUtils.setRoomWidget(roomId, app.id); + }, + }); + } + onFinished(); }; deleteButton = ; } diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index b898ad2ebc..ad5cbfa6b3 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -417,6 +417,8 @@ export default class AppTile extends React.Component { onFinished={this._closeContextMenu} showUnpin={!this.props.userWidget} userWidget={this.props.userWidget} + onEditClick={this.props.onEditClick} + onDeleteClick={this.props.onDeleteClick} /> ); } diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 3d2300b83c..b9aaee7a57 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -367,7 +367,7 @@ export default class Stickerpicker extends React.PureComponent { /** * Launch the integration manager on the stickers integration page */ - _launchManageIntegrations() { + _launchManageIntegrations = () => { // TODO: Open the right integration manager for the widget if (SettingsStore.getValue("feature_many_integration_managers")) { IntegrationManagers.sharedInstance().openAll( @@ -382,7 +382,7 @@ export default class Stickerpicker extends React.PureComponent { this.state.widgetId, ); } - } + }; render() { let stickerPicker; @@ -401,7 +401,7 @@ export default class Stickerpicker extends React.PureComponent { key="controls_hide_stickers" className={className} onClick={this._onHideStickersClick} - active={this.state.showStickers} + active={this.state.showStickers.toString()} title={_t("Hide Stickers")} > ; diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index f5734d74c5..b74da98c9c 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -332,7 +332,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { } public getContainerWidgets(room: Room, container: Container): IApp[] { - return this.byRoom[room.roomId]?.[container]?.ordered || []; + return this.byRoom[room?.roomId]?.[container]?.ordered || []; } public isInContainer(room: Room, widget: IApp, container: Container): boolean { From a6ddffe74c8f081e4a99ea087cfa51622a5c9769 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 7 Jun 2021 09:26:42 +0100 Subject: [PATCH 078/115] Add scroll token to file and notif event tiles --- src/components/structures/MessagePanel.js | 5 ----- src/components/views/rooms/EventTile.tsx | 22 ++++++++++++++-------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 6709fef814..19bf27b1d2 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -616,10 +616,6 @@ export default class MessagePanel extends React.Component { const eventId = mxEv.getId(); const highlight = (eventId === this.props.highlightedEventId); - // we can't use local echoes as scroll tokens, because their event IDs change. - // Local echos have a send "status". - const scrollToken = mxEv.status ? undefined : eventId; - const readReceipts = this._readReceiptsByEvent[eventId]; let isLastSuccessful = false; @@ -651,7 +647,6 @@ export default class MessagePanel extends React.Component { { permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); } + const scrollToken = this.props.mxEvent.status + ? undefined + : this.props.mxEvent.getId(); + let avatar; let sender; let avatarSize; @@ -1046,7 +1050,7 @@ export default class EventTile extends React.Component { case 'notif': { const room = this.context.getRoom(this.props.mxEvent.getRoomId()); return ( - + ); } case 'file_grid': { return ( - + ); } @@ -1111,7 +1115,7 @@ export default class EventTile extends React.Component { ); } return ( -
+
  • { ircTimestamp } { avatar } { sender } @@ -1129,7 +1133,7 @@ export default class EventTile extends React.Component { showUrlPreview={false} />
  • -
    + ); } default: { @@ -1143,13 +1147,15 @@ export default class EventTile extends React.Component { // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return ( - React.createElement(this.props.as || "div", { + React.createElement(this.props.as || "li", { "ref": this.ref, "className": classes, "tabIndex": -1, "aria-live": ariaLive, "aria-atomic": "true", - "data-scroll-tokens": this.props["data-scroll-tokens"], + // we can't use local echoes as scroll tokens, because their event IDs change. + // Local echos have a send "status". + "data-scroll-tokens": scrollToken, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), }, [ From d2ce670d3ec2c4c01653b3150d9b1d610bdec9e7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 10:56:10 +0100 Subject: [PATCH 079/115] Remove unused code --- src/utils/Accessibility.js | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 src/utils/Accessibility.js diff --git a/src/utils/Accessibility.js b/src/utils/Accessibility.js deleted file mode 100644 index f4909f971b..0000000000 --- a/src/utils/Accessibility.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. -*/ - -/** - * Automatically focuses the captured reference when receiving a non-null - * object. Useful in scenarios where componentDidMount does not have a - * useful reference to an element, but one needs to focus the element on - * first render. Example usage: ref={focusCapturedRef} - * @param {function} ref The React reference to focus on, if not null - */ -export function focusCapturedRef(ref) { - if (ref) { - ref.focus(); - } -} From 9315a87ebfa9adacb0cf638fca65a687378c9b43 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 10:57:11 +0100 Subject: [PATCH 080/115] Convert EventIndex to Typescript --- src/indexing/BaseEventIndexManager.ts | 10 +- src/indexing/{EventIndex.js => EventIndex.ts} | 177 ++++++++++-------- 2 files changed, 106 insertions(+), 81 deletions(-) rename src/indexing/{EventIndex.js => EventIndex.ts} (87%) diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts index 6349f31524..debcb213ca 100644 --- a/src/indexing/BaseEventIndexManager.ts +++ b/src/indexing/BaseEventIndexManager.ts @@ -35,7 +35,7 @@ export interface MatrixProfile { export interface CrawlerCheckpoint { roomId: string; token: string; - fullCrawl: boolean; + fullCrawl?: boolean; direction: string; } @@ -73,14 +73,14 @@ export interface EventAndProfile { export interface LoadArgs { roomId: string; limit: number; - fromEvent: string; - direction: string; + fromEvent?: string; + direction?: string; } export interface IndexStats { size: number; - event_count: number; - room_count: number; + eventCount: number; + roomCount: number; } /** diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.ts similarity index 87% rename from src/indexing/EventIndex.js rename to src/indexing/EventIndex.ts index 33f2d594ae..b6289969bd 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.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,33 +14,42 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { EventEmitter } from "events"; +import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; +import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set'; +import { RoomState } from 'matrix-js-sdk/src/models/room-state'; +import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window'; + import PlatformPeg from "../PlatformPeg"; -import {MatrixClientPeg} from "../MatrixClientPeg"; -import {RoomMember} from 'matrix-js-sdk/src/models/room-member'; -import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; -import {sleep} from "../utils/promise"; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import { sleep } from "../utils/promise"; import SettingsStore from "../settings/SettingsStore"; -import {EventEmitter} from "events"; -import {SettingLevel} from "../settings/SettingLevel"; +import { SettingLevel } from "../settings/SettingLevel"; +import {CrawlerCheckpoint, LoadArgs, SearchArgs} from "./BaseEventIndexManager"; + +// The time in ms that the crawler will wait loop iterations if there +// have not been any checkpoints to consume in the last iteration. +const CRAWLER_IDLE_TIME = 5000; + +// The maximum number of events our crawler should fetch in a single crawl. +const EVENTS_PER_CRAWL = 100; + +interface ICrawler { + cancel(): void; +} /* * Event indexing class that wraps the platform specific event indexing. */ export default class EventIndex extends EventEmitter { - constructor() { - super(); - this.crawlerCheckpoints = []; - // The time in ms that the crawler will wait loop iterations if there - // have not been any checkpoints to consume in the last iteration. - this._crawlerIdleTime = 5000; - // The maximum number of events our crawler should fetch in a single - // crawl. - this._eventsPerCrawl = 100; - this._crawler = null; - this._currentCheckpoint = null; - } + private crawlerCheckpoints: CrawlerCheckpoint[] = []; + private crawler: ICrawler = null; + private currentCheckpoint: CrawlerCheckpoint = null; - async init() { + public async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); this.crawlerCheckpoints = await indexManager.loadCheckpoints(); @@ -52,7 +61,7 @@ export default class EventIndex extends EventEmitter { /** * Register event listeners that are necessary for the event index to work. */ - registerListeners() { + public registerListeners() { const client = MatrixClientPeg.get(); client.on('sync', this.onSync); @@ -66,7 +75,7 @@ export default class EventIndex extends EventEmitter { /** * Remove the event index specific event listeners. */ - removeListeners() { + public removeListeners() { const client = MatrixClientPeg.get(); if (client === null) return; @@ -81,7 +90,7 @@ export default class EventIndex extends EventEmitter { /** * Get crawler checkpoints for the encrypted rooms and store them in the index. */ - async addInitialCheckpoints() { + public async addInitialCheckpoints() { const indexManager = PlatformPeg.get().getEventIndexingManager(); const client = MatrixClientPeg.get(); const rooms = client.getRooms(); @@ -102,14 +111,14 @@ export default class EventIndex extends EventEmitter { const timeline = room.getLiveTimeline(); const token = timeline.getPaginationToken("b"); - const backCheckpoint = { + const backCheckpoint: CrawlerCheckpoint = { roomId: room.roomId, token: token, direction: "b", fullCrawl: true, }; - const forwardCheckpoint = { + const forwardCheckpoint: CrawlerCheckpoint = { roomId: room.roomId, token: token, direction: "f", @@ -146,7 +155,7 @@ export default class EventIndex extends EventEmitter { * - Every other sync, tell the event index to commit all the queued up * live events */ - onSync = async (state, prevState, data) => { + private onSync = async (state: string, prevState: string, data: object) => { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (prevState === "PREPARED" && state === "SYNCING") { @@ -176,7 +185,15 @@ export default class EventIndex extends EventEmitter { * otherwise we save their event id and wait for them in the Event.decrypted * listener. */ - onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => { + private onRoomTimeline = async ( + ev: MatrixEvent, + room: Room, + toStartOfTimeline: boolean, + removed: boolean, + data: { + liveEvent: boolean; + }, + ) => { const client = MatrixClientPeg.get(); // We only index encrypted rooms locally. @@ -194,7 +211,7 @@ export default class EventIndex extends EventEmitter { await this.addLiveEventToIndex(ev); } - onRoomStateEvent = async (ev, state) => { + private onRoomStateEvent = async (ev: MatrixEvent, state: RoomState) => { if (!MatrixClientPeg.get().isRoomEncrypted(state.roomId)) return; if (ev.getType() === "m.room.encryption" && !await this.isRoomIndexed(state.roomId)) { @@ -209,7 +226,7 @@ export default class EventIndex extends EventEmitter { * Checks if the event was marked for addition in the Room.timeline * listener, if so queues it up to be added to the index. */ - onEventDecrypted = async (ev, err) => { + private onEventDecrypted = async (ev: MatrixEvent, err: Error) => { // If the event isn't in our live event set, ignore it. if (err) return; await this.addLiveEventToIndex(ev); @@ -220,7 +237,7 @@ export default class EventIndex extends EventEmitter { * * Removes a redacted event from our event index. */ - onRedaction = async (ev, room) => { + private onRedaction = async (ev: MatrixEvent, room: Room) => { // We only index encrypted rooms locally. if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; const indexManager = PlatformPeg.get().getEventIndexingManager(); @@ -238,7 +255,7 @@ export default class EventIndex extends EventEmitter { * Listens for timeline resets that are caused by a limited timeline to * re-add checkpoints for rooms that need to be crawled again. */ - onTimelineReset = async (room, timelineSet, resetAllTimelines) => { + private onTimelineReset = async (room: Room, timelineSet: EventTimelineSet, resetAllTimelines: boolean) => { if (room === null) return; if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -258,7 +275,7 @@ export default class EventIndex extends EventEmitter { * @returns {bool} Returns true if the event can be indexed, false * otherwise. */ - isValidEvent(ev) { + private isValidEvent(ev: MatrixEvent) { const isUsefulType = ["m.room.message", "m.room.name", "m.room.topic"].includes(ev.getType()); const validEventType = isUsefulType && !ev.isRedacted() && !ev.isDecryptionFailure(); @@ -282,7 +299,7 @@ export default class EventIndex extends EventEmitter { return validEventType && validMsgType && hasContentValue; } - eventToJson(ev) { + private eventToJson(ev: MatrixEvent) { const jsonEvent = ev.toJSON(); const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent; @@ -314,7 +331,7 @@ export default class EventIndex extends EventEmitter { * * @param {MatrixEvent} ev The event that should be added to the index. */ - async addLiveEventToIndex(ev) { + private async addLiveEventToIndex(ev: MatrixEvent) { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (!this.isValidEvent(ev)) return; @@ -333,11 +350,11 @@ export default class EventIndex extends EventEmitter { * Emmit that the crawler has changed the checkpoint that it's currently * handling. */ - emitNewCheckpoint() { + private emitNewCheckpoint() { this.emit("changedCheckpoint", this.currentRoom()); } - async addEventsFromLiveTimeline(timeline) { + private async addEventsFromLiveTimeline(timeline: EventTimeline) { const events = timeline.getEvents(); for (let i = 0; i < events.length; i++) { @@ -346,7 +363,7 @@ export default class EventIndex extends EventEmitter { } } - async addRoomCheckpoint(roomId, fullCrawl = false) { + private async addRoomCheckpoint(roomId: string, fullCrawl = false) { const indexManager = PlatformPeg.get().getEventIndexingManager(); const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); @@ -396,16 +413,16 @@ export default class EventIndex extends EventEmitter { * crawl, otherwise create a new checkpoint and push it to the * crawlerCheckpoints queue so we go through them in a round-robin way. */ - async crawlerFunc() { + private async crawlerFunc() { let cancelled = false; const client = MatrixClientPeg.get(); const indexManager = PlatformPeg.get().getEventIndexingManager(); - this._crawler = {}; - - this._crawler.cancel = () => { - cancelled = true; + this.crawler = { + cancel: () => { + cancelled = true; + }, }; let idle = false; @@ -417,11 +434,11 @@ export default class EventIndex extends EventEmitter { sleepTime = Math.max(sleepTime, 100); if (idle) { - sleepTime = this._crawlerIdleTime; + sleepTime = CRAWLER_IDLE_TIME; } - if (this._currentCheckpoint !== null) { - this._currentCheckpoint = null; + if (this.currentCheckpoint !== null) { + this.currentCheckpoint = null; this.emitNewCheckpoint(); } @@ -440,7 +457,7 @@ export default class EventIndex extends EventEmitter { continue; } - this._currentCheckpoint = checkpoint; + this.currentCheckpoint = checkpoint; this.emitNewCheckpoint(); idle = false; @@ -454,8 +471,11 @@ export default class EventIndex extends EventEmitter { try { res = await client.createMessagesRequest( - checkpoint.roomId, checkpoint.token, this._eventsPerCrawl, - checkpoint.direction); + checkpoint.roomId, + checkpoint.token, + EVENTS_PER_CRAWL, + checkpoint.direction, + ); } catch (e) { if (e.httpStatus === 403) { console.log("EventIndex: Removing checkpoint as we don't have ", @@ -612,23 +632,23 @@ export default class EventIndex extends EventEmitter { } } - this._crawler = null; + this.crawler = null; } /** * Start the crawler background task. */ - startCrawler() { - if (this._crawler !== null) return; + public startCrawler() { + if (this.crawler !== null) return; this.crawlerFunc(); } /** * Stop the crawler background task. */ - stopCrawler() { - if (this._crawler === null) return; - this._crawler.cancel(); + public stopCrawler() { + if (this.crawler === null) return; + this.crawler.cancel(); } /** @@ -637,7 +657,7 @@ export default class EventIndex extends EventEmitter { * This removes all the MatrixClient event listeners, stops the crawler * task, and closes the index. */ - async close() { + public async close() { const indexManager = PlatformPeg.get().getEventIndexingManager(); this.removeListeners(); this.stopCrawler(); @@ -654,7 +674,7 @@ export default class EventIndex extends EventEmitter { * @return {Promise<[SearchResult]>} A promise that will resolve to an array * of search results once the search is done. */ - async search(searchArgs) { + public async search(searchArgs: SearchArgs) { const indexManager = PlatformPeg.get().getEventIndexingManager(); return indexManager.searchEventIndex(searchArgs); } @@ -680,11 +700,16 @@ export default class EventIndex extends EventEmitter { * @returns {Promise} Resolves to an array of events that * contain URLs. */ - async loadFileEvents(room, limit = 10, fromEvent = null, direction = EventTimeline.BACKWARDS) { + public async loadFileEvents( + room: Room, + limit = 10, + fromEvent: string = null, + direction: string = EventTimeline.BACKWARDS, + ) { const client = MatrixClientPeg.get(); const indexManager = PlatformPeg.get().getEventIndexingManager(); - const loadArgs = { + const loadArgs: LoadArgs = { roomId: room.roomId, limit: limit, }; @@ -772,13 +797,13 @@ export default class EventIndex extends EventEmitter { * @returns {Promise} Resolves to true if events were added to the * timeline, false otherwise. */ - async populateFileTimeline( - timelineSet, - timeline, - room, + public async populateFileTimeline( + timelineSet: EventTimelineSet, + timeline: EventTimeline, + room: Room, limit = 10, - fromEvent = null, - direction = EventTimeline.BACKWARDS, + fromEvent: string = null, + direction: string = EventTimeline.BACKWARDS, ) { const matrixEvents = await this.loadFileEvents(room, limit, fromEvent, direction); @@ -837,7 +862,7 @@ export default class EventIndex extends EventEmitter { * @returns {Promise} Resolves to a boolean which is true if more * events were successfully retrieved. */ - paginateTimelineWindow(room, timelineWindow, direction, limit) { + public paginateTimelineWindow(room: Room, timelineWindow: TimelineWindow, direction: string, limit: number) { const tl = timelineWindow.getTimelineIndex(direction); if (!tl) return Promise.resolve(false); @@ -871,7 +896,7 @@ export default class EventIndex extends EventEmitter { * @return {Promise} A promise that will resolve to the index * statistics. */ - async getStats() { + public async getStats() { const indexManager = PlatformPeg.get().getEventIndexingManager(); return indexManager.getStats(); } @@ -885,7 +910,7 @@ export default class EventIndex extends EventEmitter { * @return {Promise} Returns true if the index contains events for * the given room, false otherwise. */ - async isRoomIndexed(roomId) { + public async isRoomIndexed(roomId) { const indexManager = PlatformPeg.get().getEventIndexingManager(); return indexManager.isRoomIndexed(roomId); } @@ -896,21 +921,21 @@ export default class EventIndex extends EventEmitter { * @returns {Room} A MatrixRoom that is being currently crawled, null * if no room is currently being crawled. */ - currentRoom() { - if (this._currentCheckpoint === null && this.crawlerCheckpoints.length === 0) { + public currentRoom() { + if (this.currentCheckpoint === null && this.crawlerCheckpoints.length === 0) { return null; } const client = MatrixClientPeg.get(); - if (this._currentCheckpoint !== null) { - return client.getRoom(this._currentCheckpoint.roomId); + if (this.currentCheckpoint !== null) { + return client.getRoom(this.currentCheckpoint.roomId); } else { return client.getRoom(this.crawlerCheckpoints[0].roomId); } } - crawlingRooms() { + public crawlingRooms() { const totalRooms = new Set(); const crawlingRooms = new Set(); @@ -918,14 +943,14 @@ export default class EventIndex extends EventEmitter { crawlingRooms.add(checkpoint.roomId); }); - if (this._currentCheckpoint !== null) { - crawlingRooms.add(this._currentCheckpoint.roomId); + if (this.currentCheckpoint !== null) { + crawlingRooms.add(this.currentCheckpoint.roomId); } const client = MatrixClientPeg.get(); const rooms = client.getRooms(); - const isRoomEncrypted = (room) => { + const isRoomEncrypted = (room: Room) => { return client.isRoomEncrypted(room.roomId); }; @@ -934,6 +959,6 @@ export default class EventIndex extends EventEmitter { totalRooms.add(room.roomId); }); - return {crawlingRooms, totalRooms}; + return { crawlingRooms, totalRooms }; } } From 3431ebb9957cb8424eb2ee32b4f87d445a6daf6b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 10:57:25 +0100 Subject: [PATCH 081/115] Convert AsyncWrapper to Typescript --- src/{AsyncWrapper.js => AsyncWrapper.tsx} | 63 ++++++++++++----------- 1 file changed, 34 insertions(+), 29 deletions(-) rename src/{AsyncWrapper.js => AsyncWrapper.tsx} (59%) diff --git a/src/AsyncWrapper.js b/src/AsyncWrapper.tsx similarity index 59% rename from src/AsyncWrapper.js rename to src/AsyncWrapper.tsx index 359828b312..1d3c42e60b 100644 --- a/src/AsyncWrapper.js +++ b/src/AsyncWrapper.tsx @@ -1,6 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2015-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,52 +14,63 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; -import * as sdk from './index'; -import PropTypes from 'prop-types'; +import React, { ComponentType } from "react"; + import { _t } from './languageHandler'; +import { IDialogProps } from "./components/views/dialogs/IDialogProps"; +import BaseDialog from "./components/views/dialogs/BaseDialog"; +import DialogButtons from "./components/views/elements/DialogButtons"; +import Spinner from "./components/views/elements/Spinner"; + +type AsyncImport = { default: T }; + +interface IProps extends IDialogProps { + // A promise which resolves with the real component + prom: Promise>; +} + +interface IState { + component?: ComponentType; + error?: Error; +} /** * Wrap an asynchronous loader function with a react component which shows a * spinner until the real component loads. */ -export default class AsyncWrapper extends React.Component { - static propTypes = { - /** A promise which resolves with the real component - */ - prom: PropTypes.object.isRequired, - }; +export default class AsyncWrapper extends React.Component { + private unmounted = false; - state = { + public state = { component: null, error: null, }; componentDidMount() { - this._unmounted = false; // XXX: temporary logging to try to diagnose // https://github.com/vector-im/element-web/issues/3148 console.log('Starting load of AsyncWrapper for modal'); this.props.prom.then((result) => { - if (this._unmounted) { - return; - } + if (this.unmounted) return; + // Take the 'default' member if it's there, then we support // passing in just an import()ed module, since ES6 async import // always returns a module *namespace*. - const component = result.default ? result.default : result; - this.setState({component}); + const component = (result as AsyncImport).default + ? (result as AsyncImport).default + : result as ComponentType; + this.setState({ component }); }).catch((e) => { console.warn('AsyncWrapper promise failed', e); - this.setState({error: e}); + this.setState({ error: e }); }); } componentWillUnmount() { - this._unmounted = true; + this.unmounted = true; } - _onWrapperCancelClick = () => { + private onWrapperCancelClick = () => { this.props.onFinished(false); }; @@ -69,20 +79,15 @@ export default class AsyncWrapper extends React.Component { const Component = this.state.component; return ; } else if (this.state.error) { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return - {_t("Unable to load! Check your network connectivity and try again.")} + return + { _t("Unable to load! Check your network connectivity and try again.") } ; } else { // show a spinner until the component is loaded. - const Spinner = sdk.getComponent("elements.Spinner"); return ; } } From b2d9dd7214c5f72add24d79c32b763b7c916187c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 10:57:39 +0100 Subject: [PATCH 082/115] Convert LifecycleStore to Typescript --- .../{LifecycleStore.js => LifecycleStore.ts} | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) rename src/stores/{LifecycleStore.js => LifecycleStore.ts} (62%) diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.ts similarity index 62% rename from src/stores/LifecycleStore.js rename to src/stores/LifecycleStore.ts index a12bac7dd6..5381fc58ed 100644 --- a/src/stores/LifecycleStore.js +++ b/src/stores/LifecycleStore.ts @@ -1,7 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2017-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,11 +13,18 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +import { Store } from 'flux/utils'; + import dis from '../dispatcher/dispatcher'; -import {Store} from 'flux/utils'; +import { ActionPayload } from "../dispatcher/payloads"; + +interface IState { + deferredAction: any; +} const INITIAL_STATE = { - deferred_action: null, + deferredAction: null, }; /** @@ -27,39 +32,38 @@ const INITIAL_STATE = { * store that listens for actions and updates its state accordingly, informing any * listeners (views) of state changes. */ -class LifecycleStore extends Store { +class LifecycleStore extends Store { + private state: IState = INITIAL_STATE; + constructor() { super(dis); - - // Initialise state - this._state = INITIAL_STATE; } - _setState(newState) { - this._state = Object.assign(this._state, newState); + private setState(newState: Partial) { + this.state = Object.assign(this.state, newState); this.__emitChange(); } - __onDispatch(payload) { + protected __onDispatch(payload: ActionPayload) { switch (payload.action) { case 'do_after_sync_prepared': - this._setState({ - deferred_action: payload.deferred_action, + this.setState({ + deferredAction: payload.deferred_action, }); break; case 'cancel_after_sync_prepared': - this._setState({ - deferred_action: null, + this.setState({ + deferredAction: null, }); break; - case 'sync_state': { + case 'syncstate': { if (payload.state !== 'PREPARED') { break; } - if (!this._state.deferred_action) break; - const deferredAction = Object.assign({}, this._state.deferred_action); - this._setState({ - deferred_action: null, + if (!this.state.deferredAction) break; + const deferredAction = Object.assign({}, this.state.deferredAction); + this.setState({ + deferredAction: null, }); dis.dispatch(deferredAction); break; @@ -71,8 +75,8 @@ class LifecycleStore extends Store { } } - reset() { - this._state = Object.assign({}, INITIAL_STATE); + private reset() { + this.state = Object.assign({}, INITIAL_STATE); } } From c8b3acb7e4a23d5cd654a2d7234d9f3a5b62405d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 10:57:46 +0100 Subject: [PATCH 083/115] Convert utils to Typescript --- src/utils/{UrlUtils.js => UrlUtils.ts} | 6 +++--- src/utils/{humanize.js => humanize.ts} | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) rename src/utils/{UrlUtils.js => UrlUtils.ts} (88%) rename src/utils/{humanize.js => humanize.ts} (83%) diff --git a/src/utils/UrlUtils.js b/src/utils/UrlUtils.ts similarity index 88% rename from src/utils/UrlUtils.js rename to src/utils/UrlUtils.ts index 7b207c128e..6f441ff98e 100644 --- a/src/utils/UrlUtils.js +++ b/src/utils/UrlUtils.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. @@ -22,7 +22,7 @@ import url from "url"; * @param {string} u The url to be abbreviated * @returns {string} The abbreviated url */ -export function abbreviateUrl(u) { +export function abbreviateUrl(u: string): string { if (!u) return ''; const parsedUrl = url.parse(u); @@ -37,7 +37,7 @@ export function abbreviateUrl(u) { return u; } -export function unabbreviateUrl(u) { +export function unabbreviateUrl(u: string): string { if (!u) return ''; let longUrl = u; diff --git a/src/utils/humanize.js b/src/utils/humanize.ts similarity index 83% rename from src/utils/humanize.js rename to src/utils/humanize.ts index 6e6f17f8f4..55bdbf760b 100644 --- a/src/utils/humanize.js +++ b/src/utils/humanize.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,22 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {_t} from "../languageHandler"; +import { _t } from "../languageHandler"; + +// These are the constants we use for when to break the text +const MILLISECONDS_RECENT = 15000; +const MILLISECONDS_1_MIN = 75000; +const MINUTES_UNDER_1_HOUR = 45; +const MINUTES_1_HOUR = 75; +const HOURS_UNDER_1_DAY = 23; +const HOURS_1_DAY = 26; /** * Converts a timestamp into human-readable, translated, text. * @param {number} timeMillis The time in millis to compare against. * @returns {string} The humanized time. */ -export function humanizeTime(timeMillis) { - // These are the constants we use for when to break the text - const MILLISECONDS_RECENT = 15000; - const MILLISECONDS_1_MIN = 75000; - const MINUTES_UNDER_1_HOUR = 45; - const MINUTES_1_HOUR = 75; - const HOURS_UNDER_1_DAY = 23; - const HOURS_1_DAY = 26; - +export function humanizeTime(timeMillis: number): string { const now = (new Date()).getTime(); let msAgo = now - timeMillis; const minutes = Math.abs(Math.ceil(msAgo / 60000)); From 50d3e04bdabc2cfd6d23381f18de11d4c7ef1287 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 10:58:22 +0100 Subject: [PATCH 084/115] Convert MatrixActionCreators to Typescript --- ...ionCreators.js => MatrixActionCreators.ts} | 125 +++++++++++------- 1 file changed, 77 insertions(+), 48 deletions(-) rename src/actions/{MatrixActionCreators.js => MatrixActionCreators.ts} (68%) diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.ts similarity index 68% rename from src/actions/MatrixActionCreators.js rename to src/actions/MatrixActionCreators.ts index 93a4fcf07c..33e72becd2 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.ts @@ -1,5 +1,5 @@ /* -Copyright 2017 New Vector Ltd +Copyright 2017, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import dis from '../dispatcher/dispatcher'; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; + +import dis from "../dispatcher/dispatcher"; +import {ActionPayload} from "../dispatcher/payloads"; // TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events // become dispatches in the same place. @@ -27,7 +33,7 @@ import dis from '../dispatcher/dispatcher'; * @param {string} prevState the previous sync state. * @returns {Object} an action of type MatrixActions.sync. */ -function createSyncAction(matrixClient, state, prevState) { +function createSyncAction(matrixClient: MatrixClient, state: string, prevState: string): ActionPayload { return { action: 'MatrixActions.sync', state, @@ -53,7 +59,7 @@ function createSyncAction(matrixClient, state, prevState) { * @param {MatrixEvent} accountDataEvent the account data event. * @returns {AccountDataAction} an action of type MatrixActions.accountData. */ -function createAccountDataAction(matrixClient, accountDataEvent) { +function createAccountDataAction(matrixClient: MatrixClient, accountDataEvent: MatrixEvent): ActionPayload { return { action: 'MatrixActions.accountData', event: accountDataEvent, @@ -81,7 +87,11 @@ function createAccountDataAction(matrixClient, accountDataEvent) { * @param {Room} room the room where account data was changed * @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData. */ -function createRoomAccountDataAction(matrixClient, accountDataEvent, room) { +function createRoomAccountDataAction( + matrixClient: MatrixClient, + accountDataEvent: MatrixEvent, + room: Room, +): ActionPayload { return { action: 'MatrixActions.Room.accountData', event: accountDataEvent, @@ -106,7 +116,7 @@ function createRoomAccountDataAction(matrixClient, accountDataEvent, room) { * @param {Room} room the Room that was stored. * @returns {RoomAction} an action of type `MatrixActions.Room`. */ -function createRoomAction(matrixClient, room) { +function createRoomAction(matrixClient: MatrixClient, room: Room): ActionPayload { return { action: 'MatrixActions.Room', room }; } @@ -127,7 +137,7 @@ function createRoomAction(matrixClient, room) { * @param {Room} room the Room whose tags were changed. * @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`. */ -function createRoomTagsAction(matrixClient, roomTagsEvent, room) { +function createRoomTagsAction(matrixClient: MatrixClient, roomTagsEvent: MatrixEvent, room: Room): ActionPayload { return { action: 'MatrixActions.Room.tags', room }; } @@ -140,7 +150,7 @@ function createRoomTagsAction(matrixClient, roomTagsEvent, room) { * @param {Room} room the room the receipt happened in. * @returns {Object} an action of type MatrixActions.Room.receipt. */ -function createRoomReceiptAction(matrixClient, event, room) { +function createRoomReceiptAction(matrixClient: MatrixClient, event: MatrixEvent, room: Room): ActionPayload { return { action: 'MatrixActions.Room.receipt', event, @@ -178,7 +188,17 @@ function createRoomReceiptAction(matrixClient, event, room) { * @param {EventTimeline} data.timeline the timeline being altered. * @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`. */ -function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTimeline, removed, data) { +function createRoomTimelineAction( + matrixClient: MatrixClient, + timelineEvent: MatrixEvent, + room: Room, + toStartOfTimeline: boolean, + removed: boolean, + data: { + liveEvent: boolean; + timeline: EventTimeline; + }, +): ActionPayload { return { action: 'MatrixActions.Room.timeline', event: timelineEvent, @@ -208,8 +228,13 @@ function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTi * @param {string} oldMembership the previous membership, can be null. * @returns {RoomMembershipAction} an action of type `MatrixActions.Room.myMembership`. */ -function createSelfMembershipAction(matrixClient, room, membership, oldMembership) { - return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership}; +function createSelfMembershipAction( + matrixClient: MatrixClient, + room: Room, + membership: string, + oldMembership: string, +): ActionPayload { + return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership }; } /** @@ -228,61 +253,65 @@ function createSelfMembershipAction(matrixClient, room, membership, oldMembershi * @param {MatrixEvent} event the matrix event that was decrypted. * @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`. */ -function createEventDecryptedAction(matrixClient, event) { +function createEventDecryptedAction(matrixClient: MatrixClient, event: MatrixEvent): ActionPayload { return { action: 'MatrixActions.Event.decrypted', event }; } +type Listener = () => void; +type ActionCreator = (matrixClient: MatrixClient, ...args: any) => ActionPayload; + +// A list of callbacks to call to unregister all listeners added +let matrixClientListenersStop: Listener[] = []; + +/** + * Start listening to events of type eventName on matrixClient and when they are emitted, + * dispatch an action created by the actionCreator function. + * @param {MatrixClient} matrixClient a MatrixClient to register a listener with. + * @param {string} eventName the event to listen to on MatrixClient. + * @param {function} actionCreator a function that should return an action to dispatch + * when given the MatrixClient as an argument as well as + * arguments emitted in the MatrixClient event. + */ +function addMatrixClientListener(matrixClient: MatrixClient, eventName: string, actionCreator: ActionCreator): void { + const listener: Listener = (...args) => { + const payload = actionCreator(matrixClient, ...args); + if (payload) { + dis.dispatch(payload, true); + } + }; + matrixClient.on(eventName, listener); + matrixClientListenersStop.push(() => { + matrixClient.removeListener(eventName, listener); + }); +} + /** * This object is responsible for dispatching actions when certain events are emitted by * the given MatrixClient. */ export default { - // A list of callbacks to call to unregister all listeners added - _matrixClientListenersStop: [], - /** * Start listening to certain events from the MatrixClient and dispatch actions when * they are emitted. * @param {MatrixClient} matrixClient the MatrixClient to listen to events from */ - start(matrixClient) { - this._addMatrixClientListener(matrixClient, 'sync', createSyncAction); - this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); - this._addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction); - this._addMatrixClientListener(matrixClient, 'Room', createRoomAction); - this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction); - this._addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction); - this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction); - this._addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction); - this._addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction); - }, - - /** - * Start listening to events of type eventName on matrixClient and when they are emitted, - * dispatch an action created by the actionCreator function. - * @param {MatrixClient} matrixClient a MatrixClient to register a listener with. - * @param {string} eventName the event to listen to on MatrixClient. - * @param {function} actionCreator a function that should return an action to dispatch - * when given the MatrixClient as an argument as well as - * arguments emitted in the MatrixClient event. - */ - _addMatrixClientListener(matrixClient, eventName, actionCreator) { - const listener = (...args) => { - const payload = actionCreator(matrixClient, ...args); - if (payload) { - dis.dispatch(payload, true); - } - }; - matrixClient.on(eventName, listener); - this._matrixClientListenersStop.push(() => { - matrixClient.removeListener(eventName, listener); - }); + start(matrixClient: MatrixClient) { + addMatrixClientListener(matrixClient, 'sync', createSyncAction); + addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); + addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction); + addMatrixClientListener(matrixClient, 'Room', createRoomAction); + addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction); + addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction); + addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction); + addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction); + addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction); }, /** * Stop listening to events. */ stop() { - this._matrixClientListenersStop.forEach((stopListener) => stopListener()); + matrixClientListenersStop.forEach((stopListener) => stopListener()); + matrixClientListenersStop = []; }, }; From f6e4943bef403230b0d99d4d0c7a07b1b138978f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 11:01:38 +0100 Subject: [PATCH 085/115] Convert RoomAliasCache to Typescript --- src/{RoomAliasCache.js => RoomAliasCache.ts} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename src/{RoomAliasCache.js => RoomAliasCache.ts} (81%) diff --git a/src/RoomAliasCache.js b/src/RoomAliasCache.ts similarity index 81% rename from src/RoomAliasCache.js rename to src/RoomAliasCache.ts index bb511ba4d7..c318db2d3f 100644 --- a/src/RoomAliasCache.js +++ b/src/RoomAliasCache.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. @@ -24,12 +24,12 @@ limitations under the License. * A similar thing could also be achieved via `pushState` with a state object, * but keeping it separate like this seems easier in case we do want to extend. */ -const aliasToIDMap = new Map(); +const aliasToIDMap = new Map(); -export function storeRoomAliasInCache(alias, id) { +export function storeRoomAliasInCache(alias: string, id: string): void { aliasToIDMap.set(alias, id); } -export function getCachedRoomIDForAlias(alias) { +export function getCachedRoomIDForAlias(alias: string): string { return aliasToIDMap.get(alias); } From 2525db6fb131ce760673463000895ed73f9d1ef8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 11:03:39 +0100 Subject: [PATCH 086/115] Convert Resend to Typescript --- src/{Resend.js => Resend.ts} | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) rename src/{Resend.js => Resend.ts} (70%) diff --git a/src/Resend.js b/src/Resend.ts similarity index 70% rename from src/Resend.js rename to src/Resend.ts index f1e5fb38f5..38b84a28e0 100644 --- a/src/Resend.js +++ b/src/Resend.ts @@ -1,6 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2015-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,35 +14,37 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from './MatrixClientPeg'; +import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event'; +import { Room } from 'matrix-js-sdk/src/models/room'; + +import { MatrixClientPeg } from './MatrixClientPeg'; import dis from './dispatcher/dispatcher'; -import { EventStatus } from 'matrix-js-sdk/src/models/event'; export default class Resend { - static resendUnsentEvents(room) { - return Promise.all(room.getPendingEvents().filter(function(ev) { + static resendUnsentEvents(room: Room): Promise { + return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) { return ev.status === EventStatus.NOT_SENT; - }).map(function(event) { + }).map(function(event: MatrixEvent) { return Resend.resend(event); })); } - static cancelUnsentEvents(room) { - room.getPendingEvents().filter(function(ev) { + static cancelUnsentEvents(room: Room): void { + room.getPendingEvents().filter(function(ev: MatrixEvent) { return ev.status === EventStatus.NOT_SENT; - }).forEach(function(event) { + }).forEach(function(event: MatrixEvent) { Resend.removeFromQueue(event); }); } - static resend(event) { + static resend(event: MatrixEvent): Promise { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); return MatrixClientPeg.get().resendEvent(event, room).then(function(res) { dis.dispatch({ action: 'message_sent', event: event, }); - }, function(err) { + }, function(err: Error) { // XXX: temporary logging to try to diagnose // https://github.com/vector-im/element-web/issues/3148 console.log('Resend got send failure: ' + err.name + '(' + err + ')'); @@ -55,7 +56,7 @@ export default class Resend { }); } - static removeFromQueue(event) { + static removeFromQueue(event: MatrixEvent): void { MatrixClientPeg.get().cancelPendingEvent(event); } } From ae04b2ce4ac0ea4ee425d848903aa5b5ec9db82f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 11:06:30 +0100 Subject: [PATCH 087/115] Fix Dialog/Modal types defn --- src/components/views/dialogs/IDialogProps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/IDialogProps.ts b/src/components/views/dialogs/IDialogProps.ts index 1027ca7607..b294fdafe1 100644 --- a/src/components/views/dialogs/IDialogProps.ts +++ b/src/components/views/dialogs/IDialogProps.ts @@ -15,5 +15,5 @@ limitations under the License. */ export interface IDialogProps { - onFinished: (bool) => void; + onFinished(...args: any): void; } From 4cc95dd1841111e93a18d1a7a8630abed681449b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 11:20:35 +0100 Subject: [PATCH 088/115] Make skinning less upset --- src/AsyncWrapper.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index 1d3c42e60b..3bbef71093 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -16,11 +16,9 @@ limitations under the License. import React, { ComponentType } from "react"; +import * as sdk from './index'; import { _t } from './languageHandler'; import { IDialogProps } from "./components/views/dialogs/IDialogProps"; -import BaseDialog from "./components/views/dialogs/BaseDialog"; -import DialogButtons from "./components/views/elements/DialogButtons"; -import Spinner from "./components/views/elements/Spinner"; type AsyncImport = { default: T }; @@ -79,6 +77,8 @@ export default class AsyncWrapper extends React.Component { const Component = this.state.component; return ; } else if (this.state.error) { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return { _t("Unable to load! Check your network connectivity and try again.") } { ; } else { // show a spinner until the component is loaded. + const Spinner = sdk.getComponent("elements.Spinner"); return ; } } From 6693c39709fb5fae664ce27e20343dfca347d278 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 7 Jun 2021 12:17:29 +0100 Subject: [PATCH 089/115] make event list summary a list item --- src/components/views/elements/EventListSummary.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 1d3b6e8764..86d3e082ad 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -63,9 +63,9 @@ const EventListSummary: React.FC = ({ // If we are only given few events then just pass them through if (events.length < threshold) { return ( -
    +
  • { children } -
  • + ); } From ea6904ce2a10e9aa368db43a900ea95fbb134911 Mon Sep 17 00:00:00 2001 From: Germain Date: Mon, 7 Jun 2021 12:37:11 +0100 Subject: [PATCH 090/115] Fix local echo comment for scroll tokens Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/EventTile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 0796475bf8..ff310e1c43 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -906,6 +906,8 @@ export default class EventTile extends React.Component { permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); } + // we can't use local echoes as scroll tokens, because their event IDs change. + // Local echos have a send "status". const scrollToken = this.props.mxEvent.status ? undefined : this.props.mxEvent.getId(); @@ -1153,8 +1155,6 @@ export default class EventTile extends React.Component { "tabIndex": -1, "aria-live": ariaLive, "aria-atomic": "true", - // we can't use local echoes as scroll tokens, because their event IDs change. - // Local echos have a send "status". "data-scroll-tokens": scrollToken, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), From f3aa5056730c1bc94ca4988f0abacba30d4348ab Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 14:14:31 +0100 Subject: [PATCH 091/115] Add warning to private space creation flow --- res/css/structures/_SpaceRoomView.scss | 39 +++++++++++++++++++++ src/components/structures/SpaceRoomView.tsx | 4 +++ src/i18n/strings/en_EN.json | 2 ++ 3 files changed, 45 insertions(+) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 4bc4af467c..12762cbc9d 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -365,6 +365,45 @@ $SpaceRoomViewInnerWidth: 428px; } } + .mx_SpaceRoomView_betaWarning { + padding: 12px 12px 12px 54px; + position: relative; + font-size: $font-15px; + line-height: $font-24px; + width: 432px; + border-radius: 8px; + background-color: $info-plinth-bg-color; + color: $secondary-fg-color; + box-sizing: border-box; + + > h3 { + font-weight: $font-semi-bold; + font-size: inherit; + line-height: inherit; + margin: 0; + } + + > p { + font-size: inherit; + line-height: inherit; + margin: 0; + } + + &::before { + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + content: ''; + width: 20px; + height: 20px; + position: absolute; + top: 14px; + left: 14px; + background-color: $secondary-fg-color; + } + } + .mx_SpaceRoomView_inviteTeammates { // XXX remove this when spaces leaves Beta .mx_SpaceRoomView_inviteTeammates_betaDisclaimer { diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 06d2bda16e..276f4ae6ca 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -587,6 +587,10 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {

    { _t("Me and my teammates") }

    { _t("A private space for you and your teammates") }
    +
    +

    { _t("Teammates might not be able to view or join any private rooms you make.") }

    +

    { _t("We're working on this as part of the beta, but just want to let you know.") }

    +
    ; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9e85ea28c8..16cfcccd11 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2723,6 +2723,8 @@ "A private space to organise your rooms": "A private space to organise your rooms", "Me and my teammates": "Me and my teammates", "A private space for you and your teammates": "A private space for you and your teammates", + "Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.", + "We're working on this as part of the beta, but just want to let you know.": "We're working on this as part of the beta, but just want to let you know.", "Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s", "Inviting...": "Inviting...", "Invite your teammates": "Invite your teammates", From 67c9fe1e070d41192d9ff8553523c5834ff9f38c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 15:25:57 +0200 Subject: [PATCH 092/115] Revert "Update curly braces styling" This reverts commit ba0f6766caefb700324a0a4441e0427689b8faf7. --- src/components/views/elements/ImageView.tsx | 131 ++++++++++---------- 1 file changed, 65 insertions(+), 66 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 948e005978..c4ec3497e4 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -19,20 +19,20 @@ limitations under the License. import React, { createRef } from 'react'; import { _t } from '../../../languageHandler'; import AccessibleTooltipButton from "./AccessibleTooltipButton"; -import { Key } from "../../../Keyboard"; +import {Key} from "../../../Keyboard"; import FocusLock from "react-focus-lock"; import MemberAvatar from "../avatars/MemberAvatar"; -import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; +import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import MessageContextMenu from "../context_menus/MessageContextMenu"; -import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu'; +import {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu'; import MessageTimestamp from "../messages/MessageTimestamp"; import SettingsStore from "../../../settings/SettingsStore"; -import { formatFullDate } from "../../../DateUtils"; +import {formatFullDate} from "../../../DateUtils"; import dis from '../../../dispatcher/dispatcher'; -import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks" -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { normalizeWheelEvent } from "../../../utils/Mouse"; +import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks" +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {normalizeWheelEvent} from "../../../utils/Mouse"; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; @@ -167,7 +167,7 @@ export default class ImageView extends React.Component { return; } if (newZoom >= this.state.maxZoom) { - this.setState({ zoom: this.state.maxZoom }); + this.setState({zoom: this.state.maxZoom}); return; } @@ -256,11 +256,11 @@ export default class ImageView extends React.Component { // Zoom in if we are completely zoomed out if (this.state.zoom === this.state.minZoom) { - this.setState({ zoom: this.state.maxZoom }); + this.setState({zoom: this.state.maxZoom}); return; } - this.setState({ moving: true }); + this.setState({moving: true}); this.previousX = this.state.translationX; this.previousY = this.state.translationY; this.initX = ev.pageX - this.state.translationX; @@ -294,7 +294,7 @@ export default class ImageView extends React.Component { this.initX = 0; this.initY = 0; } - this.setState({ moving: false }); + this.setState({moving: false}); }; private renderContextMenu() { @@ -302,14 +302,14 @@ export default class ImageView extends React.Component { if (this.state.contextMenuDisplayed) { contextMenu = ( ); @@ -347,10 +347,10 @@ export default class ImageView extends React.Component { const style = { cursor: cursor, transition: this.state.moving ? null : "transform 200ms ease 0s", - transform: `translateX(${ translatePixelsX }) - translateY(${ translatePixelsY }) - scale(${ zoom }) - rotate(${ rotationDegrees })`, + transform: `translateX(${translatePixelsX}) + translateY(${translatePixelsY}) + scale(${zoom}) + rotate(${rotationDegrees})`, }; let info; @@ -365,38 +365,37 @@ export default class ImageView extends React.Component { const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); const sender = (
    - { senderName } + {senderName}
    ); const messageTimestamp = ( ); const avatar = ( ); info = (
    - { avatar } + {avatar}
    - { sender } - { messageTimestamp } + {sender} + {messageTimestamp}
    ); @@ -414,10 +413,10 @@ export default class ImageView extends React.Component { contextMenuButton = ( ); } @@ -428,14 +427,14 @@ export default class ImageView extends React.Component { zoomOutButton = ( + title={_t("Zoom out")} + onClick={this.onZoomOutClick}> ); zoomInButton = ( ); @@ -443,57 +442,57 @@ export default class ImageView extends React.Component { return (
    - { info } + {info}
    + title={_t("Rotate Right")} + onClick={this.onRotateClockwiseClick}> - { zoomOutButton } - { zoomInButton } + {zoomOutButton} + {zoomInButton} - { contextMenuButton } + {contextMenuButton} - { this.renderContextMenu() } + {this.renderContextMenu()}
    + ref={this.imageWrapper}>
    From 5880cb3b5f032f2a274fc1bcdd4a32b9ba31b8e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 15:26:54 +0200 Subject: [PATCH 093/115] Better wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index c4ec3497e4..ebff886bb3 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -144,7 +144,7 @@ export default class ImageView extends React.Component { // image by default const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; - // If zoom is smaller than minZoom don't go beneath that value + // If zoom is smaller than minZoom don't go below that value const zoom = (this.state.zoom <= this.state.minZoom) ? minZoom : this.state.zoom; this.setState({ From 0918ebad0adf697a52354e8fc837a5b35ac56f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 15:28:37 +0200 Subject: [PATCH 094/115] calculateZoomAndRotate -> setZoomAndRotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index ebff886bb3..b5162c6084 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -104,18 +104,18 @@ export default class ImageView extends React.Component { // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); // We want to recalculate zoom whenever the window's size changes - window.addEventListener("resize", this.calculateZoomAndRotate); + window.addEventListener("resize", this.setZoomAndRotation); // After the image loads for the first time we want to calculate the zoom - this.image.current.addEventListener("load", this.calculateZoomAndRotate); + this.image.current.addEventListener("load", this.setZoomAndRotation); } componentWillUnmount() { this.focusLock.current.removeEventListener('wheel', this.onWheel); - window.removeEventListener("resize", this.calculateZoomAndRotate); - this.image.current.removeEventListener("load", this.calculateZoomAndRotate); + window.removeEventListener("resize", this.setZoomAndRotation); + this.image.current.removeEventListener("load", this.setZoomAndRotation); } - private calculateZoomAndRotate = () => { + private setZoomAndRotation = () => { const image = this.image.current; const imageWrapper = this.imageWrapper.current; @@ -203,13 +203,13 @@ export default class ImageView extends React.Component { private onRotateCounterClockwiseClick = () => { const cur = this.state.rotation; this.rotation = cur - 90; - this.calculateZoomAndRotate(); + this.setZoomAndRotation(); }; private onRotateClockwiseClick = () => { const cur = this.state.rotation; this.rotation = cur + 90; - this.calculateZoomAndRotate(); + this.setZoomAndRotation(); }; private onDownloadClick = () => { From 2e47a1176f6dd883d2b68b6d35cc13d590c52fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 15:39:11 +0200 Subject: [PATCH 095/115] Get rid of weird rotation prop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 29 ++++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index b5162c6084..e0991a1023 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -97,29 +97,34 @@ export default class ImageView extends React.Component { private initY = 0; private previousX = 0; private previousY = 0; - private rotation = 0; componentDidMount() { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); // We want to recalculate zoom whenever the window's size changes - window.addEventListener("resize", this.setZoomAndRotation); + window.addEventListener("resize", this.recalculateZoom); // After the image loads for the first time we want to calculate the zoom - this.image.current.addEventListener("load", this.setZoomAndRotation); + this.image.current.addEventListener("load", this.recalculateZoom); } componentWillUnmount() { this.focusLock.current.removeEventListener('wheel', this.onWheel); - window.removeEventListener("resize", this.setZoomAndRotation); - this.image.current.removeEventListener("load", this.setZoomAndRotation); + window.removeEventListener("resize", this.recalculateZoom); + this.image.current.removeEventListener("load", this.recalculateZoom); } - private setZoomAndRotation = () => { + private recalculateZoom = () => { + this.setZoomAndRotation(); + } + + private setZoomAndRotation = (inputRotation?: number) => { const image = this.image.current; const imageWrapper = this.imageWrapper.current; - const landscape = this.rotation % 180 === 0; + const rotation = inputRotation || this.state.rotation; + + const landscape = rotation % 180 === 0; // If the image is rotated take it into account const width = landscape ? image.naturalWidth : image.naturalHeight; @@ -135,7 +140,7 @@ export default class ImageView extends React.Component { zoom: 1, minZoom: 1, maxZoom: 1, - rotation: this.rotation, + rotation: rotation, }); return; } @@ -150,7 +155,7 @@ export default class ImageView extends React.Component { this.setState({ minZoom: minZoom, maxZoom: 1, - rotation: this.rotation, + rotation: rotation, zoom: zoom, }); } @@ -202,14 +207,12 @@ export default class ImageView extends React.Component { private onRotateCounterClockwiseClick = () => { const cur = this.state.rotation; - this.rotation = cur - 90; - this.setZoomAndRotation(); + this.setZoomAndRotation(cur - 90); }; private onRotateClockwiseClick = () => { const cur = this.state.rotation; - this.rotation = cur + 90; - this.setZoomAndRotation(); + this.setZoomAndRotation(cur + 90); }; private onDownloadClick = () => { From b5d271759f227a03f1905759dcab4c92149cc15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 15:43:43 +0200 Subject: [PATCH 096/115] Why would I format this myself if eslint can do it for me? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index e0991a1023..d0dcb1fa00 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -19,20 +19,20 @@ limitations under the License. import React, { createRef } from 'react'; import { _t } from '../../../languageHandler'; import AccessibleTooltipButton from "./AccessibleTooltipButton"; -import {Key} from "../../../Keyboard"; +import { Key } from "../../../Keyboard"; import FocusLock from "react-focus-lock"; import MemberAvatar from "../avatars/MemberAvatar"; -import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; +import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import MessageContextMenu from "../context_menus/MessageContextMenu"; -import {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu'; +import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu'; import MessageTimestamp from "../messages/MessageTimestamp"; import SettingsStore from "../../../settings/SettingsStore"; -import {formatFullDate} from "../../../DateUtils"; +import { formatFullDate } from "../../../DateUtils"; import dis from '../../../dispatcher/dispatcher'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks" -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; -import {normalizeWheelEvent} from "../../../utils/Mouse"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks" +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { normalizeWheelEvent } from "../../../utils/Mouse"; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; @@ -172,7 +172,7 @@ export default class ImageView extends React.Component { return; } if (newZoom >= this.state.maxZoom) { - this.setState({zoom: this.state.maxZoom}); + this.setState({ zoom: this.state.maxZoom }); return; } @@ -185,7 +185,7 @@ export default class ImageView extends React.Component { ev.stopPropagation(); ev.preventDefault(); - const {deltaY} = normalizeWheelEvent(ev); + const { deltaY } = normalizeWheelEvent(ev); this.zoom(-(deltaY * ZOOM_COEFFICIENT)); }; @@ -259,11 +259,11 @@ export default class ImageView extends React.Component { // Zoom in if we are completely zoomed out if (this.state.zoom === this.state.minZoom) { - this.setState({zoom: this.state.maxZoom}); + this.setState({ zoom: this.state.maxZoom }); return; } - this.setState({moving: true}); + this.setState({ moving: true }); this.previousX = this.state.translationX; this.previousY = this.state.translationY; this.initX = ev.pageX - this.state.translationX; @@ -297,7 +297,7 @@ export default class ImageView extends React.Component { this.initX = 0; this.initY = 0; } - this.setState({moving: false}); + this.setState({ moving: false }); }; private renderContextMenu() { From 255f9967109ae6024dbfbab0dd5581968691dbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 15:46:03 +0200 Subject: [PATCH 097/115] landscape -> imageIsNotFlipped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index d0dcb1fa00..298951428f 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -124,11 +124,11 @@ export default class ImageView extends React.Component { const rotation = inputRotation || this.state.rotation; - const landscape = rotation % 180 === 0; + const imageIsNotFlipped = rotation % 180 === 0; // If the image is rotated take it into account - const width = landscape ? image.naturalWidth : image.naturalHeight; - const height = landscape ? image.naturalHeight : image.naturalWidth; + const width = imageIsNotFlipped ? image.naturalWidth : image.naturalHeight; + const height = imageIsNotFlipped ? image.naturalHeight : image.naturalWidth; const zoomX = imageWrapper.clientWidth / width; const zoomY = imageWrapper.clientHeight / height; From 20a3dd67d6b3029c75adac9da58f162aed5aa1bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 16:12:06 +0200 Subject: [PATCH 098/115] Some more styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 298951428f..630280120c 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -368,7 +368,7 @@ export default class ImageView extends React.Component { const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); const sender = (
    - {senderName} + { senderName }
    ); const messageTimestamp = ( @@ -395,10 +395,10 @@ export default class ImageView extends React.Component { info = (
    - {avatar} + { avatar }
    - {sender} - {messageTimestamp} + { sender } + { messageTimestamp }
    ); @@ -438,7 +438,7 @@ export default class ImageView extends React.Component { + onClick={this.onZoomInClick}> ); } @@ -454,7 +454,7 @@ export default class ImageView extends React.Component { ref={this.focusLock} >
    - {info} + { info }
    { title={_t("Rotate Right")} onClick={this.onRotateClockwiseClick}> - {zoomOutButton} - {zoomInButton} + { zoomOutButton } + { zoomInButton } - {contextMenuButton} + { contextMenuButton } - {this.renderContextMenu()} + { this.renderContextMenu() }
    Date: Mon, 7 Jun 2021 16:21:53 +0100 Subject: [PATCH 099/115] Fix notif panel timestamp padding This restores some space around the timestamp in the notif panel. We were previously relying somewhat randomly on the presence on empty flair elements to create space. Fixes https://github.com/vector-im/element-web/issues/17580 --- res/css/structures/_NotificationPanel.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss index 1258ace069..e54feca175 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -82,7 +82,6 @@ limitations under the License. color: $primary-fg-color; font-size: $font-12px; display: inline; - padding-left: 0px; } .mx_NotificationPanel .mx_EventTile_senderDetails { @@ -103,6 +102,7 @@ limitations under the License. visibility: visible; position: initial; display: inline; + padding-left: 5px; } .mx_NotificationPanel .mx_EventTile_line { From 296186b844d286062e0fe2e401320d267bb2717b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 7 Jun 2021 16:21:53 +0100 Subject: [PATCH 100/115] Fix notif panel timestamp padding This restores some space around the timestamp in the notif panel. We were previously relying somewhat randomly on the presence on empty flair elements to create space. Fixes https://github.com/vector-im/element-web/issues/17580 --- res/css/structures/_NotificationPanel.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss index 1258ace069..e54feca175 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -82,7 +82,6 @@ limitations under the License. color: $primary-fg-color; font-size: $font-12px; display: inline; - padding-left: 0px; } .mx_NotificationPanel .mx_EventTile_senderDetails { @@ -103,6 +102,7 @@ limitations under the License. visibility: visible; position: initial; display: inline; + padding-left: 5px; } .mx_NotificationPanel .mx_EventTile_line { From dd089bf9696a7c5ad0b765bc41e5bf1d9a351466 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 7 Jun 2021 17:03:05 +0100 Subject: [PATCH 101/115] Upgrade matrix-js-sdk to 11.2.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4f6a023383..5ee56ec8cf 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "11.2.0-rc.1", + "matrix-js-sdk": "11.2.0", "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 1b07d4502b..5e324b7ad2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5673,10 +5673,10 @@ 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@11.2.0-rc.1: - version "11.2.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.2.0-rc.1.tgz#339259d892ce08195864c1130780dd0b3d996af3" - integrity sha512-ACFONqdRqiPIj7aWf7G4f/2d0S/hfouIH6tdkFGDbizbWJCmqXRJ8EN2gmu0F3A6GGDo0+k9cqaeMf3Y1KIM8w== +matrix-js-sdk@11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.2.0.tgz#b49f1ebbb671f5a8ec76eec3b21eeda97f10fb31" + integrity sha512-YsdAQNPsvoWL3fKI2fb6evO81UKfSt+hYDkBOIylzb3AA/RtUsjtSNXcT4f6BsQIVTh6xAhEjPVoMfKORchf1Q== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 187a3671110f181c8129b1f1dd47e110621cd8a0 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 7 Jun 2021 17:11:14 +0100 Subject: [PATCH 102/115] Add prop to alwaysShowTimestamps on TimelinePanel --- src/components/structures/NotificationPanel.tsx | 1 + src/components/structures/TimelinePanel.js | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index b4f13f6b37..6c22835447 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -50,6 +50,7 @@ export default class NotificationPanel extends React.PureComponent { showUrlPreview={false} tileShape="notif" empty={emptyState} + alwaysShowTimestamps={true} /> ); } else { diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 6300c7532e..a8b0dc88dd 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -120,6 +120,9 @@ class TimelinePanel extends React.Component { // which layout to use layout: LayoutPropType, + + // whether to always show timestamps for an event + alwaysShowTimestamps: PropTypes.bool, } // a map from room id to read marker event timestamp @@ -1440,7 +1443,7 @@ class TimelinePanel extends React.Component { onFillRequest={this.onMessageListFillRequest} onUnfillRequest={this.onMessageListUnfillRequest} isTwelveHour={this.state.isTwelveHour} - alwaysShowTimestamps={this.state.alwaysShowTimestamps} + alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps} className={this.props.className} tileShape={this.props.tileShape} resizeNotifier={this.props.resizeNotifier} From 2983b3ace21f55cf36efddecea9851918d336fd9 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 7 Jun 2021 17:41:18 +0100 Subject: [PATCH 103/115] Prepare changelog for v3.23.0 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3abd1f32b..94c9530941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0) + + * Upgrade to JS SDK 11.2.0 + * [Release] Fix notif panel timestamp padding + [\#6158](https://github.com/matrix-org/matrix-react-sdk/pull/6158) + Changes in [3.23.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0-rc.1) (2021-06-01) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0...v3.23.0-rc.1) From a3dac02e4c1b4a5d632c34ad35e634cd23e59813 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 7 Jun 2021 17:41:18 +0100 Subject: [PATCH 104/115] v3.23.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ee56ec8cf..17067df933 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.23.0-rc.1", + "version": "3.23.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From bcd0ba8d456f93c45ab57f8b769ece0815227571 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 7 Jun 2021 17:42:46 +0100 Subject: [PATCH 105/115] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 17067df933..dee4013006 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "bin": { "reskindex": "scripts/reskindex.js" }, - "main": "./lib/index.js", + "main": "./src/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -200,6 +200,5 @@ "coverageReporters": [ "text" ] - }, - "typings": "./lib/index.d.ts" + } } From 8ccdf9a3e8a038def7c6be8e920839f26dae2f83 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 7 Jun 2021 17:43:27 +0100 Subject: [PATCH 106/115] 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 dee4013006..2bff2a7b13 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "11.2.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 5e324b7ad2..97179550c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5673,10 +5673,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@11.2.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "11.2.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.2.0.tgz#b49f1ebbb671f5a8ec76eec3b21eeda97f10fb31" - integrity sha512-YsdAQNPsvoWL3fKI2fb6evO81UKfSt+hYDkBOIylzb3AA/RtUsjtSNXcT4f6BsQIVTh6xAhEjPVoMfKORchf1Q== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/35ecbed29d16982deff27a8c37b05167738225a2" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From a5bf17ff659eeb8f3d915163217d9c8574d53f54 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Mon, 7 Jun 2021 16:54:36 -0500 Subject: [PATCH 107/115] Revert incorrect change to forShareableRoom Signed-off-by: Aaron Raimist --- src/utils/permalinks/Permalinks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index 2f268aaa0c..c2f95defd6 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -293,12 +293,12 @@ export function makeRoomPermalink(roomId: string): string { // If the roomId isn't actually a room ID, don't try to list the servers. // Aliases are already routable, and don't need extra information. - if (roomId[0] !== '!') return getPermalinkConstructor().forShareableRoom(roomId, []); + if (roomId[0] !== '!') return getPermalinkConstructor().forRoom(roomId, []); const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); if (!room) { - return getPermalinkConstructor().forShareableRoom(roomId, []); + return getPermalinkConstructor().forRoom(roomId, []); } const permalinkCreator = new RoomPermalinkCreator(room); permalinkCreator.load(); From 773af6c7be6244775b3c2da79e38921381b401a9 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Mon, 7 Jun 2021 16:54:50 -0500 Subject: [PATCH 108/115] Fix test Signed-off-by: Aaron Raimist --- test/utils/permalinks/Permalinks-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/permalinks/Permalinks-test.js b/test/utils/permalinks/Permalinks-test.js index 0bd4466d97..3c4982b465 100644 --- a/test/utils/permalinks/Permalinks-test.js +++ b/test/utils/permalinks/Permalinks-test.js @@ -34,7 +34,7 @@ function mockRoom(roomId, members, serverACL) { return { roomId, - getCanonicalAlias: () => roomId, + getCanonicalAlias: () => null, getJoinedMembers: () => members, getMember: (userId) => members.find(m => m.userId === userId), currentState: { From 946416cf8756ac94d0229f038e561f9215ca695f Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Mon, 7 Jun 2021 17:09:43 -0500 Subject: [PATCH 109/115] Make serverCandidates optional Signed-off-by: Aaron Raimist --- src/utils/permalinks/PermalinkConstructor.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/permalinks/PermalinkConstructor.ts b/src/utils/permalinks/PermalinkConstructor.ts index 25855eb024..986d20d4d1 100644 --- a/src/utils/permalinks/PermalinkConstructor.ts +++ b/src/utils/permalinks/PermalinkConstructor.ts @@ -19,11 +19,11 @@ limitations under the License. * TODO: Convert this to a real TypeScript interface */ export default class PermalinkConstructor { - forEvent(roomId: string, eventId: string, serverCandidates: string[]): string { + forEvent(roomId: string, eventId: string, serverCandidates: string[] = []): string { throw new Error("Not implemented"); } - forRoom(roomIdOrAlias: string, serverCandidates: string[]): string { + forRoom(roomIdOrAlias: string, serverCandidates: string[] = []): string { throw new Error("Not implemented"); } @@ -73,12 +73,12 @@ export class PermalinkParts { return new PermalinkParts(null, null, null, groupId, null); } - static forRoom(roomIdOrAlias: string, viaServers: string[]): PermalinkParts { - return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers || []); + static forRoom(roomIdOrAlias: string, viaServers: string[] = []): PermalinkParts { + return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers); } - static forEvent(roomId: string, eventId: string, viaServers: string[]): PermalinkParts { - return new PermalinkParts(roomId, eventId, null, null, viaServers || []); + static forEvent(roomId: string, eventId: string, viaServers: string[] = []): PermalinkParts { + return new PermalinkParts(roomId, eventId, null, null, viaServers); } get primaryEntityId(): string { From a8dab04d5eed882f90244f0a5b1ec54cbfaaea58 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 7 Jun 2021 19:10:45 -0400 Subject: [PATCH 110/115] Remove mystery dot from forward dialog preview It was a bullet point, since EventTiles now get created as li by default :P Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index e97958973b..b89485943a 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -211,6 +211,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr layout={previewLayout} enableFlair={flairEnabled} permalinkCreator={permalinkCreator} + as="div" />

    From b00ad63a35fa2aa18c0ca04b803b34e252fa270c Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 7 Jun 2021 19:16:00 -0400 Subject: [PATCH 111/115] Fix whitespace nitpick Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index b89485943a..1c90dca432 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -201,7 +201,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr onFinished={onFinished} fixedWidth={false} > -

    {_t("Message preview")}

    +

    { _t("Message preview") }

    Date: Mon, 7 Jun 2021 19:19:14 -0400 Subject: [PATCH 112/115] Hide download links from forward dialog preview since trying to interact with them is pointless. Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 7422d39626..1976a43ab9 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -49,6 +49,11 @@ limitations under the License. .mx_EventTile_e2eIcon_unencrypted { display: none; } + + // We also hide download links to not encourage users to try interacting + .mx_MFileBody_download { + display: none; + } } > hr { From 26a030abcd76daf27c9f360000e20d54fc65cb19 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 8 Jun 2021 12:40:38 +0100 Subject: [PATCH 113/115] Better handling for widgets that fail to load --- src/components/views/elements/AppTile.js | 40 +++++++++++++++++------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index b898ad2ebc..91ffbf2c27 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -47,9 +47,14 @@ export default class AppTile extends React.Component { // The key used for PersistedElement this._persistKey = getPersistKey(this.props.app.id); - this._sgWidget = new StopGapWidget(this.props); - this._sgWidget.on("preparing", this._onWidgetPrepared); - this._sgWidget.on("ready", this._onWidgetReady); + try { + this._sgWidget = new StopGapWidget(this.props); + this._sgWidget.on("preparing", this._onWidgetPrepared); + this._sgWidget.on("ready", this._onWidgetReady); + } catch (e) { + console.log("Failed to construct widget", e); + this._sgWidget = null; + } this.iframe = null; // ref to the iframe (callback style) this.state = this._getNewState(props); @@ -97,7 +102,7 @@ export default class AppTile extends React.Component { // Force the widget to be non-persistent (able to be deleted/forgotten) ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); PersistedElement.destroyElement(this._persistKey); - this._sgWidget.stop(); + if (this._sgWidget) this._sgWidget.stop(); } this.setState({ hasPermissionToLoad }); @@ -117,7 +122,7 @@ export default class AppTile extends React.Component { componentDidMount() { // Only fetch IM token on mount if we're showing and have permission to load - if (this.state.hasPermissionToLoad) { + if (this._sgWidget && this.state.hasPermissionToLoad) { this._startWidget(); } @@ -146,10 +151,15 @@ export default class AppTile extends React.Component { if (this._sgWidget) { this._sgWidget.stop(); } - this._sgWidget = new StopGapWidget(newProps); - this._sgWidget.on("preparing", this._onWidgetPrepared); - this._sgWidget.on("ready", this._onWidgetReady); - this._startWidget(); + try { + this._sgWidget = new StopGapWidget(newProps); + this._sgWidget.on("preparing", this._onWidgetPrepared); + this._sgWidget.on("ready", this._onWidgetReady); + this._startWidget(); + } catch (e) { + console.log("Failed to construct widget", e); + this._sgWidget = null; + } } _startWidget() { @@ -161,7 +171,7 @@ export default class AppTile extends React.Component { _iframeRefChange = (ref) => { this.iframe = ref; if (ref) { - this._sgWidget.start(ref); + if (this._sgWidget) this._sgWidget.start(ref); } else { this._resetWidget(this.props); } @@ -209,7 +219,7 @@ export default class AppTile extends React.Component { // Delete the widget from the persisted store for good measure. PersistedElement.destroyElement(this._persistKey); - this._sgWidget.stop({forceDestroy: true}); + if (this._sgWidget) this._sgWidget.stop({forceDestroy: true}); } _onWidgetPrepared = () => { @@ -340,7 +350,13 @@ export default class AppTile extends React.Component {
    ); - if (!this.state.hasPermissionToLoad) { + if (this._sgWidget === null) { + appTileBody = ( +
    + +
    + ); + } else if (!this.state.hasPermissionToLoad) { // only possible for room widgets, can assert this.props.room here const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); appTileBody = ( From 90a58380fd3732f250f63de65a4d542b0a0a6837 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 8 Jun 2021 16:15:20 +0100 Subject: [PATCH 114/115] Make it translateable And also the other one that I copied from --- src/components/views/elements/AppTile.js | 4 ++-- src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 91ffbf2c27..4028909167 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -353,7 +353,7 @@ export default class AppTile extends React.Component { if (this._sgWidget === null) { appTileBody = (
    - +
    ); } else if (!this.state.hasPermissionToLoad) { @@ -380,7 +380,7 @@ export default class AppTile extends React.Component { if (this.isMixedContent()) { appTileBody = (
    - +
    ); } else { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9e85ea28c8..64613d0d88 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1926,6 +1926,8 @@ "Widgets do not use message encryption.": "Widgets do not use message encryption.", "Widget added by": "Widget added by", "This widget may use cookies.": "This widget may use cookies.", + "Error loading Widget": "Error loading Widget", + "Error - Mixed content": "Error - Mixed content", "Popout widget": "Popout widget", "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", From f84ee2276995a5abb42225fa443e8d777dddee65 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:36:53 +0100 Subject: [PATCH 115/115] Add comment --- src/components/views/right_panel/PinnedMessagesCard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index 3a4dc9cc92..a72731522f 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -160,6 +160,7 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => { } else { content =
    + { /* XXX: We reuse the classes for simplicity, but deliberately not the components for non-interactivity. */ }