- Implemented auto-update logic in `ScribeTopBar` with update notification and user interaction. - Integrated `@tauri-apps/plugin-updater` and `@tauri-apps/plugin-process` for updater functionality. - Added automatic migration feature with `autoMigrateElectron` support and UI feedback. - Refactored app architecture with new routing, components, and layout for better modularity. - Enhanced JSON response handling in API client for robust data parsing. - Updated locales to include new translations for update and migration-related UI.
471 lines
23 KiB
TypeScript
471 lines
23 KiB
TypeScript
import React, {ReactNode, useCallback, useContext, useEffect, useRef, useState} from 'react';
|
|
import {BookContext} from '@/context/BookContext';
|
|
import {ChapterProps} from '@/lib/types/chapter';
|
|
import {ChapterContext} from '@/context/ChapterContext';
|
|
import {EditorContext} from '@/context/EditorContext';
|
|
import {Editor, useEditor} from '@tiptap/react';
|
|
import StarterKit from '@tiptap/starter-kit';
|
|
import Underline from '@tiptap/extension-underline';
|
|
import TextAlign from '@tiptap/extension-text-align';
|
|
import {AlertContext, AlertContextProps, AlertProvider} from '@/context/AlertContext';
|
|
import {usePathname} from '@/lib/navigation';
|
|
import {apiPost} from '@/lib/api/client';
|
|
import {getCookie} from '@/lib/utils/cookies';
|
|
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
|
import {BookProps} from '@/lib/types/book';
|
|
import ScribeControllerBar from '@/components/layout/ScribeControllerBar';
|
|
import ScribeLeftBar from '@/components/leftbar/ScribeLeftBar';
|
|
import ComposerRightBar from '@/components/rightbar/ComposerRightBar';
|
|
import ScribeFooterBar from '@/components/layout/ScribeFooterBar';
|
|
import GuideTour from '@/components/GuideTour';
|
|
import TermsOfUse from '@/components/TermsOfUse';
|
|
import {useTranslations, changeLanguage} from '@/lib/i18n';
|
|
import {isSupportedLocale, LangContext, LangContextProps, SupportedLocale} from '@/context/LangContext';
|
|
import {ThemeContext} from '@/context/ThemeContext';
|
|
import useTheme from '@/hooks/useTheme';
|
|
import {AIUsageContext} from '@/context/AIUsageContext';
|
|
import {BookSyncCompare, compareBookSyncs, SyncedBook} from '@/lib/types/synced-book';
|
|
import {SeriesSyncCompare, compareSeriesSyncs, SyncedSeries} from '@/lib/types/synced-series';
|
|
import {BooksSyncContext} from '@/context/BooksSyncContext';
|
|
import {SeriesSyncContext} from '@/context/SeriesSyncContext';
|
|
import {LocalSyncQueueContext, LocalSyncOperation} from '@/context/SyncQueueContext';
|
|
import useAuthentication from '@/hooks/useAuthentication';
|
|
import useOnboarding from '@/hooks/useOnboarding';
|
|
import {SettingBookContext} from '@/context/SettingBookContext';
|
|
import PulseLoader from '@/components/ui/PulseLoader';
|
|
import {initCrashReporter} from '@/lib/crashReporter';
|
|
import OfflineProvider from '@/context/OfflineProvider';
|
|
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
|
import OfflinePinSetup from '@/components/offline/OfflinePinSetup';
|
|
import OfflinePinVerify from '@/components/offline/OfflinePinVerify';
|
|
import {isDesktop} from '@/lib/configs';
|
|
import * as tauri from '@/lib/tauri';
|
|
import useSyncBooks from '@/hooks/useSyncBooks';
|
|
import useSyncSeries from '@/hooks/useSyncSeries';
|
|
|
|
function AutoSyncOnReconnect() {
|
|
const {offlineMode}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
|
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
|
const {syncAllToServer: syncAllBooksToServer, syncAllFromServer: syncAllBooksFromServer, refreshBooks, booksToSyncToServer, booksToSyncFromServer} = useSyncBooks();
|
|
const {syncAllToServer: syncAllSeriesToServer, syncAllFromServer: syncAllSeriesFromServer, refreshSeries, seriesToSyncToServer, seriesToSyncFromServer} = useSyncSeries();
|
|
const isSyncingRef = useRef<boolean>(false);
|
|
const hasRefreshedRef = useRef<boolean>(false);
|
|
|
|
const saveLastOnlineTimestamp = useCallback((): void => {
|
|
const timestamp: number = Math.floor(Date.now() / 1000);
|
|
localStorage.setItem('lastOnlineTimestamp', timestamp.toString());
|
|
}, []);
|
|
|
|
// Refresh sync data when online + authenticated + DB ready
|
|
useEffect((): void => {
|
|
if (!offlineMode.isOffline && session.isConnected && offlineMode.isDatabaseInitialized) {
|
|
hasRefreshedRef.current = true;
|
|
Promise.all([refreshBooks(), refreshSeries()]);
|
|
}
|
|
}, [offlineMode.isOffline, session.isConnected, offlineMode.isDatabaseInitialized]);
|
|
|
|
// Auto-sync when diffs become available
|
|
useEffect((): void => {
|
|
if (offlineMode.isOffline || !session.isConnected || isSyncingRef.current || !hasRefreshedRef.current) return;
|
|
|
|
const syncPromises: Promise<void>[] = [];
|
|
|
|
if (booksToSyncToServer.length > 0) syncPromises.push(syncAllBooksToServer());
|
|
if (booksToSyncFromServer.length > 0) syncPromises.push(syncAllBooksFromServer());
|
|
if (seriesToSyncToServer.length > 0) syncPromises.push(syncAllSeriesToServer());
|
|
if (seriesToSyncFromServer.length > 0) syncPromises.push(syncAllSeriesFromServer());
|
|
|
|
if (syncPromises.length > 0) {
|
|
isSyncingRef.current = true;
|
|
Promise.all(syncPromises).then((): void => {
|
|
saveLastOnlineTimestamp();
|
|
isSyncingRef.current = false;
|
|
}).catch((): void => {
|
|
isSyncingRef.current = false;
|
|
});
|
|
}
|
|
}, [booksToSyncToServer, booksToSyncFromServer, seriesToSyncToServer, seriesToSyncFromServer]);
|
|
|
|
// Update lastOnlineTimestamp every 5 minutes while online
|
|
useEffect((): (() => void) | void => {
|
|
if (!offlineMode.isOffline && session.isConnected) {
|
|
const intervalId: ReturnType<typeof setInterval> = setInterval((): void => {
|
|
saveLastOnlineTimestamp();
|
|
}, 5 * 60 * 1000);
|
|
return (): void => clearInterval(intervalId);
|
|
}
|
|
}, [offlineMode.isOffline, session.isConnected, saveLastOnlineTimestamp]);
|
|
|
|
return null;
|
|
}
|
|
|
|
function ScribeContent({children}: { children: ReactNode }) {
|
|
const t = useTranslations();
|
|
const {lang: locale}: LangContextProps = useContext<LangContextProps>(LangContext);
|
|
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
|
const {isCurrentlyOffline, offlineMode}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
|
|
|
const {
|
|
session, setSession, isLoading,
|
|
currentCredits, setCurrentCredits,
|
|
amountSpent, setAmountSpent,
|
|
showPinSetup, setShowPinSetup,
|
|
showPinVerify, setShowPinVerify,
|
|
handlePinVerifySuccess
|
|
} = useAuthentication();
|
|
|
|
const {
|
|
isTermsAccepted, homeStepsGuide, setHomeStepsGuide,
|
|
handleTermsAcceptance, handleHomeTour, homeSteps
|
|
} = useOnboarding({session, setSession});
|
|
|
|
const editor: Editor | null = useEditor({
|
|
extensions: [
|
|
StarterKit,
|
|
Underline,
|
|
TextAlign.configure({
|
|
types: ['heading', 'paragraph'],
|
|
}),
|
|
],
|
|
injectCSS: false,
|
|
immediatelyRender: false,
|
|
shouldRerenderOnTransaction: true,
|
|
});
|
|
|
|
const pathname: string = usePathname();
|
|
const [currentChapter, setCurrentChapter] = useState<ChapterProps | undefined>(undefined);
|
|
const [currentBook, setCurrentBook] = useState<BookProps | null>(null);
|
|
const [bookSettingId, setBookSettingId] = useState<string>('');
|
|
|
|
const [serverSyncedBooks, setServerSyncedBooks] = useState<SyncedBook[]>([]);
|
|
const [localSyncedBooks, setLocalSyncedBooks] = useState<SyncedBook[]>([]);
|
|
const [bookSyncDiffsFromServer, setBookSyncDiffsFromServer] = useState<BookSyncCompare[]>([]);
|
|
const [bookSyncDiffsToServer, setBookSyncDiffsToServer] = useState<BookSyncCompare[]>([]);
|
|
const [serverOnlyBooks, setServerOnlyBooks] = useState<SyncedBook[]>([]);
|
|
const [localOnlyBooks, setLocalOnlyBooks] = useState<SyncedBook[]>([]);
|
|
|
|
const [serverSyncedSeries, setServerSyncedSeries] = useState<SyncedSeries[]>([]);
|
|
const [localSyncedSeries, setLocalSyncedSeries] = useState<SyncedSeries[]>([]);
|
|
const [seriesSyncDiffsFromServer, setSeriesSyncDiffsFromServer] = useState<SeriesSyncCompare[]>([]);
|
|
const [seriesSyncDiffsToServer, setSeriesSyncDiffsToServer] = useState<SeriesSyncCompare[]>([]);
|
|
const [serverOnlySeries, setServerOnlySeries] = useState<SyncedSeries[]>([]);
|
|
const [localOnlySeries, setLocalOnlySeries] = useState<SyncedSeries[]>([]);
|
|
|
|
const [localSyncQueue, setLocalSyncQueue] = useState<LocalSyncOperation[]>([]);
|
|
const [isQueueProcessing, setIsQueueProcessing] = useState<boolean>(false);
|
|
|
|
function addToLocalSyncQueue(channel: string, data: Record<string, unknown>): void {
|
|
const operation: LocalSyncOperation = {
|
|
id: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
channel,
|
|
data,
|
|
timestamp: Date.now(),
|
|
};
|
|
setLocalSyncQueue((prev: LocalSyncOperation[]): LocalSyncOperation[] => [...prev, operation]);
|
|
}
|
|
|
|
useEffect((): void => {
|
|
if (localSyncQueue.length === 0 || isQueueProcessing) return;
|
|
|
|
async function processQueue(): Promise<void> {
|
|
setIsQueueProcessing(true);
|
|
const queueCopy: LocalSyncOperation[] = [...localSyncQueue];
|
|
for (const operation of queueCopy) {
|
|
try {
|
|
await tauri.invoke(operation.channel, {data: operation.data});
|
|
setLocalSyncQueue((prev: LocalSyncOperation[]): LocalSyncOperation[] =>
|
|
prev.filter((op: LocalSyncOperation): boolean => op.id !== operation.id)
|
|
);
|
|
} catch (_e) { /* queue retry on next cycle */ }
|
|
}
|
|
setIsQueueProcessing(false);
|
|
}
|
|
|
|
processQueue().then();
|
|
}, [localSyncQueue, isQueueProcessing]);
|
|
|
|
useEffect((): void => {
|
|
if (pathname === '/') {
|
|
setCurrentBook(null);
|
|
setCurrentChapter(undefined);
|
|
}
|
|
}, [pathname]);
|
|
|
|
useEffect((): void => {
|
|
if (!session.isConnected) return;
|
|
// On desktop with DB initialized, AutoSyncOnReconnect handles fetching + tombstone exchange
|
|
if (isDesktop && offlineMode.isDatabaseInitialized) return;
|
|
getBooks().then();
|
|
getSeries().then();
|
|
}, [session, offlineMode.isDatabaseInitialized]);
|
|
|
|
useEffect((): void => {
|
|
const diffsFromServer: BookSyncCompare[] = [];
|
|
const diffsToServer: BookSyncCompare[] = [];
|
|
|
|
serverSyncedBooks.forEach((serverBook: SyncedBook): void => {
|
|
const localBook: SyncedBook | undefined = localSyncedBooks.find((book: SyncedBook): boolean => book.id === serverBook.id);
|
|
if (!localBook) return;
|
|
const diff: BookSyncCompare | null = compareBookSyncs(serverBook, localBook);
|
|
if (diff) diffsFromServer.push(diff);
|
|
});
|
|
|
|
localSyncedBooks.forEach((localBook: SyncedBook): void => {
|
|
const serverBook: SyncedBook | undefined = serverSyncedBooks.find((book: SyncedBook): boolean => book.id === localBook.id);
|
|
if (!serverBook) return;
|
|
const diff: BookSyncCompare | null = compareBookSyncs(localBook, serverBook);
|
|
if (diff) diffsToServer.push(diff);
|
|
});
|
|
|
|
setBookSyncDiffsFromServer(diffsFromServer);
|
|
setBookSyncDiffsToServer(diffsToServer);
|
|
setServerOnlyBooks(serverSyncedBooks.filter((serverBook: SyncedBook): boolean => !localSyncedBooks.find((localBook: SyncedBook): boolean => localBook.id === serverBook.id)));
|
|
setLocalOnlyBooks(localSyncedBooks.filter((localBook: SyncedBook): boolean => !serverSyncedBooks.find((serverBook: SyncedBook): boolean => serverBook.id === localBook.id)));
|
|
}, [localSyncedBooks, serverSyncedBooks]);
|
|
|
|
useEffect((): void => {
|
|
const diffsFromServer: SeriesSyncCompare[] = [];
|
|
const diffsToServer: SeriesSyncCompare[] = [];
|
|
|
|
serverSyncedSeries.forEach((serverSeries: SyncedSeries): void => {
|
|
const localSeries: SyncedSeries | undefined = localSyncedSeries.find((series: SyncedSeries): boolean => series.id === serverSeries.id);
|
|
if (!localSeries) return;
|
|
const diff: SeriesSyncCompare | null = compareSeriesSyncs(serverSeries, localSeries);
|
|
if (diff) diffsFromServer.push(diff);
|
|
});
|
|
|
|
localSyncedSeries.forEach((localSeries: SyncedSeries): void => {
|
|
const serverSeries: SyncedSeries | undefined = serverSyncedSeries.find((series: SyncedSeries): boolean => series.id === localSeries.id);
|
|
if (!serverSeries) return;
|
|
const diff: SeriesSyncCompare | null = compareSeriesSyncs(localSeries, serverSeries);
|
|
if (diff) diffsToServer.push(diff);
|
|
});
|
|
|
|
setSeriesSyncDiffsFromServer(diffsFromServer);
|
|
setSeriesSyncDiffsToServer(diffsToServer);
|
|
setServerOnlySeries(serverSyncedSeries.filter((s: SyncedSeries): boolean => !localSyncedSeries.find((l: SyncedSeries): boolean => l.id === s.id)));
|
|
setLocalOnlySeries(localSyncedSeries.filter((l: SyncedSeries): boolean => !serverSyncedSeries.find((s: SyncedSeries): boolean => s.id === l.id)));
|
|
}, [localSyncedSeries, serverSyncedSeries]);
|
|
|
|
async function getBooks(): Promise<void> {
|
|
try {
|
|
let localBooksResponse: SyncedBook[] = [];
|
|
let serverBooksResponse: SyncedBook[] = [];
|
|
|
|
if (!isCurrentlyOffline()) {
|
|
if (isDesktop && offlineMode.isDatabaseInitialized) {
|
|
localBooksResponse = await tauri.getSyncedBooks() as SyncedBook[];
|
|
|
|
const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
|
|
const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;
|
|
|
|
const localTombstones: tauri.TombstoneRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as tauri.TombstoneRecord[];
|
|
|
|
const serverResponse: { books: SyncedBook[]; tombstones: tauri.TombstoneRecord[] } = await apiPost<{
|
|
books: SyncedBook[]; tombstones: tauri.TombstoneRecord[];
|
|
}>('books/synced', { lastOnlineTimestamp, tombstones: localTombstones }, session.accessToken, locale);
|
|
|
|
serverBooksResponse = serverResponse.books;
|
|
await tauri.applyBookTombstones(serverResponse.tombstones ?? []);
|
|
} else {
|
|
const serverResponse: { books: SyncedBook[] } = await apiPost<{
|
|
books: SyncedBook[]
|
|
}>('books/synced', { lastOnlineTimestamp: 0, tombstones: [] }, session.accessToken, locale);
|
|
if (serverResponse) serverBooksResponse = serverResponse.books;
|
|
}
|
|
} else {
|
|
if (isDesktop && offlineMode.isDatabaseInitialized) {
|
|
localBooksResponse = await tauri.getSyncedBooks() as SyncedBook[];
|
|
}
|
|
}
|
|
|
|
setServerSyncedBooks(serverBooksResponse);
|
|
setLocalSyncedBooks(localBooksResponse);
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('homePage.errors.fetchBooksError'));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function getSeries(): Promise<void> {
|
|
try {
|
|
let localSeriesResponse: SyncedSeries[] = [];
|
|
let serverSeriesResponse: SyncedSeries[] = [];
|
|
|
|
if (!isCurrentlyOffline()) {
|
|
if (isDesktop && offlineMode.isDatabaseInitialized) {
|
|
localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[];
|
|
|
|
const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
|
|
const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;
|
|
|
|
const localTombstones: tauri.TombstoneRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as tauri.TombstoneRecord[];
|
|
|
|
const serverResponse: { series: SyncedSeries[]; tombstones: tauri.TombstoneRecord[] } = await apiPost<{
|
|
series: SyncedSeries[]; tombstones: tauri.TombstoneRecord[];
|
|
}>('series/synced', { lastOnlineTimestamp, tombstones: localTombstones }, session.accessToken, locale);
|
|
|
|
serverSeriesResponse = serverResponse.series;
|
|
await tauri.applySeriesTombstones(serverResponse.tombstones ?? []);
|
|
} else {
|
|
const serverResponse: { series: SyncedSeries[] } = await apiPost<{
|
|
series: SyncedSeries[]
|
|
}>('series/synced', { lastOnlineTimestamp: 0, tombstones: [] }, session.accessToken, locale);
|
|
if (serverResponse) serverSeriesResponse = serverResponse.series;
|
|
}
|
|
} else {
|
|
if (isDesktop && offlineMode.isDatabaseInitialized) {
|
|
localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[];
|
|
}
|
|
}
|
|
|
|
setServerSyncedSeries(serverSeriesResponse);
|
|
setLocalSyncedSeries(localSeriesResponse);
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('homePage.errors.fetchBooksError'));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div
|
|
className="bg-background text-text-primary h-screen flex flex-col items-center justify-center font-['Lora']">
|
|
<div className="flex flex-col items-center space-y-6">
|
|
<div className="animate-pulse">
|
|
<img src="/logo.png" alt="ERitors Logo" width={400} height={400}/>
|
|
</div>
|
|
<PulseLoader text={t('homePage.loading')}/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SessionContext.Provider value={{session: session, setSession: setSession}}>
|
|
<LocalSyncQueueContext.Provider value={{
|
|
queue: localSyncQueue,
|
|
setQueue: setLocalSyncQueue,
|
|
addToQueue: addToLocalSyncQueue,
|
|
isProcessing: isQueueProcessing,
|
|
}}>
|
|
<BooksSyncContext.Provider value={{
|
|
serverSyncedBooks,
|
|
setServerSyncedBooks,
|
|
localSyncedBooks,
|
|
setLocalSyncedBooks,
|
|
booksToSyncFromServer: bookSyncDiffsFromServer,
|
|
setBooksToSyncFromServer: setBookSyncDiffsFromServer,
|
|
booksToSyncToServer: bookSyncDiffsToServer,
|
|
setBooksToSyncToServer: setBookSyncDiffsToServer,
|
|
setServerOnlyBooks,
|
|
setLocalOnlyBooks,
|
|
serverOnlyBooks,
|
|
localOnlyBooks
|
|
}}>
|
|
<SeriesSyncContext.Provider value={{
|
|
serverSyncedSeries,
|
|
localSyncedSeries,
|
|
seriesToSyncFromServer: seriesSyncDiffsFromServer,
|
|
seriesToSyncToServer: seriesSyncDiffsToServer,
|
|
setServerSyncedSeries,
|
|
setLocalSyncedSeries,
|
|
setServerOnlySeries,
|
|
setLocalOnlySeries,
|
|
setSeriesToSyncFromServer: setSeriesSyncDiffsFromServer,
|
|
setSeriesToSyncToServer: setSeriesSyncDiffsToServer,
|
|
serverOnlySeries,
|
|
localOnlySeries
|
|
}}>
|
|
{isDesktop && <AutoSyncOnReconnect/>}
|
|
<BookContext.Provider value={{book: currentBook, setBook: setCurrentBook}}>
|
|
<ChapterContext.Provider value={{chapter: currentChapter, setChapter: setCurrentChapter}}>
|
|
<AIUsageContext.Provider value={{
|
|
totalCredits: currentCredits,
|
|
setTotalCredits: setCurrentCredits,
|
|
totalPrice: amountSpent,
|
|
setTotalPrice: setAmountSpent
|
|
}}>
|
|
<SettingBookContext.Provider value={{bookSettingId, setBookSettingId}}>
|
|
<div
|
|
className="bg-tertiary text-text-primary h-screen flex flex-col font-['Lora']">
|
|
<EditorContext.Provider value={{editor: editor}}>
|
|
<ScribeControllerBar/>
|
|
<div className="flex-1 flex overflow-hidden">
|
|
<ScribeLeftBar/>
|
|
<div
|
|
className="flex-1 min-w-0 bg-darkest-background rounded-xl m-1 overflow-hidden">
|
|
{children}
|
|
</div>
|
|
<ComposerRightBar/>
|
|
</div>
|
|
<ScribeFooterBar/>
|
|
</EditorContext.Provider>
|
|
</div>
|
|
{homeStepsGuide &&
|
|
<GuideTour stepId={0} steps={homeSteps} onComplete={handleHomeTour}
|
|
onClose={(): void => setHomeStepsGuide(false)}/>
|
|
}
|
|
{!isCurrentlyOffline() && !isTermsAccepted && <TermsOfUse onAccept={handleTermsAcceptance}/>}
|
|
{isDesktop && showPinSetup && (
|
|
<OfflinePinSetup
|
|
showOnFirstLogin={true}
|
|
onClose={(): void => setShowPinSetup(false)}
|
|
onSuccess={(): void => setShowPinSetup(false)}
|
|
/>
|
|
)}
|
|
{isDesktop && showPinVerify && (
|
|
<OfflinePinVerify
|
|
onSuccess={handlePinVerifySuccess}
|
|
onCancel={(): void => {}}
|
|
/>
|
|
)}
|
|
</SettingBookContext.Provider>
|
|
</AIUsageContext.Provider>
|
|
</ChapterContext.Provider>
|
|
</BookContext.Provider>
|
|
</SeriesSyncContext.Provider>
|
|
</BooksSyncContext.Provider>
|
|
</LocalSyncQueueContext.Provider>
|
|
</SessionContext.Provider>
|
|
);
|
|
}
|
|
|
|
export default function ScribeShell({children}: { children: ReactNode }) {
|
|
const [locale, setLocale] = useState<SupportedLocale>('fr');
|
|
const {theme, setTheme} = useTheme();
|
|
|
|
useEffect((): void => {
|
|
initCrashReporter();
|
|
}, []);
|
|
|
|
useEffect((): void => {
|
|
const lang: string | null = getCookie('lang');
|
|
if (lang && isSupportedLocale(lang)) {
|
|
setLocale(lang);
|
|
}
|
|
}, []);
|
|
|
|
useEffect((): void => {
|
|
changeLanguage(locale);
|
|
}, [locale]);
|
|
|
|
return (
|
|
<ThemeContext.Provider value={{theme, setTheme}}>
|
|
<LangContext.Provider value={{lang: locale, setLang: setLocale}}>
|
|
<OfflineProvider>
|
|
<AlertProvider>
|
|
<ScribeContent>{children}</ScribeContent>
|
|
</AlertProvider>
|
|
</OfflineProvider>
|
|
</LangContext.Provider>
|
|
</ThemeContext.Provider>
|
|
);
|
|
}
|