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
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:
@@ -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()),
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user