Add support for experimental MSC4335 M_USER_EXCEEDED_LIMIT error code

This commit is contained in:
Hugh Nimmo-Smith 2026-01-19 17:28:50 +00:00
parent 93a8b67ed0
commit aa1099fd36
4 changed files with 174 additions and 8 deletions

View File

@ -18,6 +18,7 @@ import {
type UploadOpts,
type UploadProgress,
THREAD_RELATION_TYPE,
MatrixError,
} from "matrix-js-sdk/src/matrix";
import {
type ImageInfo,
@ -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";
@ -56,6 +58,7 @@ import { createThumbnail } from "./utils/image-media";
import { attachMentions, attachRelation } from "./utils/messages.ts";
import { doMaybeLocalRoomAction } from "./utils/local-room";
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
@ -665,16 +668,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 canUpgrade =
typeof unwrappedError.data["org.matrix.msc4335.can_upgrade"] === "boolean"
? unwrappedError.data["org.matrix.msc4335.can_upgrade"]
: false;
Modal.createDialog(MSC4335UserLimitExceededDialog, {
title: _t("upload_failed_title"),
error: {
infoUri: unwrappedError.data["org.matrix.msc4335.info_uri"],
canUpgrade,
},
});
} 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 {

View File

@ -0,0 +1,79 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, useMemo } from "react";
import { _t } from "../../../languageHandler";
import BaseDialog from "./BaseDialog";
import AccessibleButton from "../elements/AccessibleButton";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
interface MSC4335Data {
infoUri: string;
canUpgrade: boolean;
}
interface IProps {
onFinished?: (success?: boolean) => void;
title?: string;
error: MSC4335Data;
}
export default function MSC4335UserLimitExceededDialog({ onFinished: _onFinished, title, error }: IProps): JSX.Element {
function onFinished(success?: boolean): void {
_onFinished?.(success);
};
function onClick(): void {
// noop as using href
};
const matrixClient = useMatrixClientContext();
const isMatrixDotOrg = useMemo(() => matrixClient?.getDomain() === "matrix.org", [matrixClient]);
return (
<BaseDialog
className="mx_ErrorDialog"
title={title || _t("msc4335_user_limit_exceeded|title")}
contentId="mx_Dialog_content"
onFinished={onFinished}
>
<div className="mx_Dialog_content" id="mx_Dialog_content">
{error.canUpgrade
? (isMatrixDotOrg ? _t(
"msc4335_matrix_org_user_limit_exceeded|soft_limit",
) : _t("msc4335_user_limit_exceeded|soft_limit"))
: (isMatrixDotOrg ? _t(
"msc4335_matrix_org_user_limit_exceeded|hard_limit",
) : _t("msc4335_user_limit_exceeded|hard_limit"))
}
</div>
<div className="mx_Dialog_buttons">
<div className="mx_Dialog_buttons_row">
<AccessibleButton
kind="primary"
element="a"
href={error.infoUri}
target="_blank"
rel="noreferrer noopener"
data-testid="learn-more"
onClick={onClick}
>
{error.canUpgrade
? (isMatrixDotOrg ? _t(
"msc4335_matrix_org_user_limit_exceeded|soft_limit_button",
) : _t("msc4335_user_limit_exceeded|soft_limit_button"))
: (isMatrixDotOrg ? _t(
"msc4335_matrix_org_user_limit_exceeded|hard_limit_button",
) : _t("msc4335_user_limit_exceeded|hard_limit_button"))
}
</AccessibleButton>
</div>
</div>
</BaseDialog>
);
}

View File

@ -1708,6 +1708,20 @@
"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_matrix_org_user_limit_exceeded": {
"hard_limit": "You have reached the usage limits of your plan on the matrix.org homeserver.",
"hard_limit_button": "Learn more",
"soft_limit": "You have reached the usage limits of your Free plan on the matrix.org homeserver. You can upgrade to Premium to increase the limits and to support the work of the Matrix.org Foundation.",
"soft_limit_button": "Get Premium",
"title": "Usage limit exceeded"
},
"msc4335_user_limit_exceeded": {
"hard_limit": "You have exceeded a usage limit imposed by your homeserver.",
"hard_limit_button": "Learn more",
"soft_limit": "You have exceeded a usage limit imposed by your homeserver. You may be able to increase your limit.",
"soft_limit_button": "Learn more",
"title": "Account limit exceeded"
},
"name_and_id": "%(name)s (%(userId)s)",
"no_more_results": "No more results",
"notif_panel": {

View File

@ -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({}) }));
@ -317,6 +318,55 @@ 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",
canUpgrade: false,
},
}),
);
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.can_upgrade": true,
}),
);
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",
canUpgrade: true,
},
}),
);
dialogSpy.mockRestore();
});
});
describe("getCurrentUploads", () => {