diff --git a/cypress/e2e/widgets/events.spec.ts b/cypress/e2e/widgets/events.spec.ts
new file mode 100644
index 0000000000..d9aeb46062
--- /dev/null
+++ b/cypress/e2e/widgets/events.spec.ts
@@ -0,0 +1,221 @@
+/*
+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 { HomeserverInstance } from "../../plugins/utils/homeserver";
+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 homeserver: HomeserverInstance;
+ let user: UserCredentials;
+ let bot: MatrixClient;
+ let demoWidgetUrl: string;
+
+ beforeEach(() => {
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+
+ cy.initTestUser(homeserver, "Mike").then((_user) => {
+ user = _user;
+ });
+ cy.getBot(homeserver, { displayName: "Bot", autoAcceptInvites: true }).then((_bot) => {
+ bot = _bot;
+ });
+ });
+ cy.serveHtmlFile(DEMO_WIDGET_HTML).then((url) => {
+ demoWidgetUrl = url;
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ 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 'm.room.topic'
+ const { room_id: roomNew } = await bot.createRoom({
+ name: "New room",
+ initial_state: [
+ {
+ type: "m.room.topic",
+ state_key: "",
+ content: {
+ topic: "topic initial",
+ },
+ },
+ ],
+ });
+
+ await bot.invite(roomNew, user.userId);
+
+ // widget should receive 'm.room.topic' 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 === "m.room.topic" &&
+ e.getContent().content.topic === "topic initial",
+ );
+ });
+ });
+
+ // update the topic
+ await bot.sendStateEvent(
+ roomNew,
+ "m.room.topic",
+ {
+ topic: "topic updated",
+ },
+ "",
+ );
+
+ await bot.invite(roomNew, user.userId, "something changed in the room");
+
+ // widget should receive updated 'm.room.topic' 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 === "m.room.topic" &&
+ e.getContent().content.topic === "topic updated",
+ );
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index e5f9c5c0fa..5fe190179e 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -519,7 +519,17 @@ 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 evRoomId = ev.getRoomId();
+ const evId = ev.getId();
+ if (evRoomId && evId) {
+ const room = this.client.getRoom(evRoomId);
+ if (room && room.getMyMembership() === "join") {
+ this.readUpToMap[evRoomId] = evId;
+ }
+ }
const raw = ev.getEffectiveEvent();
this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId).catch((e) => {