Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/fix-error-toast-styling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Fix failure toasts showing the green success style with a checkmark: errors such as "Unable to mark this room as read" now use the error colour and a warning icon.
12 changes: 8 additions & 4 deletions src/app/components/image-viewer/ImageViewer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import FileSaver from 'file-saver';
import { ImageViewer } from './ImageViewer';
import { showToast } from '$state/toast';
import { showErrorToast } from '$state/toast';
import type { IImageInfo } from '$types/matrix/common';

const downloadMedia = vi.fn<(src: string) => Promise<Blob>>();
const saveMediaToGallery =
vi.fn<(input: Blob | string, filename: string, mimeType: string) => Promise<void>>();
const toastMocks = vi.hoisted(() => ({
showToast: vi.fn<(text: string, durationMs?: number) => void>(),
showErrorToast: vi.fn<(text: string, durationMs?: number) => void>(),
}));
vi.mock('$state/toast', () => ({
showToast: toastMocks.showToast,
showErrorToast: toastMocks.showErrorToast,
}));
vi.mock('$state/toast', () => ({ showToast: toastMocks.showToast }));
const platformMocks = vi.hoisted(() => ({
isAndroidTauri: vi.fn<() => boolean>(() => false),
iosApp: vi.fn<() => boolean>(() => false),
Expand Down Expand Up @@ -229,14 +233,14 @@ describe('ImageViewer', () => {
it('shows an error toast when downloading media fails', async () => {
const error = new Error('network unavailable');
downloadMedia.mockRejectedValue(error);
vi.mocked(showToast).mockClear();
vi.mocked(showErrorToast).mockClear();

renderViewer();

fireEvent.click(screen.getByText('Download'));

await waitFor(() => {
expect(showToast).toHaveBeenCalledWith('Failed to download file: network unavailable');
expect(showErrorToast).toHaveBeenCalledWith('Failed to download file: network unavailable');
});
});

Expand Down
6 changes: 3 additions & 3 deletions src/app/components/image-viewer/ImageViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { useMenuAnchor } from '$hooks/useMenuAnchor';
import { useDismissOnBack } from '$utils/androidBack';
import { useSetting } from '$state/hooks/settings';
import { isPixelatedRendering, settingsAtom } from '$state/settings';
import { showToast } from '$state/toast';
import { showErrorToast } from '$state/toast';
import { downloadMedia } from '$utils/matrix';
import * as css from './ImageViewer.css';
import type { IImageInfo } from '$types/matrix/common';
Expand Down Expand Up @@ -168,7 +168,7 @@ export const ImageViewer = as<'div', ImageViewerProps>(
} catch (error) {
reportDownloadFailure(error, 'fetch', downloadFilename, galleryMimeType);
const message = error instanceof Error ? error.message : 'unknown error';
showToast(`Failed to save to gallery: ${message}`);
showErrorToast(`Failed to save to gallery: ${message}`);
}
};

Expand All @@ -183,7 +183,7 @@ export const ImageViewer = as<'div', ImageViewerProps>(
} catch (error) {
reportDownloadFailure(error, 'fetch', downloadFilename, galleryMimeType);
const message = error instanceof Error ? error.message : 'unknown error';
showToast(`Failed to download file: ${message}`);
showErrorToast(`Failed to download file: ${message}`);
return;
}
await saveFileToDevice(
Expand Down
10 changes: 6 additions & 4 deletions src/app/components/toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createPortal } from 'react-dom';
import { Box, Text, color, config, toRem } from 'folds';

import { Check, sizedIcon } from '$components/icons/phosphor';
import { Check, Warning, sizedIcon } from '$components/icons/phosphor';
import { useToastMessage } from '$state/toast';
import { OVERLAY_LAYER_TOP } from '$components/overlay-stack';

Expand All @@ -13,6 +13,8 @@ export function Toast({ container }: ToastProps) {
const toast = useToastMessage();
if (!toast || !container) return null;

const isError = toast.status === 'error';

return createPortal(
<div
style={{
Expand All @@ -33,14 +35,14 @@ export function Toast({ container }: ToastProps) {
gap="200"
style={{
padding: `${config.space.S200} ${config.space.S400}`,
backgroundColor: color.Success.Container,
color: color.Success.OnContainer,
backgroundColor: isError ? color.Critical.Container : color.Success.Container,
color: isError ? color.Critical.OnContainer : color.Success.OnContainer,
borderRadius: config.radii.Pill,
boxShadow: config.shadow.E200,
maxWidth: '90%',
}}
>
{sizedIcon(Check, '200')}
{sizedIcon(isError ? Warning : Check, '200')}
<Text size="T300" truncate>
{toast.text}
</Text>
Expand Down
4 changes: 2 additions & 2 deletions src/app/features/lobby/HierarchyItemMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { StateEvents } from '$types/matrix-sdk';
import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback';
import { LeaveSpacePrompt } from '$components/leave-space-prompt';
import { confirm } from '$components/confirm/confirm';
import { showToast } from '$state/toast';
import { showErrorToast } from '$state/toast';
import { ResponsiveMenu } from '$components/ResponsiveMenu';
import { useMenuAnchor } from '$hooks/useMenuAnchor';
import { useOpenRoomSettings } from '$state/hooks/roomSettings';
Expand Down Expand Up @@ -239,7 +239,7 @@ export function HierarchyItemMenu({
await mx.leave(item.roomId);
menu.close();
} catch (e) {
showToast(`Failed to leave room: ${e instanceof Error ? e.message : 'unknown error'}`);
showErrorToast(`Failed to leave room: ${e instanceof Error ? e.message : 'unknown error'}`);
}
}
};
Expand Down
10 changes: 5 additions & 5 deletions src/app/features/room/RoomTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const {
vListProps,
timelineSyncOptions,
timelineActionsOptions,
showToastMock,
showErrorToastMock,
markAsReadMock,
readMarkerInLiveTimeline,
} = vi.hoisted(() => ({
Expand Down Expand Up @@ -91,7 +91,7 @@ const {
vListProps: { shift: false, shiftValues: [] as boolean[] },
timelineSyncOptions: { current: undefined as Record<string, unknown> | undefined },
timelineActionsOptions: { current: undefined as Record<string, unknown> | undefined },
showToastMock: vi.fn<(text: string) => void>(),
showErrorToastMock: vi.fn<(text: string) => void>(),
markAsReadMock: vi.fn<() => void>(),
readMarkerInLiveTimeline: { current: false },
}));
Expand Down Expand Up @@ -157,7 +157,7 @@ vi.mock('$hooks/useRoomNavigate', () => ({
useRoomNavigate: () => ({ navigateRoom: navigateRoomMock }),
}));

vi.mock('$state/toast', () => ({ showToast: showToastMock }));
vi.mock('$state/toast', () => ({ showErrorToast: showErrorToastMock }));

vi.mock('$hooks/useSpace', () => ({ useSpaceOptionally: () => undefined }));

Expand Down Expand Up @@ -404,7 +404,7 @@ beforeEach(() => {
eventTimeline.current = liveTimeline;
liveTimeline.getEvents = () => [{ getId: () => '$evt1' }];
navigateRoomMock.mockReset();
showToastMock.mockReset();
showErrorToastMock.mockReset();
markAsReadMock.mockReset();
readMarkerInLiveTimeline.current = false;
vListProps.shift = false;
Expand Down Expand Up @@ -1014,7 +1014,7 @@ describe('jump reveal and focus-regain read receipts', () => {
const onJumpError = timelineSyncOptions.current?.onJumpError as (() => void) | undefined;
act(() => onJumpError?.());

expect(showToastMock).toHaveBeenCalledWith('Unable to load this message.');
expect(showErrorToastMock).toHaveBeenCalledWith('Unable to load this message.');
});

it('clears a notification route when an own message returns to the live timeline', async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/app/features/room/RoomTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import { getMemberDisplayName } from '$utils/room/display';
import { useRoomNavigate } from '$hooks/useRoomNavigate';
import { useSlidingSyncRoomLoading } from '$hooks/useSlidingSyncActiveRoom';
import { useOpenUserRoomProfile } from '$state/hooks/userRoomProfile';
import { showToast } from '$state/toast';
import { showErrorToast } from '$state/toast';
import { useSpaceOptionally } from '$hooks/useSpace';
import { useIgnoredUsers } from '$hooks/useIgnoredUsers';
import { useImagePackRooms } from '$hooks/useImagePackRooms';
Expand Down Expand Up @@ -537,7 +537,7 @@ export function RoomTimeline({
const handleJumpError = useCallback(() => {
scrollAnchorRef.current = undefined;
setAtBottom(true);
showToast('Unable to load this message.');
showErrorToast('Unable to load this message.');
}, [setAtBottom]);
const handleReturnToLive = useCallback(() => {
scrollAnchorRef.current = undefined;
Expand Down
4 changes: 2 additions & 2 deletions src/app/features/settings/account/AnimalCosmetics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { UserProfile } from '$hooks/useUserProfile';
import { useSetting } from '$state/hooks/settings';
import { settingsAtom } from '$state/settings';
import { profilesCacheAtom } from '$state/userRoomProfile';
import { showToast } from '$state/toast';
import { showErrorToast } from '$state/toast';
import { invalidateUserProfileCache } from '$hooks/useUserProfile';
import { Box, IconButton, Input, Text } from 'folds';
import { useSetAtom } from 'jotai';
Expand Down Expand Up @@ -92,7 +92,7 @@ export function AnimalCosmetics({ profile, userId }: Readonly<AnimalCosmeticsPro
try {
await mx.setExtendedProfileProperty?.(key, value);
} catch (error) {
showToast(
showErrorToast(
`Failed to save profile field: ${error instanceof Error ? error.message : String(error)}`
);
return;
Expand Down
7 changes: 5 additions & 2 deletions src/app/features/settings/account/Profile.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { act, render, within } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { ScreenSize, ScreenSizeProvider } from '$hooks/useScreenSize';
import { SettingsLinkProvider } from '$features/settings/SettingsLinkContext';
import { showToast } from '$state/toast';
import { showErrorToast } from '$state/toast';
import { Profile } from './Profile';

vi.mock('$state/toast', () => ({
showToast: vi.fn<(text: string) => void>(),
showErrorToast: vi.fn<(text: string) => void>(),
}));

const timezoneEditorMock = vi.hoisted(() => ({
Expand Down Expand Up @@ -150,6 +151,8 @@ describe('Profile', () => {
timezoneEditorMock.props?.onSave('Europe/Paris');
});

expect(showToast).toHaveBeenCalledWith(expect.stringContaining('Failed to save profile field'));
expect(showErrorToast).toHaveBeenCalledWith(
expect.stringContaining('Failed to save profile field')
);
});
});
8 changes: 4 additions & 4 deletions src/app/features/settings/account/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { NameColorEditor } from './NameColorEditor';
import { StatusEditor } from './StatusEditor';
import { AnimalCosmetics } from './AnimalCosmetics';
import * as prefix from '$unstable/prefixes';
import { showToast } from '$state/toast';
import { showErrorToast } from '$state/toast';
import { confirm } from '$components/confirm/confirm';
import { AvatarUploadTile } from '$components/avatar-upload-tile/AvatarUploadTile';
import { accessibleColor } from '$plugins/color';
Expand Down Expand Up @@ -158,7 +158,7 @@ function ProfileBanner({ profile, userId }: Readonly<Pick<ProfileProps, 'profile
mxc
);
} catch (error) {
showToast(
showErrorToast(
`Failed to save profile field: ${error instanceof Error ? error.message : String(error)}`
);
setStagedUrl(undefined);
Expand Down Expand Up @@ -186,7 +186,7 @@ function ProfileBanner({ profile, userId }: Readonly<Pick<ProfileProps, 'profile
null
);
} catch (error) {
showToast(
showErrorToast(
`Failed to save profile field: ${error instanceof Error ? error.message : String(error)}`
);
setIsRemoving(false);
Expand Down Expand Up @@ -432,7 +432,7 @@ function ProfileExtended({ profile, userId }: Readonly<ProfileProps>) {
try {
await mx.setExtendedProfileProperty?.(key, value);
} catch (error) {
showToast(
showErrorToast(
`Failed to save profile field: ${error instanceof Error ? error.message : String(error)}`
);
return;
Expand Down
4 changes: 2 additions & 2 deletions src/app/hooks/useRoomMenuActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { usePowerLevels } from '$hooks/usePowerLevels';
import { markAsRead } from '$utils/notifications';
import { copyToClipboard } from '$utils/dom';
import { confirm } from '$components/confirm/confirm';
import { showToast } from '$state/toast';
import { showErrorToast } from '$state/toast';
import { useOpenRoomSettings } from '$state/hooks/roomSettings';
import { getMatrixToRoom } from '$plugins/matrix-to';
import { getViaServers } from '$plugins/via-servers';
Expand Down Expand Up @@ -114,7 +114,7 @@ export function useRoomMenuActions(room: Room) {
await mx.leave(room.roomId);
return true;
} catch (e) {
showToast(`Failed to leave room: ${e instanceof Error ? e.message : 'unknown error'}`);
showErrorToast(`Failed to leave room: ${e instanceof Error ? e.message : 'unknown error'}`);
return false;
}
}, [mx, room.roomId]);
Expand Down
7 changes: 5 additions & 2 deletions src/app/pages/client/client-non-ui/notifications.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ const notificationUtils = vi.hoisted(() => ({
markAsRead: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
}));

const toast = vi.hoisted(() => ({ showToast: vi.fn<(message: string) => void>() }));
const toast = vi.hoisted(() => ({
showToast: vi.fn<(message: string) => void>(),
showErrorToast: vi.fn<(message: string) => void>(),
}));

const getTauriNotificationsApi = vi.hoisted(() =>
vi.fn<() => Promise<typeof notificationsApi>>().mockResolvedValue(notificationsApi)
Expand Down Expand Up @@ -183,6 +186,6 @@ describe('NativeNotificationActionRouting', () => {
await waitFor(() => expect(matrixClient.sendMessage).toHaveBeenCalledOnce());
await waitFor(() => expect(store.get(nativeNotificationRepliesAtom)).toEqual([]));
expect(notificationUtils.markAsRead).toHaveBeenCalledOnce();
expect(toast.showToast).not.toHaveBeenCalled();
expect(toast.showErrorToast).not.toHaveBeenCalled();
});
});
12 changes: 6 additions & 6 deletions src/app/pages/client/client-non-ui/notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import {
} from '$utils/notificationStyle';
import { isMobileOrTablet } from '$utils/platform';
import { createDebugLogger } from '$utils/debugLogger';
import { showToast } from '$state/toast';
import { showErrorToast } from '$state/toast';
import {
nativeNotificationRepliesAtom,
nativeNotificationReplyInFlightAtom,
Expand Down Expand Up @@ -761,7 +761,7 @@ export function NativeNotificationActionRouting() {
!sessionsRef.current.some((session) => session.userId === reply.userId) ||
!enqueue(reply)
) {
showToast('Reply was not sent. Open the room to retry.');
showErrorToast('Reply was not sent. Open the room to retry.');
}
};

Expand All @@ -782,12 +782,12 @@ export function NativeNotificationActionRouting() {
const expiresIn = NATIVE_REPLY_EXPIRY_MS - (Date.now() - item.createdAt);
if (expiresIn <= 0) {
remove(item.key);
showToast('Reply was not sent. Open the room to retry.');
showErrorToast('Reply was not sent. Open the room to retry.');
return undefined;
}
const expiryTimer = setTimeout(() => {
remove(item.key);
showToast('Reply was not sent. Open the room to retry.');
showErrorToast('Reply was not sent. Open the room to retry.');
}, expiresIn);
const clearExpiryTimer = () => clearTimeout(expiryTimer);
if (mx.getUserId() !== item.userId) {
Expand All @@ -807,7 +807,7 @@ export function NativeNotificationActionRouting() {
}
if (room.getMyMembership() !== 'join') {
remove(item.key);
showToast('Reply was not sent. Open the room to retry.');
showErrorToast('Reply was not sent. Open the room to retry.');
return clearExpiryTimer;
}
if (inFlight.has(item.key)) return clearExpiryTimer;
Expand All @@ -821,7 +821,7 @@ export function NativeNotificationActionRouting() {
// notification lingers while later pushes stack onto it.
.then(() => markAsRead(mx, item.roomId, hideReads).catch(() => undefined))
.catch(() => {
showToast('Reply was not sent. Open the room to retry.');
showErrorToast('Reply was not sent. Open the room to retry.');
})
.finally(() => {
setInFlight((previous: Set<string>) => {
Expand Down
4 changes: 2 additions & 2 deletions src/app/pages/client/inbox/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { useMatrixClient } from '$hooks/useMatrixClient';
import { useRoomNavigate } from '$hooks/useRoomNavigate';
import { useSetting } from '$state/hooks/settings';
import { settingsAtom } from '$state/settings';
import { showToast } from '$state/toast';
import { showErrorToast } from '$state/toast';
import { markAsRead } from '$utils/notifications';
import { fetchNotificationEvent } from '$utils/notificationEvent';
import { getRoomAvatarUrl } from '$utils/room/display';
Expand Down Expand Up @@ -186,7 +186,7 @@ function NotificationRowItem({
onClick={() => {
void markAsRead(mx, room.roomId, hideReads, true)
.then(onMarkRead)
.catch(() => showToast('Unable to mark this room as read.'));
.catch(() => showErrorToast('Unable to mark this room as read.'));
}}
before={sizedIcon(Checks, '100')}
>
Expand Down
Loading
Loading