@@ -32,7 +32,14 @@ import {
3232 type IRoomEvent ,
3333} from "matrix-widget-api" ;
3434
35- import { createRoomWidgetClient , MatrixError , MsgType , UpdateDelayedEventAction } from "../../src/matrix" ;
35+ import {
36+ createRoomWidgetClient ,
37+ EventType ,
38+ type IEvent ,
39+ MatrixError ,
40+ MsgType ,
41+ UpdateDelayedEventAction ,
42+ } from "../../src/matrix" ;
3643import { MatrixClient , ClientEvent , type ITurnServer as IClientTurnServer } from "../../src/client" ;
3744import { SyncState } from "../../src/sync" ;
3845import { type ICapabilities , type RoomWidgetClient } from "../../src/embedded" ;
@@ -42,13 +49,15 @@ import { sleep } from "../../src/utils";
4249import { SlidingSync } from "../../src/sliding-sync" ;
4350import { logger } from "../../src/logger" ;
4451import { flushPromises } from "../test-utils/flushPromises" ;
52+ import { RoomStickyEventsEvent , type RoomStickyEventsMap } from "../../src/models/room-sticky-events" ;
4553
4654const testOIDCToken = {
4755 access_token : "12345678" ,
4856 expires_in : "10" ,
4957 matrix_server_name : "homeserver.oabc" ,
5058 token_type : "Bearer" ,
5159} ;
60+
5261class MockWidgetApi extends EventEmitter {
5362 public start = vi . fn ( ) . mockResolvedValue ( undefined ) ;
5463 public getClientVersions = vi . fn ( ) ;
@@ -167,6 +176,9 @@ describe("RoomWidgetClient", () => {
167176 "org.matrix.rageshake_request" ,
168177 { request_id : 123 } ,
169178 "!1:example.org" ,
179+ undefined ,
180+ undefined ,
181+ undefined ,
170182 ) ;
171183 } ) ;
172184
@@ -422,6 +434,7 @@ describe("RoomWidgetClient", () => {
422434 "!1:example.org" ,
423435 2000 ,
424436 undefined ,
437+ undefined ,
425438 ) ;
426439 } ) ;
427440
@@ -442,6 +455,7 @@ describe("RoomWidgetClient", () => {
442455 "!1:example.org" ,
443456 undefined ,
444457 parentDelayId ,
458+ undefined ,
445459 ) ;
446460 } ) ;
447461
@@ -855,6 +869,162 @@ describe("RoomWidgetClient", () => {
855869 } ) ;
856870 } ) ;
857871
872+ describe ( "sticky events" , ( ) => {
873+ describe ( "when supported" , ( ) => {
874+ const doesServerSupportUnstableFeatureMock = vi . fn ( ( feature ) =>
875+ Promise . resolve ( feature === "org.matrix.msc4354" ) ,
876+ ) ;
877+
878+ beforeAll ( ( ) => {
879+ MatrixClient . prototype . doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock ;
880+ } ) ;
881+
882+ afterAll ( ( ) => {
883+ doesServerSupportUnstableFeatureMock . mockReset ( ) ;
884+ } ) ;
885+
886+ it ( "requests capabilities when set" , async ( ) => {
887+ await makeClient ( { sendSticky : true , receiveSticky : true } ) ;
888+ expect ( widgetApi . requestCapability ) . toHaveBeenCalledWith ( MatrixCapabilities . MSC4407SendStickyEvent ) ;
889+ expect ( widgetApi . requestCapability ) . toHaveBeenCalledWith ( MatrixCapabilities . MSC4407ReceiveStickyEvent ) ;
890+ } ) ;
891+
892+ it ( "does not request capabilities when unset" , async ( ) => {
893+ await makeClient ( { } ) ;
894+ expect ( widgetApi . requestCapability ) . not . toHaveBeenCalledWith ( MatrixCapabilities . MSC4407SendStickyEvent ) ;
895+ expect ( widgetApi . requestCapability ) . not . toHaveBeenCalledWith (
896+ MatrixCapabilities . MSC4407ReceiveStickyEvent ,
897+ ) ;
898+ } ) ;
899+
900+ it ( "sends" , async ( ) => {
901+ await makeClient ( { sendEvent : [ EventType . RTCMembership ] , sendSticky : true } ) ;
902+ expect ( widgetApi . requestCapabilityForRoomTimeline ) . toHaveBeenCalledWith ( "!1:example.org" ) ;
903+ expect ( widgetApi . requestCapabilityToSendEvent ) . toHaveBeenCalledWith ( EventType . RTCMembership ) ;
904+ expect ( widgetApi . requestCapability ) . toHaveBeenCalledWith ( MatrixCapabilities . MSC4407SendStickyEvent ) ;
905+ await client . _unstable_sendStickyEvent ( "!1:example.org" , 2000 , null , EventType . RTCMembership , {
906+ msc4354_sticky_key : "test" ,
907+ } ) ;
908+ expect ( widgetApi . sendRoomEvent ) . toHaveBeenCalledWith (
909+ EventType . RTCMembership ,
910+ { msc4354_sticky_key : "test" } ,
911+ "!1:example.org" ,
912+ undefined ,
913+ undefined ,
914+ 2000 ,
915+ ) ;
916+ } ) ;
917+
918+ it ( "receives (adds, updates, then removes when redacted)" , async ( ) => {
919+ await makeClient ( { receiveEvent : [ EventType . RTCMembership , EventType . RoomRedaction ] } ) ;
920+ const room = client . getRoom ( "!1:example.org" ) ! ;
921+
922+ function expectStickyEvents ( events : IEvent [ ] ) {
923+ expect ( [ ...room . _unstable_getStickyEvents ( ) ] . map ( ( e ) => e . getEffectiveEvent ( ) ) ) . toEqual ( events ) ;
924+ }
925+
926+ async function sendAndExpectStickyUpdate (
927+ eventToSend : IEvent ,
928+ added : IEvent [ ] ,
929+ updated : { current : IEvent ; previous : IEvent } [ ] ,
930+ removed : IEvent [ ] ,
931+ ) {
932+ const emittedStickyUpdate = new Promise <
933+ Parameters < RoomStickyEventsMap [ RoomStickyEventsEvent . Update ] >
934+ > ( ( resolve ) => room . once ( RoomStickyEventsEvent . Update , ( ...args ) => resolve ( args ) ) ) ;
935+
936+ widgetApi . emit (
937+ `action:${ WidgetApiToWidgetAction . SendEvent } ` ,
938+ new CustomEvent ( `action:${ WidgetApiToWidgetAction . SendEvent } ` , {
939+ detail : { data : eventToSend } ,
940+ } ) ,
941+ ) ;
942+
943+ const [ addedReceived , updatedReceived , removedReceived ] = await emittedStickyUpdate ;
944+ expect ( addedReceived . map ( ( e ) => e . getEffectiveEvent ( ) ) ) . toEqual ( added ) ;
945+ expect (
946+ updatedReceived . map ( ( { current, previous } ) => ( {
947+ current : current . getEffectiveEvent ( ) ,
948+ previous : previous . getEffectiveEvent ( ) ,
949+ } ) ) ,
950+ ) . toEqual ( updated ) ;
951+ expect ( removedReceived . map ( ( e ) => e . getEffectiveEvent ( ) ) ) . toEqual ( removed ) ;
952+ }
953+
954+ // First, add a new sticky event to the map. The client should emit.
955+ const event1 = new MatrixEvent ( {
956+ type : EventType . RTCMembership ,
957+ event_id : "$pduhfiidph" ,
958+ room_id : "!1:example.org" ,
959+ sender : "@alice:example.org" ,
960+ msc4354_sticky : { duration_ms : 1200000 } ,
961+ content : { msc4354_sticky_key : "test" } ,
962+ } ) . getEffectiveEvent ( ) ;
963+ await sendAndExpectStickyUpdate ( event1 , [ event1 ] , [ ] , [ ] ) ;
964+ // It should remain cached in the sticky map
965+ expectStickyEvents ( [ event1 ] ) ;
966+
967+ // Next, update the same key in the sticky map
968+ const event2 = new MatrixEvent ( {
969+ type : EventType . RTCMembership ,
970+ event_id : "$zshgyutptfh" ,
971+ room_id : "!1:example.org" ,
972+ sender : "@alice:example.org" ,
973+ msc4354_sticky : { duration_ms : 1200000 } ,
974+ content : { msc4354_sticky_key : "test" } ,
975+ } ) . getEffectiveEvent ( ) ;
976+ await sendAndExpectStickyUpdate ( event2 , [ ] , [ { current : event2 , previous : event1 } ] , [ ] ) ;
977+ expectStickyEvents ( [ event2 ] ) ;
978+
979+ // Next, redact the second event. Because it has the first as a predecessor, the map should revert to
980+ // the first event.
981+ const redaction1 = new MatrixEvent ( {
982+ type : EventType . RoomRedaction ,
983+ event_id : "$cimoexnvz" ,
984+ room_id : "!1:example.org" ,
985+ sender : "@alice:example.org" ,
986+ redacts : event2 . event_id ,
987+ content : { redacts : event2 . event_id } ,
988+ } ) . getEffectiveEvent ( ) ;
989+ await sendAndExpectStickyUpdate ( redaction1 , [ ] , [ { current : event1 , previous : event2 } ] , [ ] ) ;
990+ expectStickyEvents ( [ event1 ] ) ;
991+
992+ // Finally, redact the first event. Now everything should be gone from the map.
993+ const redaction2 = new MatrixEvent ( {
994+ type : EventType . RoomRedaction ,
995+ event_id : "$drgzmenlh" ,
996+ room_id : "!1:example.org" ,
997+ sender : "@alice:example.org" ,
998+ redacts : event1 . event_id ,
999+ content : { redacts : event1 . event_id } ,
1000+ } ) . getEffectiveEvent ( ) ;
1001+ await sendAndExpectStickyUpdate ( redaction2 , [ ] , [ ] , [ event1 ] ) ;
1002+ expectStickyEvents ( [ ] ) ;
1003+ } ) ;
1004+ } ) ;
1005+
1006+ describe ( "when unsupported" , ( ) => {
1007+ const doesServerSupportUnstableFeatureMock = vi . fn ( ) . mockResolvedValue ( false ) ;
1008+
1009+ beforeAll ( ( ) => {
1010+ MatrixClient . prototype . doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock ;
1011+ } ) ;
1012+
1013+ afterAll ( ( ) => {
1014+ doesServerSupportUnstableFeatureMock . mockReset ( ) ;
1015+ } ) ;
1016+
1017+ it ( "fails to send" , async ( ) => {
1018+ await makeClient ( { sendEvent : [ EventType . RTCMembership ] , sendSticky : true } ) ;
1019+ await expect (
1020+ client . _unstable_sendStickyEvent ( "!1:example.org" , 2000 , null , EventType . RTCMembership , {
1021+ msc4354_sticky_key : "test" ,
1022+ } ) ,
1023+ ) . rejects . toThrow ( "Server does not support" ) ;
1024+ } ) ;
1025+ } ) ;
1026+ } ) ;
1027+
8581028 describe ( "to-device messages" , ( ) => {
8591029 const unencryptedContentMap = new Map ( [
8601030 [ "@alice:example.org" , new Map ( [ [ "*" , { hello : "alice!" } ] ] ) ] ,
0 commit comments