diff --git a/package.json b/package.json index e0a5bbb5f2..cab094aef8 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "lodash": "^4.17.20", "maplibre-gl": "^1.15.2", "matrix-analytics-events": "https://github.com/matrix-org/matrix-analytics-events.git#1eab4356548c97722a183912fda1ceabbe8cc7c1", - "matrix-events-sdk": "^0.0.1-beta.2", + "matrix-events-sdk": "^0.0.1-beta.6", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^0.1.0-beta.18", "minimist": "^1.2.5", diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 80826e1021..e83c9efab6 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -20,7 +20,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { removeDirectionOverrideChars } from 'matrix-js-sdk/src/utils'; import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials"; import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; -import { EmoteEvent, NoticeEvent, MessageEvent } from "matrix-events-sdk"; +import { M_EMOTE, M_NOTICE, M_MESSAGE, MessageEvent } from "matrix-events-sdk"; import { _t } from './languageHandler'; import * as Roles from './Roles'; @@ -342,11 +342,11 @@ function textForMessageEvent(ev: MatrixEvent): () => string | null { } if (SettingsStore.isEnabled("feature_extensible_events")) { - const extev = ev.unstableExtensibleEvent; + const extev = ev.unstableExtensibleEvent as MessageEvent; if (extev) { - if (extev instanceof EmoteEvent) { + if (extev.isEquivalentTo(M_EMOTE)) { return `* ${senderDisplayName} ${extev.text}`; - } else if (extev instanceof NoticeEvent || extev instanceof MessageEvent) { + } else if (extev.isEquivalentTo(M_NOTICE) || extev.isEquivalentTo(M_MESSAGE)) { return `${senderDisplayName}: ${extev.text}`; } } diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index e6dd95d189..05eac5f1c6 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -19,8 +19,8 @@ import React, { ReactElement } from 'react'; import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { EventType, RelationType } from "matrix-js-sdk/src/@types/event"; import { Relations } from 'matrix-js-sdk/src/models/relations'; -import { POLL_START_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls"; import { LOCATION_EVENT_TYPE } from 'matrix-js-sdk/src/@types/location'; +import { M_POLL_START } from "matrix-events-sdk"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; @@ -140,7 +140,7 @@ export default class MessageContextMenu extends React.Component private canEndPoll(mxEvent: MatrixEvent): boolean { return ( - POLL_START_EVENT_TYPE.matches(mxEvent.getType()) && + M_POLL_START.matches(mxEvent.getType()) && this.state.canRedact && !isPollEnded(mxEvent, MatrixClientPeg.get(), this.props.getRelationsForEvent) ); diff --git a/src/components/views/dialogs/EndPollDialog.tsx b/src/components/views/dialogs/EndPollDialog.tsx index f3403839b9..743ff89885 100644 --- a/src/components/views/dialogs/EndPollDialog.tsx +++ b/src/components/views/dialogs/EndPollDialog.tsx @@ -18,8 +18,7 @@ import React from "react"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { Relations } from "matrix-js-sdk/src/models/relations"; -import { IPollEndContent, POLL_END_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls"; -import { TEXT_NODE_TYPE } from "matrix-js-sdk/src/@types/extensible_events"; +import { PollEndEvent } from "matrix-events-sdk"; import { _t } from "../../../languageHandler"; import { IDialogProps } from "./IDialogProps"; @@ -57,17 +56,10 @@ export default class EndPollDialog extends React.Component { ); if (endPoll) { - const endContent: IPollEndContent = { - [POLL_END_EVENT_TYPE.name]: {}, - "m.relates_to": { - "event_id": this.props.event.getId(), - "rel_type": "m.reference", - }, - [TEXT_NODE_TYPE.name]: message, - }; + const endEvent = PollEndEvent.from(this.props.event.getId(), message).serialize(); this.props.matrixClient.sendEvent( - this.props.event.getRoomId(), POLL_END_EVENT_TYPE.name, endContent, + this.props.event.getRoomId(), endEvent.type, endEvent.content, ).catch((e: any) => { console.error("Failed to submit poll response event:", e); Modal.createTrackedDialog( diff --git a/src/components/views/elements/PollCreateDialog.tsx b/src/components/views/elements/PollCreateDialog.tsx index 73e552f772..655215542d 100644 --- a/src/components/views/elements/PollCreateDialog.tsx +++ b/src/components/views/elements/PollCreateDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 - 2022 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. @@ -16,8 +16,7 @@ limitations under the License. import React, { ChangeEvent, createRef } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; -import { makePollContent } from "matrix-js-sdk/src/content-helpers"; -import { POLL_KIND_DISCLOSED, POLL_START_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls"; +import { M_POLL_KIND_DISCLOSED, PollStartEvent } from "matrix-events-sdk"; import ScrollableBaseModal, { IScrollableBaseState } from "../dialogs/ScrollableBaseModal"; import { IDialogProps } from "../dialogs/IDialogProps"; @@ -99,13 +98,12 @@ export default class PollCreateDialog extends ScrollableBaseModal a.trim()).filter(a => !!a), + M_POLL_KIND_DISCLOSED, + ).serialize(); + this.matrixClient.sendEvent(this.props.room.roomId, pollEvent.type, pollEvent.content).then( () => this.props.onFinished(true), ).catch(e => { console.error("Failed to post poll:", e); diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index bc04338c83..1778d79f15 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -19,15 +19,16 @@ import classNames from 'classnames'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Relations } from 'matrix-js-sdk/src/models/relations'; import { MatrixClient } from 'matrix-js-sdk/src/matrix'; -import { TEXT_NODE_TYPE } from "matrix-js-sdk/src/@types/extensible_events"; import { - IPollAnswer, - IPollContent, - IPollResponseContent, - POLL_END_EVENT_TYPE, - POLL_RESPONSE_EVENT_TYPE, - POLL_START_EVENT_TYPE, -} from "matrix-js-sdk/src/@types/polls"; + M_POLL_END, + M_POLL_RESPONSE, + M_POLL_START, + NamespacedValue, + PollAnswerSubevent, + PollResponseEvent, + PollStartEvent, +} from "matrix-events-sdk"; +import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations"; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; @@ -40,8 +41,8 @@ import ErrorDialog from '../dialogs/ErrorDialog'; interface IState { selected?: string; // Which option was clicked by the local user - voteRelations: Relations; // Voting (response) events - endRelations: Relations; // Poll end events + voteRelations: RelatedRelations; // Voting (response) events + endRelations: RelatedRelations; // Poll end events } export function findTopAnswer( @@ -57,28 +58,41 @@ export function findTopAnswer( return ""; } - const pollContents: IPollContent = pollEvent.getContent(); + const poll = pollEvent.unstableExtensibleEvent as PollStartEvent; + if (!poll?.isEquivalentTo(M_POLL_START)) { + console.warn("Failed to parse poll to determine top answer - assuming no best answer"); + return ""; + } const findAnswerText = (answerId: string) => { - for (const answer of pollContents[POLL_START_EVENT_TYPE.name].answers) { - if (answer.id == answerId) { - return answer[TEXT_NODE_TYPE.name]; - } - } - return ""; + return poll.answers.find(a => a.id === answerId)?.text ?? ""; }; - const voteRelations: Relations = getRelationsForEvent( - pollEvent.getId(), - "m.reference", - POLL_RESPONSE_EVENT_TYPE.name, - ); + const voteRelations = new RelatedRelations([ + getRelationsForEvent( + pollEvent.getId(), + "m.reference", + M_POLL_RESPONSE.name, + ), + getRelationsForEvent( + pollEvent.getId(), + "m.reference", + M_POLL_RESPONSE.altName, + ), + ]); - const endRelations: Relations = getRelationsForEvent( - pollEvent.getId(), - "m.reference", - POLL_END_EVENT_TYPE.name, - ); + const endRelations = new RelatedRelations([ + getRelationsForEvent( + pollEvent.getId(), + "m.reference", + M_POLL_END.name, + ), + getRelationsForEvent( + pollEvent.getId(), + "m.reference", + M_POLL_END.altName, + ), + ]); const userVotes: Map = collectUserVotes( allVotes(pollEvent, matrixClient, voteRelations, endRelations), @@ -86,7 +100,7 @@ export function findTopAnswer( null, ); - const votes: Map = countVotes(userVotes, pollEvent.getContent()); + const votes: Map = countVotes(userVotes, poll); const highestScore: number = Math.max(...votes.values()); const bestAnswerIds: string[] = []; @@ -122,11 +136,18 @@ export function isPollEnded( ); } - const endRelations = getRelationsForEvent( - pollEvent.getId(), - "m.reference", - POLL_END_EVENT_TYPE.name, - ); + const endRelations = new RelatedRelations([ + getRelationsForEvent( + pollEvent.getId(), + "m.reference", + M_POLL_END.name, + ), + getRelationsForEvent( + pollEvent.getId(), + "m.reference", + M_POLL_END.altName, + ), + ]); if (!endRelations) { return false; @@ -163,7 +184,7 @@ export default class MPollBody extends React.Component { this.removeListeners(this.state.voteRelations, this.state.endRelations); } - private addListeners(voteRelations?: Relations, endRelations?: Relations) { + private addListeners(voteRelations?: RelatedRelations, endRelations?: RelatedRelations) { if (voteRelations) { voteRelations.on("Relations.add", this.onRelationsChange); voteRelations.on("Relations.remove", this.onRelationsChange); @@ -176,7 +197,7 @@ export default class MPollBody extends React.Component { } } - private removeListeners(voteRelations?: Relations, endRelations?: Relations) { + private removeListeners(voteRelations?: RelatedRelations, endRelations?: RelatedRelations) { if (voteRelations) { voteRelations.off("Relations.add", this.onRelationsChange); voteRelations.off("Relations.remove", this.onRelationsChange); @@ -194,13 +215,13 @@ export default class MPollBody extends React.Component { return; } - if (POLL_RESPONSE_EVENT_TYPE.matches(eventType)) { + if (M_POLL_RESPONSE.matches(eventType)) { this.voteRelationsReceived = true; const newVoteRelations = this.fetchVoteRelations(); this.addListeners(newVoteRelations); this.removeListeners(this.state.voteRelations); this.setState({ voteRelations: newVoteRelations }); - } else if (POLL_END_EVENT_TYPE.matches(eventType)) { + } else if (M_POLL_END.matches(eventType)) { this.endRelationsReceived = true; const newEndRelations = this.fetchEndRelations(); this.addListeners(newEndRelations); @@ -233,20 +254,12 @@ export default class MPollBody extends React.Component { return; } - const responseContent: IPollResponseContent = { - [POLL_RESPONSE_EVENT_TYPE.name]: { - "answers": [answerId], - }, - "m.relates_to": { - "event_id": this.props.mxEvent.getId(), - "rel_type": "m.reference", - }, - }; + const response = PollResponseEvent.from([answerId], this.props.mxEvent.getId()).serialize(); this.context.sendEvent( this.props.mxEvent.getRoomId(), - POLL_RESPONSE_EVENT_TYPE.name, - responseContent, + response.type, + response.content, ).catch((e: any) => { console.error("Failed to submit poll response event:", e); @@ -269,21 +282,28 @@ export default class MPollBody extends React.Component { this.selectOption(e.currentTarget.value); }; - private fetchVoteRelations(): Relations | null { - return this.fetchRelations(POLL_RESPONSE_EVENT_TYPE.name); + private fetchVoteRelations(): RelatedRelations | null { + return this.fetchRelations(M_POLL_RESPONSE); } - private fetchEndRelations(): Relations | null { - return this.fetchRelations(POLL_END_EVENT_TYPE.name); + private fetchEndRelations(): RelatedRelations | null { + return this.fetchRelations(M_POLL_END); } - private fetchRelations(eventType: string): Relations | null { + private fetchRelations(eventType: NamespacedValue): RelatedRelations | null { if (this.props.getRelationsForEvent) { - return this.props.getRelationsForEvent( - this.props.mxEvent.getId(), - "m.reference", - eventType, - ); + return new RelatedRelations([ + this.props.getRelationsForEvent( + this.props.mxEvent.getId(), + "m.reference", + eventType.name, + ), + this.props.getRelationsForEvent( + this.props.mxEvent.getId(), + "m.reference", + eventType.altName, + ), + ]); } else { return null; } @@ -349,17 +369,13 @@ export default class MPollBody extends React.Component { } render() { - const pollStart: IPollContent = this.props.mxEvent.getContent(); - const pollInfo = pollStart[POLL_START_EVENT_TYPE.name]; - - if (pollInfo.answers.length < 1 || pollInfo.answers.length > 20) { - return null; - } + const poll = this.props.mxEvent.unstableExtensibleEvent as PollStartEvent; + if (!poll?.isEquivalentTo(M_POLL_START)) return null; // invalid const ended = this.isEnded(); const pollId = this.props.mxEvent.getId(); const userVotes = this.collectUserVotes(); - const votes = countVotes(userVotes, this.props.mxEvent.getContent()); + const votes = countVotes(userVotes, poll); const totalVotes = this.totalVotes(votes); const winCount = Math.max(...votes.values()); const userId = this.context.getUserId(); @@ -385,10 +401,10 @@ export default class MPollBody extends React.Component { } return
-

{ pollInfo.question[TEXT_NODE_TYPE.name] }

+

{ poll.question.text }

{ - pollInfo.answers.map((answer: IPollAnswer) => { + poll.answers.map((answer: PollAnswerSubevent) => { let answerVotes = 0; let votesText = ""; @@ -448,7 +464,7 @@ export default class MPollBody extends React.Component { } interface IEndedPollOptionProps { - answer: IPollAnswer; + answer: PollAnswerSubevent; checked: boolean; votesText: string; } @@ -461,7 +477,7 @@ function EndedPollOption(props: IEndedPollOptionProps) { return
- { props.answer[TEXT_NODE_TYPE.name] } + { props.answer.text }
{ props.votesText } @@ -472,7 +488,7 @@ function EndedPollOption(props: IEndedPollOptionProps) { interface ILivePollOptionProps { pollId: string; - answer: IPollAnswer; + answer: PollAnswerSubevent; checked: boolean; votesText: string; onOptionSelected: (e: React.FormEvent) => void; @@ -487,7 +503,7 @@ function LivePollOption(props: ILivePollOptionProps) { >
- { props.answer[TEXT_NODE_TYPE.name] } + { props.answer.text }
{ props.votesText } @@ -502,21 +518,23 @@ export class UserVote { } function userResponseFromPollResponseEvent(event: MatrixEvent): UserVote { - const pr = event.getContent() as IPollResponseContent; - const answers = pr[POLL_RESPONSE_EVENT_TYPE.name].answers; + const response = event.unstableExtensibleEvent as PollResponseEvent; + if (!response?.isEquivalentTo(M_POLL_RESPONSE)) { + throw new Error("Failed to parse Poll Response Event to determine user response"); + } return new UserVote( event.getTs(), event.getSender(), - answers, + response.answerIds, ); } export function allVotes( pollEvent: MatrixEvent, matrixClient: MatrixClient, - voteRelations: Relations, - endRelations: Relations, + voteRelations: RelatedRelations, + endRelations: RelatedRelations, ): Array { const endTs = pollEndTs(pollEvent, matrixClient, endRelations); @@ -546,7 +564,7 @@ export function allVotes( export function pollEndTs( pollEvent: MatrixEvent, matrixClient: MatrixClient, - endRelations: Relations, + endRelations: RelatedRelations, ): number | null { if (!endRelations) { return null; @@ -575,10 +593,7 @@ export function pollEndTs( } function isPollResponse(responseEvent: MatrixEvent): boolean { - return ( - POLL_RESPONSE_EVENT_TYPE.matches(responseEvent.getType()) && - POLL_RESPONSE_EVENT_TYPE.findIn(responseEvent.getContent()) - ); + return responseEvent.unstableExtensibleEvent?.isEquivalentTo(M_POLL_RESPONSE); } /** @@ -608,24 +623,15 @@ function collectUserVotes( function countVotes( userVotes: Map, - pollStart: IPollContent, + pollStart: PollStartEvent, ): Map { const collected = new Map(); - const pollInfo = pollStart[POLL_START_EVENT_TYPE.name]; - const maxSelections = 1; // See MSC3381 - later this will be in pollInfo - - const allowedAnswerIds = pollInfo.answers.map((ans: IPollAnswer) => ans.id); - function isValidAnswer(answerId: string) { - return allowedAnswerIds.includes(answerId); - } - for (const response of userVotes.values()) { - if (response.answers.every(isValidAnswer)) { - for (const [index, answerId] of response.answers.entries()) { - if (index >= maxSelections) { - break; - } + const tempResponse = PollResponseEvent.from(response.answers, "$irrelevant"); + tempResponse.validateAgainst(pollStart); + if (!tempResponse.spoiled) { + for (const answerId of tempResponse.answerIds) { if (collected.has(answerId)) { collected.set(answerId, collected.get(answerId) + 1); } else { diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 57aea41707..da4bda1b2f 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -17,8 +17,8 @@ limitations under the License. import React, { createRef } from 'react'; import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; import { Relations } from 'matrix-js-sdk/src/models/relations'; -import { POLL_START_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls"; import { LOCATION_EVENT_TYPE } from 'matrix-js-sdk/src/@types/location'; +import { M_POLL_START } from "matrix-events-sdk"; import * as sdk from '../../../index'; import SettingsStore from "../../../settings/SettingsStore"; @@ -125,7 +125,7 @@ export default class MessageEvent extends React.Component implements IMe BodyType = UnknownBody; } - if (type && type === POLL_START_EVENT_TYPE.name) { + if (M_POLL_START.matches(type)) { // TODO: this can all disappear when Polls comes out of labs - // instead, add something like this into this.evTypes: // [EventType.Poll]: "messages.MPollBody" diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 874d1f8ea1..8ae5a785dd 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -18,7 +18,7 @@ import React, { createRef, SyntheticEvent } from 'react'; import ReactDOM from 'react-dom'; import highlight from 'highlight.js'; import { MsgType } from "matrix-js-sdk/src/@types/event"; -import { isEventLike, LegacyMsgType, MessageEvent } from "matrix-events-sdk"; +import { isEventLike, LegacyMsgType, M_MESSAGE, MessageEvent } from "matrix-events-sdk"; import * as HtmlUtils from '../../../HtmlUtils'; import { formatDate } from '../../../DateUtils'; @@ -542,8 +542,8 @@ export default class TextualBody extends React.Component { const stripReply = !mxEvent.replacingEvent() && !!ReplyChain.getParentEventId(mxEvent); let body; if (SettingsStore.isEnabled("feature_extensible_events")) { - const extev = this.props.mxEvent.unstableExtensibleEvent; - if (extev && extev instanceof MessageEvent) { + const extev = this.props.mxEvent.unstableExtensibleEvent as MessageEvent; + if (extev?.isEquivalentTo(M_MESSAGE)) { isEmote = isEventLike(extev.wireFormat, LegacyMsgType.Emote); isNotice = isEventLike(extev.wireFormat, LegacyMsgType.Notice); body = HtmlUtils.bodyToHtml({ diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 1aae2be9a1..0118f58757 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -24,7 +24,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread'; import { logger } from "matrix-js-sdk/src/logger"; import { NotificationCountType, Room } from 'matrix-js-sdk/src/models/room'; -import { POLL_START_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls"; +import { M_POLL_START } from "matrix-events-sdk"; import ReplyChain from "../elements/ReplyChain"; import { _t } from '../../../languageHandler'; @@ -78,7 +78,8 @@ import { CardContext } from '../right_panel/BaseCard'; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', [EventType.Sticker]: 'messages.MessageEvent', - [POLL_START_EVENT_TYPE.name]: 'messages.MessageEvent', + [M_POLL_START.name]: 'messages.MessageEvent', + [M_POLL_START.altName]: 'messages.MessageEvent', [EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion', [EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion', [EventType.CallInvite]: 'messages.CallEvent', @@ -178,7 +179,7 @@ export function getHandlerTile(ev: MatrixEvent): string { } if ( - POLL_START_EVENT_TYPE.matches(type) && + M_POLL_START.matches(type) && !SettingsStore.getValue("feature_polls") ) { return undefined; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 8f737df273..0e1ab93fe8 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -19,7 +19,7 @@ import { MatrixEvent, IEventRelation } from "matrix-js-sdk/src/models/event"; import { Room } from "matrix-js-sdk/src/models/room"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RelationType } from 'matrix-js-sdk/src/@types/event'; -import { POLL_START_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls"; +import { M_POLL_START } from "matrix-events-sdk"; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; @@ -197,7 +197,7 @@ interface IPollButtonProps extends Pick { class PollButton extends React.PureComponent { private onCreateClick = () => { const canSend = this.props.room.currentState.maySendEvent( - POLL_START_EVENT_TYPE.name, + M_POLL_START.name, MatrixClientPeg.get().getUserId(), ); if (!canSend) { diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index edf7b3cc31..737ddfb2c1 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -17,7 +17,7 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { POLL_START_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls"; +import { M_POLL_START } from "matrix-events-sdk"; import { ActionPayload } from "../../dispatcher/payloads"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; @@ -68,7 +68,11 @@ function previews(): Object { // TODO: when polls comes out of labs, add this to PREVIEWS if (SettingsStore.getValue("feature_polls")) { return { - [POLL_START_EVENT_TYPE.name]: { + [M_POLL_START.name]: { + isState: false, + previewer: new PollStartEventPreview(), + }, + [M_POLL_START.altName]: { isState: false, previewer: new PollStartEventPreview(), }, diff --git a/src/stores/room-list/previews/PollStartEventPreview.ts b/src/stores/room-list/previews/PollStartEventPreview.ts index b473916bf8..a795b5714a 100644 --- a/src/stores/room-list/previews/PollStartEventPreview.ts +++ b/src/stores/room-list/previews/PollStartEventPreview.ts @@ -15,8 +15,7 @@ limitations under the License. */ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { POLL_START_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls"; -import { TEXT_NODE_TYPE } from "matrix-js-sdk/src/@types/extensible_events"; +import { InvalidEventError, M_POLL_START_EVENT_CONTENT, PollStartEvent } from "matrix-events-sdk"; import { IPreview } from "./IPreview"; import { TagID } from "../models"; @@ -37,25 +36,31 @@ export class PollStartEventPreview implements IPreview { } // Check we have the information we need, and bail out if not - if (!eventContent || !eventContent[POLL_START_EVENT_TYPE.name]) { + if (!eventContent) { return null; } - let question = - eventContent[POLL_START_EVENT_TYPE.name].question[TEXT_NODE_TYPE.name]; - question = (question || '').trim(); - question = sanitizeForTranslation(question); + try { + const poll = new PollStartEvent({ + type: event.getType(), + content: eventContent as M_POLL_START_EVENT_CONTENT, + }); - if ( - isThread || - isSelf(event) || - !shouldPrefixMessagesIn(event.getRoomId(), tagId) - ) { - return question; - } else { - return _t("%(senderName)s: %(message)s", - { senderName: getSenderName(event), message: question }, - ); + let question = poll.question.text.trim(); + question = sanitizeForTranslation(question); + + if (isThread || isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) { + return question; + } else { + return _t("%(senderName)s: %(message)s", + { senderName: getSenderName(event), message: question }, + ); + } + } catch (e) { + if (e instanceof InvalidEventError) { + return null; + } + throw e; // re-throw unknown errors } } } diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 21f75fab80..187ee23784 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -18,8 +18,8 @@ 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 { POLL_START_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls"; import { LOCATION_EVENT_TYPE } from 'matrix-js-sdk/src/@types/location'; +import { M_POLL_START } from "matrix-events-sdk"; import { MatrixClientPeg } from '../MatrixClientPeg'; import shouldHideEvent from "../shouldHideEvent"; @@ -48,7 +48,7 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean { } } else if ( mxEvent.getType() === 'm.sticker' || - POLL_START_EVENT_TYPE.matches(mxEvent.getType()) + M_POLL_START.matches(mxEvent.getType()) ) { return true; } @@ -228,11 +228,11 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent): { eventType !== EventType.RoomMessage && eventType !== EventType.Sticker && eventType !== EventType.RoomCreate && - !POLL_START_EVENT_TYPE.matches(eventType) + !M_POLL_START.matches(eventType) ); // Some non-info messages want to be rendered in the appropriate bubble column but without the bubble background const noBubbleEvent = ( - POLL_START_EVENT_TYPE.matches(eventType) || + M_POLL_START.matches(eventType) || LOCATION_EVENT_TYPE.matches(eventType) || ( eventType === EventType.RoomMessage && diff --git a/test/components/views/messages/MPollBody-test.tsx b/test/components/views/messages/MPollBody-test.tsx index 66ae8382ba..debeae09e7 100644 --- a/test/components/views/messages/MPollBody-test.tsx +++ b/test/components/views/messages/MPollBody-test.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 - 2022 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. @@ -19,13 +19,16 @@ import { mount, ReactWrapper } from "enzyme"; import { Callback, IContent, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk"; import { ISendEventResponse } from "matrix-js-sdk/src/@types/requests"; import { Relations } from "matrix-js-sdk/src/models/relations"; +import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations"; import { - IPollAnswer, - IPollContent, - POLL_END_EVENT_TYPE, - POLL_RESPONSE_EVENT_TYPE, -} from "matrix-js-sdk/src/@types/polls"; -import { TEXT_NODE_TYPE } from "matrix-js-sdk/src/@types/extensible_events"; + M_POLL_END, + M_POLL_KIND_DISCLOSED, + M_POLL_RESPONSE, + M_POLL_START, + M_POLL_START_EVENT_CONTENT, + M_TEXT, + POLL_ANSWER, +} from "matrix-events-sdk"; import * as TestUtils from "../../../test-utils"; import sdk from "../../../skinned-sdk"; @@ -56,8 +59,8 @@ describe("MPollBody", () => { allVotes( { getRoomId: () => "$room" } as MatrixEvent, MatrixClientPeg.get(), - newVoteRelations([]), - newEndRelations([]), + new RelatedRelations([newVoteRelations([])]), + new RelatedRelations([newEndRelations([])]), ), ).toEqual([]); }); @@ -67,34 +70,43 @@ describe("MPollBody", () => { const ev2 = responseEvent(); const badEvent = badResponseEvent(); - const voteRelations = newVoteRelations([ev1, badEvent, ev2]); + const voteRelations = new RelatedRelations([ + newVoteRelations([ev1, badEvent, ev2]), + ]); expect( allVotes( { getRoomId: () => "$room" } as MatrixEvent, MatrixClientPeg.get(), voteRelations, - newEndRelations([]), + new RelatedRelations([newEndRelations([])]), ), ).toEqual([ new UserVote( ev1.getTs(), ev1.getSender(), - ev1.getContent()[POLL_RESPONSE_EVENT_TYPE.name].answers, + ev1.getContent()[M_POLL_RESPONSE.name].answers, + ), + new UserVote( + badEvent.getTs(), + badEvent.getSender(), + [], // should be spoiled ), new UserVote( ev2.getTs(), ev2.getSender(), - ev2.getContent()[POLL_RESPONSE_EVENT_TYPE.name].answers, + ev2.getContent()[M_POLL_RESPONSE.name].answers, ), ]); }); it("finds the first end poll event", () => { - const endRelations = newEndRelations([ - endEvent("@me:example.com", 25), - endEvent("@me:example.com", 12), - endEvent("@me:example.com", 45), - endEvent("@me:example.com", 13), + const endRelations = new RelatedRelations([ + newEndRelations([ + endEvent("@me:example.com", 25), + endEvent("@me:example.com", 12), + endEvent("@me:example.com", 45), + endEvent("@me:example.com", 13), + ]), ]); const matrixClient = TestUtils.createTestClient(); @@ -110,11 +122,13 @@ describe("MPollBody", () => { }); it("ignores unauthorised end poll event when finding end ts", () => { - const endRelations = newEndRelations([ - endEvent("@me:example.com", 25), - endEvent("@unauthorised:example.com", 12), - endEvent("@me:example.com", 45), - endEvent("@me:example.com", 13), + const endRelations = new RelatedRelations([ + newEndRelations([ + endEvent("@me:example.com", 25), + endEvent("@unauthorised:example.com", 12), + endEvent("@me:example.com", 45), + endEvent("@me:example.com", 13), + ]), ]); const matrixClient = TestUtils.createTestClient(); @@ -130,15 +144,19 @@ describe("MPollBody", () => { }); it("counts only votes before the end poll event", () => { - const voteRelations = newVoteRelations([ - responseEvent("sf@matrix.org", "wings", 13), - responseEvent("jr@matrix.org", "poutine", 40), - responseEvent("ak@matrix.org", "poutine", 37), - responseEvent("id@matrix.org", "wings", 13), - responseEvent("ps@matrix.org", "wings", 19), + const voteRelations = new RelatedRelations([ + newVoteRelations([ + responseEvent("sf@matrix.org", "wings", 13), + responseEvent("jr@matrix.org", "poutine", 40), + responseEvent("ak@matrix.org", "poutine", 37), + responseEvent("id@matrix.org", "wings", 13), + responseEvent("ps@matrix.org", "wings", 19), + ]), ]); - const endRelations = newEndRelations([ - endEvent("@me:example.com", 25), + const endRelations = new RelatedRelations([ + newEndRelations([ + endEvent("@me:example.com", 25), + ]), ]); expect( allVotes( @@ -298,7 +316,7 @@ describe("MPollBody", () => { const body = newMPollBody(votes); const props: IBodyProps = body.instance().props as IBodyProps; const voteRelations: Relations = props.getRelationsForEvent( - "$mypoll", "m.reference", POLL_RESPONSE_EVENT_TYPE.name); + "$mypoll", "m.reference", M_POLL_RESPONSE.name); clickRadio(body, "pizza"); // When a new vote from me comes in @@ -319,7 +337,7 @@ describe("MPollBody", () => { const body = newMPollBody(votes); const props: IBodyProps = body.instance().props as IBodyProps; const voteRelations: Relations = props.getRelationsForEvent( - "$mypoll", "m.reference", POLL_RESPONSE_EVENT_TYPE.name); + "$mypoll", "m.reference", M_POLL_RESPONSE.name); clickRadio(body, "pizza"); // When a new vote from someone else comes in @@ -400,7 +418,7 @@ describe("MPollBody", () => { }); it("treats any invalid answer as a spoiled ballot", () => { - // Note that tr's second vote has a valid first answer, but + // Note that uy's second vote has a valid first answer, but // the ballot is still spoiled because the second answer is // invalid, even though we would ignore it if we continued. const votes = [ @@ -442,14 +460,16 @@ describe("MPollBody", () => { expect(body.html()).toBe(""); }); - it("renders nothing if poll has more than 20 answers", () => { - const answers = [...Array(21).keys()].map((i) => { - return { "id": `id${i}`, "org.matrix.msc1767.text": `Name ${i}` }; + it("renders the first 20 answers if 21 were given", () => { + const answers = Array.from(Array(21).keys()).map((i) => { + return { "id": `id${i}`, [M_TEXT.name]: `Name ${i}` }; }); const votes = []; const ends = []; const body = newMPollBody(votes, ends, answers); - expect(body.html()).toBe(""); + expect( + body.find('.mx_MPollBody_option').length, + ).toBe(20); }); it("sends a vote event when I choose an option", () => { @@ -835,7 +855,7 @@ describe("MPollBody", () => { (eventId: string, relationType: string, eventType: string) => { expect(eventId).toBe("$mypoll"); expect(relationType).toBe("m.reference"); - expect(eventType).toBe(POLL_END_EVENT_TYPE.name); + expect(M_POLL_END.matches(eventType)).toBe(true); return undefined; }; expect( @@ -926,11 +946,11 @@ describe("MPollBody", () => { }); function newVoteRelations(relationEvents: Array): Relations { - return newRelations(relationEvents, POLL_RESPONSE_EVENT_TYPE.name); + return newRelations(relationEvents, M_POLL_RESPONSE.name); } function newEndRelations(relationEvents: Array): Relations { - return newRelations(relationEvents, POLL_END_EVENT_TYPE.name); + return newRelations(relationEvents, M_POLL_END.name); } function newRelations( @@ -947,22 +967,14 @@ function newRelations( function newMPollBody( relationEvents: Array, endEvents: Array = [], - answers?: IPollAnswer[], + answers?: POLL_ANSWER[], ): ReactWrapper { - const voteRelations = new Relations( - "m.reference", POLL_RESPONSE_EVENT_TYPE.name, null); - for (const ev of relationEvents) { - voteRelations.addEvent(ev); - } - - const endRelations = new Relations( - "m.reference", POLL_END_EVENT_TYPE.name, null); - for (const ev of endEvents) { - endRelations.addEvent(ev); - } + const voteRelations = newVoteRelations(relationEvents); + const endRelations = newEndRelations(endEvents); return mount( { expect(eventId).toBe("$mypoll"); expect(relationType).toBe("m.reference"); - if (POLL_RESPONSE_EVENT_TYPE.matches(eventType)) { + if (M_POLL_RESPONSE.matches(eventType)) { return voteRelations; - } else if (POLL_END_EVENT_TYPE.matches(eventType)) { + } else if (M_POLL_END.matches(eventType)) { return endRelations; } else { fail("Unexpected eventType: " + eventType); @@ -1023,25 +1035,25 @@ function endedVotesCount(wrapper: ReactWrapper, value: string): string { ).text(); } -function newPollStart(answers?: IPollAnswer[]): IPollContent { +function newPollStart(answers?: POLL_ANSWER[]): M_POLL_START_EVENT_CONTENT { if (!answers) { answers = [ - { "id": "pizza", "org.matrix.msc1767.text": "Pizza" }, - { "id": "poutine", "org.matrix.msc1767.text": "Poutine" }, - { "id": "italian", "org.matrix.msc1767.text": "Italian" }, - { "id": "wings", "org.matrix.msc1767.text": "Wings" }, + { "id": "pizza", [M_TEXT.name]: "Pizza" }, + { "id": "poutine", [M_TEXT.name]: "Poutine" }, + { "id": "italian", [M_TEXT.name]: "Italian" }, + { "id": "wings", [M_TEXT.name]: "Wings" }, ]; } return { - "org.matrix.msc3381.poll.start": { + [M_POLL_START.name]: { "question": { - "org.matrix.msc1767.text": "What should we order for the party?", + [M_TEXT.name]: "What should we order for the party?", }, - "kind": "org.matrix.msc3381.poll.disclosed", + "kind": M_POLL_KIND_DISCLOSED.name, "answers": answers, }, - "org.matrix.msc1767.text": "What should we order for the party?\n" + + [M_TEXT.name]: "What should we order for the party?\n" + "1. Pizza\n2. Poutine\n3. Italian\n4. Wings", }; } @@ -1050,7 +1062,8 @@ function badResponseEvent(): MatrixEvent { return new MatrixEvent( { "event_id": nextId(), - "type": POLL_RESPONSE_EVENT_TYPE.name, + "type": M_POLL_RESPONSE.name, + "sender": "@malicious:example.com", "content": { "m.relates_to": { "rel_type": "m.reference", @@ -1073,14 +1086,14 @@ function responseEvent( "event_id": nextId(), "room_id": "#myroom:example.com", "origin_server_ts": ts, - "type": POLL_RESPONSE_EVENT_TYPE.name, + "type": M_POLL_RESPONSE.name, "sender": sender, "content": { "m.relates_to": { "rel_type": "m.reference", "event_id": "$mypoll", }, - [POLL_RESPONSE_EVENT_TYPE.name]: { + [M_POLL_RESPONSE.name]: { "answers": ans, }, }, @@ -1091,7 +1104,7 @@ function responseEvent( function expectedResponseEvent(answer: string) { return { "content": { - [POLL_RESPONSE_EVENT_TYPE.name]: { + [M_POLL_RESPONSE.name]: { "answers": [answer], }, "m.relates_to": { @@ -1099,7 +1112,7 @@ function expectedResponseEvent(answer: string) { "rel_type": "m.reference", }, }, - "eventType": POLL_RESPONSE_EVENT_TYPE.name, + "eventType": M_POLL_RESPONSE.name, "roomId": "#myroom:example.com", "txnId": undefined, "callback": undefined, @@ -1115,15 +1128,15 @@ function endEvent( "event_id": nextId(), "room_id": "#myroom:example.com", "origin_server_ts": ts, - "type": POLL_END_EVENT_TYPE.name, + "type": M_POLL_END.name, "sender": sender, "content": { "m.relates_to": { "rel_type": "m.reference", "event_id": "$mypoll", }, - [POLL_END_EVENT_TYPE.name]: {}, - [TEXT_NODE_TYPE.name]: "The poll has ended. Something.", + [M_POLL_END.name]: {}, + [M_TEXT.name]: "The poll has ended. Something.", }, }, ); @@ -1133,6 +1146,7 @@ function runIsPollEnded(ends: MatrixEvent[]) { const pollEvent = new MatrixEvent({ "event_id": "$mypoll", "room_id": "#myroom:example.com", + "type": M_POLL_START.name, "content": newPollStart(), }); @@ -1143,7 +1157,7 @@ function runIsPollEnded(ends: MatrixEvent[]) { (eventId: string, relationType: string, eventType: string) => { expect(eventId).toBe("$mypoll"); expect(relationType).toBe("m.reference"); - expect(eventType).toBe(POLL_END_EVENT_TYPE.name); + expect(M_POLL_END.matches(eventType)).toBe(true); return newEndRelations(ends); }; @@ -1154,6 +1168,7 @@ function runFindTopAnswer(votes: MatrixEvent[], ends: MatrixEvent[]) { const pollEvent = new MatrixEvent({ "event_id": "$mypoll", "room_id": "#myroom:example.com", + "type": M_POLL_START.name, "content": newPollStart(), }); @@ -1161,9 +1176,9 @@ function runFindTopAnswer(votes: MatrixEvent[], ends: MatrixEvent[]) { (eventId: string, relationType: string, eventType: string) => { expect(eventId).toBe("$mypoll"); expect(relationType).toBe("m.reference"); - if (POLL_RESPONSE_EVENT_TYPE.matches(eventType)) { + if (M_POLL_RESPONSE.matches(eventType)) { return newVoteRelations(votes); - } else if (POLL_END_EVENT_TYPE.matches(eventType)) { + } else if (M_POLL_END.matches(eventType)) { return newEndRelations(ends); } else { fail(`eventType should be end or vote but was ${eventType}`); diff --git a/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap b/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap index 7385d0f463..b969af06bb 100644 --- a/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap +++ b/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap @@ -38,6 +38,7 @@ exports[`MPollBody renders a finished poll 1`] = ` }, "event_id": "$mypoll", "room_id": "#myroom:example.com", + "type": "org.matrix.msc3381.poll.start", } } > @@ -78,6 +79,7 @@ exports[`MPollBody renders a finished poll 1`] = ` }, "event_id": "$mypoll", "room_id": "#myroom:example.com", + "type": "org.matrix.msc3381.poll.start", } } > @@ -97,9 +99,23 @@ exports[`MPollBody renders a finished poll 1`] = ` > @@ -371,6 +430,7 @@ exports[`MPollBody renders a finished poll with multiple winners 1`] = ` }, "event_id": "$mypoll", "room_id": "#myroom:example.com", + "type": "org.matrix.msc3381.poll.start", } } > @@ -390,9 +450,23 @@ exports[`MPollBody renders a finished poll with multiple winners 1`] = ` > @@ -664,6 +781,7 @@ exports[`MPollBody renders a finished poll with no votes 1`] = ` }, "event_id": "$mypoll", "room_id": "#myroom:example.com", + "type": "org.matrix.msc3381.poll.start", } } > @@ -683,9 +801,23 @@ exports[`MPollBody renders a finished poll with no votes 1`] = ` > @@ -957,6 +1132,7 @@ exports[`MPollBody renders a poll that I have not voted in 1`] = ` }, "event_id": "$mypoll", "room_id": "#myroom:example.com", + "type": "org.matrix.msc3381.poll.start", } } > @@ -976,9 +1152,23 @@ exports[`MPollBody renders a poll that I have not voted in 1`] = ` > @@ -1350,6 +1583,7 @@ exports[`MPollBody renders a poll with local, non-local and invalid votes 1`] = }, "event_id": "$mypoll", "room_id": "#myroom:example.com", + "type": "org.matrix.msc3381.poll.start", } } > @@ -1369,9 +1603,23 @@ exports[`MPollBody renders a poll with local, non-local and invalid votes 1`] = > @@ -1751,6 +2042,7 @@ exports[`MPollBody renders a poll with no votes 1`] = ` }, "event_id": "$mypoll", "room_id": "#myroom:example.com", + "type": "org.matrix.msc3381.poll.start", } } > @@ -1770,9 +2062,23 @@ exports[`MPollBody renders a poll with no votes 1`] = ` > @@ -2144,6 +2493,7 @@ exports[`MPollBody renders a poll with only non-local votes 1`] = ` }, "event_id": "$mypoll", "room_id": "#myroom:example.com", + "type": "org.matrix.msc3381.poll.start", } } > @@ -2163,9 +2513,23 @@ exports[`MPollBody renders a poll with only non-local votes 1`] = ` > { function newPollStartEvent( question: string, sender: string, - answers?: IPollAnswer[], + answers?: POLL_ANSWER[], ): MatrixEvent { if (!answers) { answers = [ - { "id": "socks", "org.matrix.msc1767.text": "Socks" }, - { "id": "shoes", "org.matrix.msc1767.text": "Shoes" }, + { "id": "socks", [M_TEXT.name]: "Socks" }, + { "id": "shoes", [M_TEXT.name]: "Shoes" }, ]; } @@ -55,15 +55,16 @@ function newPollStartEvent( "event_id": "$mypoll", "room_id": "#myroom:example.com", "sender": sender, + "type": M_POLL_START.name, "content": { - "org.matrix.msc3381.poll.start": { + [M_POLL_START.name]: { "question": { - "org.matrix.msc1767.text": question, + [M_TEXT.name]: question, }, - "kind": "org.matrix.msc3381.poll.disclosed", + "kind": M_POLL_KIND_DISCLOSED.name, "answers": answers, }, - "org.matrix.msc1767.text": `${question}: answers`, + [M_TEXT.name]: `${question}: answers`, }, }, ); diff --git a/yarn.lock b/yarn.lock index 8739b117a5..4fd8bf3e16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6163,10 +6163,10 @@ mathml-tag-names@^2.1.3: version "0.0.1" resolved "https://github.com/matrix-org/matrix-analytics-events.git#1eab4356548c97722a183912fda1ceabbe8cc7c1" -matrix-events-sdk@^0.0.1-beta.2: - version "0.0.1-beta.2" - resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.2.tgz#28efdcc3259152c4d53094cedb72b3843e5f772e" - integrity sha512-a3VIZeb9IxxxPrvFnUbt4pjP7A6irv7eWLv1GBoq+80m7v5n3QhzT/mmeUGJx2KNt7jLboFau4g1iIU82H3wEg== +matrix-events-sdk@^0.0.1-beta.6: + version "0.0.1-beta.6" + resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.6.tgz#9001090ed2e2bf29efc113d6b29871bcc6520749" + integrity sha512-VMqPXe3Bg4R9yC9PNqGv6bDFwWlVYadYxp0Ke1ihhXUCpGcx7e28kOYcqK2T3RxLXK4KK7VH4JRbY53Do3r+Fw== "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "15.3.0"