diff --git a/.changeset/chubby-lights-send.md b/.changeset/chubby-lights-send.md
new file mode 100644
index 00000000..fc52b12e
--- /dev/null
+++ b/.changeset/chubby-lights-send.md
@@ -0,0 +1,6 @@
+---
+'@matrix-widget-toolkit/testing': minor
+'@matrix-widget-toolkit/api': minor
+---
+
+Add support for the delayed events
diff --git a/example-widget-mui/src/DicePage/DicePage.test.tsx b/example-widget-mui/src/DicePage/DicePage.test.tsx
index 4f504ffe..dee3c163 100644
--- a/example-widget-mui/src/DicePage/DicePage.test.tsx
+++ b/example-widget-mui/src/DicePage/DicePage.test.tsx
@@ -19,7 +19,11 @@ import { MockedWidgetApi, mockWidgetApi } from '@matrix-widget-toolkit/testing';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import axe from 'axe-core';
-import { EventDirection, WidgetEventCapability } from 'matrix-widget-api';
+import {
+ EventDirection,
+ UpdateDelayedEventAction,
+ WidgetEventCapability,
+} from 'matrix-widget-api';
import { ComponentType, PropsWithChildren } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
@@ -55,7 +59,7 @@ describe('', () => {
screen.getByText(/nobody has thrown the dice in this room yet/i),
).toBeInTheDocument();
expect(
- screen.getByRole('button', { name: /throw dice/i }),
+ screen.getByRole('button', { name: /throw dice$/i }),
).toBeInTheDocument();
});
@@ -79,7 +83,7 @@ describe('', () => {
),
]);
- const button = await screen.findByRole('button', { name: /throw dice/i });
+ const button = await screen.findByRole('button', { name: /throw dice$/i });
await userEvent.click(button);
expect(widgetApi.requestCapabilities).toHaveBeenCalledWith([
@@ -124,7 +128,7 @@ describe('', () => {
it('should throw a dice', async () => {
render(, { wrapper });
- const button = await screen.findByRole('button', { name: /throw dice/i });
+ const button = await screen.findByRole('button', { name: /throw dice$/i });
await userEvent.click(button);
await expect(
@@ -138,4 +142,75 @@ describe('', () => {
},
);
});
+
+ it('should throw a dice delayed', async () => {
+ render(, { wrapper });
+
+ const button = await screen.findByRole('button', {
+ name: /throw dice 10 seconds delayed/i,
+ });
+ await userEvent.click(button);
+
+ await expect(
+ screen.findByText(/your last throw: ./i),
+ ).resolves.toBeInTheDocument();
+ await expect(
+ screen.findByText(/your last delay id: ./i),
+ ).resolves.toBeInTheDocument();
+ await expect(
+ screen.findByText(/throw dice delayed event actions:/i),
+ ).resolves.toBeInTheDocument();
+
+ expect(widgetApi.sendDelayedRoomEvent).toHaveBeenCalledWith(
+ 'net.nordeck.throw_dice',
+ {
+ pips: expect.any(Number),
+ },
+ expect.any(Number),
+ );
+ });
+
+ it.each([
+ UpdateDelayedEventAction.Cancel,
+ UpdateDelayedEventAction.Restart,
+ UpdateDelayedEventAction.Send,
+ ])('should update (%s) a throw dice delayed', async (action) => {
+ render(, { wrapper });
+
+ const button = await screen.findByRole('button', {
+ name: /throw dice 10 seconds delayed/i,
+ });
+ await userEvent.click(button);
+
+ await expect(
+ screen.findByText(/your last throw: ./i),
+ ).resolves.toBeInTheDocument();
+ await expect(
+ screen.findByText(/Your last delay id: ./i),
+ ).resolves.toBeInTheDocument();
+
+ expect(widgetApi.sendDelayedRoomEvent).toHaveBeenCalledWith(
+ 'net.nordeck.throw_dice',
+ {
+ pips: expect.any(Number),
+ },
+ expect.any(Number),
+ );
+
+ const updateButton = await screen.findByRole('button', {
+ name: new RegExp(action, 'i'),
+ });
+ await userEvent.click(updateButton);
+
+ expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith(
+ 'syd_wlGAStYmBRRdjnWiHSDA',
+ action,
+ );
+
+ if (action === UpdateDelayedEventAction.Restart) {
+ expect(updateButton).toBeInTheDocument();
+ } else {
+ expect(updateButton).not.toBeInTheDocument();
+ }
+ });
});
diff --git a/example-widget-mui/src/DicePage/DicePage.tsx b/example-widget-mui/src/DicePage/DicePage.tsx
index d12f9cc3..acaba8e1 100644
--- a/example-widget-mui/src/DicePage/DicePage.tsx
+++ b/example-widget-mui/src/DicePage/DicePage.tsx
@@ -18,21 +18,29 @@ import { MuiCapabilitiesGuard } from '@matrix-widget-toolkit/mui';
import { useWidgetApi } from '@matrix-widget-toolkit/react';
import {
Alert,
+ AlertTitle,
Box,
Button,
+ ButtonGroup,
Card,
CardContent,
Typography,
} from '@mui/material';
-import { EventDirection, WidgetEventCapability } from 'matrix-widget-api';
-import { ReactElement, useEffect, useState } from 'react';
+import {
+ EventDirection,
+ MatrixCapabilities,
+ UpdateDelayedEventAction,
+ WidgetEventCapability,
+} from 'matrix-widget-api';
+import { ReactElement, useEffect, useRef, useState } from 'react';
import { filter, map } from 'rxjs';
import { NavigationBar } from '../NavigationPage';
import {
+ isValidThrowDiceEvent,
STATE_EVENT_THROW_DICE,
ThrowDiceEvent,
- isValidThrowDiceEvent,
} from '../events';
+import { isError } from '../utils';
/**
* A component that reads and writes room events via the widget API.
@@ -58,11 +66,19 @@ export const DicePage = (): ReactElement => {
);
};
+type Timeout = ReturnType;
+const eventDelayMs = 10000;
+
export const DiceView = (): ReactElement => {
const widgetApi = useWidgetApi();
const [lastOwnDice, setLastOwnDice] = useState();
+ const [lastDelayId, setLastDelayId] = useState();
+ const [lastDelayIdExpired, setLastDelayIdExpired] = useState(false);
+ const [lastDelayError, setLastDelayError] = useState();
const [dices, setDices] = useState([]);
+ const lastDelayIdTimeoutRef = useRef();
+
useEffect(() => {
setDices([]);
@@ -81,6 +97,12 @@ export const DiceView = (): ReactElement => {
};
}, [widgetApi]);
+ useEffect(() => {
+ return () => {
+ clearTimeout(lastDelayIdTimeoutRef.current);
+ };
+ }, []);
+
async function handleThrowDice() {
await widgetApi.requestCapabilities([
WidgetEventCapability.forRoomEvent(
@@ -97,9 +119,68 @@ export const DiceView = (): ReactElement => {
setLastOwnDice(result.content.pips);
}
+ async function handleThrowDiceDelayed() {
+ await widgetApi.requestCapabilities([
+ MatrixCapabilities.MSC4157SendDelayedEvent,
+ WidgetEventCapability.forRoomEvent(
+ EventDirection.Send,
+ STATE_EVENT_THROW_DICE,
+ ),
+ ]);
+
+ const pips = Math.floor(Math.random() * 6) + 1;
+ try {
+ const { delay_id } = await widgetApi.sendDelayedRoomEvent(
+ STATE_EVENT_THROW_DICE,
+ { pips },
+ eventDelayMs,
+ );
+ setLastOwnDice(pips);
+ setLastDelayId(delay_id);
+ setLastDelayIdExpired(false);
+ setLastDelayError(undefined);
+
+ clearTimeout(lastDelayIdTimeoutRef.current);
+ lastDelayIdTimeoutRef.current = setTimeout(() => {
+ setLastDelayIdExpired(true);
+ }, eventDelayMs);
+ } catch {
+ setLastDelayError(
+ 'Could not send a delayed event. Please check if homeserver supports delayed events.',
+ );
+ }
+ }
+
+ async function handleThrowDiceDelayedUpdate(
+ action: UpdateDelayedEventAction,
+ ) {
+ if (!lastDelayId) {
+ return;
+ }
+
+ await widgetApi.requestCapabilities([
+ MatrixCapabilities.MSC4157UpdateDelayedEvent,
+ ]);
+
+ try {
+ await widgetApi.updateDelayedEvent(lastDelayId, action);
+ if (action === UpdateDelayedEventAction.Restart) {
+ clearTimeout(lastDelayIdTimeoutRef.current);
+ lastDelayIdTimeoutRef.current = setTimeout(() => {
+ setLastDelayIdExpired(true);
+ }, eventDelayMs);
+ } else {
+ setLastDelayIdExpired(true);
+ }
+ setLastDelayError(undefined);
+ } catch (e) {
+ setLastDelayError(isError(e) ? e.message : JSON.stringify(e));
+ }
+ }
+
return (
<>
-
+
Dice Simulator
@@ -113,15 +194,66 @@ export const DiceView = (): ReactElement => {
-