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

Commit fdd5494

Browse files
author
Germain
authored
Add dialog to navigate long room topics (#8517)
1 parent e0415d0 commit fdd5494

File tree

13 files changed

+347
-41
lines changed

13 files changed

+347
-41
lines changed

res/css/views/rooms/_RoomHeader.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ limitations under the License.
140140
cursor: pointer;
141141
}
142142

143+
.mx_RoomTopic {
144+
position: relative;
145+
}
146+
143147
.mx_RoomHeader_topic {
144148
$lineHeight: $font-16px;
145149
$lines: 2;
@@ -209,6 +213,7 @@ limitations under the License.
209213
.mx_RoomHeader_appsButton::before {
210214
mask-image: url('$(res)/img/element-icons/room/apps.svg');
211215
}
216+
212217
.mx_RoomHeader_appsButton_highlight::before {
213218
background-color: $accent;
214219
}
@@ -239,6 +244,7 @@ limitations under the License.
239244
padding: 0;
240245
margin: 0;
241246
}
247+
242248
.mx_RoomHeader {
243249
overflow: hidden;
244250
}

src/components/structures/SpaceRoomView.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,7 @@ const SpaceLanding = ({ space }: { space: Room }) => {
267267
{ settingsButton }
268268
</div>
269269
</div>
270-
<RoomTopic room={space}>
271-
{ (topic, ref) => (
272-
<div className="mx_SpaceRoomView_landing_topic" ref={ref}>
273-
{ topic }
274-
</div>
275-
) }
276-
</RoomTopic>
270+
<RoomTopic room={space} className="mx_SpaceRoomView_landing_topic" />
277271

278272
<SpaceHierarchy space={space} showRoom={showRoom} additionalButtons={addRoomButton} />
279273
</div>;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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, { useEffect, useRef } from "react";
18+
import linkifyElement from "linkify-element";
19+
20+
interface Props {
21+
as?: string;
22+
children: React.ReactNode;
23+
}
24+
25+
export function Linkify({
26+
as = "div",
27+
children,
28+
}: Props): JSX.Element {
29+
const ref = useRef();
30+
31+
useEffect(() => {
32+
linkifyElement(ref.current);
33+
}, [children]);
34+
35+
return React.createElement(as, {
36+
children,
37+
ref,
38+
});
39+
}
Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2021 The Matrix.org Foundation C.I.C.
2+
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -14,35 +14,87 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { useEffect, useState } from "react";
18-
import { EventType } from "matrix-js-sdk/src/@types/event";
17+
import React, { useCallback, useContext, useEffect, useRef } from "react";
1918
import { Room } from "matrix-js-sdk/src/models/room";
20-
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
21-
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
19+
import classNames from "classnames";
20+
import { EventType } from "matrix-js-sdk/src/@types/event";
2221

23-
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
2422
import { linkifyElement } from "../../../HtmlUtils";
23+
import { useTopic } from "../../../hooks/room/useTopic";
24+
import useHover from "../../../hooks/useHover";
25+
import Tooltip, { Alignment } from "./Tooltip";
26+
import { _t } from "../../../languageHandler";
27+
import dis from "../../../dispatcher/dispatcher";
28+
import { Action } from "../../../dispatcher/actions";
29+
import Modal from "../../../Modal";
30+
import InfoDialog from "../dialogs/InfoDialog";
31+
import { useDispatcher } from "../../../hooks/useDispatcher";
32+
import MatrixClientContext from "../../../contexts/MatrixClientContext";
33+
import AccessibleButton from "./AccessibleButton";
34+
import { Linkify } from "./Linkify";
2535

26-
interface IProps {
36+
interface IProps extends React.HTMLProps<HTMLDivElement> {
2737
room?: Room;
28-
children?(topic: string, ref: (element: HTMLElement) => void): JSX.Element;
2938
}
3039

31-
export const getTopic = room => room?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
40+
export default function RoomTopic({
41+
room,
42+
...props
43+
}: IProps) {
44+
const client = useContext(MatrixClientContext);
45+
const ref = useRef<HTMLDivElement>();
46+
const hovered = useHover(ref);
47+
48+
const topic = useTopic(room);
49+
50+
const onClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
51+
props.onClick?.(e);
52+
const target = e.target as HTMLElement;
53+
if (target.tagName.toUpperCase() === "A") {
54+
return;
55+
}
3256

33-
const RoomTopic = ({ room, children }: IProps): JSX.Element => {
34-
const [topic, setTopic] = useState(getTopic(room));
35-
useTypedEventEmitter(room.currentState, RoomStateEvent.Events, (ev: MatrixEvent) => {
36-
if (ev.getType() !== EventType.RoomTopic) return;
37-
setTopic(getTopic(room));
57+
dis.fire(Action.ShowRoomTopic);
58+
}, [props]);
59+
60+
useDispatcher(dis, (payload) => {
61+
if (payload.action === Action.ShowRoomTopic) {
62+
const canSetTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, client.getUserId());
63+
64+
const modal = Modal.createDialog(InfoDialog, {
65+
title: room.name,
66+
description: <div>
67+
<Linkify as="p">{ topic }</Linkify>
68+
{ canSetTopic && <AccessibleButton
69+
kind="primary_outline"
70+
onClick={() => {
71+
modal.close();
72+
dis.dispatch({ action: "open_room_settings" });
73+
}}>
74+
{ _t("Edit topic") }
75+
</AccessibleButton> }
76+
</div>,
77+
hasCloseButton: true,
78+
button: false,
79+
});
80+
}
3881
});
82+
3983
useEffect(() => {
40-
setTopic(getTopic(room));
41-
}, [room]);
84+
linkifyElement(ref.current);
85+
}, [topic]);
4286

43-
const ref = e => e && linkifyElement(e);
44-
if (children) return children(topic, ref);
45-
return <span ref={ref}>{ topic }</span>;
46-
};
87+
const className = classNames(props.className, "mx_RoomTopic");
4788

48-
export default RoomTopic;
89+
return <div {...props}
90+
ref={ref}
91+
onClick={onClick}
92+
dir="auto"
93+
className={className}
94+
>
95+
{ topic }
96+
{ hovered && (
97+
<Tooltip label={_t("Click to read topic")} alignment={Alignment.Bottom} />
98+
) }
99+
</div>;
100+
}

src/components/views/rooms/RoomHeader.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,10 @@ export default class RoomHeader extends React.Component<IProps, IState> {
186186
</ContextMenuTooltipButton>
187187
);
188188

189-
const topicElement = <RoomTopic room={this.props.room}>
190-
{ (topic, ref) => <div className="mx_RoomHeader_topic" ref={ref} title={topic} dir="auto">
191-
{ topic }
192-
</div> }
193-
</RoomTopic>;
189+
const topicElement = <RoomTopic
190+
room={this.props.room}
191+
className="mx_RoomHeader_topic"
192+
/>;
194193

195194
let roomAvatar;
196195
if (this.props.room) {

src/components/views/rooms/RoomPreviewCard.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,7 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
182182
<RoomName room={room} />
183183
</h1>
184184
<RoomInfoLine room={room} />
185-
<RoomTopic room={room}>
186-
{ (topic, ref) =>
187-
topic ? <div className="mx_RoomPreviewCard_topic" ref={ref}>
188-
{ topic }
189-
</div> : null
190-
}
191-
</RoomTopic>
185+
<RoomTopic room={room} className="mx_RoomPreviewCard_topic" />
192186
{ room.getJoinRule() === "public" && <RoomFacePile room={room} /> }
193187
{ notice ? <div className="mx_RoomPreviewCard_notice">
194188
{ notice }

src/components/views/spaces/SpaceSettingsGeneralTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ import AccessibleButton from "../elements/AccessibleButton";
2525
import SpaceBasicSettings from "./SpaceBasicSettings";
2626
import { avatarUrlForRoom } from "../../../Avatar";
2727
import { IDialogProps } from "../dialogs/IDialogProps";
28-
import { getTopic } from "../elements/RoomTopic";
2928
import { leaveSpace } from "../../../utils/leave-behaviour";
29+
import { getTopic } from "../../../hooks/room/useTopic";
3030

3131
interface IProps extends IDialogProps {
3232
matrixClient: MatrixClient;

src/dispatcher/actions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,4 +313,9 @@ export enum Action {
313313
* logs. Fires with no payload.
314314
*/
315315
DumpDebugLogs = "dump_debug_logs",
316+
317+
/**
318+
* Show current room topic
319+
*/
320+
ShowRoomTopic = "show_room_topic"
316321
}

src/hooks/room/useTopic.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 { useEffect, useState } from "react";
18+
import { EventType } from "matrix-js-sdk/src/@types/event";
19+
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
20+
import { Room } from "matrix-js-sdk/src/models/room";
21+
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
22+
23+
import { useTypedEventEmitter } from "../useEventEmitter";
24+
25+
export const getTopic = (room: Room) => {
26+
return room?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
27+
};
28+
29+
export function useTopic(room: Room): string {
30+
const [topic, setTopic] = useState(getTopic(room));
31+
useTypedEventEmitter(room.currentState, RoomStateEvent.Events, (ev: MatrixEvent) => {
32+
if (ev.getType() !== EventType.RoomTopic) return;
33+
setTopic(getTopic(room));
34+
});
35+
useEffect(() => {
36+
setTopic(getTopic(room));
37+
}, [room]);
38+
39+
return topic;
40+
}

src/hooks/useHover.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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, { useEffect, useState } from "react";
18+
19+
export default function useHover(ref: React.MutableRefObject<HTMLElement>) {
20+
const [hovered, setHoverState] = useState(false);
21+
22+
const handleMouseOver = () => setHoverState(true);
23+
const handleMouseOut = () => setHoverState(false);
24+
25+
useEffect(
26+
() => {
27+
const node = ref.current;
28+
if (node) {
29+
node.addEventListener("mouseover", handleMouseOver);
30+
node.addEventListener("mouseout", handleMouseOut);
31+
32+
return () => {
33+
node.removeEventListener("mouseover", handleMouseOver);
34+
node.removeEventListener("mouseout", handleMouseOut);
35+
};
36+
}
37+
},
38+
[ref],
39+
);
40+
41+
return hovered;
42+
}

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2370,6 +2370,8 @@
23702370
"Including %(commaSeparatedMembers)s": "Including %(commaSeparatedMembers)s",
23712371
"%(count)s people you know have already joined|other": "%(count)s people you know have already joined",
23722372
"%(count)s people you know have already joined|one": "%(count)s person you know has already joined",
2373+
"Edit topic": "Edit topic",
2374+
"Click to read topic": "Click to read topic",
23732375
"Message search initialisation failed, check <a>your settings</a> for more information": "Message search initialisation failed, check <a>your settings</a> for more information",
23742376
"Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files",
23752377
"Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages",

0 commit comments

Comments
 (0)