mv element.io @types __mocks__/ debian docker module_system/ playwright res src test webapp Dockerfile .dockerignore .eslintignore .stylelintrc.cjs babel.config.cjs recorder-worklet-loader.cjs .modernizr.json components.json config.json config.sample.json package.json project.json tsconfig.json tsconfig.module_system.json jest.config.ts playwright.config.ts webpack.config.ts build_config.sample.yaml apps/web/

mkdir apps/web/scripts
mv scripts/{cleanup.sh,ci_package.sh,copy-res.ts,deploy.py,package.sh} apps/web/scripts

And a couple of gitignore tweaks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2026-02-24 15:43:58 +00:00
parent e7509c92a1
commit 91a3cb03c1
3408 changed files with 28 additions and 32 deletions
@@ -0,0 +1,101 @@
/*
* 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 { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";
import { DecryptionFailureReason } from "@element-hq/web-shared-components";
import { DecryptionFailureBodyViewModel } from "../../../src/viewmodels/message-body/DecryptionFailureBodyViewModel";
describe("DecryptionFailureBodyViewModel", () => {
it("should return the snapshot", () => {
const vm = new DecryptionFailureBodyViewModel({
decryptionFailureCode: null,
verificationState: true,
});
expect(vm.getSnapshot()).toMatchObject({
decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT,
isLocalDeviceVerified: true,
});
});
it("should return the snapshot with extra class names", () => {
const vm = new DecryptionFailureBodyViewModel({
decryptionFailureCode: null,
verificationState: true,
extraClassNames: ["custom-class"],
});
expect(vm.getSnapshot()).toMatchObject({
decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT,
isLocalDeviceVerified: true,
extraClassNames: ["mx_DecryptionFailureBody", "mx_EventTile_content", "custom-class"],
});
});
it.each([
{
code: DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
reason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
},
{
code: DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP,
reason: DecryptionFailureReason.HISTORICAL_MESSAGE_NO_KEY_BACKUP,
},
{
code: DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED,
reason: DecryptionFailureReason.HISTORICAL_MESSAGE_USER_NOT_JOINED,
},
{
code: DecryptionFailureCode.MEGOLM_KEY_WITHHELD,
reason: DecryptionFailureReason.UNABLE_TO_DECRYPT,
},
{
code: DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
reason: DecryptionFailureReason.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
},
{
code: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
reason: DecryptionFailureReason.UNABLE_TO_DECRYPT,
},
{
code: DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
reason: DecryptionFailureReason.UNABLE_TO_DECRYPT,
},
{
code: DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED,
reason: DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED,
},
{
code: DecryptionFailureCode.UNKNOWN_ERROR,
reason: DecryptionFailureReason.UNABLE_TO_DECRYPT,
},
{
code: DecryptionFailureCode.UNKNOWN_SENDER_DEVICE,
reason: DecryptionFailureReason.UNABLE_TO_DECRYPT,
},
{
code: DecryptionFailureCode.UNSIGNED_SENDER_DEVICE,
reason: DecryptionFailureReason.UNSIGNED_SENDER_DEVICE,
},
])("should return the snapshot with code converted to reason (%s)", ({ code, reason }) => {
const vm = new DecryptionFailureBodyViewModel({
decryptionFailureCode: code,
});
expect(vm.getSnapshot().decryptionFailureReason).toBe(reason);
});
it("should update snapshot when setProps is called with new verificationState", () => {
const vm = new DecryptionFailureBodyViewModel({
decryptionFailureCode: DecryptionFailureCode.UNKNOWN_ERROR,
verificationState: false,
});
expect(vm.getSnapshot().isLocalDeviceVerified).toBe(false);
vm.setVerificationState(true);
expect(vm.getSnapshot().isLocalDeviceVerified).toBe(true);
});
});
@@ -0,0 +1,123 @@
/*
* 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 * as DateUtils from "../../../src/DateUtils";
import { MessageTimestampViewModel } from "../../../src/viewmodels/message-body/MessageTimestampViewModel";
jest.mock("../../../src/settings/SettingsStore");
describe("MessageTimestampViewModel", () => {
// Friday Dec 17 2021, 9:09am
const nowDate = new Date("2021-12-17T08:09:00.000Z");
const HOUR_MS = 3600000;
const DAY_MS = HOUR_MS * 24;
afterEach(() => {
jest.restoreAllMocks();
});
it("should return the snapshot", () => {
const vm = new MessageTimestampViewModel({
ts: nowDate.getTime(),
});
expect(vm.getSnapshot()).toMatchObject({
ts: "08:09",
tsSentAt: "Fri, Dec 17, 2021, 08:09:00",
});
});
it("should return the snapshot with tsReceivedAt", () => {
const vm = new MessageTimestampViewModel({
ts: nowDate.getTime(),
receivedTs: nowDate.getTime() + DAY_MS,
});
expect(vm.getSnapshot()).toMatchObject({
ts: "08:09",
tsSentAt: "Fri, Dec 17, 2021, 08:09:00",
tsReceivedAt: "Sat, Dec 18, 2021, 08:09:00",
});
});
it("should return the snapshot with extra class names", () => {
const vm = new MessageTimestampViewModel({
ts: nowDate.getTime(),
});
expect(vm.getSnapshot()).toMatchObject({
ts: "08:09",
tsSentAt: "Fri, Dec 17, 2021, 08:09:00",
className: "mx_MessageTimestamp",
});
});
it("should use formatRelativeTime when showRelative is true", () => {
jest.spyOn(DateUtils, "formatFullDate").mockReturnValue("SENT_AT");
const formatRelativeTimeSpy = jest.spyOn(DateUtils, "formatRelativeTime").mockReturnValue("RELATIVE");
const vm = new MessageTimestampViewModel({
ts: nowDate.getTime(),
showRelative: true,
showTwelveHour: true,
});
expect(vm.getSnapshot()).toMatchObject({
ts: "RELATIVE",
tsSentAt: "SENT_AT",
});
expect(formatRelativeTimeSpy).toHaveBeenCalledWith(expect.any(Date), true);
});
it("should use full date when showFullDate is true and respect showSeconds", () => {
const formatFullDateSpy = jest
.spyOn(DateUtils, "formatFullDate")
.mockImplementation((_date, _showTwelveHour, showSeconds) =>
showSeconds === false ? "FULL_NO_SECONDS" : "SENT_AT",
);
const vm = new MessageTimestampViewModel({
ts: nowDate.getTime(),
showFullDate: true,
showSeconds: false,
});
expect(vm.getSnapshot()).toMatchObject({
ts: "FULL_NO_SECONDS",
tsSentAt: "SENT_AT",
});
expect(formatFullDateSpy).toHaveBeenCalled();
});
it("should use full time when showSeconds is true without full date", () => {
jest.spyOn(DateUtils, "formatFullDate").mockReturnValue("SENT_AT");
const formatFullTimeSpy = jest.spyOn(DateUtils, "formatFullTime").mockReturnValue("FULL_TIME");
const formatTimeSpy = jest.spyOn(DateUtils, "formatTime").mockReturnValue("TIME");
const vm = new MessageTimestampViewModel({
ts: nowDate.getTime(),
showSeconds: true,
});
expect(vm.getSnapshot()).toMatchObject({
ts: "FULL_TIME",
tsSentAt: "SENT_AT",
});
expect(formatFullTimeSpy).toHaveBeenCalled();
expect(formatTimeSpy).not.toHaveBeenCalled();
});
it("should include tooltip inhibition and href in the snapshot", () => {
const vm = new MessageTimestampViewModel({
ts: nowDate.getTime(),
inhibitTooltip: true,
href: "https://example.test",
});
expect(vm.getSnapshot()).toMatchObject({
inhibitTooltip: true,
href: "https://example.test",
});
});
});
@@ -0,0 +1,172 @@
/*
* 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 { type MatrixClient, type MatrixEvent, type Room, type RoomMember } from "matrix-js-sdk/src/matrix";
import {
ReactionsRowButtonTooltipViewModel,
type ReactionsRowButtonTooltipViewModelProps,
} from "../../../src/viewmodels/message-body/ReactionsRowButtonTooltipViewModel";
import { stubClient, mkStubRoom, mkEvent } from "../../test-utils";
import { unicodeToShortcode } from "../../../src/HtmlUtils";
jest.mock("../../../src/HtmlUtils", () => ({
...jest.requireActual("../../../src/HtmlUtils"),
unicodeToShortcode: jest.fn(),
}));
const mockedUnicodeToShortcode = jest.mocked(unicodeToShortcode);
describe("ReactionsRowButtonTooltipViewModel", () => {
let client: MatrixClient;
let room: Room;
let mxEvent: MatrixEvent;
const createReactionEvent = (senderId: string, content?: Record<string, unknown>): MatrixEvent => {
return mkEvent({
event: true,
type: "m.reaction",
room: room.roomId,
user: senderId,
content: {
"m.relates_to": { rel_type: "m.annotation", event_id: mxEvent.getId(), key: "👍" },
...content,
},
});
};
const createProps = (
overrides?: Partial<ReactionsRowButtonTooltipViewModelProps>,
): ReactionsRowButtonTooltipViewModelProps => ({
client,
mxEvent,
content: "👍",
reactionEvents: [],
customReactionImagesEnabled: false,
...overrides,
});
beforeEach(() => {
client = stubClient();
room = mkStubRoom("!room:example.org", "Test Room", client);
jest.spyOn(client, "getRoom").mockReturnValue(room);
mxEvent = mkEvent({
event: true,
type: "m.room.message",
room: room.roomId,
user: "@sender:example.org",
content: { body: "Test message", msgtype: "m.text" },
});
mockedUnicodeToShortcode.mockImplementation((char: string) => {
if (char === "👍") return ":thumbsup:";
return "";
});
});
afterEach(() => {
jest.restoreAllMocks();
mockedUnicodeToShortcode.mockReset();
});
it("should return undefined snapshot when room is not found", () => {
jest.spyOn(client, "getRoom").mockReturnValue(null);
const vm = new ReactionsRowButtonTooltipViewModel(createProps());
const snapshot = vm.getSnapshot();
expect(snapshot.formattedSenders).toBeUndefined();
expect(snapshot.caption).toBeUndefined();
});
it("should return undefined snapshot when MatrixClient is unavailable", () => {
const vm = new ReactionsRowButtonTooltipViewModel(createProps({ client: null }));
const snapshot = vm.getSnapshot();
expect(snapshot.formattedSenders).toBeUndefined();
expect(snapshot.caption).toBeUndefined();
});
it("should compute formattedSenders and caption from reaction events", () => {
const reactionEvent = createReactionEvent("@alice:example.org");
jest.spyOn(room, "getMember").mockReturnValue({ name: "Alice", userId: "@alice:example.org" } as RoomMember);
const vm = new ReactionsRowButtonTooltipViewModel(createProps({ reactionEvents: [reactionEvent] }));
const snapshot = vm.getSnapshot();
expect(snapshot.formattedSenders).toBe("Alice");
expect(snapshot.caption).toContain(":thumbsup:");
});
it("should fall back to sender ID when member is not found", () => {
const reactionEvent = createReactionEvent("@unknown:example.org");
jest.spyOn(room, "getMember").mockReturnValue(null);
const vm = new ReactionsRowButtonTooltipViewModel(createProps({ reactionEvents: [reactionEvent] }));
expect(vm.getSnapshot().formattedSenders).toBe("@unknown:example.org");
});
it("should use custom reaction shortcode when customReactionImagesEnabled is true", () => {
mockedUnicodeToShortcode.mockReturnValue("");
const reactionEvent = createReactionEvent("@alice:example.org", {
"com.beeper.reaction.shortcode": "custom_emoji",
});
jest.spyOn(room, "getMember").mockReturnValue({ name: "Alice", userId: "@alice:example.org" } as RoomMember);
const vm = new ReactionsRowButtonTooltipViewModel(
createProps({
content: "mxc://custom/emoji",
reactionEvents: [reactionEvent],
customReactionImagesEnabled: true,
}),
);
expect(vm.getSnapshot().caption).toContain("custom_emoji");
});
it("should not use custom reaction shortcode when customReactionImagesEnabled is false", () => {
mockedUnicodeToShortcode.mockReturnValue("");
const reactionEvent = createReactionEvent("@alice:example.org", {
"com.beeper.reaction.shortcode": "custom_emoji",
});
jest.spyOn(room, "getMember").mockReturnValue({ name: "Alice", userId: "@alice:example.org" } as RoomMember);
const vm = new ReactionsRowButtonTooltipViewModel(
createProps({
content: "mxc://custom/emoji",
reactionEvents: [reactionEvent],
customReactionImagesEnabled: false,
}),
);
expect(vm.getSnapshot().caption).toBeUndefined();
});
it("should update snapshot and notify subscribers when setProps is called", () => {
const aliceReaction = createReactionEvent("@alice:example.org");
const bobReaction = createReactionEvent("@bob:example.org");
jest.spyOn(room, "getMember").mockImplementation((userId) => {
const names: Record<string, string> = { "@alice:example.org": "Alice", "@bob:example.org": "Bob" };
return names[userId!] ? ({ name: names[userId!], userId } as RoomMember) : null;
});
const vm = new ReactionsRowButtonTooltipViewModel(createProps({ reactionEvents: [aliceReaction] }));
expect(vm.getSnapshot().formattedSenders).toBe("Alice");
const subscriber = jest.fn();
vm.subscribe(subscriber);
vm.setProps({ reactionEvents: [aliceReaction, bobReaction] });
expect(subscriber).toHaveBeenCalled();
expect(vm.getSnapshot().formattedSenders).toContain("Alice");
expect(vm.getSnapshot().formattedSenders).toContain("Bob");
});
});