From 9edd49818c4a8e23656b800e5daea7b361f28f4b Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 20 Jul 2022 15:07:06 +0200 Subject: [PATCH] Prepare room components for local rooms (#9082) --- res/css/views/rooms/_RoomHeader.pcss | 4 +- src/components/views/rooms/EventTile.tsx | 4 + src/components/views/rooms/NewRoomIntro.tsx | 17 +- src/components/views/rooms/RoomHeader.tsx | 184 ++++++++++-------- src/i18n/strings/en_EN.json | 11 +- .../views/rooms/NewRoomIntro-test.tsx | 78 ++++++++ .../views/rooms/RoomHeader-test.tsx | 63 ++++-- 7 files changed, 252 insertions(+), 109 deletions(-) create mode 100644 test/components/views/rooms/NewRoomIntro-test.tsx diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss index 1ae9aae8b8..dc2817ee68 100644 --- a/res/css/views/rooms/_RoomHeader.pcss +++ b/res/css/views/rooms/_RoomHeader.pcss @@ -100,7 +100,7 @@ limitations under the License. display: flex; user-select: none; - &:hover { + &:not(.mx_RoomHeader_name--textonly):hover { background-color: $quinary-content; } @@ -139,7 +139,7 @@ limitations under the License. opacity: 0.6; } -.mx_RoomHeader_name, +.mx_RoomHeader_name:not(.mx_RoomHeader_name--textonly), .mx_RoomHeader_avatar { cursor: pointer; } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 3c5e419d94..8e7038a255 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -80,6 +80,7 @@ import { haveRendererForEvent, isMessageEvent, renderTile } from "../../../event import ThreadSummary, { ThreadMessagePreview } from "./ThreadSummary"; import { ReadReceiptGroup } from './ReadReceiptGroup'; import { useTooltip } from "../../../utils/useTooltip"; +import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom'; export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations; @@ -766,6 +767,9 @@ export class UnwrappedEventTile extends React.Component { private renderE2EPadlock() { const ev = this.props.mxEvent; + // no icon for local rooms + if (isLocalRoom(ev.getRoomId())) return; + // event could not be decrypted if (ev.getContent().msgtype === 'm.bad.encrypted') { return ; diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 40d3d4ce0c..7cd43aded0 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -38,6 +38,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; import { privateShouldBeEncrypted } from "../../../utils/rooms"; +import { LocalRoom } from "../../../models/LocalRoom"; function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean { const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId); @@ -49,11 +50,19 @@ const NewRoomIntro = () => { const cli = useContext(MatrixClientContext); const { room, roomId } = useContext(RoomContext); - const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId); + const isLocalRoom = room instanceof LocalRoom; + const dmPartner = isLocalRoom + ? room.targets[0]?.userId + : DMRoomMap.shared().getUserIdForRoomId(roomId); + let body; if (dmPartner) { + let introMessage = _t("This is the beginning of your direct message history with ."); let caption; - if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) { + + if (isLocalRoom) { + introMessage = _t("Send your first message to invite to chat"); + } else if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) { caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join."); } @@ -75,7 +84,7 @@ const NewRoomIntro = () => {

{ room.name }

-

{ _t("This is the beginning of your direct message history with .", {}, { +

{ _t(introMessage, {}, { displayName: () => { displayName }, }) }

{ caption &&

{ caption }

} @@ -200,7 +209,7 @@ const NewRoomIntro = () => { ); let subButton; - if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.get())) { + if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.get()) && !isLocalRoom) { subButton = ( { _t("Enable encryption in settings.") } ); diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 5e7962b48f..81ed22e35b 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -65,6 +65,8 @@ interface IProps { appsShown: boolean; searchInfo: ISearchInfo; excludedRightPanelPhaseButtons?: Array; + showButtons?: boolean; + enableRoomOptionsMenu?: boolean; } interface IState { @@ -76,6 +78,8 @@ export default class RoomHeader extends React.Component { editing: false, inRoom: false, excludedRightPanelPhaseButtons: [], + showButtons: true, + enableRoomOptionsMenu: true, }; static contextType = RoomContext; @@ -130,81 +134,7 @@ export default class RoomHeader extends React.Component { this.setState({ contextMenuPosition: null }); }; - public render() { - let searchStatus = null; - - // don't display the search count until the search completes and - // gives us a valid (possibly zero) searchCount. - if (this.props.searchInfo && - this.props.searchInfo.searchCount !== undefined && - this.props.searchInfo.searchCount !== null) { - searchStatus =
  - { _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) } -
; - } - - // XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'... - let settingsHint = false; - const members = this.props.room ? this.props.room.getJoinedMembers() : undefined; - if (members) { - if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) { - const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', ''); - if (!nameEvent || !nameEvent.getContent().name) { - settingsHint = true; - } - } - } - - let oobName = _t("Join Room"); - if (this.props.oobData && this.props.oobData.name) { - oobName = this.props.oobData.name; - } - - let contextMenu: JSX.Element; - if (this.state.contextMenuPosition && this.props.room) { - contextMenu = ( - - ); - } - - const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint }); - const name = ( - - - { (name) => { - const roomName = name || oobName; - return
{ roomName }
; - } } -
- { this.props.room &&
} - { contextMenu } - - ); - - const topicElement = ; - - let roomAvatar; - if (this.props.room) { - roomAvatar = ; - } - + private renderButtons(): JSX.Element[] { const buttons: JSX.Element[] = []; if (this.props.inRoom && @@ -269,10 +199,105 @@ export default class RoomHeader extends React.Component { buttons.push(inviteButton); } - const rightRow = -
- { buttons } + return buttons; + } + + private renderName(oobName) { + let contextMenu: JSX.Element; + if (this.state.contextMenuPosition && this.props.room) { + contextMenu = ( + + ); + } + + // XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'... + let settingsHint = false; + const members = this.props.room ? this.props.room.getJoinedMembers() : undefined; + if (members) { + if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) { + const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', ''); + if (!nameEvent || !nameEvent.getContent().name) { + settingsHint = true; + } + } + } + + const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint }); + const roomName = + { (name) => { + const roomName = name || oobName; + return
{ roomName }
; + } } +
; + + if (this.props.enableRoomOptionsMenu) { + return ( + + { roomName } + { this.props.room &&
} + { contextMenu } + + ); + } + + return
+ { roomName } +
; + } + + public render() { + let searchStatus = null; + + // don't display the search count until the search completes and + // gives us a valid (possibly zero) searchCount. + if (this.props.searchInfo && + this.props.searchInfo.searchCount !== undefined && + this.props.searchInfo.searchCount !== null) { + searchStatus =
  + { _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
; + } + + let oobName = _t("Join Room"); + if (this.props.oobData && this.props.oobData.name) { + oobName = this.props.oobData.name; + } + + const name = this.renderName(oobName); + + const topicElement = ; + + let roomAvatar; + if (this.props.room) { + roomAvatar = ; + } + + let buttons; + if (this.props.showButtons) { + buttons = +
+ { this.renderButtons() } +
+ +
; + } const e2eIcon = this.props.e2eStatus ? : undefined; @@ -294,8 +319,7 @@ export default class RoomHeader extends React.Component { { searchStatus } { topicElement } { betaPill } - { rightRow } - + { buttons }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a2568949d9..597f4735eb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1730,8 +1730,9 @@ "Code block": "Code block", "Quote": "Quote", "Insert link": "Insert link", - "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Only the two of you are in this conversation, unless either of you invites anyone to join.", "This is the beginning of your direct message history with .": "This is the beginning of your direct message history with .", + "Send your first message to invite to chat": "Send your first message to invite to chat", + "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Only the two of you are in this conversation, unless either of you invites anyone to join.", "Topic: %(topic)s (edit)": "Topic: %(topic)s (edit)", "Topic: %(topic)s ": "Topic: %(topic)s ", "Add a topic to help people know what it is about.": "Add a topic to help people know what it is about.", @@ -1771,15 +1772,15 @@ "Room %(name)s": "Room %(name)s", "Recently visited rooms": "Recently visited rooms", "No recently visited rooms": "No recently visited rooms", - "(~%(count)s results)|other": "(~%(count)s results)", - "(~%(count)s results)|one": "(~%(count)s result)", - "Join Room": "Join Room", - "Room options": "Room options", "Forget room": "Forget room", "Hide Widgets": "Hide Widgets", "Show Widgets": "Show Widgets", "Search": "Search", "Invite": "Invite", + "Room options": "Room options", + "(~%(count)s results)|other": "(~%(count)s results)", + "(~%(count)s results)|one": "(~%(count)s result)", + "Join Room": "Join Room", "Video rooms are a beta feature": "Video rooms are a beta feature", "Video room": "Video room", "Public space": "Public space", diff --git a/test/components/views/rooms/NewRoomIntro-test.tsx b/test/components/views/rooms/NewRoomIntro-test.tsx new file mode 100644 index 0000000000..988a749f50 --- /dev/null +++ b/test/components/views/rooms/NewRoomIntro-test.tsx @@ -0,0 +1,78 @@ +/* +Copyright 2015 - 2022 The Matrix.org Foundation C.I.C. +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + +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 { render, screen } from "@testing-library/react"; +import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; + +import { LocalRoom } from "../../../../src/models/LocalRoom"; +import { createTestClient } from "../../../test-utils"; +import RoomContext from "../../../../src/contexts/RoomContext"; +import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; +import NewRoomIntro from "../../../../src/components/views/rooms/NewRoomIntro"; +import { IRoomState } from "../../../../src/components/structures/RoomView"; +import DMRoomMap from "../../../../src/utils/DMRoomMap"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { DirectoryMember } from "../../../../src/utils/direct-messages"; + +const renderNewRoomIntro = (client: MatrixClient, room: Room|LocalRoom) => { + render( + + + + + , + ); +}; + +describe("NewRoomIntro", () => { + let client: MatrixClient; + const roomId = "!room:example.com"; + const userId = "@user:example.com"; + + beforeEach(() => { + client = createTestClient(); + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client); + DMRoomMap.makeShared(); + }); + + describe("for a DM Room", () => { + beforeEach(() => { + jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(userId); + renderNewRoomIntro(client, new Room(roomId, client, client.getUserId())); + }); + + it("should render the expected intro", () => { + const expected = `This is the beginning of your direct message history with ${userId}.`; + screen.getByText((id, element) => element.tagName === "SPAN" && element.textContent === expected); + }); + }); + + describe("for a DM LocalRoom", () => { + beforeEach(() => { + jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(userId); + const localRoom = new LocalRoom(roomId, client, client.getUserId()); + localRoom.targets.push(new DirectoryMember({ user_id: userId })); + renderNewRoomIntro(client, localRoom); + }); + + it("should render the expected intro", () => { + const expected = `Send your first message to invite ${userId} to chat`; + screen.getByText((id, element) => element.tagName === "SPAN" && element.textContent === expected); + }); + }); +}); diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index b7c07f320d..31b6e818f3 100644 --- a/test/components/views/rooms/RoomHeader-test.tsx +++ b/test/components/views/rooms/RoomHeader-test.tsx @@ -127,7 +127,7 @@ describe('RoomHeader', () => { it("hides call buttons when the room is tombstoned", () => { const room = createRoom({ name: "Room", isDm: false, userIds: [] }); - const wrapper = render(room, { + const wrapper = render(room, {}, { tombstone: mkEvent({ event: true, type: "m.room.tombstone", @@ -142,6 +142,30 @@ describe('RoomHeader', () => { expect(wrapper.find('[aria-label="Voice call"]').hostNodes()).toHaveLength(0); expect(wrapper.find('[aria-label="Video call"]').hostNodes()).toHaveLength(0); }); + + it("should render buttons if not passing showButtons (default true)", () => { + const room = createRoom({ name: "Room", isDm: false, userIds: [] }); + const wrapper = render(room); + expect(wrapper.find(".mx_RoomHeader_buttons")).toHaveLength(1); + }); + + it("should not render buttons if passing showButtons = false", () => { + const room = createRoom({ name: "Room", isDm: false, userIds: [] }); + const wrapper = render(room, { showButtons: false }); + expect(wrapper.find(".mx_RoomHeader_buttons")).toHaveLength(0); + }); + + it("should render the room options context menu if not passing enableRoomOptionsMenu (default true)", () => { + const room = createRoom({ name: "Room", isDm: false, userIds: [] }); + const wrapper = render(room); + expect(wrapper.find(".mx_RoomHeader_name.mx_AccessibleButton")).toHaveLength(1); + }); + + it("should not render the room options context menu if passing enableRoomOptionsMenu = false", () => { + const room = createRoom({ name: "Room", isDm: false, userIds: [] }); + const wrapper = render(room, { enableRoomOptionsMenu: false }); + expect(wrapper.find(".mx_RoomHeader_name.mx_AccessibleButton")).toHaveLength(0); + }); }); interface IRoomCreationInfo { @@ -185,25 +209,28 @@ function createRoom(info: IRoomCreationInfo) { return room; } -function render(room: Room, roomContext?: Partial): ReactWrapper { +function render(room: Room, propsOverride = {}, roomContext?: Partial): ReactWrapper { + const props = { + room, + inRoom: true, + onSearchClick: () => {}, + onInviteClick: null, + onForgetClick: () => {}, + onCallPlaced: (_type) => { }, + onAppsClick: () => {}, + e2eStatus: E2EStatus.Normal, + appsShown: true, + searchInfo: { + searchTerm: "", + searchScope: SearchScope.Room, + searchCount: 0, + }, + ...propsOverride, + }; + return mount(( - {}} - onInviteClick={null} - onForgetClick={() => {}} - onCallPlaced={(_type) => { }} - onAppsClick={() => {}} - e2eStatus={E2EStatus.Normal} - appsShown={true} - searchInfo={{ - searchTerm: "", - searchScope: SearchScope.Room, - searchCount: 0, - }} - /> + )); }