Move ToastContext and utilities to shared components (#33949)

* Move ToastContext and utilities to shared components

* lint

* fix type

* cleanup

* fix broken test

* fix lint

* Add more tests for ToastContext

* Potential fix for pull request finding 'Unused variable, import, function or class'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
This commit is contained in:
Will Hunt
2026-06-29 18:50:36 +00:00
committed by GitHub
co-authored by Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
parent 6ce24e2668
commit 0ddc418cf9
8 changed files with 89 additions and 49 deletions
@@ -23,6 +23,7 @@ import LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock";
import LabsIcon from "@vector-im/compound-design-tokens/assets/web/icons/labs";
import BlockIcon from "@vector-im/compound-design-tokens/assets/web/icons/block";
import HelpIcon from "@vector-im/compound-design-tokens/assets/web/icons/help";
import { ToastContext, useActiveToast } from "@element-hq/web-shared-components";
import TabbedView, { Tab, useActiveTabWithDefault } from "../../structures/TabbedView";
import { _t, _td } from "../../../languageHandler";
@@ -46,7 +47,6 @@ import { type NonEmptyArray } from "../../../@types/common";
import { SDKContext, type SdkContextClass } from "../../../contexts/SDKContext";
import { useSettingValue } from "../../../hooks/useSettings";
import { NoChange, useEventEmitterAsyncState, type AsyncStateCallbackResult } from "../../../hooks/useEventEmitter";
import { ToastContext, useActiveToast } from "../../../contexts/ToastContext";
import { EncryptionUserSettingsTab, type State } from "../settings/tabs/user/EncryptionUserSettingsTab";
interface IProps {
@@ -11,14 +11,13 @@ import { logger } from "matrix-js-sdk/src/logger";
import { EditInPlace, Alert, ErrorMessage } from "@vector-im/compound-web";
import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out";
import SignOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/sign-out";
import { Flex } from "@element-hq/web-shared-components";
import { Flex, useToastContext } from "@element-hq/web-shared-components";
import { _t } from "../../../languageHandler";
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
import AvatarSetting from "./AvatarSetting";
import PosthogTrackers from "../../../PosthogTrackers";
import { formatBytes } from "../../../utils/FormattingUtils";
import { useToastContext } from "../../../contexts/ToastContext";
import InlineSpinner from "../elements/InlineSpinner";
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
import CopyableText from "../elements/CopyableText";
-85
View File
@@ -1,85 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024 The Matrix.org Foundation C.I.C.
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 ReactNode, createContext, useCallback, useContext, useEffect, useState, useMemo } from "react";
/**
* A ToastContext helps components display any kind of toast message and can be provided
* by a parent component such that their children can display toasts, eg. a settings dialog
* can provide a ToastContext such that controls within it can display toasts at the bottom
* of the dialog.
*
* It is not (at time of writing) used by the *other* toasts that appear in the top right
* corner of the app, however the name 'toast' as used in this class refers to the component
* of the same name in compound that it is written to manage.
*/
export const ToastContext = createContext(null as any);
ToastContext.displayName = "ToastContext";
/**
* Returns the ToastRack in context in order to display toasts
*/
export function useToastContext(): ToastRack {
return useContext(ToastContext);
}
/**
* For components that wish to display toasts, return the currently active toast and
* the ToastRack object that should be provided to the context
*/
export function useActiveToast(): [ReactNode | undefined, ToastRack] {
const toastRack = useMemo(() => new ToastRack(), []);
const [activeToast, setActiveToast] = useState<ReactNode | undefined>(toastRack.getActiveToast());
const updateCallback = useCallback(() => {
setActiveToast(toastRack.getActiveToast());
}, [setActiveToast, toastRack]);
useEffect(() => {
toastRack.setCallback(updateCallback);
}, [toastRack, updateCallback]);
return [activeToast, toastRack];
}
interface DisplayedToast {
id: number;
contents: ReactNode;
}
type RemoveCallback = () => void;
export class ToastRack {
private currentToast: DisplayedToast | undefined;
private updateCallback?: () => void;
private idSeq = 0;
public setCallback(cb: () => void): void {
this.updateCallback = cb;
}
public displayToast(contents: ReactNode): RemoveCallback {
const newToastId = ++this.idSeq;
this.currentToast = { id: newToastId, contents: contents };
this.updateCallback?.();
const removeFn = (): void => {
if (this.currentToast?.id === newToastId) {
this.currentToast = undefined;
this.updateCallback?.();
}
};
return removeFn;
}
public getActiveToast(): ReactNode | undefined {
return this.currentToast?.contents;
}
}
@@ -12,10 +12,10 @@ import { type MatrixClient, type UploadResponse } from "matrix-js-sdk/src/matrix
import { mocked } from "jest-mock";
import userEvent from "@testing-library/user-event";
import { TooltipProvider } from "@vector-im/compound-web";
import { ToastContext, type ToastRack } from "@element-hq/web-shared-components";
import UserProfileSettings from "../../../../../src/components/views/settings/UserProfileSettings";
import { mkStubRoom, stubClient } from "../../../../test-utils";
import { ToastContext, type ToastRack } from "../../../../../src/contexts/ToastContext";
import { OwnProfileStore } from "../../../../../src/stores/OwnProfileStore";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import Modal from "../../../../../src/Modal";
@@ -72,7 +72,7 @@ const renderProfileSettings = (toastRack: Partial<ToastRack>, client: MatrixClie
return render(
<TooltipProvider>
<MatrixClientContext.Provider value={client}>
<ToastContext.Provider value={toastRack}>
<ToastContext.Provider value={toastRack as ToastRack}>
<UserProfileSettings canSetAvatar={true} canSetDisplayName={true} />
</ToastContext.Provider>
</MatrixClientContext.Provider>
@@ -12,6 +12,7 @@ import { type MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import userEvent from "@testing-library/user-event";
import { type MockedObject } from "jest-mock-vitest-adapter";
import { ToastContext, ToastRack } from "@element-hq/web-shared-components";
import AccountUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/AccountUserSettingsTab";
import { SdkContextClass, SDKContext } from "../../../../../../../src/contexts/SDKContext";
@@ -54,7 +55,9 @@ describe("<AccountUserSettingsTab />", () => {
const getComponent = () => (
<MatrixClientContext.Provider value={mockClient}>
<SDKContext.Provider value={stores}>
<AccountUserSettingsTab {...defaultProps} />
<ToastContext.Provider value={new ToastRack()}>
<AccountUserSettingsTab {...defaultProps} />
</ToastContext.Provider>
</SDKContext.Provider>
</MatrixClientContext.Provider>
);
@@ -1,35 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024 The Matrix.org Foundation C.I.C.
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 { ToastRack } from "../../../src/contexts/ToastContext";
describe("ToastRack", () => {
it("should return a toast once one is displayed", () => {
const toastRack = new ToastRack();
toastRack.displayToast("Hello, world!");
expect(toastRack.getActiveToast()).toBe("Hello, world!");
});
it("calls update callback when a toast is added", () => {
const toastRack = new ToastRack();
const updateCallbackFn = jest.fn();
toastRack.setCallback(updateCallbackFn);
toastRack.displayToast("Hello, world!");
expect(updateCallbackFn).toHaveBeenCalled();
});
it("removes toast when remove function is called", () => {
const toastRack = new ToastRack();
const removeFn = toastRack.displayToast("Hello, world!");
expect(toastRack.getActiveToast()).toBe("Hello, world!");
removeFn();
expect(toastRack.getActiveToast()).toBeUndefined();
});
});