mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
🆙 Bug fixed in localstorage
This commit is contained in:
@@ -12,11 +12,14 @@ const MESSENGER_HISTORY_MAX = 1000;
|
||||
let CHAT_HISTORY_COUNTER: number = 0;
|
||||
let MESSENGER_HISTORY_COUNTER: number = 0;
|
||||
|
||||
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) =>
|
||||
|
||||
@@ -23,11 +23,6 @@ const useChatWidgetState = () =>
|
||||
const { addChatEntry, updateChatEntry } = useChatHistory();
|
||||
const { settings, translateIncoming, consumeOutgoingTranslation } = useTranslation();
|
||||
const isDisposed = useRef(false);
|
||||
// Reactive: re-renders if the session-data snapshot flips (e.g.
|
||||
// reconnect under a different user id). Safe to call here —
|
||||
// useChatWidget is NOT wrapped in useBetween (see export below),
|
||||
// so the real React dispatcher is in scope and
|
||||
// useSyncExternalStore installs correctly.
|
||||
const ownUserId = (useUserDataSnapshot().userId || -1);
|
||||
|
||||
const applyTranslationToBubble = useCallback((chatMessage: ChatBubbleMessage, originalText: string, translatedText: string, detectedLanguage: string, targetLanguage: string) =>
|
||||
@@ -230,22 +225,25 @@ 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) : {})
|
||||
});
|
||||
|
||||
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,36 @@
|
||||
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;
|
||||
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>
|
||||
{
|
||||
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 +48,82 @@ 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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const trimmed = trimArrayForQuota(projected as T);
|
||||
SetLocalStorage(key, trimmed);
|
||||
NitroLogger.warn(`[useLocalStorage] quota exceeded for ${ key }, trimmed payload`);
|
||||
}
|
||||
catch(retryError)
|
||||
{
|
||||
NitroLogger.error(retryError);
|
||||
try { window.localStorage.removeItem(key); } catch(_) { }
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
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 +132,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