feat: add automatic identity synchronization (#167)
This commit is contained in:
@@ -39,6 +39,7 @@ import { NavigationRail } from "@/components/layout/navigation-rail";
|
|||||||
import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal";
|
import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal";
|
||||||
import { InlineAppView } from "@/components/layout/inline-app-view";
|
import { InlineAppView } from "@/components/layout/inline-app-view";
|
||||||
import { useSidebarApps } from "@/hooks/use-sidebar-apps";
|
import { useSidebarApps } from "@/hooks/use-sidebar-apps";
|
||||||
|
import { useIdentitySync } from "@/hooks/use-identity-sync";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { FilePreviewModal } from "@/components/files/file-preview-modal";
|
import { FilePreviewModal } from "@/components/files/file-preview-modal";
|
||||||
import { isFilePreviewable } from "@/lib/file-preview";
|
import { isFilePreviewable } from "@/lib/file-preview";
|
||||||
@@ -77,6 +78,7 @@ export default function Home() {
|
|||||||
const markAsReadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const markAsReadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const { isAuthenticated, client, logout, checkAuth, isLoading: authLoading, connectionLost, isRateLimited, rateLimitUntil } = useAuthStore();
|
const { isAuthenticated, client, logout, checkAuth, isLoading: authLoading, connectionLost, isRateLimited, rateLimitUntil } = useAuthStore();
|
||||||
const { identities } = useIdentityStore();
|
const { identities } = useIdentityStore();
|
||||||
|
useIdentitySync();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isRateLimited || !rateLimitUntil) {
|
if (!isRateLimited || !rateLimitUntil) {
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ function useSyncIdentities() {
|
|||||||
const syncIdentities = useAuthStore((state) => state.syncIdentities);
|
const syncIdentities = useAuthStore((state) => state.syncIdentities);
|
||||||
return syncIdentities;
|
return syncIdentities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useRefreshIdentities() {
|
||||||
|
return useAuthStore((state) => state.refreshIdentities);
|
||||||
|
}
|
||||||
import type { Identity, EmailAddress } from '@/lib/jmap/types';
|
import type { Identity, EmailAddress } from '@/lib/jmap/types';
|
||||||
import { toast } from '@/stores/toast-store';
|
import { toast } from '@/stores/toast-store';
|
||||||
import { useFocusTrap } from '@/hooks/use-focus-trap';
|
import { useFocusTrap } from '@/hooks/use-focus-trap';
|
||||||
@@ -49,6 +53,15 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
|
|||||||
const setPreferredPrimary = useIdentityStore((state) => state.setPreferredPrimary);
|
const setPreferredPrimary = useIdentityStore((state) => state.setPreferredPrimary);
|
||||||
const syncIdentities = useSyncIdentities();
|
const syncIdentities = useSyncIdentities();
|
||||||
|
|
||||||
|
const refreshIdentitiesFromServer = useRefreshIdentities();
|
||||||
|
|
||||||
|
// Refresh identities from server whenever the modal is opened
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
refreshIdentitiesFromServer();
|
||||||
|
}
|
||||||
|
}, [isOpen, refreshIdentitiesFromServer]);
|
||||||
|
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
|
|
||||||
|
// Re-sync identities every 30 minutes while the app is open
|
||||||
|
const SYNC_INTERVAL_MS = 30 * 60 * 1000;
|
||||||
|
|
||||||
|
export function useIdentitySync() {
|
||||||
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||||
|
const refreshIdentities = useAuthStore((s) => s.refreshIdentities);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAuthenticated) return;
|
||||||
|
|
||||||
|
// Sync when the user returns to the tab (e.g. after adding an alias in Stalwart)
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
refreshIdentities();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
const interval = setInterval(refreshIdentities, SYNC_INTERVAL_MS);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [isAuthenticated, refreshIdentities]);
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ interface AuthState {
|
|||||||
checkAuth: () => Promise<void>;
|
checkAuth: () => Promise<void>;
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
syncIdentities: () => void;
|
syncIdentities: () => void;
|
||||||
|
refreshIdentities: () => Promise<void>;
|
||||||
getClientForAccount: (accountId: string) => JMAPClient | undefined;
|
getClientForAccount: (accountId: string) => JMAPClient | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1513,6 +1514,18 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
set({ identities, primaryIdentity });
|
set({ identities, primaryIdentity });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
refreshIdentities: async () => {
|
||||||
|
const { client, username } = get();
|
||||||
|
if (!client || !username) return;
|
||||||
|
try {
|
||||||
|
const rawIdentities = await client.getIdentities();
|
||||||
|
const { identities, primaryIdentity } = loadIdentities(rawIdentities, username);
|
||||||
|
set({ identities, primaryIdentity });
|
||||||
|
} catch {
|
||||||
|
// Silently fail — background sync should not surface errors to the user
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
getClientForAccount: (accountId: string) => {
|
getClientForAccount: (accountId: string) => {
|
||||||
return clients.get(accountId);
|
return clients.get(accountId);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user