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
+118
View File
@@ -0,0 +1,118 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
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 { EventType, KNOWN_SAFE_ROOM_VERSION, type MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { LOCAL_ROOM_ID_PREFIX, LocalRoom } from "../../../src/models/LocalRoom";
import { determineCreateRoomEncryptionOption, type Member } from "../../../src/utils/direct-messages";
import { MEGOLM_ENCRYPTION_ALGORITHM } from "../crypto";
/**
* Create a DM local room. This room will not be send to the server and only exists inside the client.
* It sets up the local room with some artificial state events
* so that can be used in most components instead of a „real“ room.
*
* @async
* @param {MatrixClient} client
* @param {Member[]} targets DM partners
* @returns {Promise<LocalRoom>} Resolves to the new local room
*/
export async function createDmLocalRoom(client: MatrixClient, targets: Member[]): Promise<LocalRoom> {
const userId = client.getUserId()!;
const localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + client.makeTxnId(), client, userId);
const events: MatrixEvent[] = [];
events.push(
new MatrixEvent({
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
type: EventType.RoomCreate,
content: {
creator: userId,
room_version: KNOWN_SAFE_ROOM_VERSION,
},
state_key: "",
sender: userId,
room_id: localRoom.roomId,
origin_server_ts: Date.now(),
}),
);
if (await determineCreateRoomEncryptionOption(client, targets)) {
localRoom.encrypted = true;
events.push(
new MatrixEvent({
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
type: EventType.RoomEncryption,
content: {
algorithm: MEGOLM_ENCRYPTION_ALGORITHM,
},
sender: userId,
state_key: "",
room_id: localRoom.roomId,
origin_server_ts: Date.now(),
}),
);
}
events.push(
new MatrixEvent({
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
type: EventType.RoomMember,
content: {
displayname: userId,
membership: KnownMembership.Join,
},
state_key: userId,
sender: userId,
room_id: localRoom.roomId,
}),
);
targets.forEach((target: Member) => {
events.push(
new MatrixEvent({
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
type: EventType.RoomMember,
content: {
displayname: target.name,
avatar_url: target.getMxcAvatarUrl() ?? undefined,
membership: KnownMembership.Invite,
isDirect: true,
},
state_key: target.userId,
sender: userId,
room_id: localRoom.roomId,
}),
);
events.push(
new MatrixEvent({
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
type: EventType.RoomMember,
content: {
displayname: target.name,
avatar_url: target.getMxcAvatarUrl() ?? undefined,
membership: KnownMembership.Join,
},
state_key: target.userId,
sender: target.userId,
room_id: localRoom.roomId,
}),
);
});
localRoom.targets = targets;
localRoom.updateMyMembership(KnownMembership.Join);
localRoom.addLiveEvents(events, { addToState: true });
localRoom.currentState.setStateEvents(events);
localRoom.name = localRoom.getDefaultRoomName(client.getUserId()!);
client.store.storeRoom(localRoom);
return localRoom;
}
@@ -0,0 +1,61 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
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.
*/
interface FilterValidMDirectResult {
/** Whether the entire content is valid */
valid: boolean;
/** Filtered content with only the valid parts */
filteredContent: Record<string, string[]>;
}
/**
* Filter m.direct content to be compliant to https://spec.matrix.org/v1.6/client-server-api/#mdirect.
*
* @param content - Raw event content to be filerted
* @returns value as a flag whether to content was valid.
* filteredContent with only values from the content that are spec compliant.
*/
export const filterValidMDirect = (content: unknown): FilterValidMDirectResult => {
if (content === null || typeof content !== "object") {
return {
valid: false,
filteredContent: {},
};
}
const filteredContent = new Map();
let valid = true;
for (const [userId, roomIds] of Object.entries(content)) {
if (typeof userId !== "string") {
valid = false;
continue;
}
if (!Array.isArray(roomIds)) {
valid = false;
continue;
}
const filteredRoomIds: string[] = [];
filteredContent.set(userId, filteredRoomIds);
for (const roomId of roomIds) {
if (typeof roomId === "string") {
filteredRoomIds.push(roomId);
} else {
valid = false;
}
}
}
return {
valid,
filteredContent: Object.fromEntries(filteredContent.entries()),
};
};
+95
View File
@@ -0,0 +1,95 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
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 { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import DMRoomMap from "../DMRoomMap";
import { isLocalRoom } from "../localRoom/isLocalRoom";
import { isJoinedOrNearlyJoined } from "../membership";
import { getFunctionalMembers } from "../room/getFunctionalMembers";
/**
* Iterates the rooms and tries to find a DM room with the user identified by UserId.
* A DM room is assumed if one of the following matches:
* - Has two members and contains a membership for the user identified by userId
* - findRoomWithThirdpartyInvites is true and has one member and a third pending third party invite
*
* If multiple rooms match it will return the one with the most recent event.
*
* @param rooms - Rooms to iterate
* @param userId - User Id of the other user
* @param [findRoomWithThirdpartyInvites] - Whether to find a DM for a pending thirdparty invite
* @returns DM room if found or undefined if not
*/
function extractSuitableRoom(rooms: Room[], userId: string, findRoomWithThirdpartyInvites: boolean): Room | undefined {
const suitableRooms = rooms
.filter((r) => {
// Validate that we are joined and the other person is also joined. We'll also make sure
// that the room also looks like a DM (until we have canonical DMs to tell us). For now,
// a DM is a room of two people that contains those two people exactly. This does mean
// that bots, assistants, etc will ruin a room's DM-ness, though this is a problem for
// canonical DMs to solve.
if (r && r.getMyMembership() === KnownMembership.Join) {
if (isLocalRoom(r)) return false;
const functionalUsers = getFunctionalMembers(r);
const members = r.currentState.getMembers();
const joinedMembers = members.filter(
(m) => !functionalUsers.includes(m.userId) && m.membership && isJoinedOrNearlyJoined(m.membership),
);
const otherMember = joinedMembers.find((m) => m.userId === userId);
if (otherMember && joinedMembers.length === 2) {
return true;
}
const thirdPartyInvites = r.currentState.getStateEvents("m.room.third_party_invite") || [];
// match room with pending third-party invite
return findRoomWithThirdpartyInvites && joinedMembers.length === 1 && thirdPartyInvites.length === 1;
}
return false;
})
.sort((r1, r2) => {
return r2.getLastActiveTimestamp() - r1.getLastActiveTimestamp();
});
if (suitableRooms.length) {
return suitableRooms[0];
}
return undefined;
}
/**
* Tries to find a DM room with a specific user.
*
* @param {MatrixClient} client
* @param {string} userId ID of the user to find the DM for
* @returns {Room | undefined} Room if found
*/
export function findDMForUser(client: MatrixClient, userId: string): Room | undefined {
const roomIdsForUserId = DMRoomMap.shared().getDMRoomsForUserId(userId);
const roomsForUserId = roomIdsForUserId.map((id) => client.getRoom(id)).filter((r): r is Room => r !== null);
// Call with findRoomWithThirdpartyInvites = true to also include rooms with pending thirdparty invites.
// roomsForUserId can only contain rooms with the other user here,
// because they have been queried by getDMRoomsForUserId().
const suitableRoomForUserId = extractSuitableRoom(roomsForUserId, userId, true);
if (suitableRoomForUserId) {
return suitableRoomForUserId;
}
// Try to find in all rooms as a fallback
const allRoomIds = DMRoomMap.shared().getRoomIds();
const allRooms = Array.from(allRoomIds)
.map((id) => client.getRoom(id))
.filter((r): r is Room => r !== null);
return extractSuitableRoom(allRooms, userId, false);
}
+31
View File
@@ -0,0 +1,31 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
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 { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { type Member } from "../direct-messages";
import DMRoomMap from "../DMRoomMap";
import { findDMForUser } from "./findDMForUser";
/**
* Tries to find a DM room with some other users.
*
* @param {MatrixClient} client
* @param {Member[]} targets The Members to try to find the room for
* @returns {Room | null} Resolved so the room if found, else null
*/
export function findDMRoom(client: MatrixClient, targets: Member[]): Room | null {
const targetIds = targets.map((t) => t.userId);
let existingRoom: Room | null;
if (targetIds.length === 1) {
existingRoom = findDMForUser(client, targetIds[0]) ?? null;
} else {
existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
}
return existingRoom;
}
+85
View File
@@ -0,0 +1,85 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
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 { type IInvite3PID, type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { Action } from "../../dispatcher/actions";
import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { determineCreateRoomEncryptionOption, type Member } from "../direct-messages";
import DMRoomMap from "../DMRoomMap";
import { isLocalRoom } from "../localRoom/isLocalRoom";
import { findDMForUser } from "./findDMForUser";
import dis from "../../dispatcher/dispatcher";
import { getAddressType } from "../../UserAddress";
import createRoom, { type IOpts } from "../../createRoom";
/**
* Start a DM.
*
* @returns {Promise<string | null} Resolves to the room id.
*/
export async function startDm(client: MatrixClient, targets: Member[], showSpinner = true): Promise<string | null> {
const targetIds = targets.map((t) => t.userId);
// Check if there is already a DM with these people and reuse it if possible.
let existingRoom: Room | undefined;
if (targetIds.length === 1) {
existingRoom = findDMForUser(client, targetIds[0]);
} else {
existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds) ?? undefined;
}
if (existingRoom && !isLocalRoom(existingRoom)) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: existingRoom.roomId,
should_peek: false,
joining: false,
metricsTrigger: "MessageUser",
});
return Promise.resolve(existingRoom.roomId);
}
const createRoomOptions: IOpts = { inlineErrors: true };
if (await determineCreateRoomEncryptionOption(client, targets)) {
createRoomOptions.encryption = true;
}
// Check if it's a traditional DM and create the room if required.
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
const isSelf = targetIds.length === 1 && targetIds[0] === client.getUserId();
if (targetIds.length === 1 && !isSelf) {
createRoomOptions.dmUserId = targetIds[0];
}
if (targetIds.length > 1) {
createRoomOptions.createOpts = targetIds.reduce<{
invite_3pid: IInvite3PID[];
invite: string[];
}>(
(roomOptions, address) => {
const type = getAddressType(address);
if (type === "email") {
const invite: IInvite3PID = {
id_server: client.getIdentityServerUrl(true)!,
medium: "email",
address,
};
roomOptions.invite_3pid.push(invite);
} else if (type === "mx-user-id") {
roomOptions.invite.push(address);
}
return roomOptions;
},
{ invite: [], invite_3pid: [] },
);
}
createRoomOptions.spinner = showSpinner;
return createRoom(client, createRoomOptions);
}