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