Skip to content

Commit fa37dad

Browse files
committed
Merge branch 'develop' of ssh://github.com/element-hq/element-web into t3chguy/consolidate-build-test-ci
# Conflicts: # pnpm-lock.yaml
2 parents bf2aedb + 7c60752 commit fa37dad

14 files changed

Lines changed: 252 additions & 397 deletions

File tree

apps/web/playwright/e2e/crypto/device-verification.spec.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,17 +270,27 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
270270
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
271271
}
272272

273-
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {
273+
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts, app }) => {
274+
/* Log in but don't verify the device */
274275
await logIntoElement(page, credentials);
275-
276-
/* Dismiss "Verify this device" */
277276
const authPage = page.locator(".mx_AuthPage");
278277
await authPage.getByRole("button", { name: "Skip verification for now" }).click();
279278
await authPage.getByRole("button", { name: "I'll verify later" }).click();
280279

281280
await page.waitForSelector(".mx_MatrixChat");
282281
const elementDeviceId = await page.evaluate(() => window.mxMatrixClientPeg.get().getDeviceId());
283282

283+
/* Create an encrypted room so the "Verify this device" toast appears */
284+
await app.client.createRoom({
285+
initial_state: [
286+
{
287+
type: "m.room.encryption",
288+
state_key: "",
289+
content: { algorithm: "m.megolm.v1.aes-sha2" },
290+
},
291+
],
292+
});
293+
284294
/* Now initiate a verification request from the *bot* device. */
285295
const botVerificationRequest = await aliceBotClient.evaluateHandle(
286296
async (client, { userId, deviceId }) => {

apps/web/playwright/e2e/voip/element-call.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ function assertCommonCallParameters(
3737
expect(hash.get("userId")).toEqual(user.userId);
3838
expect(hash.get("deviceId")).toEqual(user.deviceId);
3939
expect(hash.get("roomId")).toEqual(room.roomId);
40-
expect(hash.get("preload")).toEqual("false");
4140
}
4241

4342
async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "notification", intent?: string) {

apps/web/src/PosthogAnalytics.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ export class PosthogAnalytics {
164164
dis.register(this.onAction);
165165
SettingsStore.monitorSetting("layout", null);
166166
SettingsStore.monitorSetting("useCompactLayout", null);
167+
SettingsStore.monitorSetting("urlPreviewsEnabled", null);
167168
this.onLayoutUpdated();
169+
this.onUrlPreviewSettingUpdated(SettingsStore.getValue("urlPreviewsEnabled"));
168170
this.updateCryptoSuperProperty();
169171
}
170172

@@ -188,11 +190,17 @@ export class PosthogAnalytics {
188190
this.setProperty("WebLayout", layout);
189191
};
190192

193+
private readonly onUrlPreviewSettingUpdated = (value: boolean): void => {
194+
this.setProperty("URLPreviewsEnabled", value);
195+
};
196+
191197
private onAction = (payload: ActionPayload): void => {
192198
if (payload.action !== Action.SettingUpdated) return;
193199
const settingsPayload = payload as SettingUpdatedPayload;
194200
if (["layout", "useCompactLayout"].includes(settingsPayload.settingName)) {
195201
this.onLayoutUpdated();
202+
} else if (settingsPayload.settingName === "urlPreviewsEnabled" && !settingsPayload.roomId) {
203+
this.onUrlPreviewSettingUpdated(settingsPayload.newValue as boolean);
196204
}
197205
};
198206

apps/web/src/PosthogTrackers.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/*
2+
Copyright 2026 Element Creations Ltd.
23
Copyright 2024 New Vector Ltd.
34
Copyright 2022 The Matrix.org Foundation C.I.C.
45
@@ -11,11 +12,14 @@ import { type WebScreen as ScreenEvent } from "@matrix-org/analytics-events/type
1112
import { type Interaction as InteractionEvent } from "@matrix-org/analytics-events/types/typescript/Interaction";
1213
import { type PinUnpinAction } from "@matrix-org/analytics-events/types/typescript/PinUnpinAction";
1314
import { type RoomListSortingAlgorithmChanged } from "@matrix-org/analytics-events/types/typescript/RoomListSortingAlgorithmChanged";
15+
import { type UrlPreviewRendered } from "@matrix-org/analytics-events/types/typescript/UrlPreviewRendered";
16+
import { type UrlPreview } from "@element-hq/web-shared-components";
1417

1518
import PageType from "./PageTypes";
1619
import Views from "./Views";
1720
import { PosthogAnalytics } from "./PosthogAnalytics";
1821
import { SortingAlgorithm } from "./stores/room-list-v3/skip-list/sorters";
22+
import { LruCache } from "./utils/LruCache";
1923

2024
export type ScreenName = ScreenEvent["$current_url"];
2125
export type InteractionName = InteractionEvent["name"];
@@ -49,6 +53,8 @@ const SortingAlgorithmMap: Record<SortingAlgorithm, RoomListSortingAlgorithmChan
4953
export default class PosthogTrackers {
5054
private static internalInstance: PosthogTrackers;
5155

56+
private readonly previewedEventIds = new LruCache<string, true>(1000);
57+
5258
public static get instance(): PosthogTrackers {
5359
if (!PosthogTrackers.internalInstance) {
5460
PosthogTrackers.internalInstance = new PosthogTrackers();
@@ -136,6 +142,29 @@ export default class PosthogTrackers {
136142
newAlgorithm: SortingAlgorithmMap[newAlgorithm],
137143
});
138144
}
145+
146+
/**
147+
* Track if an event has had a previewed rendered in the client.
148+
* This function makes a best-effort attempt to prevent double counting.
149+
*
150+
* @param eventId EventID for deduplication.
151+
* @param isEncrypted Whether the event (and effectively the room) was encrypted.
152+
* @param previews The previews generated from the event.
153+
*/
154+
public trackUrlPreview(eventId: string, isEncrypted: boolean, previews: UrlPreview[]): void {
155+
// Discount any previews that we have already tracked.
156+
if (this.previewedEventIds.get(eventId)) {
157+
return;
158+
}
159+
PosthogAnalytics.instance.trackEvent<UrlPreviewRendered>({
160+
eventName: "UrlPreviewRendered",
161+
previewKind: "LegacyCard",
162+
hasThumbnail: previews.some((p) => !!p.image),
163+
previewCount: previews.length,
164+
encryptedRoom: isEncrypted,
165+
});
166+
this.previewedEventIds.set(eventId, true);
167+
}
139168
}
140169

141170
export class PosthogScreenTracker extends PureComponent<{ screenName: ScreenName }> {

apps/web/src/components/views/messages/TextualBody.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
useCreateAutoDisposedViewModel,
1515
EventContentBodyView,
1616
LINKIFIED_DATA_ATTRIBUTE,
17+
useViewModel,
1718
} from "@element-hq/web-shared-components";
1819
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
1920

@@ -39,6 +40,7 @@ import { UrlPreviewGroupViewModel } from "../../../viewmodels/message-body/UrlPr
3940
import { useMediaVisible } from "../../../hooks/useMediaVisible.ts";
4041
import ImageView from "../elements/ImageView.tsx";
4142
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx";
43+
import PosthogTrackers from "../../../PosthogTrackers.ts";
4244

4345
const logger = rootLogger.getChild("TextualBody");
4446

@@ -427,5 +429,14 @@ export default function TextualBody(props: IBodyProps): React.ReactElement {
427429
})();
428430
}, [vm, props.showUrlPreview, mediaVisible]);
429431

432+
const { previews } = useViewModel(vm);
433+
434+
useEffect(() => {
435+
if (previews.length === 0) {
436+
return;
437+
}
438+
PosthogTrackers.instance.trackUrlPreview(props.mxEvent.getId()!, props.mxEvent.isEncrypted(), previews);
439+
}, [props.mxEvent, previews]);
440+
430441
return <InnerTextualBody urlPreviewViewModel={vm} {...props} />;
431442
}

apps/web/src/device-listener/DeviceListenerCurrentDevice.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,6 @@ export class DeviceListenerCurrentDevice {
153153
return;
154154
}
155155

156-
const crossSigningReady = await crypto.isCrossSigningReady();
157156
const secretStorageStatus = await crypto.getSecretStorageStatus();
158157
const crossSigningStatus = await crypto.getCrossSigningStatus();
159158
const allCrossSigningSecretsCached =
@@ -238,7 +237,6 @@ export class DeviceListenerCurrentDevice {
238237
// missing locally, that is handled by the
239238
// `!allCrossSigningSecretsCached` branch above.
240239
logSpan.warn("4S is missing secrets or backup key not cached", {
241-
crossSigningReady,
242240
secretStorageStatus,
243241
allCrossSigningSecretsCached,
244242
isCurrentDeviceTrusted,

apps/web/src/models/Call.ts

Lines changed: 5 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ export class ElementCall extends Call {
660660
): void {
661661
const room = client.getRoom(roomId);
662662
if (!room) {
663-
// If the room isn't known, or the room is a video room then skip setting an intent.
663+
// If the room isn't known then skip setting an intent.
664664
return;
665665
} else if (isVideoRoom(room)) {
666666
// Video rooms already exist, so just treat as if we're joining a group call.
@@ -669,35 +669,27 @@ export class ElementCall extends Call {
669669
params.append("returnToLobby", "true");
670670
// Never skip the lobby, we always want to give the caller a chance to explicitly join.
671671
params.append("skipLobby", "false");
672-
// Never preload, as per below warning.
673-
params.append("preload", "false");
674672
return;
675673
}
674+
676675
const isDM = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId);
677676
const oldestCallMember = client.matrixRTC.getRoomSession(room).getOldestMembership();
678677
const hasCallStarted = !!oldestCallMember && oldestCallMember.sender !== client.getSafeUserId();
679-
// XXX: @element-hq/element-call-embedded <= 0.15.0 sets the wrong parameter for
680-
// preload by default so we override here. This can be removed when that package
681-
// is released and upgraded.
682678
if (isDM) {
683679
if (hasCallStarted) {
684680
params.append(
685681
"intent",
686682
voiceOnly ? ElementCallIntent.JoinExistingDMVoice : ElementCallIntent.JoinExistingDM,
687683
);
688-
params.append("preload", "false");
689684
} else {
690685
params.append("intent", voiceOnly ? ElementCallIntent.StartCallDMVoice : ElementCallIntent.StartCallDM);
691-
params.append("preload", "false");
692686
}
693687
} else {
694688
// Group chats do not have a voice option.
695689
if (hasCallStarted) {
696690
params.append("intent", ElementCallIntent.JoinExisting);
697-
params.append("preload", "false");
698691
} else {
699692
params.append("intent", ElementCallIntent.StartCall);
700-
params.append("preload", "false");
701693
}
702694
}
703695
}
@@ -836,58 +828,18 @@ export class ElementCall extends Call {
836828
);
837829
}
838830

839-
/**
840-
* Get the correct intent for a widget, so that Element Call presents the correct
841-
* default config.
842-
* @param client The matrix client.
843-
* @param roomId
844-
* @param voiceOnly Should the call be voice-only, or video (default).
845-
*/
846-
public static getWidgetIntent(client: MatrixClient, roomId: string, voiceOnly?: boolean): ElementCallIntent {
847-
const room = client.getRoom(roomId);
848-
if (room !== null && !isVideoRoom(room)) {
849-
const isDM = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId);
850-
const oldestCallMember = client.matrixRTC.getRoomSession(room).getOldestMembership();
851-
const hasCallStarted = !!oldestCallMember && oldestCallMember.sender !== client.getSafeUserId();
852-
if (isDM) {
853-
if (hasCallStarted) {
854-
return voiceOnly ? ElementCallIntent.JoinExistingDMVoice : ElementCallIntent.JoinExistingDM;
855-
} else {
856-
return voiceOnly ? ElementCallIntent.StartCallDMVoice : ElementCallIntent.StartCallDM;
857-
}
858-
} else {
859-
if (hasCallStarted) {
860-
return ElementCallIntent.JoinExisting;
861-
} else {
862-
return ElementCallIntent.StartCall;
863-
}
864-
}
865-
}
866-
// If unknown, default to joining an existing call.
867-
return ElementCallIntent.JoinExisting;
868-
}
869-
870831
private static getWidgetData(
871832
client: MatrixClient,
872833
roomId: string,
873834
currentData: IWidgetData,
874835
overwriteData: IWidgetData,
875-
voiceOnly?: boolean,
876836
): IWidgetData {
877-
let perParticipantE2EE = false;
878-
if (
879-
client.getRoom(roomId)?.hasEncryptionStateEvent() &&
880-
!SettingsStore.getValue("feature_disable_call_per_sender_encryption")
881-
)
882-
perParticipantE2EE = true;
883-
884-
const intent = ElementCall.getWidgetIntent(client, roomId, voiceOnly);
885-
886837
return {
887838
...currentData,
888839
...overwriteData,
889-
intent,
890-
perParticipantE2EE,
840+
perParticipantE2EE:
841+
client.getRoom(roomId)?.hasEncryptionStateEvent() &&
842+
!SettingsStore.getValue("feature_disable_call_per_sender_encryption"),
891843
};
892844
}
893845

apps/web/src/toasts/SetupEncryptionToast.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,12 @@ export const showToast = (state: DeviceStateForToast): void => {
365365
overrideWidth: state === "key_storage_out_of_sync" ? "366px" : undefined,
366366
},
367367
component: GenericToast,
368-
priority: state === "verify_this_session" ? 95 : 40,
368+
// verify_this_session is more important than most toasts, but
369+
// needs to appear below an incoming verification request, so we can fix
370+
// the problem by accepting it.
371+
//
372+
// Other states are less urgent.
373+
priority: state === "verify_this_session" ? 85 : 40,
369374
});
370375
};
371376

apps/web/src/viewmodels/message-body/UrlPreviewGroupViewModel.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ import {
1414
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
1515
import { type IPreviewUrlResponse, type MatrixClient, MatrixError, type MatrixEvent } from "matrix-js-sdk/src/matrix";
1616
import { decode } from "html-entities";
17+
import { type UrlPreviewVisibilityChanged } from "@matrix-org/analytics-events/types/typescript/UrlPreviewVisibilityChanged";
1718

1819
import { isPermalinkHost } from "../../utils/permalinks/Permalinks";
1920
import { mediaFromMxc } from "../../customisations/Media";
2021
import PlatformPeg from "../../PlatformPeg";
2122
import { thumbHeight } from "../../ImageUtils";
2223
import SettingsStore from "../../settings/SettingsStore";
24+
import { PosthogAnalytics } from "../../PosthogAnalytics";
2325

2426
const logger = rootLogger.getChild("UrlPreviewGroupViewModel");
2527

@@ -404,6 +406,13 @@ export class UrlPreviewGroupViewModel
404406
// FIXME: persist this somewhere smarter than local storage
405407
globalThis.localStorage?.setItem(this.storageKey, "1");
406408
this.urlPreviewEnabledByUser = false;
409+
PosthogAnalytics.instance.trackEvent<UrlPreviewVisibilityChanged>({
410+
eventName: "UrlPreviewVisibilityChanged",
411+
previewKind: "LegacyCard",
412+
hasThumbnail: this.snapshot.current.previews.some((p) => !!p.image),
413+
previewCount: this.snapshot.current.previews.length,
414+
visible: this.urlPreviewEnabledByUser,
415+
});
407416
return this.computeSnapshot();
408417
};
409418

0 commit comments

Comments
 (0)