Added recovery keys

This commit is contained in:
Bryan1029384756
2026-02-18 09:24:53 -06:00
parent bebf0bf989
commit ce9902d95d
16 changed files with 642 additions and 44 deletions

View File

@@ -538,6 +538,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
const typingTimeoutRef = useRef(null);
const lastTypingEmitRef = useRef(0);
const isInitialLoadRef = useRef(true);
const initialScrollScheduledRef = useRef(false);
const decryptionDoneRef = useRef(false);
const channelLoadIdRef = useRef(0);
const jumpToMessageIdRef = useRef(null);
@@ -559,6 +560,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
const prevMessageCountRef = useRef(0);
const prevFirstMsgIdRef = useRef(null);
const isAtBottomRef = useRef(true);
const isLoadingMoreRef = useRef(false);
const convex = useConvex();
@@ -576,6 +578,9 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
useEffect(() => {
statusRef.current = status;
loadMoreRef.current = loadMore;
if (status !== 'LoadingMore') {
isLoadingMoreRef.current = false;
}
}, [status, loadMore]);
const typingData = useQuery(
@@ -632,7 +637,21 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
});
};
setDecryptedMessages(buildFromCache());
const newMessages = buildFromCache();
// Adjust firstItemIndex atomically with data to prevent Virtuoso scroll jump
const prevCount = prevMessageCountRef.current;
const newCount = newMessages.length;
if (newCount > prevCount && prevCount > 0) {
if (prevFirstMsgIdRef.current && newMessages[0]?.id !== prevFirstMsgIdRef.current) {
const prependedCount = newCount - prevCount;
setFirstItemIndex(prev => prev - prependedCount);
}
}
prevMessageCountRef.current = newCount;
prevFirstMsgIdRef.current = newMessages[0]?.id || null;
setDecryptedMessages(newMessages);
// Phase 2: Batch-decrypt only uncached messages in background
const processUncached = async () => {
@@ -661,7 +680,8 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
decryptionDoneRef.current = true;
setDecryptedMessages(buildFromCache());
if (isInitialLoadRef.current && virtuosoRef.current && !jumpToMessageIdRef.current) {
if (isInitialLoadRef.current && !initialScrollScheduledRef.current && virtuosoRef.current && !jumpToMessageIdRef.current) {
initialScrollScheduledRef.current = true;
const loadId = channelLoadIdRef.current;
const scrollEnd = () => { const el = scrollerElRef.current; if (el) el.scrollTop = el.scrollHeight; };
requestAnimationFrame(() => requestAnimationFrame(() => {
@@ -670,6 +690,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
setTimeout(() => { if (channelLoadIdRef.current === loadId && isInitialLoadRef.current) scrollEnd(); }, 300);
setTimeout(() => { if (channelLoadIdRef.current === loadId && isInitialLoadRef.current) scrollEnd(); }, 800);
setTimeout(() => { if (channelLoadIdRef.current === loadId && isInitialLoadRef.current) scrollEnd(); }, 1200);
setTimeout(() => { if (channelLoadIdRef.current === loadId) isInitialLoadRef.current = false; }, 1500);
}
}
return;
@@ -786,7 +807,8 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
// After decryption, items may be taller — re-scroll to bottom.
// Double-rAF waits for paint + ResizeObserver cycle; escalating timeouts are safety nets.
if (isInitialLoadRef.current && virtuosoRef.current && !jumpToMessageIdRef.current) {
if (isInitialLoadRef.current && !initialScrollScheduledRef.current && virtuosoRef.current && !jumpToMessageIdRef.current) {
initialScrollScheduledRef.current = true;
const loadId = channelLoadIdRef.current;
const scrollEnd = () => { const el = scrollerElRef.current; if (el) el.scrollTop = el.scrollHeight; };
requestAnimationFrame(() => requestAnimationFrame(() => {
@@ -795,6 +817,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
setTimeout(() => { if (channelLoadIdRef.current === loadId && isInitialLoadRef.current) scrollEnd(); }, 300);
setTimeout(() => { if (channelLoadIdRef.current === loadId && isInitialLoadRef.current) scrollEnd(); }, 800);
setTimeout(() => { if (channelLoadIdRef.current === loadId && isInitialLoadRef.current) scrollEnd(); }, 1200);
setTimeout(() => { if (channelLoadIdRef.current === loadId) isInitialLoadRef.current = false; }, 1500);
}
};
@@ -825,6 +848,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
channelLoadIdRef.current += 1;
setDecryptedMessages([]);
isInitialLoadRef.current = true;
initialScrollScheduledRef.current = false;
decryptionDoneRef.current = false;
pingSeededRef.current = false;
notifiedMessageIdsRef.current = new Set();
@@ -837,6 +861,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
setSlashQuery(null);
setEphemeralMessages([]);
floodAbortRef.current = true;
isLoadingMoreRef.current = false;
setFirstItemIndex(INITIAL_FIRST_INDEX);
prevMessageCountRef.current = 0;
prevFirstMsgIdRef.current = null;
@@ -1016,24 +1041,13 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
// Virtuoso: startReached replaces IntersectionObserver
const handleStartReached = useCallback(() => {
if (isLoadingMoreRef.current) return;
if (statusRef.current === 'CanLoadMore') {
isLoadingMoreRef.current = true;
loadMoreRef.current(50);
}
}, []);
// Virtuoso: firstItemIndex management for prepend without jitter
useEffect(() => {
const prevCount = prevMessageCountRef.current;
const newCount = decryptedMessages.length;
if (newCount > prevCount && prevCount > 0) {
if (prevFirstMsgIdRef.current && decryptedMessages[0]?.id !== prevFirstMsgIdRef.current) {
const prependedCount = newCount - prevCount;
setFirstItemIndex(prev => prev - prependedCount);
}
}
prevMessageCountRef.current = newCount;
prevFirstMsgIdRef.current = decryptedMessages[0]?.id || null;
}, [decryptedMessages]);
// Virtuoso: followOutput auto-scrolls on new messages and handles initial load
const followOutput = useCallback((isAtBottom) => {
@@ -1068,12 +1082,8 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
if (channelLoadIdRef.current === loadId) {
isInitialLoadRef.current = false;
}
}, 1500);
}, 300);
}
} else if (isInitialLoadRef.current && decryptionDoneRef.current) {
// Content resize pushed us off bottom during initial load — snap back
const el = scrollerElRef.current;
if (el) el.scrollTop = el.scrollHeight;
}
}, [markChannelAsRead]);
@@ -1668,26 +1678,9 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
return (
<>
{status === 'LoadingMore' && (
<>
{[
{ name: 80, lines: [260, 180] },
{ name: 60, lines: [310] },
{ name: 100, lines: [240, 140] },
{ name: 70, lines: [290] },
{ name: 90, lines: [200, 260] },
{ name: 55, lines: [330] },
].map((s, i) => (
<div key={i} className="skeleton-message" style={{ animationDelay: `${i * 0.1}s` }}>
<div className="skeleton-avatar" />
<div style={{ flex: 1 }}>
<div className="skeleton-name" style={{ width: s.name }} />
{s.lines.map((w, j) => (
<div key={j} className="skeleton-line" style={{ width: w }} />
))}
</div>
</div>
))}
</>
<div style={{ display: 'flex', justifyContent: 'center', padding: '8px 0' }}>
<div className="loading-spinner" style={{ width: '20px', height: '20px', borderWidth: '2px' }} />
</div>
)}
{status === 'Exhausted' && (decryptedMessages.length > 0 || rawMessages.length === 0) && (
<div className="channel-beginning">