Prepare room components for local rooms (#9082)

pull/28788/head^2
Michael Weimann 2022-07-20 15:07:06 +02:00 committed by GitHub
parent c980885d6b
commit 9edd49818c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 252 additions and 109 deletions

View File

@ -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;
}

View File

@ -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<IProps, IState> {
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 <E2ePadlockUndecryptable />;

View File

@ -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 <displayName/>.");
let caption;
if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) {
if (isLocalRoom) {
introMessage = _t("Send your first message to invite <displayName/> 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 = () => {
<h2>{ room.name }</h2>
<p>{ _t("This is the beginning of your direct message history with <displayName/>.", {}, {
<p>{ _t(introMessage, {}, {
displayName: () => <b>{ displayName }</b>,
}) }</p>
{ caption && <p>{ caption }</p> }
@ -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 = (
<AccessibleButton kind='link_inline' onClick={openRoomSettings}>{ _t("Enable encryption in settings.") }</AccessibleButton>
);

View File

@ -65,6 +65,8 @@ interface IProps {
appsShown: boolean;
searchInfo: ISearchInfo;
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
showButtons?: boolean;
enableRoomOptionsMenu?: boolean;
}
interface IState {
@ -76,6 +78,8 @@ export default class RoomHeader extends React.Component<IProps, IState> {
editing: false,
inRoom: false,
excludedRightPanelPhaseButtons: [],
showButtons: true,
enableRoomOptionsMenu: true,
};
static contextType = RoomContext;
@ -130,81 +134,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
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 = <div className="mx_RoomHeader_searchStatus">&nbsp;
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
</div>;
}
// 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 = (
<RoomContextMenu
{...contextMenuBelow(this.state.contextMenuPosition)}
room={this.props.room}
onFinished={this.onContextMenuCloseClick}
/>
);
}
const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
const name = (
<ContextMenuTooltipButton
className="mx_RoomHeader_name"
onClick={this.onContextMenuOpenClick}
isExpanded={!!this.state.contextMenuPosition}
title={_t("Room options")}
>
<RoomName room={this.props.room}>
{ (name) => {
const roomName = name || oobName;
return <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>;
} }
</RoomName>
{ this.props.room && <div className="mx_RoomHeader_chevron" /> }
{ contextMenu }
</ContextMenuTooltipButton>
);
const topicElement = <RoomTopic
room={this.props.room}
className="mx_RoomHeader_topic"
/>;
let roomAvatar;
if (this.props.room) {
roomAvatar = <DecoratedRoomAvatar
room={this.props.room}
avatarSize={24}
oobData={this.props.oobData}
viewAvatarOnClick={true}
/>;
}
private renderButtons(): JSX.Element[] {
const buttons: JSX.Element[] = [];
if (this.props.inRoom &&
@ -269,10 +199,105 @@ export default class RoomHeader extends React.Component<IProps, IState> {
buttons.push(inviteButton);
}
const rightRow =
<div className="mx_RoomHeader_buttons">
{ buttons }
return buttons;
}
private renderName(oobName) {
let contextMenu: JSX.Element;
if (this.state.contextMenuPosition && this.props.room) {
contextMenu = (
<RoomContextMenu
{...contextMenuBelow(this.state.contextMenuPosition)}
room={this.props.room}
onFinished={this.onContextMenuCloseClick}
/>
);
}
// 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 = <RoomName room={this.props.room}>
{ (name) => {
const roomName = name || oobName;
return <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>;
} }
</RoomName>;
if (this.props.enableRoomOptionsMenu) {
return (
<ContextMenuTooltipButton
className="mx_RoomHeader_name"
onClick={this.onContextMenuOpenClick}
isExpanded={!!this.state.contextMenuPosition}
title={_t("Room options")}
>
{ roomName }
{ this.props.room && <div className="mx_RoomHeader_chevron" /> }
{ contextMenu }
</ContextMenuTooltipButton>
);
}
return <div className="mx_RoomHeader_name mx_RoomHeader_name--textonly">
{ roomName }
</div>;
}
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 = <div className="mx_RoomHeader_searchStatus">&nbsp;
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
</div>;
}
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 = <RoomTopic
room={this.props.room}
className="mx_RoomHeader_topic"
/>;
let roomAvatar;
if (this.props.room) {
roomAvatar = <DecoratedRoomAvatar
room={this.props.room}
avatarSize={24}
oobData={this.props.oobData}
viewAvatarOnClick={true}
/>;
}
let buttons;
if (this.props.showButtons) {
buttons = <React.Fragment>
<div className="mx_RoomHeader_buttons">
{ this.renderButtons() }
</div>
<RoomHeaderButtons room={this.props.room} excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons} />
</React.Fragment>;
}
const e2eIcon = this.props.e2eStatus ? <E2EIcon status={this.props.e2eStatus} /> : undefined;
@ -294,8 +319,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
{ searchStatus }
{ topicElement }
{ betaPill }
{ rightRow }
<RoomHeaderButtons room={this.props.room} excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons} />
{ buttons }
</div>
<RoomLiveShareWarning roomId={this.props.room.roomId} />
</div>

View File

@ -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 <displayName/>.": "This is the beginning of your direct message history with <displayName/>.",
"Send your first message to invite <displayName/> to chat": "Send your first message to invite <displayName/> 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 (<a>edit</a>)": "Topic: %(topic)s (<a>edit</a>)",
"Topic: %(topic)s ": "Topic: %(topic)s ",
"<a>Add a topic</a> to help people know what it is about.": "<a>Add a topic</a> 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",

View File

@ -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(
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={{ room, roomId: room.roomId } as unknown as IRoomState}>
<NewRoomIntro />
</RoomContext.Provider>
</MatrixClientContext.Provider>,
);
};
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);
});
});
});

View File

@ -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<IRoomState>): ReactWrapper {
function render(room: Room, propsOverride = {}, roomContext?: Partial<IRoomState>): 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((
<RoomContext.Provider value={{ ...roomContext, room } as IRoomState}>
<RoomHeader
room={room}
inRoom={true}
onSearchClick={() => {}}
onInviteClick={null}
onForgetClick={() => {}}
onCallPlaced={(_type) => { }}
onAppsClick={() => {}}
e2eStatus={E2EStatus.Normal}
appsShown={true}
searchInfo={{
searchTerm: "",
searchScope: SearchScope.Room,
searchCount: 0,
}}
/>
<RoomHeader {...props} />
</RoomContext.Provider>
));
}