diff --git a/cypress/e2e/widgets/events.spec.ts b/cypress/e2e/widgets/events.spec.ts
new file mode 100644
index 0000000000..aeef1d5c76
--- /dev/null
+++ b/cypress/e2e/widgets/events.spec.ts
@@ -0,0 +1,205 @@
+/*
+Copyright 2022 Mikhail Aheichyk
+Copyright 2022 Nordeck IT + Consulting GmbH.
+
+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 { IWidget } from "matrix-widget-api/src/interfaces/IWidget";
+
+import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk/src/matrix";
+import { SynapseInstance } from "../../plugins/synapsedocker";
+import { UserCredentials } from "../../support/login";
+
+const DEMO_WIDGET_ID = "demo-widget-id";
+const DEMO_WIDGET_NAME = "Demo Widget";
+const DEMO_WIDGET_TYPE = "demo";
+const ROOM_NAME = "Demo";
+
+const DEMO_WIDGET_HTML = `
+
+
+ Demo Widget
+
+
+
+ Demo
+
+
+`;
+
+function waitForRoom(win: Cypress.AUTWindow, roomId: string, predicate: (room: Room) => boolean): Promise {
+ const matrixClient = win.mxMatrixClientPeg.get();
+
+ return new Promise((resolve, reject) => {
+ const room = matrixClient.getRoom(roomId);
+
+ if (predicate(room)) {
+ resolve();
+ return;
+ }
+
+ function onEvent(ev: MatrixEvent) {
+ if (ev.getRoomId() !== roomId) return;
+
+ if (predicate(room)) {
+ matrixClient.removeListener(win.matrixcs.ClientEvent.Event, onEvent);
+ resolve();
+ }
+ }
+
+ matrixClient.on(win.matrixcs.ClientEvent.Event, onEvent);
+ });
+}
+
+describe("Widget Events", () => {
+ let synapse: SynapseInstance;
+ let user: UserCredentials;
+ let bot: MatrixClient;
+ let demoWidgetUrl: string;
+
+ beforeEach(() => {
+ cy.startSynapse("default").then(data => {
+ synapse = data;
+
+ cy.initTestUser(synapse, "Mike").then(_user => {
+ user = _user;
+ });
+ cy.getBot(synapse, { displayName: "Bot", autoAcceptInvites: true }).then(_bot => {
+ bot = _bot;
+ });
+ });
+ cy.serveHtmlFile(DEMO_WIDGET_HTML).then(url => {
+ demoWidgetUrl = url;
+ });
+ });
+
+ afterEach(() => {
+ cy.stopSynapse(synapse);
+ cy.stopWebServers();
+ });
+
+ it('should be updated if user is re-invited into the room with updated state event', () => {
+ cy.createRoom({
+ name: ROOM_NAME,
+ invite: [bot.getUserId()],
+ }).then(roomId => {
+ // setup widget via state event
+ cy.getClient().then(async matrixClient => {
+ const content: IWidget = {
+ id: DEMO_WIDGET_ID,
+ creatorUserId: 'somebody',
+ type: DEMO_WIDGET_TYPE,
+ name: DEMO_WIDGET_NAME,
+ url: demoWidgetUrl,
+ };
+ await matrixClient.sendStateEvent(roomId, 'im.vector.modular.widgets', content, DEMO_WIDGET_ID);
+ }).as('widgetEventSent');
+
+ // set initial layout
+ cy.getClient().then(async matrixClient => {
+ const content = {
+ widgets: {
+ [DEMO_WIDGET_ID]: {
+ container: 'top', index: 1, width: 100, height: 0,
+ },
+ },
+ };
+ await matrixClient.sendStateEvent(roomId, 'io.element.widgets.layout', content, "");
+ }).as('layoutEventSent');
+
+ // open the room
+ cy.viewRoomByName(ROOM_NAME);
+
+ // approve capabilities
+ cy.contains('.mx_WidgetCapabilitiesPromptDialog button', 'Approve').click();
+
+ cy.all([
+ cy.get("@widgetEventSent"),
+ cy.get("@layoutEventSent"),
+ ]).then(async () => {
+ // bot creates a new room with 'net.metadata_invite_shared' state event
+ const { room_id: roomNew } = await bot.createRoom({
+ name: "New room",
+ initial_state: [
+ {
+ type: 'net.metadata_invite_shared',
+ state_key: '',
+ content: {
+ value: "initial",
+ },
+ },
+ ],
+ });
+
+ await bot.invite(roomNew, user.userId);
+
+ // widget should receive 'net.metadata_invite_shared' event after invite
+ cy.window().then(async win => {
+ await waitForRoom(win, roomId, (room) => {
+ const events = room.getLiveTimeline().getEvents();
+ return events.some(e => e.getType() === 'net.widget_echo'
+ && e.getContent().type === 'net.metadata_invite_shared'
+ && e.getContent().content.value === 'initial');
+ });
+ });
+
+ await bot.sendStateEvent(roomNew, 'net.metadata_invite_shared', {
+ value: "new_value",
+ }, '');
+
+ await bot.invite(roomNew, user.userId, 'something changed in the room');
+
+ // widget should receive updated 'net.metadata_invite_shared' event after re-invite
+ cy.window().then(async win => {
+ await waitForRoom(win, roomId, (room) => {
+ const events = room.getLiveTimeline().getEvents();
+ return events.some(e => e.getType() === 'net.widget_echo'
+ && e.getContent().type === 'net.metadata_invite_shared'
+ && e.getContent().content.value === 'new_value');
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml
index aaad3420b9..e282e790e9 100644
--- a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml
+++ b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml
@@ -74,3 +74,7 @@ suppress_key_server_warning: true
ui_auth:
session_timeout: "300s"
+
+room_prejoin_state:
+ additional_event_types:
+ - net.metadata_invite_shared
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 9230dda12e..f7586e86ec 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -520,7 +520,13 @@ export class StopGapWidget extends EventEmitter {
}
}
- this.readUpToMap[ev.getRoomId()] = ev.getId();
+ // Skip marker assignment if membership is 'invite', otherwise 'm.room.member' from
+ // invitation room will assign it and new state events will be not forwarded to the widget
+ // because of empty timeline for invitation room and assigned marker.
+ const room = this.client.getRoom(ev.getRoomId());
+ if (room && room.getMyMembership() !== 'invite') {
+ this.readUpToMap[ev.getRoomId()] = ev.getId();
+ }
const raw = ev.getEffectiveEvent();
this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId).catch((e) => {