mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
🆙 Bug fixed in localstorage
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetConfiguration, GetRoomContentLoader, GetRoomEngine, GetSessionDataManager, GetTickerTime, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomEngineObjectPlacedEvent, RoomObjectPlacementSource, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer';
|
||||
import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetRoomEngine, GetSessionDataManager, GetTickerTime, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomEngineObjectPlacedEvent, RoomObjectPlacementSource, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { BuilderFurniPlaceableStatus, CatalogPage, CatalogType, DispatchUiEvent, FurniCategory, GetFurnitureData, GetProductDataForLocalization, GetRoomSession, ICatalogNode, ICatalogPage, IPageLocalization, IProduct, IPurchasableOffer, IPurchaseOptions, LocalizeText, NotificationAlertType, Offer, PageLocalization, PlacedObjectPurchaseData, PlaySound, Product, ProductTypeEnum, RequestedPage, SearchResult, SendMessageComposer, SoundNames } from '../../api';
|
||||
@@ -89,27 +89,6 @@ const useCatalogStore = () =>
|
||||
setCurrentType(normalizeCatalogType(type));
|
||||
}, []);
|
||||
|
||||
// Real-time furni importati: ri-mergia il chunk custom/imported.json5 nelle Map
|
||||
// furnidata + RoomContentLoader all'apertura del catalogo, SENZA reload del client.
|
||||
const refreshImportedFurnidata = useCallback(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const base = GetConfiguration().getValue<string>('furnidata.url');
|
||||
|
||||
if(!base || !base.length) return;
|
||||
|
||||
const importedUrl = base.replace(/\/+$/, '') + '/custom/imported.json5';
|
||||
|
||||
GetSessionDataManager().mergeFurnitureDataFromUrl(importedUrl).then(added =>
|
||||
{
|
||||
if(added && added.length) GetRoomContentLoader().processFurnitureData(added);
|
||||
}).catch(() => {});
|
||||
}
|
||||
catch
|
||||
{}
|
||||
}, []);
|
||||
|
||||
const openCatalogByType = useCallback((type?: string) =>
|
||||
{
|
||||
const catalogType = normalizeCatalogType(type);
|
||||
@@ -119,10 +98,8 @@ const useCatalogStore = () =>
|
||||
resetVisibleCatalogState(catalogType);
|
||||
}
|
||||
|
||||
refreshImportedFurnidata();
|
||||
|
||||
setIsVisible(true);
|
||||
}, [ currentType, resetVisibleCatalogState, refreshImportedFurnidata ]);
|
||||
}, [ currentType, resetVisibleCatalogState ]);
|
||||
|
||||
const toggleCatalogByType = useCallback((type?: string) =>
|
||||
{
|
||||
@@ -140,10 +117,8 @@ const useCatalogStore = () =>
|
||||
resetVisibleCatalogState(catalogType);
|
||||
}
|
||||
|
||||
refreshImportedFurnidata();
|
||||
|
||||
setIsVisible(true);
|
||||
}, [ isVisible, currentType, resetVisibleCatalogState, refreshImportedFurnidata ]);
|
||||
}, [ isVisible, currentType, resetVisibleCatalogState ]);
|
||||
|
||||
const getBuilderFurniPlaceableStatus = useCallback((offer: IPurchasableOffer) =>
|
||||
{
|
||||
|
||||
@@ -12,11 +12,27 @@ const MESSENGER_HISTORY_MAX = 1000;
|
||||
let CHAT_HISTORY_COUNTER: number = 0;
|
||||
let MESSENGER_HISTORY_COUNTER: number = 0;
|
||||
|
||||
/**
|
||||
* Project a list of chat entries to the slim shape we want to persist in
|
||||
* localStorage. `imageUrl` is a base64 data URL of the avatar / pet head
|
||||
* (10-50 KB each) - keeping it in storage blows past the browser quota
|
||||
* inside minutes in a pet-heavy room. The avatar can always be re-rendered
|
||||
* from `look` via ChatBubbleUtilities.getUserImage(), and pet images are
|
||||
* regenerated from the bubble flow when needed; we just don't restore
|
||||
* head thumbnails for entries loaded from a previous session.
|
||||
*
|
||||
* `style` / `chatType` / `color` are kept because they're tiny but
|
||||
* meaningful for re-rendering the bubble. Translation fields are kept
|
||||
* because they're already text.
|
||||
*/
|
||||
const slimChatEntriesForStorage = (entries: IChatEntry[]): IChatEntry[] =>
|
||||
entries.map(entry => entry.imageUrl ? { ...entry, imageUrl: undefined } : entry);
|
||||
|
||||
const useChatHistoryState = () =>
|
||||
{
|
||||
const [ chatHistory, setChatHistory ] = useLocalStorage<IChatEntry[]>('chatHistory', []);
|
||||
const [ chatHistory, setChatHistory ] = useLocalStorage<IChatEntry[]>('chatHistory', [], { toStorage: slimChatEntriesForStorage });
|
||||
const [ roomHistory, setRoomHistory ] = useLocalStorage<IRoomHistoryEntry[]>('roomHistory', []);
|
||||
const [ messengerHistory, setMessengerHistory ] = useLocalStorage<IChatEntry[]>('messengerHistory', []);
|
||||
const [ messengerHistory, setMessengerHistory ] = useLocalStorage<IChatEntry[]>('messengerHistory', [], { toStorage: slimChatEntriesForStorage });
|
||||
const [ needsRoomInsert, setNeedsRoomInsert ] = useLocalStorage('needsRoomInsert', false);
|
||||
|
||||
const addChatEntry = (entry: IChatEntry) =>
|
||||
|
||||
@@ -40,4 +40,19 @@ describe('mentionsStore', () =>
|
||||
expect(getUnreadCount()).toBe(1);
|
||||
expect(getMentionsSnapshot().find(m => m.mentionId === 1)!.read).toBe(true);
|
||||
});
|
||||
|
||||
it('drops mentions with non-positive id (defensive against id=0 spam)', () =>
|
||||
{
|
||||
addMention(make(0));
|
||||
addMention(make(-1));
|
||||
expect(getMentionsSnapshot()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('dedupes duplicate ids even after the legacy id !== 0 carve-out is gone', () =>
|
||||
{
|
||||
addMention(make(7));
|
||||
addMention(make(7));
|
||||
addMention(make(7));
|
||||
expect(getMentionsSnapshot()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './useMentionsSnapshot';
|
||||
export * from './useMentionMessages';
|
||||
export * from './useMentionAutocomplete';
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import { IMentionEntry } from '../../api/mentions';
|
||||
|
||||
// Hard cap on how many mentions we hold in memory at once. The server's
|
||||
// initial list is capped (mentions.store.limit, default 50) but live
|
||||
// MentionReceived packets feed into addMention unbounded - so a server bug
|
||||
// or a hostile/injected stream could otherwise grow the array and the DOM
|
||||
// forever. 200 is comfortably more than any realistic active user has and
|
||||
// well below anything that would inflate memory.
|
||||
const MAX_MENTIONS = 200;
|
||||
|
||||
let mentions: IMentionEntry[] = [];
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
const emit = () => { for(const l of listeners) l(); };
|
||||
|
||||
const cap = (list: IMentionEntry[]): IMentionEntry[] =>
|
||||
(list.length > MAX_MENTIONS) ? list.slice(0, MAX_MENTIONS) : list;
|
||||
|
||||
export const subscribeMentions = (onChange: () => void): (() => void) =>
|
||||
{
|
||||
listeners.add(onChange);
|
||||
@@ -17,14 +28,21 @@ export const getUnreadCount = (): number => mentions.reduce((n, m) => n + (m.rea
|
||||
|
||||
export const setMentions = (list: IMentionEntry[]): void =>
|
||||
{
|
||||
mentions = [...list].sort((a, b) => b.mentionId - a.mentionId);
|
||||
mentions = cap([...list].sort((a, b) => b.mentionId - a.mentionId));
|
||||
emit();
|
||||
};
|
||||
|
||||
export const addMention = (entry: IMentionEntry): void =>
|
||||
{
|
||||
if(mentions.some(m => m.mentionId === entry.mentionId && entry.mentionId !== 0)) return;
|
||||
mentions = [entry, ...mentions];
|
||||
// Drop entries the server failed to persist (generatedId 0 / negative).
|
||||
// The server hardening already refuses to push these, but the client
|
||||
// stays defensive in case a stale gameserver or an injected packet sends
|
||||
// one - without this guard, the old "id !== 0" dedup carve-out let
|
||||
// every duplicate through.
|
||||
if(!entry || !Number.isFinite(entry.mentionId) || entry.mentionId <= 0) return;
|
||||
if(mentions.some(m => m.mentionId === entry.mentionId)) return;
|
||||
|
||||
mentions = cap([entry, ...mentions]);
|
||||
emit();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import { MentionReceivedEvent, MentionsListEvent, RequestMentionsComposer } from '@nitrots/nitro-renderer';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { GetConfigurationValue, IMentionEntry, PlaySound, SendMessageComposer } from '../../api';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { GetConfigurationValue, IMentionEntry, LocalizeText, NotificationBubbleType, PlaySound, SendMessageComposer } from '../../api';
|
||||
import { useMessageEvent } from '../events';
|
||||
import { useNotificationActions } from '../notification';
|
||||
import { addMention, setMentions } from './mentionsStore';
|
||||
import { pushMentionToast } from './mentionToastsStore';
|
||||
|
||||
// Dedicated mention chime served from nitro-assets/sounds/<sample>.mp3.
|
||||
const MENTION_SOUND_SAMPLE = 'mentions_notification';
|
||||
|
||||
// Floor on the gap between bubble/chime notifications. Even if the server
|
||||
// (or an injected packet stream) pushes mentions faster than this, the user
|
||||
// gets at most one chime + bubble per window. The mentions list itself
|
||||
// still updates in real time - this only throttles the in-screen feedback.
|
||||
const NOTIFICATION_THROTTLE_MS = 1500;
|
||||
// Drop any single mention packet whose mention id we've already seen this
|
||||
// session, so a replay attack can't re-trigger the bubble + sound even if
|
||||
// the client store dropped the entry already.
|
||||
const SEEN_IDS_MAX = 500;
|
||||
|
||||
export const useMentionMessages = (): void =>
|
||||
{
|
||||
const { showSingleBubble } = useNotificationActions();
|
||||
const lastNotificationRef = useRef<number>(0);
|
||||
const seenIdsRef = useRef<Set<number>>(new Set());
|
||||
|
||||
const onMentionsList = useCallback((event: MentionsListEvent) =>
|
||||
{
|
||||
const list = event.getParser().mentions;
|
||||
@@ -18,7 +32,7 @@ export const useMentionMessages = (): void =>
|
||||
mentionId: m.mentionId,
|
||||
senderId: m.senderId,
|
||||
senderUsername: m.senderUsername,
|
||||
senderFigure: m.senderFigure,
|
||||
senderFigure: m.senderFigure ?? '',
|
||||
roomId: m.roomId,
|
||||
roomName: m.roomName,
|
||||
message: m.message,
|
||||
@@ -34,11 +48,22 @@ export const useMentionMessages = (): void =>
|
||||
|
||||
const m = event.getParser().mention;
|
||||
|
||||
if(!m || !Number.isFinite(m.mentionId) || m.mentionId <= 0) return;
|
||||
|
||||
const seen = seenIdsRef.current;
|
||||
if(seen.has(m.mentionId)) return;
|
||||
seen.add(m.mentionId);
|
||||
if(seen.size > SEEN_IDS_MAX)
|
||||
{
|
||||
const first = seen.values().next().value as number | undefined;
|
||||
if(first !== undefined) seen.delete(first);
|
||||
}
|
||||
|
||||
const entry: IMentionEntry = {
|
||||
mentionId: m.mentionId,
|
||||
senderId: m.senderId,
|
||||
senderUsername: m.senderUsername,
|
||||
senderFigure: m.senderFigure,
|
||||
senderFigure: m.senderFigure ?? '',
|
||||
roomId: m.roomId,
|
||||
roomName: m.roomName,
|
||||
message: m.message,
|
||||
@@ -49,11 +74,20 @@ export const useMentionMessages = (): void =>
|
||||
|
||||
addMention(entry);
|
||||
|
||||
const now = Date.now();
|
||||
if((now - lastNotificationRef.current) < NOTIFICATION_THROTTLE_MS) return;
|
||||
lastNotificationRef.current = now;
|
||||
|
||||
if(GetConfigurationValue<boolean>('mentions_ui.sound', true)) PlaySound(MENTION_SOUND_SAMPLE);
|
||||
|
||||
// Notifica laterale custom (avatar + messaggio + dismiss) invece del bubble generico.
|
||||
pushMentionToast(entry);
|
||||
}, []);
|
||||
showSingleBubble(
|
||||
LocalizeText('mentions.notification', [ 'sender', 'room' ], [ entry.senderUsername, entry.roomName ]),
|
||||
NotificationBubbleType.INFO,
|
||||
null,
|
||||
'mentions/toggle',
|
||||
entry.senderUsername
|
||||
);
|
||||
}, [ showSingleBubble ]);
|
||||
|
||||
useMessageEvent<MentionsListEvent>(MentionsListEvent, onMentionsList);
|
||||
useMessageEvent<MentionReceivedEvent>(MentionReceivedEvent, onMentionReceived);
|
||||
|
||||
@@ -64,6 +64,6 @@ export const useNavigatorUiStore = createNitroStore<NavigatorUiState & Navigator
|
||||
markInitDone: () => set({ needsInit: false }),
|
||||
requestSearch: () => set({ needsSearch: true }),
|
||||
consumeSearchRequest: () => set({ needsSearch: false }),
|
||||
setTab: (code) => set({ currentTabCode: code, currentFilter: '', isCreatorOpen: false }),
|
||||
setTab: (code) => set({ currentTabCode: code, currentFilter: '' }),
|
||||
setFilter: (value) => set({ currentFilter: value })
|
||||
}));
|
||||
|
||||
@@ -230,22 +230,31 @@ const useChatWidgetState = () =>
|
||||
|
||||
return newValue;
|
||||
});
|
||||
const chatEntryId = addChatEntry({
|
||||
id: -1,
|
||||
webId: userData.webID,
|
||||
entityId: userData.roomIndex,
|
||||
name: username,
|
||||
imageUrl,
|
||||
style: styleId,
|
||||
chatType: chatType,
|
||||
entityType: userData.type,
|
||||
message: formattedText,
|
||||
timestamp: ChatHistoryCurrentDate(),
|
||||
type: ChatEntryType.TYPE_CHAT,
|
||||
roomId: roomSession.roomId,
|
||||
color,
|
||||
...(outgoingTranslation ? buildTranslatedEntryPatch(outgoingTranslation.originalText, outgoingTranslation.translatedText, outgoingTranslation.detectedLanguage, outgoingTranslation.targetLanguage) : {})
|
||||
});
|
||||
|
||||
// Pet, Bot and Rentable Bot chat is fire-and-forget ("UDP-style"):
|
||||
// the live bubble already rendered above, but we deliberately skip
|
||||
// addChatEntry so the entry never lands in localStorage. A pet-heavy
|
||||
// room used to push 30+ KB per message (base64 head data URL) into
|
||||
// the chat history, exhausting the localStorage quota in minutes.
|
||||
// Real users still go through the full persisted path.
|
||||
const chatEntryId = (userType === RoomObjectType.USER)
|
||||
? addChatEntry({
|
||||
id: -1,
|
||||
webId: userData.webID,
|
||||
entityId: userData.roomIndex,
|
||||
name: username,
|
||||
imageUrl,
|
||||
style: styleId,
|
||||
chatType: chatType,
|
||||
entityType: userData.type,
|
||||
message: formattedText,
|
||||
timestamp: ChatHistoryCurrentDate(),
|
||||
type: ChatEntryType.TYPE_CHAT,
|
||||
roomId: roomSession.roomId,
|
||||
color,
|
||||
...(outgoingTranslation ? buildTranslatedEntryPatch(outgoingTranslation.originalText, outgoingTranslation.translatedText, outgoingTranslation.detectedLanguage, outgoingTranslation.targetLanguage) : {})
|
||||
})
|
||||
: -1;
|
||||
|
||||
if(!settings.enabled || outgoingTranslation || !isTranslatableChatType || !text.trim().length) return;
|
||||
|
||||
|
||||
@@ -1,10 +1,43 @@
|
||||
import { NitroLogger } from '@nitrots/nitro-renderer';
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
|
||||
import { GetLocalStorage, SetLocalStorage } from '../api';
|
||||
|
||||
const userId = new URLSearchParams(window.location.search).get('userid') || 0;
|
||||
|
||||
const useLocalStorageState = <T>(key: string, initialValue: T): [ T, Dispatch<SetStateAction<T>>] =>
|
||||
const STORAGE_WRITE_DEBOUNCE_MS = 250;
|
||||
const QUOTA_TRIM_FACTOR = 0.5; // on quota error, keep the newest 50%.
|
||||
const MIN_RETAINED_ENTRIES = 50;
|
||||
|
||||
const isQuotaError = (error: unknown): boolean =>
|
||||
{
|
||||
if(!error || typeof error !== 'object') return false;
|
||||
const name = (error as { name?: string }).name;
|
||||
if(name === 'QuotaExceededError') return true;
|
||||
// Firefox legacy:
|
||||
if(name === 'NS_ERROR_DOM_QUOTA_REACHED') return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const trimArrayForQuota = <T>(value: T): T =>
|
||||
{
|
||||
if(!Array.isArray(value)) return value;
|
||||
if(value.length <= MIN_RETAINED_ENTRIES) return [] as unknown as T;
|
||||
const keep = Math.max(MIN_RETAINED_ENTRIES, Math.floor(value.length * QUOTA_TRIM_FACTOR));
|
||||
return value.slice(value.length - keep) as unknown as T;
|
||||
};
|
||||
|
||||
interface UseLocalStorageOptions<T>
|
||||
{
|
||||
/**
|
||||
* Optional projection applied right before the value is written to
|
||||
* localStorage. The in-memory React state is unaffected. Use this to
|
||||
* strip heavy ephemeral fields (e.g. base64 image URLs) that would
|
||||
* otherwise blow past the storage quota.
|
||||
*/
|
||||
toStorage?: (value: T) => unknown;
|
||||
}
|
||||
|
||||
const useLocalStorageState = <T>(key: string, initialValue: T, options: UseLocalStorageOptions<T> = {}): [ T, Dispatch<SetStateAction<T>>] =>
|
||||
{
|
||||
key = userId ? `${ key }.${ userId }` : key;
|
||||
|
||||
@@ -22,6 +55,91 @@ const useLocalStorageState = <T>(key: string, initialValue: T): [ T, Dispatch<Se
|
||||
}
|
||||
});
|
||||
|
||||
const pendingWriteRef = useRef<T | null>(null);
|
||||
const writeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const optionsRef = useRef(options);
|
||||
|
||||
// Keep the latest toStorage projection without re-running effects.
|
||||
optionsRef.current = options;
|
||||
|
||||
const flushWrite = (value: T) =>
|
||||
{
|
||||
if(typeof window === 'undefined') return;
|
||||
|
||||
const project = optionsRef.current.toStorage;
|
||||
const projected = project ? project(value) : value;
|
||||
|
||||
try
|
||||
{
|
||||
SetLocalStorage(key, projected);
|
||||
return;
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
if(!isQuotaError(error))
|
||||
{
|
||||
NitroLogger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Quota exceeded - trim and retry once. Anything that isn't an
|
||||
// array gets cleared, since we have no generic trimming rule.
|
||||
try
|
||||
{
|
||||
const trimmed = trimArrayForQuota(projected as T);
|
||||
SetLocalStorage(key, trimmed);
|
||||
NitroLogger.warn(`[useLocalStorage] quota exceeded for ${ key }, trimmed payload`);
|
||||
}
|
||||
catch(retryError)
|
||||
{
|
||||
NitroLogger.error(retryError);
|
||||
// Last resort: drop the key entirely so future writes have room.
|
||||
try { window.localStorage.removeItem(key); } catch(_) { /* ignore */ }
|
||||
}
|
||||
};
|
||||
|
||||
// Debounce: high-frequency chat would otherwise trigger one full
|
||||
// JSON.stringify + setItem per message. We coalesce bursts into one
|
||||
// write per STORAGE_WRITE_DEBOUNCE_MS window with the latest value.
|
||||
const scheduleWrite = (value: T) =>
|
||||
{
|
||||
pendingWriteRef.current = value;
|
||||
if(writeTimerRef.current) clearTimeout(writeTimerRef.current);
|
||||
writeTimerRef.current = setTimeout(() =>
|
||||
{
|
||||
writeTimerRef.current = null;
|
||||
if(pendingWriteRef.current !== null)
|
||||
{
|
||||
flushWrite(pendingWriteRef.current);
|
||||
pendingWriteRef.current = null;
|
||||
}
|
||||
}, STORAGE_WRITE_DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
// Flush a pending write on tab close / hide so we don't lose the last
|
||||
// burst of activity.
|
||||
useEffect(() =>
|
||||
{
|
||||
const flushOnLeave = () =>
|
||||
{
|
||||
if(pendingWriteRef.current === null) return;
|
||||
if(writeTimerRef.current) clearTimeout(writeTimerRef.current);
|
||||
writeTimerRef.current = null;
|
||||
flushWrite(pendingWriteRef.current);
|
||||
pendingWriteRef.current = null;
|
||||
};
|
||||
|
||||
window.addEventListener('pagehide', flushOnLeave);
|
||||
window.addEventListener('beforeunload', flushOnLeave);
|
||||
|
||||
return () =>
|
||||
{
|
||||
window.removeEventListener('pagehide', flushOnLeave);
|
||||
window.removeEventListener('beforeunload', flushOnLeave);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const setValue = (value: T) =>
|
||||
{
|
||||
try
|
||||
@@ -30,7 +148,7 @@ const useLocalStorageState = <T>(key: string, initialValue: T): [ T, Dispatch<Se
|
||||
|
||||
setStoredValue(valueToStore);
|
||||
|
||||
if(typeof window !== 'undefined') SetLocalStorage(key, valueToStore);
|
||||
scheduleWrite(valueToStore);
|
||||
}
|
||||
|
||||
catch(error)
|
||||
|
||||
Reference in New Issue
Block a user