Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit c6d264f

Browse files
author
Kerry
authored
Merge branch 'develop' into psf-910/timeline-beacon-happy-path
2 parents 055fda2 + ceae8bb commit c6d264f

File tree

5 files changed

+231
-68
lines changed

5 files changed

+231
-68
lines changed

src/components/views/messages/MessageActionBar.tsx

Lines changed: 82 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
1616
limitations under the License.
1717
*/
1818

19-
import React, { ReactElement, useEffect } from 'react';
19+
import React, { ReactElement, useContext, useEffect } from 'react';
2020
import { EventStatus, MatrixEvent, MatrixEventEvent } from 'matrix-js-sdk/src/models/event';
2121
import classNames from 'classnames';
2222
import { MsgType, RelationType } from 'matrix-js-sdk/src/@types/event';
@@ -45,6 +45,7 @@ import { Key } from "../../../Keyboard";
4545
import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts";
4646
import { UserTab } from '../dialogs/UserTab';
4747
import { Action } from '../../../dispatcher/actions';
48+
import SdkConfig from "../../../SdkConfig";
4849

4950
interface IOptionsButtonProps {
5051
mxEvent: MatrixEvent;
@@ -154,6 +155,76 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
154155
</React.Fragment>;
155156
};
156157

158+
interface IReplyInThreadButton {
159+
mxEvent: MatrixEvent;
160+
}
161+
162+
const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
163+
const context = useContext(CardContext);
164+
165+
const relationType = mxEvent?.getRelation()?.rel_type;
166+
const hasARelation = !!relationType && relationType !== RelationType.Thread;
167+
const firstTimeSeeingThreads = localStorage.getItem("mx_seen_feature_thread") === null &&
168+
!SettingsStore.getValue("feature_thread");
169+
170+
const onClick = (): void => {
171+
if (localStorage.getItem("mx_seen_feature_thread") === null) {
172+
localStorage.setItem("mx_seen_feature_thread", "true");
173+
}
174+
175+
if (!SettingsStore.getValue("feature_thread")) {
176+
dis.dispatch({
177+
action: Action.ViewUserSettings,
178+
initialTabId: UserTab.Labs,
179+
});
180+
} else if (mxEvent.isThreadRelation) {
181+
showThread({
182+
rootEvent: mxEvent.getThread().rootEvent,
183+
initialEvent: mxEvent,
184+
scroll_into_view: true,
185+
highlighted: true,
186+
push: context.isCard,
187+
});
188+
} else {
189+
showThread({
190+
rootEvent: mxEvent,
191+
push: context.isCard,
192+
});
193+
}
194+
};
195+
196+
return <RovingAccessibleTooltipButton
197+
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
198+
199+
disabled={hasARelation}
200+
tooltip={<>
201+
<div className="mx_Tooltip_title">
202+
{ !hasARelation
203+
? _t("Reply in thread")
204+
: _t("Can't create a thread from an event with an existing relation") }
205+
</div>
206+
{ !hasARelation && (
207+
<div className="mx_Tooltip_sub">
208+
{ SettingsStore.getValue("feature_thread")
209+
? _t("Beta feature")
210+
: _t("Beta feature. Click to learn more.")
211+
}
212+
</div>
213+
) }
214+
</>}
215+
216+
title={!hasARelation
217+
? _t("Reply in thread")
218+
: _t("Can't create a thread from an event with an existing relation")}
219+
220+
onClick={onClick}
221+
>
222+
{ firstTimeSeeingThreads && (
223+
<div className="mx_Indicator" />
224+
) }
225+
</RovingAccessibleTooltipButton>;
226+
};
227+
157228
interface IMessageActionBarProps {
158229
mxEvent: MatrixEvent;
159230
reactions?: Relations;
@@ -222,32 +293,6 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
222293
});
223294
};
224295

225-
private onThreadClick = (isCard: boolean): void => {
226-
if (localStorage.getItem("mx_seen_feature_thread") === null) {
227-
localStorage.setItem("mx_seen_feature_thread", "true");
228-
}
229-
230-
if (!SettingsStore.getValue("feature_thread")) {
231-
dis.dispatch({
232-
action: Action.ViewUserSettings,
233-
initialTabId: UserTab.Labs,
234-
});
235-
} else if (this.props.mxEvent.isThreadRelation) {
236-
showThread({
237-
rootEvent: this.props.mxEvent.getThread().rootEvent,
238-
initialEvent: this.props.mxEvent,
239-
scroll_into_view: true,
240-
highlighted: true,
241-
push: isCard,
242-
});
243-
} else {
244-
showThread({
245-
rootEvent: this.props.mxEvent,
246-
push: isCard,
247-
});
248-
}
249-
};
250-
251296
private onEditClick = (): void => {
252297
editEvent(this.props.mxEvent, this.context.timelineRenderingType, this.props.getRelationsForEvent);
253298
};
@@ -257,6 +302,15 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
257302
];
258303

259304
private get showReplyInThreadAction(): boolean {
305+
if (!SettingsStore.getBetaInfo("feature_thread") &&
306+
!SettingsStore.getValue("feature_thread") &&
307+
!SdkConfig.get("show_labs_settings")
308+
) {
309+
// Hide the beta prompt if there is no UI to enable it,
310+
// e.g if config.json disables it and doesn't enable show labs flags
311+
return false;
312+
}
313+
260314
const inNotThreadTimeline = this.context.timelineRenderingType !== TimelineRenderingType.Thread;
261315

262316
const isAllowedMessageType = !this.forbiddenThreadHeadMsgType.includes(
@@ -319,44 +373,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
319373
key="cancel"
320374
/>;
321375

322-
const relationType = this.props.mxEvent?.getRelation()?.rel_type;
323-
const hasARelation = !!relationType && relationType !== RelationType.Thread;
324-
const firstTimeSeeingThreads = localStorage.getItem("mx_seen_feature_thread") === null &&
325-
!SettingsStore.getValue("feature_thread");
326-
const threadTooltipButton = <CardContext.Consumer key="thread">
327-
{ context =>
328-
<RovingAccessibleTooltipButton
329-
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
330-
331-
disabled={hasARelation}
332-
tooltip={<>
333-
<div className="mx_Tooltip_title">
334-
{ !hasARelation
335-
? _t("Reply in thread")
336-
: _t("Can't create a thread from an event with an existing relation") }
337-
</div>
338-
{ !hasARelation && (
339-
<div className="mx_Tooltip_sub">
340-
{ SettingsStore.getValue("feature_thread")
341-
? _t("Beta feature")
342-
: _t("Beta feature. Click to learn more.")
343-
}
344-
</div>
345-
) }
346-
</>}
347-
348-
title={!hasARelation
349-
? _t("Reply in thread")
350-
: _t("Can't create a thread from an event with an existing relation")}
351-
352-
onClick={this.onThreadClick.bind(null, context.isCard)}
353-
>
354-
{ firstTimeSeeingThreads && (
355-
<div className="mx_Indicator" />
356-
) }
357-
</RovingAccessibleTooltipButton>
358-
}
359-
</CardContext.Consumer>;
376+
const threadTooltipButton = <ReplyInThreadButton mxEvent={this.props.mxEvent} />;
360377

361378
// We show a different toolbar for failed events, so detect that first.
362379
const mxEvent = this.props.mxEvent;

src/components/views/rooms/RoomListHeader.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
140140
}
141141
});
142142

143+
const canShowMainMenu = activeSpace || spaceKey === MetaSpace.Home;
144+
145+
useEffect(() => {
146+
if (mainMenuDisplayed && !canShowMainMenu) {
147+
// Space changed under us and we no longer has a main menu to draw
148+
closeMainMenu();
149+
}
150+
}, [closeMainMenu, canShowMainMenu, mainMenuDisplayed]);
151+
143152
// we pass null for the queryLength to inhibit the metrics hook for when there is no filterCondition
144153
useWebSearchMetrics(count, filterCondition ? filterCondition.search.length : null, false);
145154

@@ -168,7 +177,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
168177
const canShowPlusMenu = canCreateRooms || canExploreRooms || activeSpace;
169178

170179
let contextMenu: JSX.Element;
171-
if (mainMenuDisplayed) {
180+
if (mainMenuDisplayed && mainMenuHandle.current) {
172181
let ContextMenuComponent;
173182
if (activeSpace) {
174183
ContextMenuComponent = SpaceContextMenu;
@@ -364,7 +373,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
364373
.join("\n");
365374

366375
let contextMenuButton: JSX.Element = <div className="mx_RoomListHeader_contextLessTitle">{ title }</div>;
367-
if (activeSpace || spaceKey === MetaSpace.Home) {
376+
if (canShowMainMenu) {
368377
contextMenuButton = <ContextMenuTooltipButton
369378
inputRef={mainMenuHandle}
370379
onClick={openMainMenu}

src/i18n/strings/en_EN.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2081,11 +2081,11 @@
20812081
"Error processing audio message": "Error processing audio message",
20822082
"View live location": "View live location",
20832083
"React": "React",
2084-
"Edit": "Edit",
20852084
"Reply in thread": "Reply in thread",
20862085
"Can't create a thread from an event with an existing relation": "Can't create a thread from an event with an existing relation",
20872086
"Beta feature": "Beta feature",
20882087
"Beta feature. Click to learn more.": "Beta feature. Click to learn more.",
2088+
"Edit": "Edit",
20892089
"Reply": "Reply",
20902090
"Collapse quotes": "Collapse quotes",
20912091
"Expand quotes": "Expand quotes",
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
import { mount } from 'enzyme';
19+
import { MatrixClient } from 'matrix-js-sdk/src/client';
20+
import { act } from "react-dom/test-utils";
21+
22+
import SpaceStore from "../../../../src/stores/spaces/SpaceStore";
23+
import { MetaSpace } from "../../../../src/stores/spaces";
24+
import RoomListHeader from "../../../../src/components/views/rooms/RoomListHeader";
25+
import * as testUtils from "../../../test-utils";
26+
import { createTestClient, mkSpace } from "../../../test-utils";
27+
import DMRoomMap from "../../../../src/utils/DMRoomMap";
28+
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
29+
import SettingsStore from "../../../../src/settings/SettingsStore";
30+
import { SettingLevel } from "../../../../src/settings/SettingLevel";
31+
32+
describe("RoomListHeader", () => {
33+
let client: MatrixClient;
34+
35+
beforeEach(() => {
36+
client = createTestClient();
37+
});
38+
39+
it("renders a main menu for the home space", () => {
40+
act(() => {
41+
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
42+
});
43+
44+
const wrapper = mount(<MatrixClientContext.Provider value={client}>
45+
<RoomListHeader />
46+
</MatrixClientContext.Provider>);
47+
48+
expect(wrapper.text()).toBe("Home");
49+
act(() => {
50+
wrapper.find('[aria-label="Home options"]').hostNodes().simulate("click");
51+
});
52+
wrapper.update();
53+
54+
const menu = wrapper.find(".mx_IconizedContextMenu");
55+
const items = menu.find(".mx_IconizedContextMenu_item").hostNodes();
56+
expect(items).toHaveLength(1);
57+
expect(items.at(0).text()).toBe("Show all rooms");
58+
});
59+
60+
it("renders a main menu for spaces", async () => {
61+
const testSpace = mkSpace(client, "!space:server");
62+
testSpace.name = "Test Space";
63+
client.getRoom = () => testSpace;
64+
65+
const getUserIdForRoomId = jest.fn();
66+
const getDMRoomsForUserId = jest.fn();
67+
// @ts-ignore
68+
DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId };
69+
70+
await testUtils.setupAsyncStoreWithClient(SpaceStore.instance, client);
71+
act(() => {
72+
SpaceStore.instance.setActiveSpace(testSpace.roomId);
73+
});
74+
75+
const wrapper = mount(<MatrixClientContext.Provider value={client}>
76+
<RoomListHeader />
77+
</MatrixClientContext.Provider>);
78+
79+
expect(wrapper.text()).toBe("Test Space");
80+
act(() => {
81+
wrapper.find('[aria-label="Test Space menu"]').hostNodes().simulate("click");
82+
});
83+
wrapper.update();
84+
85+
const menu = wrapper.find(".mx_IconizedContextMenu");
86+
const items = menu.find(".mx_IconizedContextMenu_item").hostNodes();
87+
expect(items).toHaveLength(6);
88+
expect(items.at(0).text()).toBe("Space home");
89+
expect(items.at(1).text()).toBe("Manage & explore rooms");
90+
expect(items.at(2).text()).toBe("Preferences");
91+
expect(items.at(3).text()).toBe("Settings");
92+
expect(items.at(4).text()).toBe("Room");
93+
expect(items.at(4).text()).toBe("Room");
94+
});
95+
96+
it("closes menu if space changes from under it", async () => {
97+
await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, {
98+
[MetaSpace.Home]: true,
99+
[MetaSpace.Favourites]: true,
100+
});
101+
102+
const testSpace = mkSpace(client, "!space:server");
103+
testSpace.name = "Test Space";
104+
client.getRoom = () => testSpace;
105+
106+
const getUserIdForRoomId = jest.fn();
107+
const getDMRoomsForUserId = jest.fn();
108+
// @ts-ignore
109+
DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId };
110+
111+
await testUtils.setupAsyncStoreWithClient(SpaceStore.instance, client);
112+
act(() => {
113+
SpaceStore.instance.setActiveSpace(testSpace.roomId);
114+
});
115+
116+
const wrapper = mount(<MatrixClientContext.Provider value={client}>
117+
<RoomListHeader />
118+
</MatrixClientContext.Provider>);
119+
120+
expect(wrapper.text()).toBe("Test Space");
121+
act(() => {
122+
wrapper.find('[aria-label="Test Space menu"]').hostNodes().simulate("click");
123+
});
124+
wrapper.update();
125+
126+
act(() => {
127+
SpaceStore.instance.setActiveSpace(MetaSpace.Favourites);
128+
});
129+
wrapper.update();
130+
131+
expect(wrapper.text()).toBe("Favourites");
132+
133+
const menu = wrapper.find(".mx_IconizedContextMenu");
134+
expect(menu).toHaveLength(0);
135+
});
136+
});

test/test-utils/test-utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ export function mkStubRoom(roomId: string = null, name: string, client: MatrixCl
379379
getJoinRule: jest.fn().mockReturnValue("invite"),
380380
loadMembersIfNeeded: jest.fn(),
381381
client,
382+
canInvite: jest.fn(),
382383
} as unknown as Room;
383384
}
384385

0 commit comments

Comments
 (0)