feat: show call participants in room list (Discord-style)
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled

This commit is contained in:
sorB
2026-05-10 14:25:35 +02:00
parent b797925316
commit 3da363517f
4610 changed files with 827237 additions and 1 deletions
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,74 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
// Note: eslint-plugin-jsdoc doesn't like import types as parameters, so we
// get around it with @typedef
/**
* @typedef {import("@element-hq/element-web-module-api").Api} Api
*/
export default class CustomComponentModule {
static moduleApiVersion = "^1.2.0";
/**
* Basic module for testing.
* @param {Api} api API object
*/
constructor(api) {
this.api = api;
this.api.customComponents.registerMessageRenderer(
(evt) => evt.content.body === "Do not show edits",
(_props, originalComponent) => {
return originalComponent();
},
{ allowEditingEvent: false },
);
this.api.customComponents.registerMessageRenderer(
(evt) => evt.content.body === "Fall through here",
(props) => {
const body = props.mxEvent.content.body;
return `Fallthrough text for ${body}`;
},
);
this.api.customComponents.registerMessageRenderer(
(evt) => {
if (evt.content.body === "Crash the filter!") {
throw new Error("Fail test!");
}
return false;
},
() => {
return `Should not render!`;
},
);
this.api.customComponents.registerMessageRenderer(
(evt) => evt.content.body === "Crash the renderer!",
() => {
throw new Error("Fail test!");
},
);
this.api.customComponents.registerMessageRenderer(
(mxEvent) => mxEvent.type === "m.room.message" && mxEvent.content.msgtype === "m.image",
(_props, originalComponent) => {
return originalComponent();
},
{ allowDownloadingMedia: async (mxEvent) => mxEvent.content.body !== "bad.png" },
);
// Order is specific here to avoid this overriding the other renderers
this.api.customComponents.registerMessageRenderer("m.room.message", (props, originalComponent) => {
const body = props.mxEvent.content.body;
if (body === "Do not replace me") {
return originalComponent();
} else if (body === "Fall through here") {
return null;
}
return `Custom text for ${body}`;
});
}
async load() {}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,24 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
export default class ExampleModule {
static moduleApiVersion = "^1.0.0";
constructor(api) {
this.api = api;
this.api.i18n.register({
key: {
en: "%(brand)s module loading successful!",
de: "%(brand)s-Module erfolgreich geladen!",
},
});
}
async load() {
const brand = this.api.config.get("brand");
alert(this.api.i18n.translate("key", { brand }));
}
}
@@ -0,0 +1,53 @@
<!doctype html>
<style>
body {
background: rgb(139, 192, 253);
}
</style>
<!-- element-call.spec.ts will insert the widget API in this block -->
<script>
widgetCodeHere;
</script>
<div>
<p>Fake Element Call</p>
<p>State: <span id="state">Loading</span></p>
<button id="send-button">Send Room Message</button>
</div>
<!-- Minimal fake implementation of Element Call. Just enough for testing the leagkin widgets.-->
<script>
const stateIndicator = document.querySelector("#state");
const { WidgetApi, WidgetApiToWidgetAction, MatrixCapabilities } = mxwidgets();
const widgetId = new URLSearchParams(window.location.search).get("widgetId");
const params = new URLSearchParams(window.location.hash.slice(1));
const roomId = params.get("roomId");
const api = new WidgetApi(widgetId, "*");
document.querySelector("#send-button").onclick = async () => {
await api.sendRoomEvent(
"m.room.message",
{ msgtype: "m.text", body: "I sent this once!!" },
roomId,
undefined,
undefined,
undefined,
);
};
api.requestCapability(MatrixCapabilities.AlwaysOnScreen);
api.requestCapability(`org.matrix.msc2762.timeline:${roomId}`);
api.requestCapabilityToSendMessage("m.text");
api.on("ready", (ev) => {
stateIndicator.innerHTML = "Ready";
});
// Start the messaging
api.start();
// If waitForIframeLoad is false, tell the client that we're good to go
api.sendContentLoaded();
</script>
@@ -0,0 +1,87 @@
<!doctype html>
<style>
body {
background: rgb(139, 192, 253);
}
</style>
<!-- element-call.spec.ts will insert the widget API in this block -->
<script>
widgetCodeHere;
</script>
<div>
<p>Fake Element Call</p>
<p>State: <span id="state">Loading</span></p>
<button id="join-button">Join Call</button>
<button id="close-button">Close</button>
</div>
<!-- Minimal fake implementation of Element Call. Just enough for testing persistent widgets.-->
<script>
const content = {
"application": "m.call",
"call_id": "",
"device_id": "gycSobuY0z",
"expires": 14400000,
"foci_preferred": [
{
livekit_alias: "any-alias",
livekit_service_url: "https://example.org",
type: "livekit",
},
],
"focus_active": {
focus_selection: "oldest_membership",
type: "livekit",
},
"m.call.intent": "video",
"scope": "m.room",
};
const stateIndicator = document.querySelector("#state");
const { WidgetApi, WidgetApiToWidgetAction, MatrixCapabilities } = mxwidgets();
const widgetId = new URLSearchParams(window.location.search).get("widgetId");
const params = new URLSearchParams(window.location.hash.slice(1));
const userId = params.get("userId");
const deviceId = params.get("deviceId");
const roomId = params.get("roomId");
const api = new WidgetApi(widgetId, "*");
const stateKey = `_${userId}_${deviceId}_m.call`;
async function hangup() {
await api.sendStateEvent("org.matrix.msc3401.call.member", stateKey, {}, roomId);
await api.setAlwaysOnScreen(false);
await api.transport.send("io.element.close", {});
stateIndicator.innerHTML = "Ended";
}
document.querySelector("#join-button").onclick = async () => {
await api.setAlwaysOnScreen(true);
await api.transport.send("io.element.join", {});
await api.sendStateEvent("org.matrix.msc3401.call.member", stateKey, content, roomId);
stateIndicator.innerHTML = "In call";
};
document.querySelector("#close-button").onclick = () => {
hangup();
};
api.requestCapability(MatrixCapabilities.AlwaysOnScreen);
api.requestCapability(`org.matrix.msc2762.timeline:${roomId}`);
api.requestCapabilityToSendState("org.matrix.msc3401.call.member", stateKey);
api.on("ready", (ev) => {
stateIndicator.innerHTML = "Ready";
// Pretend to join a call.
});
api.on("action:im.vector.hangup", async () => {
await hangup();
});
// Start the messaging
api.start();
// If waitForIframeLoad is false, tell the client that we're good to go
api.sendContentLoaded();
</script>
+32
View File
@@ -0,0 +1,32 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { readFile } from "node:fs/promises";
import { readFileSync } from "node:fs";
const __dirname = dirname(fileURLToPath(import.meta.url));
export function getSampleFilePath(file: string): string {
return join(__dirname, file);
}
export function readSampleFile(file: string, encoding: null): Promise<NonSharedBuffer>;
export function readSampleFile(file: string, encoding?: BufferEncoding): Promise<string>;
export function readSampleFile(
file: string,
encoding: BufferEncoding | null = "utf-8",
): Promise<NonSharedBuffer | string> {
return readFile(getSampleFilePath(file), encoding);
}
export function readSampleFileSync(file: string, encoding: null): NonSharedBuffer;
export function readSampleFileSync(file: string, encoding?: BufferEncoding): string;
export function readSampleFileSync(file: string, encoding: BufferEncoding | null = "utf-8"): NonSharedBuffer | string {
return readFileSync(getSampleFilePath(file), encoding);
}
@@ -0,0 +1,39 @@
{
"versions": [
"r0.0.1",
"r0.1.0",
"r0.2.0",
"r0.3.0",
"r0.4.0",
"r0.5.0",
"r0.6.0",
"r0.6.1",
"v1.1",
"v1.2",
"v1.3",
"v1.4"
],
"unstable_features": {
"org.matrix.label_based_filtering": true,
"org.matrix.e2e_cross_signing": true,
"org.matrix.msc2432": true,
"uk.half-shot.msc2666.mutual_rooms": true,
"io.element.e2ee_forced.public": false,
"io.element.e2ee_forced.private": false,
"io.element.e2ee_forced.trusted_private": false,
"org.matrix.msc3026.busy_presence": false,
"org.matrix.msc2285.stable": true,
"org.matrix.msc3827.stable": true,
"org.matrix.msc2716": false,
"org.matrix.msc3030": false,
"org.matrix.msc3440.stable": true,
"org.matrix.msc3771": true,
"org.matrix.msc3773": false,
"fi.mau.msc2815": false,
"org.matrix.msc3882": false,
"org.matrix.msc3881": false,
"org.matrix.msc3874": false,
"org.matrix.msc3886": false,
"org.matrix.msc3912": false
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.