mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
Take #2 Desktop cacta 100%
This commit is contained in:
@@ -12,6 +12,19 @@ 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);
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@ 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) =>
|
||||
@@ -226,6 +231,12 @@ const useChatWidgetState = () =>
|
||||
return newValue;
|
||||
});
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -13,6 +13,7 @@ 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;
|
||||
};
|
||||
@@ -27,6 +28,12 @@ const trimArrayForQuota = <T>(value: T): 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;
|
||||
}
|
||||
|
||||
@@ -52,6 +59,7 @@ const useLocalStorageState = <T>(key: string, initialValue: T, options: UseLocal
|
||||
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) =>
|
||||
@@ -75,6 +83,8 @@ const useLocalStorageState = <T>(key: string, initialValue: T, options: UseLocal
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -84,10 +94,14 @@ const useLocalStorageState = <T>(key: string, initialValue: T, options: UseLocal
|
||||
catch(retryError)
|
||||
{
|
||||
NitroLogger.error(retryError);
|
||||
try { window.localStorage.removeItem(key); } catch(_) { }
|
||||
// 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;
|
||||
@@ -103,6 +117,8 @@ const useLocalStorageState = <T>(key: string, initialValue: T, options: UseLocal
|
||||
}, 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 = () =>
|
||||
|
||||
Reference in New Issue
Block a user