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

Commit

Permalink
Render poll end events in timeline (#10027)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Kerry authored Feb 7, 2023
1 parent 013fd0a commit 583050c
Show file tree
Hide file tree
Showing 14 changed files with 572 additions and 89 deletions.
1 change: 1 addition & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
@import "./views/messages/_MLocationBody.pcss";
@import "./views/messages/_MNoticeBody.pcss";
@import "./views/messages/_MPollBody.pcss";
@import "./views/messages/_MPollEndBody.pcss";
@import "./views/messages/_MStickerBody.pcss";
@import "./views/messages/_MTextBody.pcss";
@import "./views/messages/_MVideoBody.pcss";
Expand Down
22 changes: 22 additions & 0 deletions res/css/views/messages/_MPollEndBody.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
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.
*/

.mx_MPollEndBody_icon {
height: 14px;
margin-right: $spacing-8;
vertical-align: middle;
color: $secondary-content;
}
6 changes: 3 additions & 3 deletions res/img/element-icons/room/composer/poll.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 changes: 109 additions & 0 deletions src/components/views/messages/MPollEndBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
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, { useEffect, useState, useContext } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { M_TEXT } from "matrix-js-sdk/src/@types/extensible_events";
import { logger } from "matrix-js-sdk/src/logger";

import { Icon as PollIcon } from "../../../../res/img/element-icons/room/composer/poll.svg";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { textForEvent } from "../../../TextForEvent";
import { IBodyProps } from "./IBodyProps";
import MPollBody from "./MPollBody";

const getRelatedPollStartEventId = (event: MatrixEvent): string | undefined => {
const relation = event.getRelation();
return relation?.event_id;
};

/**
* Attempt to retrieve the related poll start event for this end event
* If the event already exists in the rooms timeline, return it
* Otherwise try to fetch the event from the server
* @param event
* @returns
*/
const usePollStartEvent = (event: MatrixEvent): { pollStartEvent?: MatrixEvent; isLoadingPollStartEvent: boolean } => {
const matrixClient = useContext(MatrixClientContext);
const [pollStartEvent, setPollStartEvent] = useState<MatrixEvent>();
const [isLoadingPollStartEvent, setIsLoadingPollStartEvent] = useState(false);

const pollStartEventId = getRelatedPollStartEventId(event);

useEffect(() => {
const room = matrixClient.getRoom(event.getRoomId());
const fetchPollStartEvent = async (roomId: string, pollStartEventId: string): Promise<void> => {
setIsLoadingPollStartEvent(true);
try {
const startEventJson = await matrixClient.fetchRoomEvent(roomId, pollStartEventId);
const startEvent = new MatrixEvent(startEventJson);
// add the poll to the room polls state
room?.processPollEvents([startEvent, event]);

// end event is not a valid end to the related start event
// if not sent by the same user
if (startEvent.getSender() === event.getSender()) {
setPollStartEvent(startEvent);
}
} catch (error) {
logger.error("Failed to fetch related poll start event", error);
} finally {
setIsLoadingPollStartEvent(false);
}
};

if (pollStartEvent || !room || !pollStartEventId) {
return;
}

const timelineSet = room.getUnfilteredTimelineSet();
const localEvent = timelineSet
?.getTimelineForEvent(pollStartEventId)
?.getEvents()
.find((e) => e.getId() === pollStartEventId);

if (localEvent) {
// end event is not a valid end to the related start event
// if not sent by the same user
if (localEvent.getSender() === event.getSender()) {
setPollStartEvent(localEvent);
}
} else {
// pollStartEvent is not in the current timeline,
// fetch it
fetchPollStartEvent(room.roomId, pollStartEventId);
}
}, [event, pollStartEventId, pollStartEvent, matrixClient]);

return { pollStartEvent, isLoadingPollStartEvent };
};

export const MPollEndBody = React.forwardRef<any, IBodyProps>(({ mxEvent, ...props }, ref) => {
const { pollStartEvent, isLoadingPollStartEvent } = usePollStartEvent(mxEvent);

if (!pollStartEvent) {
const pollEndFallbackMessage = M_TEXT.findIn(mxEvent.getContent()) || textForEvent(mxEvent);
return (
<>
<PollIcon className="mx_MPollEndBody_icon" />
{!isLoadingPollStartEvent && pollEndFallbackMessage}
</>
);
}

return <MPollBody mxEvent={pollStartEvent} {...props} />;
});
5 changes: 4 additions & 1 deletion src/components/views/messages/MessageEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import React, { createRef } from "react";
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
import { M_LOCATION } from "matrix-js-sdk/src/@types/location";
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import { M_POLL_END, M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import { MatrixEventEvent } from "matrix-js-sdk/src/models/event";

import SettingsStore from "../../../settings/SettingsStore";
Expand All @@ -37,6 +37,7 @@ import MVoiceOrAudioBody from "./MVoiceOrAudioBody";
import MVideoBody from "./MVideoBody";
import MStickerBody from "./MStickerBody";
import MPollBody from "./MPollBody";
import { MPollEndBody } from "./MPollEndBody";
import MLocationBody from "./MLocationBody";
import MjolnirBody from "./MjolnirBody";
import MBeaconBody from "./MBeaconBody";
Expand Down Expand Up @@ -73,6 +74,8 @@ const baseEvTypes = new Map<string, React.ComponentType<Partial<IBodyProps>>>([
[EventType.Sticker, MStickerBody],
[M_POLL_START.name, MPollBody],
[M_POLL_START.altName, MPollBody],
[M_POLL_END.name, MPollEndBody],
[M_POLL_END.altName, MPollEndBody],
[M_BEACON_INFO.name, MBeaconBody],
[M_BEACON_INFO.altName, MBeaconBody],
]);
Expand Down
10 changes: 8 additions & 2 deletions src/events/EventTileFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
import { Optional } from "matrix-events-sdk";
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import { M_POLL_END, M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { GroupCallIntent } from "matrix-js-sdk/src/webrtc/groupCall";

Expand Down Expand Up @@ -99,6 +99,8 @@ const EVENT_TILE_TYPES = new Map<string, Factory>([
[EventType.Sticker, MessageEventFactory],
[M_POLL_START.name, MessageEventFactory],
[M_POLL_START.altName, MessageEventFactory],
[M_POLL_END.name, MessageEventFactory],
[M_POLL_END.altName, MessageEventFactory],
[EventType.KeyVerificationCancel, KeyVerificationConclFactory],
[EventType.KeyVerificationDone, KeyVerificationConclFactory],
[EventType.CallInvite, LegacyCallEventFactory], // note that this requires a special factory type
Expand Down Expand Up @@ -412,7 +414,11 @@ export function renderReplyTile(
// XXX: this'll eventually be dynamic based on the fields once we have extensible event types
const messageTypes = [EventType.RoomMessage, EventType.Sticker];
export function isMessageEvent(ev: MatrixEvent): boolean {
return messageTypes.includes(ev.getType() as EventType) || M_POLL_START.matches(ev.getType());
return (
messageTypes.includes(ev.getType() as EventType) ||
M_POLL_START.matches(ev.getType()) ||
M_POLL_END.matches(ev.getType())
);
}

export function haveRendererForEvent(mxEvent: MatrixEvent, showHiddenEvents: boolean): boolean {
Expand Down
4 changes: 2 additions & 2 deletions src/events/forward/getForwardableEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

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

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

Expand Down
3 changes: 2 additions & 1 deletion src/utils/EventRenderingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ limitations under the License.

import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import { M_POLL_END, M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
import { IContent } from "matrix-js-sdk/src/matrix";

Expand All @@ -41,6 +41,7 @@ const calcIsInfoMessage = (
eventType !== EventType.Sticker &&
eventType !== EventType.RoomCreate &&
!M_POLL_START.matches(eventType) &&
!M_POLL_END.matches(eventType) &&
!M_BEACON_INFO.matches(eventType) &&
!(eventType === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started)
);
Expand Down
3 changes: 2 additions & 1 deletion src/utils/EventUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType, EVENT_VISIBILITY_CHANGE_TYPE, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import { M_POLL_END, M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import { M_LOCATION } from "matrix-js-sdk/src/@types/location";
import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
Expand Down Expand Up @@ -57,6 +57,7 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean {
} else if (
mxEvent.getType() === "m.sticker" ||
M_POLL_START.matches(mxEvent.getType()) ||
M_POLL_END.matches(mxEvent.getType()) ||
M_BEACON_INFO.matches(mxEvent.getType()) ||
(mxEvent.getType() === VoiceBroadcastInfoEventType &&
mxEvent.getContent()?.state === VoiceBroadcastInfoState.Started)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ describe("<PollHistoryDialog />", () => {
expect(getByText("There are no polls in this room")).toBeTruthy();
});

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

0 comments on commit 583050c

Please sign in to comment.