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 CHAT_HISTORY_COUNTER: number = 0;
|
||||||
let MESSENGER_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 useChatHistoryState = () =>
|
||||||
{
|
{
|
||||||
const [ chatHistory, setChatHistory ] = useLocalStorage<IChatEntry[]>('chatHistory', []);
|
const [ chatHistory, setChatHistory ] = useLocalStorage<IChatEntry[]>('chatHistory', [], { toStorage: slimChatEntriesForStorage });
|
||||||
const [ roomHistory, setRoomHistory ] = useLocalStorage<IRoomHistoryEntry[]>('roomHistory', []);
|
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 [ needsRoomInsert, setNeedsRoomInsert ] = useLocalStorage('needsRoomInsert', false);
|
||||||
|
|
||||||
const addChatEntry = (entry: IChatEntry) =>
|
const addChatEntry = (entry: IChatEntry) =>
|
||||||
|
|||||||
@@ -23,11 +23,6 @@ const useChatWidgetState = () =>
|
|||||||
const { addChatEntry, updateChatEntry } = useChatHistory();
|
const { addChatEntry, updateChatEntry } = useChatHistory();
|
||||||
const { settings, translateIncoming, consumeOutgoingTranslation } = useTranslation();
|
const { settings, translateIncoming, consumeOutgoingTranslation } = useTranslation();
|
||||||
const isDisposed = useRef(false);
|
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 ownUserId = (useUserDataSnapshot().userId || -1);
|
||||||
|
|
||||||
const applyTranslationToBubble = useCallback((chatMessage: ChatBubbleMessage, originalText: string, translatedText: string, detectedLanguage: string, targetLanguage: string) =>
|
const applyTranslationToBubble = useCallback((chatMessage: ChatBubbleMessage, originalText: string, translatedText: string, detectedLanguage: string, targetLanguage: string) =>
|
||||||
@@ -230,22 +225,25 @@ const useChatWidgetState = () =>
|
|||||||
|
|
||||||
return newValue;
|
return newValue;
|
||||||
});
|
});
|
||||||
const chatEntryId = addChatEntry({
|
|
||||||
id: -1,
|
const chatEntryId = (userType === RoomObjectType.USER)
|
||||||
webId: userData.webID,
|
? addChatEntry({
|
||||||
entityId: userData.roomIndex,
|
id: -1,
|
||||||
name: username,
|
webId: userData.webID,
|
||||||
imageUrl,
|
entityId: userData.roomIndex,
|
||||||
style: styleId,
|
name: username,
|
||||||
chatType: chatType,
|
imageUrl,
|
||||||
entityType: userData.type,
|
style: styleId,
|
||||||
message: formattedText,
|
chatType: chatType,
|
||||||
timestamp: ChatHistoryCurrentDate(),
|
entityType: userData.type,
|
||||||
type: ChatEntryType.TYPE_CHAT,
|
message: formattedText,
|
||||||
roomId: roomSession.roomId,
|
timestamp: ChatHistoryCurrentDate(),
|
||||||
color,
|
type: ChatEntryType.TYPE_CHAT,
|
||||||
...(outgoingTranslation ? buildTranslatedEntryPatch(outgoingTranslation.originalText, outgoingTranslation.translatedText, outgoingTranslation.detectedLanguage, outgoingTranslation.targetLanguage) : {})
|
roomId: roomSession.roomId,
|
||||||
});
|
color,
|
||||||
|
...(outgoingTranslation ? buildTranslatedEntryPatch(outgoingTranslation.originalText, outgoingTranslation.translatedText, outgoingTranslation.detectedLanguage, outgoingTranslation.targetLanguage) : {})
|
||||||
|
})
|
||||||
|
: -1;
|
||||||
|
|
||||||
if(!settings.enabled || outgoingTranslation || !isTranslatableChatType || !text.trim().length) return;
|
if(!settings.enabled || outgoingTranslation || !isTranslatableChatType || !text.trim().length) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,36 @@
|
|||||||
import { NitroLogger } from '@nitrots/nitro-renderer';
|
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';
|
import { GetLocalStorage, SetLocalStorage } from '../api';
|
||||||
|
|
||||||
const userId = new URLSearchParams(window.location.search).get('userid') || 0;
|
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;
|
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) =>
|
const setValue = (value: T) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -30,7 +132,7 @@ const useLocalStorageState = <T>(key: string, initialValue: T): [ T, Dispatch<Se
|
|||||||
|
|
||||||
setStoredValue(valueToStore);
|
setStoredValue(valueToStore);
|
||||||
|
|
||||||
if(typeof window !== 'undefined') SetLocalStorage(key, valueToStore);
|
scheduleWrite(valueToStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
catch(error)
|
catch(error)
|
||||||
|
|||||||
Reference in New Issue
Block a user