Compare commits

..
Author SHA1 Message Date
Robin dde07f6d54 Ellipsise app bar text on mobile
We already had text-overflow: ellipsis set on the title and subtitle elements of the app bar, but due to an annoying quirk of place-items: center, this had no effect. place-items: center stretch seems to give the text elements the right size constraints.
2026-10-26 13:25:13 +01:00
Johannes Marbach de05b82cfe Merge pull request #4369 from element-hq/renovate/npm-storybook-vulnerability
Update dependency storybook to v10.6.2 [SECURITY]
2026-10-26 10:16:21 +01:00
Johannes Marbach b0e985c014 Update related storybook dependencies 2026-10-26 09:57:30 +01:00
Johannes Marbach 33368ab8da Merge pull request #4361 from element-hq/renovate/compound
Update dependency @vector-im/compound-design-tokens to v11.3.1
2026-10-26 09:49:54 +01:00
Johannes Marbach d85a4a1929 Merge pull request #4360 from element-hq/renovate/livekit-client
Update dependency livekit-client to v2.23.0
2026-10-26 09:48:08 +01:00
renovate[bot] a6930c5aa9 Update dependency storybook to v10.6.2 [SECURITY] 2026-10-26 08:31:56 +00:00
Johannes Marbach e83093feb2 Merge pull request #4365 from element-hq/renovate/npm-vite-vulnerability
Update dependency vite to v8.2.4 [SECURITY]
2026-10-26 09:30:23 +01:00
renovate[bot] c50127ab62 Update dependency vite to v8.2.4 [SECURITY] 2026-10-22 17:44:29 +00:00
renovate[bot] c3ae1c516b Update dependency livekit-client to v2.23.0 2026-10-21 14:25:29 +00:00
renovate[bot] b975dce53e Update dependency @vector-im/compound-design-tokens to v11.3.1 2026-10-19 01:30:27 +00:00
6 changed files with 612 additions and 486 deletions
+535 -264
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -7,6 +7,7 @@
padding-top: env(safe-area-inset-top);
opacity: 1;
transition: opacity 0.15s;
text-align: center;
}
/* Pseudo-element for the gradient background */
@@ -52,7 +53,7 @@
". . ."
"primaryButton title secondaryButton"
". . .";
place-items: center;
place-items: center stretch;
column-gap: var(--cpd-space-2x);
}
-122
View File
@@ -1,122 +0,0 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { renderHook } from "@testing-library/react";
import {
EventType,
JoinRule,
KnownMembership,
type MatrixClient,
MatrixEvent,
type Room,
} from "matrix-js-sdk";
import { describe, expect, test } from "vitest";
import { useGroupCallRooms } from "./useGroupCallRooms";
import { mockMatrixRoom } from "../utils/test";
describe("useGroupCallRooms", () => {
test("lists a joined room with legacy call member state", () => {
const room = mockCallRoom({ legacyMemberState: true });
expect(listedRoomIds([room])).toEqual([room.roomId]);
});
test("lists a joined room with a sticky RTC membership", () => {
const room = mockCallRoom({
stickyEvents: [new MatrixEvent({ type: EventType.RTCMembership })],
});
expect(listedRoomIds([room])).toEqual([room.roomId]);
});
test("lists a joined room with an RTC membership in its timeline", () => {
const room = mockCallRoom({
timelineEvents: [new MatrixEvent({ type: EventType.RTCMembership })],
});
expect(listedRoomIds([room])).toEqual([room.roomId]);
});
test("lists a knocked room without any call events", () => {
const room = mockCallRoom({
membership: KnownMembership.Knock,
joinRule: JoinRule.Knock,
});
expect(listedRoomIds([room])).toEqual([room.roomId]);
});
test("omits a joined room without any call events", () => {
const room = mockCallRoom({
stickyEvents: [new MatrixEvent({ type: EventType.RoomMessage })],
timelineEvents: [new MatrixEvent({ type: EventType.RoomMessage })],
});
expect(listedRoomIds([room])).toEqual([]);
});
test.each([KnownMembership.Invite, KnownMembership.Leave])(
"omits a room with %s membership despite call events",
(membership) => {
const room = mockCallRoom({
membership,
legacyMemberState: true,
stickyEvents: [new MatrixEvent({ type: EventType.RTCMembership })],
timelineEvents: [new MatrixEvent({ type: EventType.RTCMembership })],
});
expect(listedRoomIds([room])).toEqual([]);
},
);
});
function listedRoomIds(rooms: Room[]): string[] {
const client = {
getRooms: () => rooms,
getUserId: () => "@alice:example.org",
on: () => {},
off: () => {},
matrixRTC: {
getRoomSession: () => ({ memberships: [] }),
on: () => {},
off: () => {},
},
} as unknown as MatrixClient;
const { result } = renderHook(() => useGroupCallRooms(client));
return result.current.map(({ room }) => room.roomId);
}
let nextRoomId = 0;
function mockCallRoom({
membership = KnownMembership.Join,
joinRule = JoinRule.Public,
legacyMemberState = false,
stickyEvents = [],
timelineEvents = [],
}: {
membership?: KnownMembership;
joinRule?: JoinRule;
legacyMemberState?: boolean;
stickyEvents?: MatrixEvent[];
timelineEvents?: MatrixEvent[];
}): Room {
const stateEvents = new Map(
legacyMemberState ? [[EventType.GroupCallMemberPrefix, new Map()]] : [],
);
return mockMatrixRoom({
roomId: `!room${nextRoomId++}:example.org`,
name: "Call room",
getMyMembership: () => membership,
getJoinRule: () => joinRule,
hasEncryptionStateEvent: () => true,
getCanonicalAlias: () => null,
getMxcAvatarUrl: () => null,
getLiveTimeline: () =>
({
getState: () => ({ events: stateEvents }),
getEvents: () => timelineEvents,
}) as unknown as ReturnType<Room["getLiveTimeline"]>,
_unstable_getStickyEvents: () =>
stickyEvents as unknown as ReturnType<Room["_unstable_getStickyEvents"]>,
});
}
+6 -4
View File
@@ -141,7 +141,7 @@ const roomHasCallMembershipEvents = (room: Room): boolean => {
return true;
}
// Check for recent calls using sticky events.
// Check for *active* calls using sticky events.
for (const sticky of room._unstable_getStickyEvents()) {
if (sticky.getType() === EventType.RTCMembership) {
return true;
@@ -150,9 +150,11 @@ const roomHasCallMembershipEvents = (room: Room): boolean => {
// Otherwise, check recent event history to see if anyone had
// sent a call membership in here.
return timeline
.getEvents()
.some((e) => e.getType() === EventType.RTCMembership);
return timeline.getEvents().some(
(e) =>
// Membership events only count if both of these are true
e.unstableStickyInfo && e.getType() === EventType.GroupCallMemberPrefix,
);
// Otherwise, it's *unlikely* this room was ever a call.
};
@@ -5,14 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import EventEmitter from "events";
import { expect, test } from "vitest";
import { test } from "vitest";
import {
EventType,
type IRoomTimelineData,
MatrixEvent,
type Room,
RoomEvent,
RoomStickyEventsEvent,
} from "matrix-js-sdk";
import { type RTCCallIntent } from "matrix-js-sdk/lib/matrixrtc";
import { map, mergeMap, NEVER, type Observable, startWith } from "rxjs";
@@ -27,7 +25,6 @@ import {
import {
type CallNotificationWrapper,
createCallNotificationLifecycle$,
createReceivedDecline$,
type RingAttempt,
} from "./CallNotificationLifecycle";
import { constant } from "../Behavior";
@@ -184,16 +181,22 @@ test("ring attempt can be declined", () => {
a: mockRingEvent("$notif1", 30),
}),
receivedDecline$: hot("--d", {
d: new MatrixEvent({
type: EventType.RTCDecline,
sender: alice.userId,
content: {
"m.relates_to": {
rel_type: "m.reference",
event_id: "$notif1",
d: [
new MatrixEvent({
type: EventType.RTCDecline,
sender: alice.userId,
content: {
"m.relates_to": {
rel_type: "m.reference",
event_id: "$notif1",
},
},
},
}),
}),
{} as Room,
undefined,
false,
{} as IRoomTimelineData,
],
}),
});
@@ -213,16 +216,22 @@ test("ring attempt times out if recipient declines too late", () => {
a: mockRingEvent("$notif1", 30),
}),
receivedDecline$: hot("100ms d", {
d: new MatrixEvent({
type: EventType.RTCDecline,
sender: alice.userId,
content: {
"m.relates_to": {
rel_type: "m.reference",
event_id: "$notif1",
d: [
new MatrixEvent({
type: EventType.RTCDecline,
sender: alice.userId,
content: {
"m.relates_to": {
rel_type: "m.reference",
event_id: "$notif1",
},
},
},
}),
}),
{} as Room,
undefined,
false,
{} as IRoomTimelineData,
],
}),
});
@@ -242,16 +251,22 @@ test("decline event relating to wrong event is ignored (times out)", () => {
a: mockRingEvent("$notif1", 30),
}),
receivedDecline$: hot("--d", {
d: new MatrixEvent({
type: EventType.RTCDecline,
sender: alice.userId,
content: {
"m.relates_to": {
rel_type: "m.reference",
event_id: "$other", // <---- WRONG
d: [
new MatrixEvent({
type: EventType.RTCDecline,
sender: alice.userId,
content: {
"m.relates_to": {
rel_type: "m.reference",
event_id: "$other", // <---- WRONG
},
},
},
}),
}),
{} as Room,
undefined,
false,
{} as IRoomTimelineData,
],
}),
});
@@ -271,16 +286,22 @@ test("decline event from wrong sender is ignored (times out)", () => {
a: mockRingEvent("$notif1", 30),
}),
receivedDecline$: hot("--d", {
d: new MatrixEvent({
type: EventType.RTCDecline,
sender: local.userId, // <---- WRONG
content: {
"m.relates_to": {
rel_type: "m.reference",
event_id: "$notif1",
d: [
new MatrixEvent({
type: EventType.RTCDecline,
sender: local.userId, // <---- WRONG
content: {
"m.relates_to": {
rel_type: "m.reference",
event_id: "$notif1",
},
},
},
}),
}),
{} as Room,
undefined,
false,
{} as IRoomTimelineData,
],
}),
});
@@ -290,32 +311,3 @@ test("decline event from wrong sender is ignored (times out)", () => {
});
});
});
test("declines are received from the timeline and the sticky section", () => {
const room = new EventEmitter();
const received: string[] = [];
const subscription = createReceivedDecline$(
room as unknown as Room,
).subscribe((event) => received.push(event.getId()!));
const event = (eventId: string, type = EventType.RTCDecline): MatrixEvent =>
new MatrixEvent({ event_id: eventId, type, sender: alice.userId });
const timelineArgs = [room, false, false, {}];
room.emit(RoomEvent.Timeline, event("$timeline"), ...timelineArgs);
room.emit(
RoomEvent.Timeline,
event("$message", EventType.RoomMessage),
...timelineArgs,
);
// A gappy sync delivers the decline only in the sticky section
room.emit(RoomStickyEventsEvent.Update, [event("$sticky")], [], []);
room.emit(
RoomStickyEventsEvent.Update,
[],
[{ previous: event("$old"), current: event("$replacement") }],
[event("$removed")],
);
subscription.unsubscribe();
expect(received).toEqual(["$timeline", "$sticky", "$replacement"]);
});
@@ -25,17 +25,13 @@ import {
EMPTY,
race,
take,
mergeMap,
} from "rxjs";
import {
type EventTimelineSetHandlerMap,
EventType,
type MatrixEvent,
type Room as MatrixRoom,
RoomEvent,
RoomStickyEventsEvent,
} from "matrix-js-sdk";
import { type RoomStickyEventsMap } from "matrix-js-sdk/lib/models/room-sticky-events";
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { type Behavior, type ObservableScope } from "@element-hq/matrixrtc-sdk";
@@ -72,28 +68,12 @@ export function createSentCallNotification$(
export function createReceivedDecline$(
matrixRoom: MatrixRoom,
): Observable<MatrixEvent> {
// A decline is sticky, so after a gappy sync it arrives only in the sticky
// section, which never reaches the timeline. Racing for the first outcome
// makes a decline seen in both places harmless.
const timeline$ = (
): Observable<Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>> {
return (
fromEvent(matrixRoom, RoomEvent.Timeline) as Observable<
Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>
>
).pipe(map(([event]) => event));
const sticky$ = (
fromEvent(matrixRoom, RoomStickyEventsEvent.Update) as Observable<
Parameters<RoomStickyEventsMap[RoomStickyEventsEvent.Update]>
>
).pipe(
mergeMap(([added, updated]) => [
...added,
...updated.map(({ current }) => current),
]),
);
return merge(timeline$, sticky$).pipe(
filter((event) => event.getType() === EventType.RTCDecline),
);
).pipe(filter(([event]) => event.getType() === EventType.RTCDecline));
}
export interface Props {
@@ -103,7 +83,9 @@ export interface Props {
/** The user ids of the room's members, ours included. */
roomMemberUserIds$: Behavior<string[]>;
sentCallNotification$: Observable<CallNotificationWrapper | null>;
receivedDecline$: Observable<MatrixEvent>;
receivedDecline$: Observable<
Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>
>;
options: { waitForCallPickup?: boolean; autoLeaveWhenOthersLeft?: boolean };
localUser: { deviceId: string; userId: string };
}
@@ -164,7 +146,7 @@ export function createCallNotificationLifecycle$({
// Call is declined when we receive a decline event
const decline$ = receivedDecline$.pipe(
filter(
(event) =>
([event]) =>
event.getRelation()?.rel_type === "m.reference" &&
event.getRelation()?.event_id === notificationEvent.event_id &&
event.getSender() === recipient,