This commit is contained in:
nvrl committed 2026-06-15 20:18:51 +02:00
1 parent 3d01bfb4d1
commit cc916e472a
6 files changed
+31 -12

No files matched your search

+11
View File
@@ -74,6 +74,17 @@ Verify mutations `invalidateQueries`; consider optimistic insert so reviews appe
--- ---
## Implementation status ## Implementation status
- [x] Q4 — dropped the `as object` cast. Exported `RetriableConfig` from
`lib/api.ts`; `AuthContext.refetchUser` now passes `{ skipAuthRedirect: true }
as RetriableConfig` (the field is part of that type) instead of the
`...({ ... } as object)` spread hack. `tsc` + ESLint (0 errors) clean.
- [x] U3 — mark-all-read no longer fails silently. `NotificationContext.markAllRead`
catch block now fires an error toast (`community.markAllReadFailed`, EN+DE) via
`useToast().notify` instead of swallowing. Root cause was also a provider-order
bug: `ToastProvider` sat *inside* `NotificationProvider`, so the context couldn't
reach `useToast` — hoisted `ToastProvider` above `AuthProvider`/`NotificationProvider`
in `app/providers.tsx` (it only needs the MUI theme, which stays outermost).
`tsc` + ESLint (0 errors) clean.
- [x] U2 — killed the hard `window.location.href='/'` reloads. `Login.tsx` - [x] U2 — killed the hard `window.location.href='/'` reloads. `Login.tsx`
(post-login) now `await refetchUser()` + `navigate('/')`; `Settings.tsx` (post-login) now `await refetchUser()` + `navigate('/')`; `Settings.tsx`
(post-account-deletion) `queryClient.clear()` + `await refetchUser()` + (post-account-deletion) `queryClient.clear()` + `await refetchUser()` +
+6 -6
View File
@@ -12,13 +12,13 @@ import '../i18n';
export const AppProviders = ({ children }: { children: ReactNode }) => ( export const AppProviders = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ThemeProvider> <ThemeProvider>
<AuthProvider> <ToastProvider>
<NotificationProvider> <AuthProvider>
<ToastProvider> <NotificationProvider>
<ConfirmProvider>{children}</ConfirmProvider> <ConfirmProvider>{children}</ConfirmProvider>
</ToastProvider> </NotificationProvider>
</NotificationProvider> </AuthProvider>
</AuthProvider> </ToastProvider>
</ThemeProvider> </ThemeProvider>
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />} {import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider> </QueryClientProvider>
+3 -3
View File
@@ -1,6 +1,6 @@
import { createContext, useCallback, useEffect, useState, type ReactNode } from 'react'; import { createContext, useCallback, useEffect, useState, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import api from '../lib/api'; import api, { type RetriableConfig } from '../lib/api';
export interface AuthUser { export interface AuthUser {
username: string; username: string;
@@ -51,8 +51,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
const refetchUser = useCallback(async () => { const refetchUser = useCallback(async () => {
try { try {
const res = await api.get('users/me/', { const res = await api.get('users/me/', {
...({ skipAuthRedirect: true } as object), skipAuthRedirect: true,
}); } as RetriableConfig);
if (res.data?.username) { if (res.data?.username) {
setUser(res.data); setUser(res.data);
if (res.data.theme) localStorage.setItem('theme', res.data.theme); if (res.data.theme) localStorage.setItem('theme', res.data.theme);
@@ -1,6 +1,8 @@
import { createContext, useCallback, useEffect, type ReactNode } from 'react'; import { createContext, useCallback, useEffect, type ReactNode } from 'react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../hooks/useAuth'; import { useAuth } from '../hooks/useAuth';
import { useToast } from '../hooks/useToast';
import { queryKeys } from '../api/keys'; import { queryKeys } from '../api/keys';
import { useNotificationsQuery, type Notification } from '../api/queries/useNotifications'; import { useNotificationsQuery, type Notification } from '../api/queries/useNotifications';
import { import {
@@ -22,6 +24,8 @@ export const NotificationContext = createContext<NotificationContextValue | null
export const NotificationProvider = ({ children }: { children: ReactNode }) => { export const NotificationProvider = ({ children }: { children: ReactNode }) => {
const { user, setUser, refetchUser } = useAuth(); const { user, setUser, refetchUser } = useAuth();
const qc = useQueryClient(); const qc = useQueryClient();
const { notify } = useToast();
const { t } = useTranslation();
const { data: notifications = [], refetch } = useNotificationsQuery(!!user); const { data: notifications = [], refetch } = useNotificationsQuery(!!user);
const markAllMutation = useMarkAllNotificationsRead(); const markAllMutation = useMarkAllNotificationsRead();
@@ -46,9 +50,11 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => {
await markAllMutation.mutateAsync(); await markAllMutation.mutateAsync();
if (user) setUser({ ...user, unread_notifications_count: 0 }); if (user) setUser({ ...user, unread_notifications_count: 0 });
} catch { } catch {
/* ignore */ // Surface the failure — otherwise the badge silently stays and the user
// thinks the click did nothing.
notify({ message: t('community.markAllReadFailed'), severity: 'error' });
} }
}, [markAllMutation, setUser, user]); }, [markAllMutation, setUser, user, notify, t]);
const markRead = useCallback( const markRead = useCallback(
async (id: number) => { async (id: number) => {
+2
View File
@@ -192,6 +192,7 @@ const resources = {
notifTicketReplyAdmin: 'replied to a support ticket', notifTicketReplyAdmin: 'replied to a support ticket',
noNotifications: 'No notifications.', noNotifications: 'No notifications.',
markAllRead: 'Mark all read', markAllRead: 'Mark all read',
markAllReadFailed: 'Failed to mark notifications as read',
searchFriends: 'Search friends...', searchFriends: 'Search friends...',
writeReply: 'Add a reply...', writeReply: 'Add a reply...',
rated: 'rated', rated: 'rated',
@@ -502,6 +503,7 @@ const resources = {
notifTicketReplyAdmin: 'hat auf ein Support-Ticket geantwortet', notifTicketReplyAdmin: 'hat auf ein Support-Ticket geantwortet',
noNotifications: 'Keine Benachrichtigungen.', noNotifications: 'Keine Benachrichtigungen.',
markAllRead: 'Alle als gelesen markieren', markAllRead: 'Alle als gelesen markieren',
markAllReadFailed: 'Benachrichtigungen konnten nicht als gelesen markiert werden',
searchFriends: 'Freunde suchen...', searchFriends: 'Freunde suchen...',
writeReply: 'Eine Antwort hinzufügen...', writeReply: 'Eine Antwort hinzufügen...',
rated: 'bewertete', rated: 'bewertete',
+1 -1
View File
@@ -18,7 +18,7 @@ const api = axios.create({
withCredentials: true, withCredentials: true,
}); });
type RetriableConfig = AxiosRequestConfig & { export type RetriableConfig = AxiosRequestConfig & {
_retry?: boolean; _retry?: boolean;
/** When true, a 401 that survives refresh will silently reject instead of /** When true, a 401 that survives refresh will silently reject instead of
* navigating to /login. Use for background probes (e.g. /users/me/) so * navigating to /login. Use for background probes (e.g. /users/me/) so