Take #2 Desktop cacta 100%

This commit is contained in:
duckietm
2026-06-05 14:32:55 +02:00
parent 5c282101ee
commit f4d41dd3c9
81 changed files with 2898 additions and 1449 deletions
+13
View File
@@ -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);
+11
View File
@@ -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,
+17 -1
View File
@@ -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 = () =>