Add room topic and animation (#11352)
* Create useRoomName hook Mark RoomName component as deprecated * Pass out-of-band data to relevant RoomHeader component * Mark LegacyRoomHeader as deprecated * Fix incorrect search&replace in _RoomHeader.pcss * lintfix * Mark room as optional in room topic hook * Fix i18n * Discard use of useCallback * Change export of useRoomName * fix ts issue * lints * Add room topic to room header * lintfix * lintfix & clamp to one line * Revert optimisations to DecoratedRoomAvatar * Add test for opening the room summary * Make transition honour prefer-reduced-motion * Fallback when room is undefined * fix snapshot
This commit is contained in:
parent
8166306e0f
commit
33299af5c9
5 changed files with 123 additions and 40 deletions
|
@ -14,40 +14,45 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
:root {
|
|
||||||
--RoomHeader-indicator-dot-size: 8px;
|
|
||||||
--RoomHeader-indicator-dot-offset: -3px;
|
|
||||||
--RoomHeader-indicator-pulseColor: $alert;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader {
|
.mx_RoomHeader {
|
||||||
flex: 0 0 50px;
|
|
||||||
border-bottom: 1px solid $primary-hairline-color;
|
|
||||||
background-color: $background;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_wrapper {
|
|
||||||
height: 44px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
height: 64px;
|
||||||
margin: 0 20px 0 16px;
|
gap: var(--cpd-space-3x);
|
||||||
padding-top: 6px;
|
padding: 0 var(--cpd-space-3x);
|
||||||
border-bottom: 1px solid $separator;
|
border-bottom: 1px solid $separator;
|
||||||
|
background-color: $background;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* To remove when compound is integrated */
|
||||||
.mx_RoomHeader_name {
|
.mx_RoomHeader_name {
|
||||||
flex: 0 1 auto;
|
font: var(--cpd-font-body-lg-semibold);
|
||||||
overflow: hidden;
|
}
|
||||||
color: $primary-content;
|
|
||||||
font: var(--cpd-font-heading-sm-semibold);
|
.mx_RoomHeader_topic {
|
||||||
font-weight: var(--cpd-font-weight-semibold);
|
/* To remove when compound is integrated */
|
||||||
min-height: 24px;
|
font: var(--cpd-font-body-sm-regular);
|
||||||
align-items: center;
|
|
||||||
border-radius: 6px;
|
height: 0;
|
||||||
margin: 0 3px;
|
opacity: 0;
|
||||||
padding: 1px 4px;
|
display: -webkit-box;
|
||||||
display: flex;
|
-webkit-box-orient: vertical;
|
||||||
user-select: none;
|
-webkit-line-clamp: 1;
|
||||||
cursor: pointer;
|
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-all;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
transition: all var(--transition-standard) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomHeader:hover .mx_RoomHeader_topic {
|
||||||
|
/* height needed to compute the transition, it equals to the `line-height`
|
||||||
|
value in pixels */
|
||||||
|
height: calc($font-13px * 1.5);
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,16 +19,36 @@ import React from "react";
|
||||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||||
import { useRoomName } from "../../../hooks/useRoomName";
|
import { useRoomName } from "../../../hooks/useRoomName";
|
||||||
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
|
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||||
|
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||||
|
import { useTopic } from "../../../hooks/room/useTopic";
|
||||||
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
|
||||||
export default function RoomHeader({ room, oobData }: { room?: Room; oobData?: IOOBData }): JSX.Element {
|
export default function RoomHeader({ room, oobData }: { room?: Room; oobData?: IOOBData }): JSX.Element {
|
||||||
const roomName = useRoomName(room, oobData);
|
const roomName = useRoomName(room, oobData);
|
||||||
|
const roomTopic = useTopic(room);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="mx_RoomHeader light-panel">
|
<header
|
||||||
<div className="mx_RoomHeader_wrapper">
|
className="mx_RoomHeader light-panel"
|
||||||
<div className="mx_RoomHeader_name" dir="auto" title={roomName} role="heading" aria-level={1}>
|
onClick={() => {
|
||||||
|
const rightPanel = RightPanelStore.instance;
|
||||||
|
rightPanel.isOpen
|
||||||
|
? rightPanel.togglePanel(null)
|
||||||
|
: rightPanel.setCard({ phase: RightPanelPhases.RoomSummary });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{room ? (
|
||||||
|
<DecoratedRoomAvatar room={room} oobData={oobData} avatarSize={40} displayBadge={false} />
|
||||||
|
) : (
|
||||||
|
<RoomAvatar oobData={oobData} width={40} height={40} />
|
||||||
|
)}
|
||||||
|
<div className="mx_RoomHeader_info">
|
||||||
|
<div dir="auto" title={roomName} role="heading" aria-level={1} className="mx_RoomHeader_name">
|
||||||
{roomName}
|
{roomName}
|
||||||
</div>
|
</div>
|
||||||
|
{roomTopic && <div className="mx_RoomHeader_topic">{roomTopic.text}</div>}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
|
@ -25,14 +25,14 @@ import { Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { useTypedEventEmitter } from "../useEventEmitter";
|
import { useTypedEventEmitter } from "../useEventEmitter";
|
||||||
|
|
||||||
export const getTopic = (room: Room): Optional<TopicState> => {
|
export const getTopic = (room?: Room): Optional<TopicState> => {
|
||||||
const content = room?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent<MRoomTopicEventContent>();
|
const content = room?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent<MRoomTopicEventContent>();
|
||||||
return !!content ? parseTopicContent(content) : null;
|
return !!content ? parseTopicContent(content) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useTopic(room: Room): Optional<TopicState> {
|
export function useTopic(room?: Room): Optional<TopicState> {
|
||||||
const [topic, setTopic] = useState(getTopic(room));
|
const [topic, setTopic] = useState(getTopic(room));
|
||||||
useTypedEventEmitter(room.currentState, RoomStateEvent.Events, (ev: MatrixEvent) => {
|
useTypedEventEmitter(room?.currentState, RoomStateEvent.Events, (ev: MatrixEvent) => {
|
||||||
if (ev.getType() !== EventType.RoomTopic) return;
|
if (ev.getType() !== EventType.RoomTopic) return;
|
||||||
setTopic(getTopic(room));
|
setTopic(getTopic(room));
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,23 +15,35 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Mocked } from "jest-mock";
|
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { EventType, MatrixEvent, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { stubClient } from "../../../test-utils";
|
import { stubClient } from "../../../test-utils";
|
||||||
import RoomHeader from "../../../../src/components/views/rooms/RoomHeader";
|
import RoomHeader from "../../../../src/components/views/rooms/RoomHeader";
|
||||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
|
import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore";
|
||||||
|
import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||||
|
|
||||||
describe("Roomeader", () => {
|
describe("Roomeader", () => {
|
||||||
let client: Mocked<MatrixClient>;
|
|
||||||
let room: Room;
|
let room: Room;
|
||||||
|
|
||||||
const ROOM_ID = "!1:example.org";
|
const ROOM_ID = "!1:example.org";
|
||||||
|
|
||||||
|
let setCardSpy: jest.SpyInstance | undefined;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
stubClient();
|
stubClient();
|
||||||
room = new Room(ROOM_ID, client, "@alice:example.org");
|
room = new Room(ROOM_ID, MatrixClientPeg.get()!, "@alice:example.org", {
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
|
DMRoomMap.setShared({
|
||||||
|
getUserIdForRoomId: jest.fn(),
|
||||||
|
} as unknown as DMRoomMap);
|
||||||
|
|
||||||
|
setCardSpy = jest.spyOn(RightPanelStore.instance, "setCard");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders with no props", () => {
|
it("renders with no props", () => {
|
||||||
|
@ -55,4 +67,29 @@ describe("Roomeader", () => {
|
||||||
);
|
);
|
||||||
expect(container).toHaveTextContent(OOB_NAME);
|
expect(container).toHaveTextContent(OOB_NAME);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders the room topic", async () => {
|
||||||
|
const TOPIC = "Hello World!";
|
||||||
|
|
||||||
|
const roomTopic = new MatrixEvent({
|
||||||
|
type: EventType.RoomTopic,
|
||||||
|
event_id: "$00002",
|
||||||
|
room_id: room.roomId,
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
origin_server_ts: 1,
|
||||||
|
content: { topic: TOPIC },
|
||||||
|
state_key: "",
|
||||||
|
});
|
||||||
|
await room.addLiveEvents([roomTopic]);
|
||||||
|
|
||||||
|
const { container } = render(<RoomHeader room={room} />);
|
||||||
|
expect(container).toHaveTextContent(TOPIC);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens the room summary", async () => {
|
||||||
|
const { container } = render(<RoomHeader room={room} />);
|
||||||
|
|
||||||
|
await userEvent.click(container.firstChild! as Element);
|
||||||
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,8 +5,29 @@ exports[`Roomeader renders with no props 1`] = `
|
||||||
<header
|
<header
|
||||||
class="mx_RoomHeader light-panel"
|
class="mx_RoomHeader light-panel"
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 26px; width: 40px; line-height: 40px;"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
loading="lazy"
|
||||||
|
src="data:image/png;base64,00"
|
||||||
|
style="width: 40px; height: 40px;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomHeader_wrapper"
|
class="mx_RoomHeader_info"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-level="1"
|
aria-level="1"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue