Merge branch 'fkwp/delegation_of_delayed_events' into fkwp/feature_relax_homeserver_offline

This commit is contained in:
fkwp
2026-04-22 21:11:23 +02:00
5 changed files with 264 additions and 176 deletions
+4
View File
@@ -18,3 +18,7 @@ keys:
devkey: secret devkey: secret
room: room:
auto_create: false auto_create: false
webhook:
api_key: devkey
urls:
- https://matrix-rtc.othersite.m.localhost/livekit/jwt/sfu_webhook
+4
View File
@@ -18,3 +18,7 @@ keys:
devkey: secret devkey: secret
room: room:
auto_create: false auto_create: false
webhook:
api_key: devkey
urls:
- https://matrix-rtc.m.localhost/livekit/jwt/sfu_webhook
+183 -175
View File
@@ -1,175 +1,183 @@
networks: networks:
ecbackend: ecbackend:
services: services:
auth-service: auth-service:
image: ghcr.io/element-hq/lk-jwt-service:pr_171 image: ghcr.io/element-hq/lk-jwt-service:pr_171
pull_policy: always pull_policy: always
hostname: auth-server hostname: auth-server
environment: environment:
- LIVEKIT_JWT_PORT=6080 - LIVEKIT_JWT_PORT=6080
- LIVEKIT_URL=wss://matrix-rtc.m.localhost/livekit/sfu - LIVEKIT_URL=wss://matrix-rtc.m.localhost/livekit/sfu
- LIVEKIT_KEY=devkey - LIVEKIT_KEY=devkey
- LIVEKIT_SECRET=secret - LIVEKIT_SECRET=secret
# If the configured homeserver runs on localhost, it'll probably be using # If the configured homeserver runs on localhost, it'll probably be using
# a self-signed certificate # a self-signed certificate
- LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING - LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING
- LIVEKIT_FULL_ACCESS_HOMESERVERS=* - LIVEKIT_FULL_ACCESS_HOMESERVERS=*
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
ports: ports:
# HOST_PORT:CONTAINER_PORT # HOST_PORT:CONTAINER_PORT
- 6080:6080 - 6080:6080
networks: networks:
- ecbackend - ecbackend
auth-service-1: auth-service-1:
image: ghcr.io/element-hq/lk-jwt-service:pr_171 image: ghcr.io/element-hq/lk-jwt-service:pr_171
pull_policy: always pull_policy: always
hostname: auth-server-1 hostname: auth-server-1
environment: environment:
- LIVEKIT_JWT_PORT=16080 - LIVEKIT_JWT_PORT=16080
- LIVEKIT_URL=wss://matrix-rtc.othersite.m.localhost/livekit/sfu - LIVEKIT_URL=wss://matrix-rtc.othersite.m.localhost/livekit/sfu
- LIVEKIT_KEY=devkey - LIVEKIT_KEY=devkey
- LIVEKIT_SECRET=secret - LIVEKIT_SECRET=secret
# If the configured homeserver runs on localhost, it'll probably be using # If the configured homeserver runs on localhost, it'll probably be using
# a self-signed certificate # a self-signed certificate
- LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING - LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING
- LIVEKIT_FULL_ACCESS_HOMESERVERS=* - LIVEKIT_FULL_ACCESS_HOMESERVERS=*
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
ports: ports:
# HOST_PORT:CONTAINER_PORT # HOST_PORT:CONTAINER_PORT
- 16080:16080 - 16080:16080
networks: networks:
- ecbackend - ecbackend
livekit: livekit:
image: livekit/livekit-server:v1.10.1 image: livekit/livekit-server:v1.10.1
pull_policy: always pull_policy: always
hostname: livekit-sfu hostname: livekit-sfu
command: --dev --config /etc/livekit.yaml command: --dev --config /etc/livekit.yaml
restart: unless-stopped restart: unless-stopped
# The SFU seems to work far more reliably when we let it share the host # The SFU seems to work far more reliably when we let it share the host
# network rather than opening specific ports (but why?? we're not missing # network rather than opening specific ports (but why?? we're not missing
# any…) # any…)
ports: ports:
# HOST_PORT:CONTAINER_PORT # HOST_PORT:CONTAINER_PORT
- 7880:7880/tcp - 7880:7880/tcp
- 7881:7881/tcp - 7881:7881/tcp
- 7882:7882/tcp - 7882:7882/tcp
- 50100-50200:50100-50200/udp - 50100-50200:50100-50200/udp
volumes: volumes:
- ./backend/dev_livekit.yaml:/etc/livekit.yaml:Z - ./backend/dev_tls_m.localhost.crt:/local_cert.pem:Z
networks: - ./backend/dev_livekit.yaml:/etc/livekit.yaml:Z
- ecbackend environment:
- SSL_CERT_FILE=/local_cert.pem
livekit-1: networks:
image: livekit/livekit-server:v1.10.1 - ecbackend
pull_policy: always
hostname: livekit-sfu-1 livekit-1:
command: --dev --config /etc/livekit.yaml image: livekit/livekit-server:v1.10.1
restart: unless-stopped pull_policy: always
# The SFU seems to work far more reliably when we let it share the host hostname: livekit-sfu-1
# network rather than opening specific ports (but why?? we're not missing command: --dev --config /etc/livekit.yaml
# any…) restart: unless-stopped
ports: # The SFU seems to work far more reliably when we let it share the host
# HOST_PORT:CONTAINER_PORT # network rather than opening specific ports (but why?? we're not missing
- 17880:17880/tcp # any…)
- 17881:17881/tcp ports:
- 17882:17882/tcp # HOST_PORT:CONTAINER_PORT
- 50300-50400:50300-50400/udp - 17880:17880/tcp
volumes: - 17881:17881/tcp
- ./backend/dev_livekit-othersite.yaml:/etc/livekit.yaml:Z - 17882:17882/tcp
networks: - 50300-50400:50300-50400/udp
- ecbackend volumes:
- ./backend/dev_tls_m.localhost.crt:/local_cert.pem:Z
synapse: - ./backend/dev_livekit-othersite.yaml:/etc/livekit.yaml:Z
hostname: homeserver environment:
image: ghcr.io/element-hq/synapse:latest - SSL_CERT_FILE=/local_cert.pem
pull_policy: always networks:
environment: - ecbackend
- SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml
# Needed for rootless podman-compose such that the uid/gid mapping does synapse:
# fit local user uid. If the container runs as root (uid 0) it is fine as hostname: homeserver
# it actually maps to your non-root user on the host (e.g. 1000). image: ghcr.io/element-hq/synapse:latest
# Otherwise uid mapping will not match your non-root user. pull_policy: always
- UID=0 environment:
- GID=0 - SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml
volumes: # Needed for rootless podman-compose such that the uid/gid mapping does
- ./backend/synapse_tmp:/data:Z # fit local user uid. If the container runs as root (uid 0) it is fine as
- ./backend/dev_homeserver.yaml:/data/cfg/homeserver.yaml:Z # it actually maps to your non-root user on the host (e.g. 1000).
networks: # Otherwise uid mapping will not match your non-root user.
- ecbackend - UID=0
- GID=0
synapse-1: volumes:
hostname: homeserver-1 - ./backend/synapse_tmp:/data:Z
image: ghcr.io/element-hq/synapse:latest - ./backend/dev_homeserver.yaml:/data/cfg/homeserver.yaml:Z
pull_policy: always networks:
environment: - ecbackend
- SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml
# Needed for rootless podman-compose such that the uid/gid mapping does synapse-1:
# fit local user uid. If the container runs as root (uid 0) it is fine as hostname: homeserver-1
# it actually maps to your non-root user on the host (e.g. 1000). image: ghcr.io/element-hq/synapse:latest
# Otherwise uid mapping will not match your non-root user. pull_policy: always
- UID=0 environment:
- GID=0 - SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml
volumes: # Needed for rootless podman-compose such that the uid/gid mapping does
- ./backend/synapse_tmp_othersite:/data:Z # fit local user uid. If the container runs as root (uid 0) it is fine as
- ./backend/dev_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z # it actually maps to your non-root user on the host (e.g. 1000).
networks: # Otherwise uid mapping will not match your non-root user.
- ecbackend - UID=0
- GID=0
element-web: volumes:
image: ghcr.io/element-hq/element-web:develop - ./backend/synapse_tmp_othersite:/data:Z
pull_policy: always - ./backend/dev_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
volumes: networks:
- ./backend/ew.test.config.json:/app/config.json:Z - ecbackend
environment:
ELEMENT_WEB_PORT: 8081 element-web:
ports: image: ghcr.io/element-hq/element-web:develop
- "8081:8081" pull_policy: always
networks: volumes:
- ecbackend - ./backend/ew.test.config.json:/app/config.json:Z
environment:
element-web-1: ELEMENT_WEB_PORT: 8081
image: ghcr.io/element-hq/element-web:develop ports:
pull_policy: always - "8081:8081"
volumes: networks:
- ./backend/ew.test.othersite.config.json:/app/config.json:Z - ecbackend
environment:
ELEMENT_WEB_PORT: 18081 element-web-1:
ports: image: ghcr.io/element-hq/element-web:develop
# HOST_PORT:CONTAINER_PORT pull_policy: always
- "18081:18081" volumes:
networks: - ./backend/ew.test.othersite.config.json:/app/config.json:Z
- ecbackend environment:
ELEMENT_WEB_PORT: 18081
nginx: ports:
# see backend/dev_tls_setup for how to generate the tls certs # HOST_PORT:CONTAINER_PORT
hostname: synapse.m.localhost - "18081:18081"
image: nginx:latest networks:
pull_policy: always - ecbackend
volumes:
- ./backend/dev_nginx.conf:/etc/nginx/conf.d/default.conf:Z nginx:
- ./backend/dev_tls_m.localhost.key:/root/ssl/key.pem:Z # see backend/dev_tls_setup for how to generate the tls certs
- ./backend/dev_tls_m.localhost.crt:/root/ssl/cert.pem:Z hostname: synapse.m.localhost
ports: image: nginx:latest
# HOST_PORT:CONTAINER_PORT pull_policy: always
- "443:443" volumes:
- "8008:80" - ./backend/dev_nginx.conf:/etc/nginx/conf.d/default.conf:Z
- "4443:443" - ./backend/dev_tls_m.localhost.key:/root/ssl/key.pem:Z
- "8448:8448" - ./backend/dev_tls_m.localhost.crt:/root/ssl/cert.pem:Z
extra_hosts: ports:
- "host.docker.internal:host-gateway" # HOST_PORT:CONTAINER_PORT
depends_on: - "443:443"
- synapse - "8008:80"
networks: - "4443:443"
ecbackend: - "8448:8448"
aliases: extra_hosts:
- synapse.m.localhost - "host.docker.internal:host-gateway"
- synapse.othersite.m.localhost - "auth-server:127.0.0.1"
- matrix-rtc.m.localhost - "auth-server-1:127.0.0.1"
- matrix-rtc.othersite.m.localhost depends_on:
- synapse
networks:
ecbackend:
aliases:
- synapse.m.localhost
- synapse.othersite.m.localhost
- matrix-rtc.m.localhost
- matrix-rtc.othersite.m.localhost
+71 -1
View File
@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/ */
import { render } from "@testing-library/react"; import { render } from "@testing-library/react";
import { type FC, useRef } from "react"; import { type FC, useRef, useState } from "react";
import { expect, test, vi } from "vitest"; import { expect, test, vi } from "vitest";
import { Button } from "@vector-im/compound-web"; import { Button } from "@vector-im/compound-web";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
@@ -17,6 +17,7 @@ import {
ReactionSet, ReactionSet,
ReactionsRowSize, ReactionsRowSize,
} from "./reactions"; } from "./reactions";
import { type Controls } from "./controls";
// Test Explanation: // Test Explanation:
// - The main objective is to test `useCallViewKeyboardShortcuts`. // - The main objective is to test `useCallViewKeyboardShortcuts`.
@@ -27,6 +28,7 @@ interface TestComponentProps {
onButtonClick?: () => void; onButtonClick?: () => void;
sendReaction?: () => void; sendReaction?: () => void;
toggleHandRaised?: () => void; toggleHandRaised?: () => void;
initialModalOpen?: boolean;
} }
const TestComponent: FC<TestComponentProps> = ({ const TestComponent: FC<TestComponentProps> = ({
@@ -34,7 +36,9 @@ const TestComponent: FC<TestComponentProps> = ({
onButtonClick = (): void => {}, onButtonClick = (): void => {},
sendReaction = (reaction: ReactionOption): void => {}, sendReaction = (reaction: ReactionOption): void => {},
toggleHandRaised = (): void => {}, toggleHandRaised = (): void => {},
initialModalOpen = false,
}) => { }) => {
const [modalOpen, setModalOpen] = useState(initialModalOpen);
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
useCallViewKeyboardShortcuts( useCallViewKeyboardShortcuts(
ref, ref,
@@ -47,6 +51,19 @@ const TestComponent: FC<TestComponentProps> = ({
return ( return (
<div ref={ref}> <div ref={ref}>
<Button onClick={onButtonClick}>TEST</Button> <Button onClick={onButtonClick}>TEST</Button>
{modalOpen && (
<dialog
open
onKeyDown={(e) => {
if (e.key === "Escape") {
e.preventDefault();
setModalOpen(false);
}
}}
>
<button>InModalButton</button>
</dialog>
)}
</div> </div>
); );
}; };
@@ -118,6 +135,27 @@ test("raised hand can be sent via keyboard presses", async () => {
expect(toggleHandRaised).toHaveBeenCalledOnce(); expect(toggleHandRaised).toHaveBeenCalledOnce();
}); });
test("raised hand cannot be sent via keyboard presses if modal open and focussed", async () => {
const user = userEvent.setup();
const toggleHandRaised = vi.fn();
const { getByRole } = render(
<TestComponent
toggleHandRaised={toggleHandRaised}
initialModalOpen={true}
/>,
);
getByRole("button", { name: "InModalButton" }).focus();
await user.keyboard("h");
expect(toggleHandRaised).not.toHaveBeenCalledOnce();
// once we press esc...
await user.keyboard("[Escape]");
// we can toggle the hand raise...
await user.keyboard("h");
expect(toggleHandRaised).toHaveBeenCalledOnce();
});
test("unmuting happens in place of the default action", async () => { test("unmuting happens in place of the default action", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const defaultPrevented = vi.fn(); const defaultPrevented = vi.fn();
@@ -138,3 +176,35 @@ test("unmuting happens in place of the default action", async () => {
await user.keyboard("[Space]"); await user.keyboard("[Space]");
expect(defaultPrevented).toBeCalledWith(true); expect(defaultPrevented).toBeCalledWith(true);
}); });
test("escape button triggers the controls back action", async () => {
const user = userEvent.setup();
window.controls = { onBackButtonPressed: vi.fn() } as unknown as Controls;
// In the real application, we mostly just want the spacebar shortcut to avoid
// scrolling the page. But to test that here in JSDOM, we need some kind of
// container element that can be interactive and receive focus / keydown
// events. <video> is kind of a weird choice, but it'll do the job.
render(<TestComponent setAudioEnabled={() => {}} />);
await user.keyboard("[Escape]");
expect(window.controls.onBackButtonPressed).toHaveBeenCalled();
});
test("escape button does not trigger back if sth else is focused", async () => {
const user = userEvent.setup();
window.controls = { onBackButtonPressed: vi.fn() } as unknown as Controls;
const { getByRole } = render(<TestComponent initialModalOpen={true} />);
getByRole("button", { name: "InModalButton" }).focus();
// First Escape: the dialog's onKeyDown intercepts it and closes the modal.
await user.keyboard("[Escape]");
expect(window.controls.onBackButtonPressed).not.toHaveBeenCalled();
// Second Escape: modal is gone, focus has fallen back to document.body,
// which *does* contain the ref div, so the hook fires and back IS triggered.
await user.keyboard("[Escape]");
expect(window.controls.onBackButtonPressed).toHaveBeenCalled();
});
+2
View File
@@ -68,6 +68,8 @@ export function useCallViewKeyboardShortcuts(
} else if (KeyToReactionMap[event.key]) { } else if (KeyToReactionMap[event.key]) {
event.preventDefault(); event.preventDefault();
sendReaction(KeyToReactionMap[event.key]); sendReaction(KeyToReactionMap[event.key]);
} else if (event.key === "Escape") {
window.controls.onBackButtonPressed?.();
} }
}, },
[ [