improvement: notification feature polished. feature tabs following the same notification socket emmited data to be updated.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// src/lib/api/client.ts
|
||||
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
||||
import type { ApiError } from '@/types/api';
|
||||
import { notifyAccessTokenRefreshed } from '@/lib/auth/accessTokenEvents';
|
||||
|
||||
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
|
||||
_retry?: boolean;
|
||||
@@ -79,6 +80,7 @@ apiClient.interceptors.response.use(
|
||||
}
|
||||
}
|
||||
|
||||
notifyAccessTokenRefreshed();
|
||||
return apiClient(originalRequest);
|
||||
} catch (refreshError) {
|
||||
if (typeof window !== 'undefined') {
|
||||
|
||||
11
frontend/src/lib/auth/accessTokenEvents.ts
Normal file
11
frontend/src/lib/auth/accessTokenEvents.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/** Dispatched after a successful access-token refresh so realtime can reconnect with the new cookie. */
|
||||
export const ACCESS_TOKEN_REFRESHED_EVENT = 'dyolink:access-token-refreshed';
|
||||
|
||||
export function notifyAccessTokenRefreshed() {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.dispatchEvent(new Event(ACCESS_TOKEN_REFRESHED_EVENT));
|
||||
}
|
||||
|
||||
export function accessTokenRefreshedEventName() {
|
||||
return ACCESS_TOKEN_REFRESHED_EVENT;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { authApi } from '@/lib/api/auth';
|
||||
import { notifyAccessTokenRefreshed } from '@/lib/auth/accessTokenEvents';
|
||||
|
||||
const STORAGE_KEY = 'dyolink.accessTokenExpiresAt';
|
||||
/** Refresh this long before the access JWT expires. */
|
||||
@@ -91,6 +92,7 @@ async function refreshAccessTokenWithOrg(): Promise<string | undefined> {
|
||||
}
|
||||
|
||||
lastRefreshAt = Date.now();
|
||||
notifyAccessTokenRefreshed();
|
||||
return expiresAt;
|
||||
} finally {
|
||||
refreshInFlight = null;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
rememberAccessTokenExpiresAt,
|
||||
startProactiveSessionRefresh,
|
||||
} from '@/lib/auth/proactiveRefresh';
|
||||
import { notifyAccessTokenRefreshed } from '@/lib/auth/accessTokenEvents';
|
||||
import { asApiError, legacyStatusCode, type ApiError } from '@/types/api';
|
||||
import { consumeAuthRedirect } from '@/lib/auth/postAuthRedirect';
|
||||
|
||||
@@ -162,6 +163,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
if (orgId) {
|
||||
await authApi.selectOrganization(orgId);
|
||||
}
|
||||
notifyAccessTokenRefreshed();
|
||||
const retry = await authApi.getProfile();
|
||||
if (retry.success) {
|
||||
const { user: userData, organizations: orgs } = normalizeProfilePayload(retry.data);
|
||||
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
} from 'react';
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
import { useAuth } from '@/lib/hooks/useAuth';
|
||||
import { accessTokenRefreshedEventName } from '@/lib/auth/accessTokenEvents';
|
||||
import { notifyTabBadgesChanged } from '@/lib/tabBadgeUtils';
|
||||
import { notifyPendingConnectionsChanged } from '@/lib/hooks/usePendingConnectionsCount';
|
||||
import type { UserNotificationItem } from '@/types/notifications';
|
||||
|
||||
type RealtimeContextValue = {
|
||||
@@ -37,13 +40,33 @@ function apiOrigin(): string {
|
||||
}
|
||||
}
|
||||
|
||||
function invalidateSidebarBadges(notification?: UserNotificationItem | null) {
|
||||
// Sidebar Cases/Tasks/Treatment badges + open tab soft-refetch (via window event).
|
||||
notifyTabBadgesChanged();
|
||||
// Orgs pending badge/list — only for connection-related inbox events.
|
||||
if (
|
||||
notification?.type === 'CONNECTION_REQUEST' ||
|
||||
notification?.href?.startsWith('/organizations')
|
||||
) {
|
||||
notifyPendingConnectionsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
export function RealtimeProvider({ children }: { children: ReactNode }) {
|
||||
const { user, currentOrganization, isAuthReady } = useAuth();
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [lastNotification, setLastNotification] = useState<UserNotificationItem | null>(null);
|
||||
const [unreadCount, setUnreadCount] = useState<number | null>(null);
|
||||
/** Bumped after access-token refresh so the socket reconnects with the new cookie. */
|
||||
const [socketEpoch, setSocketEpoch] = useState(0);
|
||||
const socketRef = useRef<Socket | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onRefreshed = () => setSocketEpoch((n) => n + 1);
|
||||
window.addEventListener(accessTokenRefreshedEventName(), onRefreshed);
|
||||
return () => window.removeEventListener(accessTokenRefreshedEventName(), onRefreshed);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthReady || !user || !currentOrganization?.id) {
|
||||
socketRef.current?.disconnect();
|
||||
@@ -63,6 +86,9 @@ export function RealtimeProvider({ children }: { children: ReactNode }) {
|
||||
socket.on('notification.created', (payload: { notification?: UserNotificationItem }) => {
|
||||
if (payload?.notification) {
|
||||
setLastNotification(payload.notification);
|
||||
invalidateSidebarBadges(payload.notification);
|
||||
} else {
|
||||
invalidateSidebarBadges(null);
|
||||
}
|
||||
});
|
||||
socket.on('notification.unreadCount', (payload: { count?: number }) => {
|
||||
@@ -76,7 +102,7 @@ export function RealtimeProvider({ children }: { children: ReactNode }) {
|
||||
socketRef.current = null;
|
||||
setConnected(false);
|
||||
};
|
||||
}, [isAuthReady, user, currentOrganization?.id]);
|
||||
}, [isAuthReady, user, currentOrganization?.id, socketEpoch]);
|
||||
|
||||
const setUnreadCountStable = useCallback((count: number) => {
|
||||
setUnreadCount(count);
|
||||
|
||||
Reference in New Issue
Block a user