Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions src/ContentMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type UploadOpts,
type UploadProgress,
THREAD_RELATION_TYPE,
MatrixError,
} from "matrix-js-sdk/src/matrix";
import {
type ImageInfo,
Expand All @@ -31,6 +32,7 @@ import encrypt from "matrix-encrypt-attachment";
import extractPngChunks from "png-chunks-extract";
import { logger } from "matrix-js-sdk/src/logger";
import { removeElement } from "matrix-js-sdk/src/utils";
import { type ReactNode } from "react";

import dis from "./dispatcher/dispatcher";
import { _t } from "./languageHandler";
Expand All @@ -57,6 +59,7 @@ import { attachMentions, attachRelation } from "./components/views/rooms/SendMes
import { doMaybeLocalRoomAction } from "./utils/local-room";
import { SdkContextClass } from "./contexts/SDKContext";
import { blobIsAnimated } from "./utils/Image.ts";
import MSC4335UserLimitExceededDialog from "./components/views/dialogs/MSC4335UserLimitExceededDialog.tsx";

// scraped out of a macOS hidpi (5660ppm) screenshot png
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
Expand Down Expand Up @@ -653,16 +656,36 @@ export default class ContentMessages {
}

if (!upload.cancelled) {
let desc = _t("upload_failed_generic", { fileName: upload.fileName });
if (unwrappedError instanceof HTTPError && unwrappedError.httpStatus === 413) {
desc = _t("upload_failed_size", {
fileName: upload.fileName,
if (
unwrappedError instanceof MatrixError &&
unwrappedError.errcode === "ORG.MATRIX.MSC4335_USER_LIMIT_EXCEEDED" &&
typeof unwrappedError.data["org.matrix.msc4335.info_uri"] === "string"
) {
// Support for experimental MSC4335 M_USER_LIMIT_EXCEEDED error
const softLimit =
typeof unwrappedError.data["org.matrix.msc4335.soft_limit"] === "boolean"
? unwrappedError.data["org.matrix.msc4335.soft_limit"]
: false;
Modal.createDialog(MSC4335UserLimitExceededDialog, {
title: _t("upload_failed_title"),
error: {
infoUri: unwrappedError.data["org.matrix.msc4335.info_uri"],
increaseUri: softLimit ? unwrappedError.data["org.matrix.msc4335.increase_uri"] : undefined,
},
});
} else {
let desc: ReactNode = _t("upload_failed_generic", { fileName: upload.fileName });
if (unwrappedError instanceof HTTPError && unwrappedError.httpStatus === 413) {
desc = _t("upload_failed_size", {
fileName: upload.fileName,
});
}
Modal.createDialog(ErrorDialog, {
title: _t("upload_failed_title"),
description: desc,
});
}
Modal.createDialog(ErrorDialog, {
title: _t("upload_failed_title"),
description: desc,
});

dis.dispatch<UploadErrorPayload>({ action: Action.UploadFailed, upload, error });
}
} finally {
Expand Down
96 changes: 96 additions & 0 deletions src/components/views/dialogs/MSC4335UserLimitExceededDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
Copyright 2025 New Vector 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 React from "react";

import { _t } from "../../../languageHandler";
import BaseDialog from "./BaseDialog";
import AccessibleButton from "../elements/AccessibleButton";

interface MSC4335Data {
infoUri: string;
increaseUri?: string;
}

interface IProps {
onFinished?: (success?: boolean) => void;
title?: string;
error: MSC4335Data;
}

interface IState {
onFinished: (success: boolean) => void;
}

export default class MSC4335UserLimitExceededDialog extends React.Component<IProps, IState> {
private onFinished = (success?: boolean): void => {
this.props.onFinished?.(success);
};
private onClick = (): void => {
// noop as using href
};

public render(): React.ReactNode {
const softLimit = !!this.props.error.increaseUri;

return (
<BaseDialog
className="mx_ErrorDialog"
title={this.props.title || _t("msc4335_user_limit_exceeded|title")}
contentId="mx_Dialog_content"
onFinished={this.onFinished}
>
<div className="mx_Dialog_content" id="mx_Dialog_content">
{softLimit
? _t(
"msc4335_user_limit_exceeded|soft_limit",
{},
{
a: (sub) => (
<a href={this.props.error.infoUri} target="_blank" rel="noreferrer">
{sub}
</a>
),
},
)
: _t("msc4335_user_limit_exceeded|hard_limit")}
</div>
<div className="mx_Dialog_buttons">
<div className="mx_Dialog_buttons_row">
{softLimit && (
<AccessibleButton
kind="link"
element="a"
href={this.props.error.infoUri}
target="_blank"
rel="noreferrer noopener"
data-testid="learn-more"
onClick={this.onClick}
>
{_t("msc4335_user_limit_exceeded|learn_more")}
</AccessibleButton>
)}
<AccessibleButton
kind="primary"
element="a"
href={softLimit ? this.props.error.increaseUri : this.props.error.infoUri}
target="_blank"
rel="noreferrer noopener"
autoFocus={true}
onClick={this.onClick}
data-testid="primary-button"
>
{softLimit
? _t("msc4335_user_limit_exceeded|increase_limit")
: _t("msc4335_user_limit_exceeded|learn_more")}
</AccessibleButton>
</div>
</div>
</BaseDialog>
);
}
}
7 changes: 7 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,13 @@
"toast_description": "%(brand)s is experimental on a mobile web browser. For a better experience and the latest features, use our free native app.",
"toast_title": "Use app for a better experience"
},
"msc4335_user_limit_exceeded": {
"hard_limit": "You have exceeded a limit imposed by your homeserver.",
"increase_limit": "Increase limit",
"learn_more": "Learn more",
"soft_limit": "You have exceeded a limit imposed by your homeserver. The limit can be increased.",
"title": "Account limit exceeded"
},
"name_and_id": "%(name)s (%(userId)s)",
"no_more_results": "No more results",
"notif_panel": {
Expand Down
51 changes: 51 additions & 0 deletions test/unit-tests/ContentMessages-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { BlurhashEncoder } from "../../src/BlurhashEncoder";
import Modal from "../../src/Modal";
import ErrorDialog from "../../src/components/views/dialogs/ErrorDialog";
import { _t } from "../../src/languageHandler";
import MSC4335UserLimitExceededDialog from "../../src/components/views/dialogs/MSC4335UserLimitExceededDialog";

jest.mock("matrix-encrypt-attachment", () => ({ encryptAttachment: jest.fn().mockResolvedValue({}) }));

Expand Down Expand Up @@ -317,6 +318,56 @@ describe("ContentMessages", () => {
);
dialogSpy.mockRestore();
});

it("handles MSC4335 M_USER_LIMIT_EXCEEDED error with hard limit", async () => {
mocked(client.uploadContent).mockRejectedValue(
new MatrixError({
"errcode": "ORG.MATRIX.MSC4335_USER_LIMIT_EXCEEDED",
"error": "User limit exceeded",
"org.matrix.msc4335.info_uri": "https://example.com/info",
}),
);
const file = new File([], "fileName", { type: "image/jpeg" });
const dialogSpy = jest.spyOn(Modal, "createDialog");
await contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined);
expect(dialogSpy).toHaveBeenCalledWith(
MSC4335UserLimitExceededDialog,
expect.objectContaining({
title: _t("upload_failed_title"),
error: {
infoUri: "https://example.com/info",
increaseUri: undefined,
},
}),
);
dialogSpy.mockRestore();
});

it("handles MSC4335 M_USER_LIMIT_EXCEEDED error with soft limit", async () => {
mocked(client.uploadContent).mockRejectedValue(
new MatrixError({
"errcode": "ORG.MATRIX.MSC4335_USER_LIMIT_EXCEEDED",
"error": "User limit exceeded",
"org.matrix.msc4335.info_uri": "https://example.com/info",
"org.matrix.msc4335.soft_limit": true,
"org.matrix.msc4335.increase_uri": "https://example.com/increase",
}),
);
const file = new File([], "fileName", { type: "image/jpeg" });
const dialogSpy = jest.spyOn(Modal, "createDialog");
await contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined);
expect(dialogSpy).toHaveBeenCalledWith(
MSC4335UserLimitExceededDialog,
expect.objectContaining({
title: _t("upload_failed_title"),
error: {
infoUri: "https://example.com/info",
increaseUri: "https://example.com/increase",
},
}),
);
dialogSpy.mockRestore();
});
});

describe("getCurrentUploads", () => {
Expand Down
Loading