mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 13:20:08 +03:00
Compare commits
2
Commits
email
...
syncSettings
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b457795216 | ||
|
|
16d9c34a5e |
+33
@@ -36,6 +36,8 @@ import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.model.api.user.UserSettingsRequest;
|
||||
import stirling.software.proprietary.security.model.api.user.UserSettingsResponse;
|
||||
import stirling.software.proprietary.security.model.api.user.UsernameAndPass;
|
||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
@@ -59,6 +61,37 @@ public class UserController {
|
||||
private final Optional<EmailService> emailService;
|
||||
private final UserLicenseSettingsService licenseSettingsService;
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@GetMapping("/settings")
|
||||
public ResponseEntity<?> getUserSettings(Principal principal) {
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(Map.of("error", "notAuthenticated", "message", "User not authenticated"));
|
||||
}
|
||||
Map<String, String> settings = userService.getUserSettings(principal.getName());
|
||||
if (settings == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(Map.of("error", "userNotFound", "message", "User not found"));
|
||||
}
|
||||
return ResponseEntity.ok(new UserSettingsResponse(settings));
|
||||
}
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PutMapping("/settings")
|
||||
@Audited(type = AuditEventType.USER_PROFILE_UPDATE, level = AuditLevel.BASIC)
|
||||
public ResponseEntity<?> saveUserSettings(
|
||||
@RequestBody UserSettingsRequest request, Principal principal)
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(Map.of("error", "notAuthenticated", "message", "User not authenticated"));
|
||||
}
|
||||
Map<String, String> updates =
|
||||
request != null && request.settings() != null ? request.settings() : Map.of();
|
||||
userService.updateUserSettings(principal.getName(), updates);
|
||||
return ResponseEntity.ok(new UserSettingsResponse(updates));
|
||||
}
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<?> register(@RequestBody UsernameAndPass usernameAndPass)
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package stirling.software.proprietary.security.model.api.user;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
/** Request payload for updating a user's stored settings map. */
|
||||
public record UserSettingsRequest(
|
||||
@Schema(description = "Key/value map of settings to persist for the user")
|
||||
Map<String, String> settings) {}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package stirling.software.proprietary.security.model.api.user;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
/** Response payload containing the user's stored settings map. */
|
||||
public record UserSettingsResponse(
|
||||
@Schema(description = "Key/value map of the user's saved settings")
|
||||
Map<String, String> settings) {}
|
||||
+16
-1
@@ -3,6 +3,7 @@ package stirling.software.proprietary.security.service;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -371,14 +372,28 @@ public class UserService implements UserServiceInterface {
|
||||
if (settingsMap == null) {
|
||||
settingsMap = new HashMap<>();
|
||||
}
|
||||
Map<String, String> sanitizedUpdates =
|
||||
updates == null ? Collections.emptyMap() : new HashMap<>(updates);
|
||||
settingsMap.clear();
|
||||
settingsMap.putAll(updates);
|
||||
settingsMap.putAll(sanitizedUpdates);
|
||||
user.setSettings(settingsMap);
|
||||
userRepository.save(user);
|
||||
databaseService.exportDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, String> getUserSettings(String username) {
|
||||
Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username);
|
||||
if (userOpt.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Map<String, String> settingsMap = userOpt.get().getSettings();
|
||||
if (settingsMap == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
return new HashMap<>(settingsMap);
|
||||
}
|
||||
|
||||
public Optional<User> findByUsername(String username) {
|
||||
return userRepository.findByUsername(username);
|
||||
}
|
||||
|
||||
@@ -332,7 +332,10 @@
|
||||
"mode": {
|
||||
"fullscreen": "Fullscreen",
|
||||
"sidebar": "Sidebar"
|
||||
}
|
||||
},
|
||||
"syncSettings": "Sync settings across devices",
|
||||
"syncSettingsDescription": "Carry your theme, language, favourites, and hotkeys to other browsers when signed in.",
|
||||
"syncSettingsTooltip": "Automatically back up your preferences to your account so they follow you on every device."
|
||||
},
|
||||
"hotkeys": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Menu, Button, ScrollArea, ActionIcon, Tooltip } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supportedLanguages } from '@app/i18n';
|
||||
import { emitLocalSettingsEvent } from '@app/utils/localSettingsEvents';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import styles from '@app/components/shared/LanguageSelector.module.css';
|
||||
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex';
|
||||
@@ -178,6 +179,7 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({ position = 'bottom-
|
||||
// Simulate processing time for smooth transition
|
||||
setTimeout(() => {
|
||||
i18n.changeLanguage(value);
|
||||
emitLocalSettingsEvent(['i18nextLng'], 'local');
|
||||
|
||||
setTimeout(() => {
|
||||
setPendingLanguage(null);
|
||||
|
||||
@@ -168,6 +168,28 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
label={t('settings.general.syncSettingsTooltip', 'Automatically back up your preferences to your account so they follow you on every device.')}
|
||||
multiline
|
||||
w={300}
|
||||
withArrow
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'help' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t('settings.general.syncSettings', 'Sync settings across devices')}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('settings.general.syncSettingsDescription', 'Carry your theme, language, favorites, and hotkeys to other browsers when signed in.')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={preferences.syncSettingsAcrossDevices}
|
||||
onChange={(event) => updatePreference('syncSettingsAcrossDevices', event.currentTarget.checked)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { HotkeyBinding, bindingEquals, bindingMatchesEvent, deserializeBindings,
|
||||
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||
import { ToolId } from '@app/types/toolId';
|
||||
import { ToolCategoryId, ToolRegistryEntry } from '@app/data/toolsTaxonomy';
|
||||
import { addLocalSettingsListener, emitLocalSettingsEvent } from '@app/utils/localSettingsEvents';
|
||||
|
||||
type Bindings = Partial<Record<ToolId, HotkeyBinding>>;
|
||||
|
||||
@@ -110,8 +111,25 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(STORAGE_KEY, serializeBindings(customBindings));
|
||||
emitLocalSettingsEvent([STORAGE_KEY], 'local');
|
||||
}, [customBindings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
return addLocalSettingsListener(detail => {
|
||||
if (detail.origin !== 'remote') {
|
||||
return;
|
||||
}
|
||||
if (detail.keys.includes(STORAGE_KEY)) {
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY);
|
||||
setCustomBindings(deserializeBindings(stored));
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isBindingAvailable = useCallback((binding: HotkeyBinding, excludeToolId?: ToolId) => {
|
||||
const normalized = normalizeBinding(binding);
|
||||
return Object.entries(resolved).every(([toolId, existing]) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||
import { preferencesService, UserPreferences } from '@app/services/preferencesService';
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
||||
import { preferencesService, UserPreferences, PREFERENCES_STORAGE_KEY } from '@app/services/preferencesService';
|
||||
import { addLocalSettingsListener } from '@app/utils/localSettingsEvents';
|
||||
|
||||
interface PreferencesContextValue {
|
||||
preferences: UserPreferences;
|
||||
@@ -18,6 +19,17 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
|
||||
return preferencesService.getAllPreferences();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return addLocalSettingsListener(detail => {
|
||||
if (detail.origin !== 'remote') {
|
||||
return;
|
||||
}
|
||||
if (detail.keys.includes(PREFERENCES_STORAGE_KEY)) {
|
||||
setPreferences(preferencesService.getAllPreferences());
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updatePreference = useCallback(
|
||||
<K extends keyof UserPreferences>(key: K, value: UserPreferences[K]) => {
|
||||
preferencesService.setPreference(key, value);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { ToolId } from '@app/types/toolId';
|
||||
import { addLocalSettingsListener, emitLocalSettingsEvent } from '@app/utils/localSettingsEvents';
|
||||
|
||||
const RECENT_TOOLS_KEY = 'stirlingpdf.recentTools';
|
||||
const FAVORITE_TOOLS_KEY = 'stirlingpdf.favoriteTools';
|
||||
@@ -36,6 +37,44 @@ export function useToolHistory() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
return addLocalSettingsListener(detail => {
|
||||
if (detail.origin !== 'remote') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detail.keys.includes(FAVORITE_TOOLS_KEY)) {
|
||||
const favoritesStr = window.localStorage.getItem(FAVORITE_TOOLS_KEY);
|
||||
if (favoritesStr) {
|
||||
try {
|
||||
setFavoriteTools(JSON.parse(favoritesStr));
|
||||
} catch {
|
||||
setFavoriteTools([]);
|
||||
}
|
||||
} else {
|
||||
setFavoriteTools([]);
|
||||
}
|
||||
}
|
||||
|
||||
if (detail.keys.includes(RECENT_TOOLS_KEY)) {
|
||||
const recentStr = window.localStorage.getItem(RECENT_TOOLS_KEY);
|
||||
if (recentStr) {
|
||||
try {
|
||||
setRecentTools(JSON.parse(recentStr));
|
||||
} catch {
|
||||
setRecentTools([]);
|
||||
}
|
||||
} else {
|
||||
setRecentTools([]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
// Toggle favorite status
|
||||
const toggleFavorite = useCallback((toolId: ToolId) => {
|
||||
@@ -49,6 +88,7 @@ export function useToolHistory() {
|
||||
? prev.filter((id) => id !== toolId)
|
||||
: [...prev, toolId];
|
||||
window.localStorage.setItem(FAVORITE_TOOLS_KEY, JSON.stringify(updated));
|
||||
emitLocalSettingsEvent([FAVORITE_TOOLS_KEY], 'local');
|
||||
return updated;
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type ToolPanelMode, DEFAULT_TOOL_PANEL_MODE } from '@app/constants/toolPanel';
|
||||
import { type ThemeMode, getSystemTheme } from '@app/constants/theme';
|
||||
import { emitLocalSettingsEvent } from '@app/utils/localSettingsEvents';
|
||||
|
||||
export interface UserPreferences {
|
||||
autoUnzip: boolean;
|
||||
@@ -9,6 +10,7 @@ export interface UserPreferences {
|
||||
toolPanelModePromptSeen: boolean;
|
||||
showLegacyToolDescriptions: boolean;
|
||||
hasCompletedOnboarding: boolean;
|
||||
syncSettingsAcrossDevices: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_PREFERENCES: UserPreferences = {
|
||||
@@ -19,9 +21,11 @@ export const DEFAULT_PREFERENCES: UserPreferences = {
|
||||
toolPanelModePromptSeen: false,
|
||||
showLegacyToolDescriptions: false,
|
||||
hasCompletedOnboarding: false,
|
||||
syncSettingsAcrossDevices: false,
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'stirlingpdf_preferences';
|
||||
export const PREFERENCES_STORAGE_KEY = 'stirlingpdf_preferences';
|
||||
const STORAGE_KEY = PREFERENCES_STORAGE_KEY;
|
||||
|
||||
class PreferencesService {
|
||||
getPreference<K extends keyof UserPreferences>(
|
||||
@@ -51,6 +55,7 @@ class PreferencesService {
|
||||
const preferences = stored ? JSON.parse(stored) : {};
|
||||
preferences[key] = value;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
|
||||
emitLocalSettingsEvent([STORAGE_KEY], 'local');
|
||||
} catch (error) {
|
||||
console.error('Error writing preference:', key, error);
|
||||
}
|
||||
@@ -76,6 +81,7 @@ class PreferencesService {
|
||||
clearAllPreferences(): void {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
emitLocalSettingsEvent([STORAGE_KEY], 'local');
|
||||
} catch (error) {
|
||||
console.error('Error clearing preferences:', error);
|
||||
throw error;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
export const LOCAL_SETTINGS_EVENT = 'stirlingpdf:local-settings-changed';
|
||||
|
||||
export type LocalSettingsEventOrigin = 'local' | 'remote';
|
||||
|
||||
export interface LocalSettingsEventDetail {
|
||||
keys: string[];
|
||||
origin: LocalSettingsEventOrigin;
|
||||
}
|
||||
|
||||
export function emitLocalSettingsEvent(keys: string[], origin: LocalSettingsEventOrigin) {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const uniqueKeys = Array.from(new Set(keys)).filter(Boolean);
|
||||
if (uniqueKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event = new CustomEvent<LocalSettingsEventDetail>(LOCAL_SETTINGS_EVENT, {
|
||||
detail: {
|
||||
keys: uniqueKeys,
|
||||
origin,
|
||||
},
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
export function addLocalSettingsListener(
|
||||
listener: (detail: LocalSettingsEventDetail) => void
|
||||
): () => void {
|
||||
if (typeof window === 'undefined') {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const handler = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<LocalSettingsEventDetail>;
|
||||
if (customEvent?.detail) {
|
||||
listener(customEvent.detail);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(LOCAL_SETTINGS_EVENT, handler as EventListener);
|
||||
return () => window.removeEventListener(LOCAL_SETTINGS_EVENT, handler as EventListener);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AppProviders as CoreAppProviders, AppProvidersProps } from "@core/components/AppProviders";
|
||||
import { AuthProvider } from "@app/auth/UseSession";
|
||||
import { UserSettingsSyncProvider } from "@app/components/UserSettingsSyncProvider";
|
||||
|
||||
export function AppProviders({ children, appConfigRetryOptions, appConfigProviderProps }: AppProvidersProps) {
|
||||
return (
|
||||
@@ -8,7 +9,9 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide
|
||||
appConfigProviderProps={appConfigProviderProps}
|
||||
>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
<UserSettingsSyncProvider>
|
||||
{children}
|
||||
</UserSettingsSyncProvider>
|
||||
</AuthProvider>
|
||||
</CoreAppProviders>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { useUserSettingsSync } from '@app/hooks/useUserSettingsSync';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function UserSettingsSyncProvider({ children }: Props) {
|
||||
useUserSettingsSync();
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useAuth } from '@app/auth/UseSession';
|
||||
import { usePreferences } from '@app/contexts/PreferencesContext';
|
||||
import { userSettingsService } from '@app/services/userSettingsService';
|
||||
import { PREFERENCES_STORAGE_KEY } from '@app/services/preferencesService';
|
||||
import { addLocalSettingsListener, emitLocalSettingsEvent } from '@app/utils/localSettingsEvents';
|
||||
import i18n from '@app/i18n';
|
||||
|
||||
const LANGUAGE_STORAGE_KEY = 'i18nextLng';
|
||||
const HOTKEY_STORAGE_KEY = 'stirlingpdf.hotkeys';
|
||||
const FAVORITE_TOOLS_KEY = 'stirlingpdf.favoriteTools';
|
||||
|
||||
const SYNCABLE_KEYS = [
|
||||
PREFERENCES_STORAGE_KEY,
|
||||
LANGUAGE_STORAGE_KEY,
|
||||
HOTKEY_STORAGE_KEY,
|
||||
FAVORITE_TOOLS_KEY,
|
||||
] as const;
|
||||
|
||||
const SYNCABLE_KEY_SET = new Set<string>(SYNCABLE_KEYS);
|
||||
|
||||
function collectLocalSettings(): Record<string, string> {
|
||||
if (typeof window === 'undefined') {
|
||||
return {};
|
||||
}
|
||||
|
||||
return SYNCABLE_KEYS.reduce<Record<string, string>>((acc, key) => {
|
||||
const value = window.localStorage.getItem(key);
|
||||
if (value !== null) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function useUserSettingsSync() {
|
||||
const { session } = useAuth();
|
||||
const { preferences } = usePreferences();
|
||||
const syncEnabled = Boolean(session && preferences.syncSettingsAcrossDevices);
|
||||
const uploadTimeoutRef = useRef<number | null>(null);
|
||||
const isFetchingRef = useRef(false);
|
||||
|
||||
const applyRemoteSettings = useCallback((settings?: Record<string, string>) => {
|
||||
if (!settings || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const appliedKeys: string[] = [];
|
||||
SYNCABLE_KEYS.forEach(key => {
|
||||
if (Object.prototype.hasOwnProperty.call(settings, key)) {
|
||||
window.localStorage.setItem(key, settings[key]);
|
||||
appliedKeys.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
const remoteLanguage = settings[LANGUAGE_STORAGE_KEY];
|
||||
if (remoteLanguage && i18n.language !== remoteLanguage) {
|
||||
i18n.changeLanguage(remoteLanguage).catch(() => {
|
||||
// ignore change errors
|
||||
});
|
||||
}
|
||||
|
||||
if (appliedKeys.length > 0) {
|
||||
emitLocalSettingsEvent(appliedKeys, 'remote');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const flushUpload = useCallback(async () => {
|
||||
if (!session || !syncEnabled || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const snapshot = collectLocalSettings();
|
||||
await userSettingsService.save(snapshot);
|
||||
} catch (error) {
|
||||
console.error('[UserSettingsSync] Failed to sync settings', error);
|
||||
}
|
||||
}, [session, syncEnabled]);
|
||||
|
||||
const scheduleUpload = useCallback(() => {
|
||||
if (!session || !syncEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (uploadTimeoutRef.current) {
|
||||
window.clearTimeout(uploadTimeoutRef.current);
|
||||
}
|
||||
|
||||
uploadTimeoutRef.current = window.setTimeout(() => {
|
||||
uploadTimeoutRef.current = null;
|
||||
flushUpload();
|
||||
}, 750);
|
||||
}, [session, syncEnabled, flushUpload]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
if (isFetchingRef.current) {
|
||||
return;
|
||||
}
|
||||
isFetchingRef.current = true;
|
||||
try {
|
||||
const response = await userSettingsService.fetch();
|
||||
if (!cancelled) {
|
||||
applyRemoteSettings(response?.settings);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[UserSettingsSync] Failed to load user settings', error);
|
||||
} finally {
|
||||
isFetchingRef.current = false;
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [session, applyRemoteSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session) {
|
||||
if (uploadTimeoutRef.current) {
|
||||
window.clearTimeout(uploadTimeoutRef.current);
|
||||
uploadTimeoutRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (syncEnabled) {
|
||||
flushUpload();
|
||||
} else if (uploadTimeoutRef.current) {
|
||||
window.clearTimeout(uploadTimeoutRef.current);
|
||||
uploadTimeoutRef.current = null;
|
||||
}
|
||||
}, [session, syncEnabled, flushUpload]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
return addLocalSettingsListener(detail => {
|
||||
if (detail.origin !== 'local') {
|
||||
return;
|
||||
}
|
||||
if (!session || !syncEnabled) {
|
||||
return;
|
||||
}
|
||||
const relevant = detail.keys.filter(key => SYNCABLE_KEY_SET.has(key));
|
||||
if (relevant.length === 0) {
|
||||
return;
|
||||
}
|
||||
scheduleUpload();
|
||||
});
|
||||
}, [session, syncEnabled, scheduleUpload]);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import apiClient from '@app/services/apiClient';
|
||||
|
||||
export interface UserSettingsResponse {
|
||||
settings: Record<string, string>;
|
||||
}
|
||||
|
||||
export const userSettingsService = {
|
||||
async fetch(): Promise<UserSettingsResponse> {
|
||||
const response = await apiClient.get<UserSettingsResponse>('/api/v1/user/settings');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async save(settings: Record<string, string>): Promise<UserSettingsResponse> {
|
||||
const response = await apiClient.put<UserSettingsResponse>('/api/v1/user/settings', {
|
||||
settings,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user