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

Commit 583050c

Browse files
author
Kerry
authored
Render poll end events in timeline (#10027)
* wip * remove dupe * use poll model relations in all cases * update mpollbody tests to use poll instance * update poll fetching login in pinned messages card * add pinned polls to room polls state * add spinner while relations are still loading * handle no poll in end poll dialog * strict errors * render a poll body that errors for poll end events * add fetching logic to pollend tile * extract poll testing utilities * test mpollend * strict fix * more strict fix * strict fix for forwardref * update poll test utils * implicit anys * tidy and add jsdoc
1 parent 013fd0a commit 583050c

File tree

14 files changed

+572
-89
lines changed

14 files changed

+572
-89
lines changed

res/css/_components.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@
236236
@import "./views/messages/_MLocationBody.pcss";
237237
@import "./views/messages/_MNoticeBody.pcss";
238238
@import "./views/messages/_MPollBody.pcss";
239+
@import "./views/messages/_MPollEndBody.pcss";
239240
@import "./views/messages/_MStickerBody.pcss";
240241
@import "./views/messages/_MTextBody.pcss";
241242
@import "./views/messages/_MVideoBody.pcss";
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
Copyright 2023 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+
.mx_MPollEndBody_icon {
18+
height: 14px;
19+
margin-right: $spacing-8;
20+
vertical-align: middle;
21+
color: $secondary-content;
22+
}
Lines changed: 3 additions & 3 deletions
Loading
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
Copyright 2023 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, useContext } from "react";
18+
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
19+
import { M_TEXT } from "matrix-js-sdk/src/@types/extensible_events";
20+
import { logger } from "matrix-js-sdk/src/logger";
21+
22+
import { Icon as PollIcon } from "../../../../res/img/element-icons/room/composer/poll.svg";
23+
import MatrixClientContext from "../../../contexts/MatrixClientContext";
24+
import { textForEvent } from "../../../TextForEvent";
25+
import { IBodyProps } from "./IBodyProps";
26+
import MPollBody from "./MPollBody";
27+
28+
const getRelatedPollStartEventId = (event: MatrixEvent): string | undefined => {
29+
const relation = event.getRelation();
30+
return relation?.event_id;
31+
};
32+
33+
/**
34+
* Attempt to retrieve the related poll start event for this end event
35+
* If the event already exists in the rooms timeline, return it
36+
* Otherwise try to fetch the event from the server
37+
* @param event
38+
* @returns
39+
*/
40+
const usePollStartEvent = (event: MatrixEvent): { pollStartEvent?: MatrixEvent; isLoadingPollStartEvent: boolean } => {
41+
const matrixClient = useContext(MatrixClientContext);
42+
const [pollStartEvent, setPollStartEvent] = useState<MatrixEvent>();
43+
const [isLoadingPollStartEvent, setIsLoadingPollStartEvent] = useState(false);
44+
45+
const pollStartEventId = getRelatedPollStartEventId(event);
46+
47+
useEffect(() => {
48+
const room = matrixClient.getRoom(event.getRoomId());
49+
const fetchPollStartEvent = async (roomId: string, pollStartEventId: string): Promise<void> => {
50+
setIsLoadingPollStartEvent(true);
51+
try {
52+
const startEventJson = await matrixClient.fetchRoomEvent(roomId, pollStartEventId);
53+
const startEvent = new MatrixEvent(startEventJson);
54+
// add the poll to the room polls state
55+
room?.processPollEvents([startEvent, event]);
56+
57+
// end event is not a valid end to the related start event
58+
// if not sent by the same user
59+
if (startEvent.getSender() === event.getSender()) {
60+
setPollStartEvent(startEvent);
61+
}
62+
} catch (error) {
63+
logger.error("Failed to fetch related poll start event", error);
64+
} finally {
65+
setIsLoadingPollStartEvent(false);
66+
}
67+
};
68+
69+
if (pollStartEvent || !room || !pollStartEventId) {
70+
return;
71+
}
72+
73+
const timelineSet = room.getUnfilteredTimelineSet();
74+
const localEvent = timelineSet
75+
?.getTimelineForEvent(pollStartEventId)
76+
?.getEvents()
77+
.find((e) => e.getId() === pollStartEventId);
78+
79+
if (localEvent) {
80+
// end event is not a valid end to the related start event
81+
// if not sent by the same user
82+
if (localEvent.getSender() === event.getSender()) {
83+
setPollStartEvent(localEvent);
84+
}
85+
} else {
86+
// pollStartEvent is not in the current timeline,
87+
// fetch it
88+
fetchPollStartEvent(room.roomId, pollStartEventId);
89+
}
90+
}, [event, pollStartEventId, pollStartEvent, matrixClient]);
91+
92+
return { pollStartEvent, isLoadingPollStartEvent };
93+
};
94+
95+
export const MPollEndBody = React.forwardRef<any, IBodyProps>(({ mxEvent, ...props }, ref) => {
96+
const { pollStartEvent, isLoadingPollStartEvent } = usePollStartEvent(mxEvent);
97+
98+
if (!pollStartEvent) {
99+
const pollEndFallbackMessage = M_TEXT.findIn(mxEvent.getContent()) || textForEvent(mxEvent);
100+
return (
101+
<>
102+
<PollIcon className="mx_MPollEndBody_icon" />
103+
{!isLoadingPollStartEvent && pollEndFallbackMessage}
104+
</>
105+
);
106+
}
107+
108+
return <MPollBody mxEvent={pollStartEvent} {...props} />;
109+
});

src/components/views/messages/MessageEvent.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import React, { createRef } from "react";
1818
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
1919
import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
2020
import { M_LOCATION } from "matrix-js-sdk/src/@types/location";
21-
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
21+
import { M_POLL_END, M_POLL_START } from "matrix-js-sdk/src/@types/polls";
2222
import { MatrixEventEvent } from "matrix-js-sdk/src/models/event";
2323

2424
import SettingsStore from "../../../settings/SettingsStore";
@@ -37,6 +37,7 @@ import MVoiceOrAudioBody from "./MVoiceOrAudioBody";
3737
import MVideoBody from "./MVideoBody";
3838
import MStickerBody from "./MStickerBody";
3939
import MPollBody from "./MPollBody";
40+
import { MPollEndBody } from "./MPollEndBody";
4041
import MLocationBody from "./MLocationBody";
4142
import MjolnirBody from "./MjolnirBody";
4243
import MBeaconBody from "./MBeaconBody";
@@ -73,6 +74,8 @@ const baseEvTypes = new Map<string, React.ComponentType<Partial<IBodyProps>>>([
7374
[EventType.Sticker, MStickerBody],
7475
[M_POLL_START.name, MPollBody],
7576
[M_POLL_START.altName, MPollBody],
77+
[M_POLL_END.name, MPollEndBody],
78+
[M_POLL_END.altName, MPollEndBody],
7679
[M_BEACON_INFO.name, MBeaconBody],
7780
[M_BEACON_INFO.altName, MBeaconBody],
7881
]);

src/events/EventTileFactory.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import React from "react";
1818
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
1919
import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
2020
import { Optional } from "matrix-events-sdk";
21-
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
21+
import { M_POLL_END, M_POLL_START } from "matrix-js-sdk/src/@types/polls";
2222
import { MatrixClient } from "matrix-js-sdk/src/client";
2323
import { GroupCallIntent } from "matrix-js-sdk/src/webrtc/groupCall";
2424

@@ -99,6 +99,8 @@ const EVENT_TILE_TYPES = new Map<string, Factory>([
9999
[EventType.Sticker, MessageEventFactory],
100100
[M_POLL_START.name, MessageEventFactory],
101101
[M_POLL_START.altName, MessageEventFactory],
102+
[M_POLL_END.name, MessageEventFactory],
103+
[M_POLL_END.altName, MessageEventFactory],
102104
[EventType.KeyVerificationCancel, KeyVerificationConclFactory],
103105
[EventType.KeyVerificationDone, KeyVerificationConclFactory],
104106
[EventType.CallInvite, LegacyCallEventFactory], // note that this requires a special factory type
@@ -412,7 +414,11 @@ export function renderReplyTile(
412414
// XXX: this'll eventually be dynamic based on the fields once we have extensible event types
413415
const messageTypes = [EventType.RoomMessage, EventType.Sticker];
414416
export function isMessageEvent(ev: MatrixEvent): boolean {
415-
return messageTypes.includes(ev.getType() as EventType) || M_POLL_START.matches(ev.getType());
417+
return (
418+
messageTypes.includes(ev.getType() as EventType) ||
419+
M_POLL_START.matches(ev.getType()) ||
420+
M_POLL_END.matches(ev.getType())
421+
);
416422
}
417423

418424
export function haveRendererForEvent(mxEvent: MatrixEvent, showHiddenEvents: boolean): boolean {

src/events/forward/getForwardableEvent.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
17+
import { M_POLL_END, M_POLL_START } from "matrix-js-sdk/src/@types/polls";
1818
import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
1919
import { MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
2020

@@ -26,7 +26,7 @@ import { VoiceBroadcastInfoEventType } from "../../voice-broadcast/types";
2626
* If an event is not forwardable return null
2727
*/
2828
export const getForwardableEvent = (event: MatrixEvent, cli: MatrixClient): MatrixEvent | null => {
29-
if (M_POLL_START.matches(event.getType())) {
29+
if (M_POLL_START.matches(event.getType()) || M_POLL_END.matches(event.getType())) {
3030
return null;
3131
}
3232

src/utils/EventRenderingUtils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ limitations under the License.
1616

1717
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
1818
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
19-
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
19+
import { M_POLL_END, M_POLL_START } from "matrix-js-sdk/src/@types/polls";
2020
import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
2121
import { IContent } from "matrix-js-sdk/src/matrix";
2222

@@ -41,6 +41,7 @@ const calcIsInfoMessage = (
4141
eventType !== EventType.Sticker &&
4242
eventType !== EventType.RoomCreate &&
4343
!M_POLL_START.matches(eventType) &&
44+
!M_POLL_END.matches(eventType) &&
4445
!M_BEACON_INFO.matches(eventType) &&
4546
!(eventType === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started)
4647
);

src/utils/EventUtils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
1818
import { EventType, EVENT_VISIBILITY_CHANGE_TYPE, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
1919
import { MatrixClient } from "matrix-js-sdk/src/client";
2020
import { logger } from "matrix-js-sdk/src/logger";
21-
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
21+
import { M_POLL_END, M_POLL_START } from "matrix-js-sdk/src/@types/polls";
2222
import { M_LOCATION } from "matrix-js-sdk/src/@types/location";
2323
import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
2424
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
@@ -57,6 +57,7 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean {
5757
} else if (
5858
mxEvent.getType() === "m.sticker" ||
5959
M_POLL_START.matches(mxEvent.getType()) ||
60+
M_POLL_END.matches(mxEvent.getType()) ||
6061
M_BEACON_INFO.matches(mxEvent.getType()) ||
6162
(mxEvent.getType() === VoiceBroadcastInfoEventType &&
6263
mxEvent.getContent()?.state === VoiceBroadcastInfoState.Started)

test/components/views/dialogs/polls/PollHistoryDialog-test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,10 @@ describe("<PollHistoryDialog />", () => {
6969
expect(getByText("There are no polls in this room")).toBeTruthy();
7070
});
7171

72-
it("renders a list of polls when there are polls in the timeline", () => {
73-
const pollStart1 = makePollStartEvent("Question?", userId, undefined, 1675300825090, "$1");
74-
const pollStart2 = makePollStartEvent("Where?", userId, undefined, 1675300725090, "$2");
75-
const pollStart3 = makePollStartEvent("What?", userId, undefined, 1675200725090, "$3");
72+
it("renders a list of polls when there are polls in the timeline", async () => {
73+
const pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: 1675300825090, id: "$1" });
74+
const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: 1675300725090, id: "$2" });
75+
const pollStart3 = makePollStartEvent("What?", userId, undefined, { ts: 1675200725090, id: "$3" });
7676
const message = new MatrixEvent({
7777
type: "m.room.message",
7878
content: {},

0 commit comments

Comments
 (0)