mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-06 07:00:12 +00:00
feat: implement caching for cryptographic keys to improve performance and reduce overhead
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { JSX, RefObject } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
import { createPortal } from 'preact/compat';
|
||||
import { useMemo, useState } from 'preact/hooks';
|
||||
import { Archive, ArrowUpDown, Check, CheckCheck, FolderInput, GripVertical, Plus, RefreshCw, RotateCcw, Trash2, X } from 'lucide-preact';
|
||||
@@ -106,7 +107,7 @@ interface CipherListItemBodyProps {
|
||||
onSelectCipher?: (cipherId: string) => void;
|
||||
}
|
||||
|
||||
function CipherListItemBody(props: CipherListItemBodyProps) {
|
||||
const CipherListItemBody = memo(function CipherListItemBody(props: CipherListItemBodyProps) {
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
@@ -143,12 +144,12 @@ function CipherListItemBody(props: CipherListItemBodyProps) {
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const animateLayoutChanges: AnimateLayoutChanges = (args) =>
|
||||
args.isSorting || args.wasDragging ? defaultAnimateLayoutChanges(args) : false;
|
||||
|
||||
function SortableCipherListItem(props: SortableCipherListItemProps) {
|
||||
const SortableCipherListItem = memo(function SortableCipherListItem(props: SortableCipherListItemProps) {
|
||||
const { attributes, listeners, setActivatorNodeRef, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: props.cipher.id,
|
||||
disabled: !props.canReorder,
|
||||
@@ -184,9 +185,9 @@ function SortableCipherListItem(props: SortableCipherListItemProps) {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function PlainCipherListItem(props: SortableCipherListItemProps) {
|
||||
const PlainCipherListItem = memo(function PlainCipherListItem(props: SortableCipherListItemProps) {
|
||||
return (
|
||||
<div
|
||||
className={`list-item ${props.selected ? 'active' : ''}`}
|
||||
@@ -206,7 +207,7 @@ function PlainCipherListItem(props: SortableCipherListItemProps) {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default function VaultListPanel(props: VaultListPanelProps) {
|
||||
const [activeDragId, setActiveDragId] = useState('');
|
||||
|
||||
@@ -43,6 +43,10 @@ interface VaultSidebarProps {
|
||||
}
|
||||
|
||||
export default function VaultSidebar(props: VaultSidebarProps) {
|
||||
const nameCollator = useMemo(
|
||||
() => new Intl.Collator(undefined, { sensitivity: 'base', numeric: true }),
|
||||
[]
|
||||
);
|
||||
const sortedFolders = useMemo(() => {
|
||||
const sorted = [...props.folders];
|
||||
sorted.sort((a, b) => {
|
||||
@@ -67,14 +71,14 @@ export default function VaultSidebar(props: VaultSidebarProps) {
|
||||
}
|
||||
if (aValid !== bValid) return aValid ? -1 : 1;
|
||||
}
|
||||
const nameDiff = String(a.decName || a.name || '').localeCompare(
|
||||
String(b.decName || b.name || ''), undefined, { sensitivity: 'base', numeric: true }
|
||||
const nameDiff = nameCollator.compare(
|
||||
String(a.decName || a.name || ''), String(b.decName || b.name || '')
|
||||
);
|
||||
if (nameDiff !== 0) return nameDiff;
|
||||
return String(a.id || '').localeCompare(String(b.id || ''));
|
||||
});
|
||||
return sorted;
|
||||
}, [props.folders, props.folderSortMode]);
|
||||
}, [props.folders, props.folderSortMode, nameCollator]);
|
||||
|
||||
return (
|
||||
<aside className={`sidebar ${props.isMobileLayout ? 'mobile-sidebar-sheet' : ''} ${props.isMobileLayout && props.mobileSidebarOpen ? 'open' : ''}`}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'preact/hooks';
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import {
|
||||
CreditCard,
|
||||
FileKey2,
|
||||
@@ -436,44 +436,85 @@ export function firstPasskeyCreationTime(cipher: Cipher | null): string | null {
|
||||
}
|
||||
|
||||
const failedIconHosts = new Set<string>();
|
||||
const ICON_LOAD_ROOT_MARGIN = '180px 0px';
|
||||
|
||||
export function VaultListIcon({ cipher }: { cipher: Cipher }) {
|
||||
const host = useMemo(() => hostFromUri(firstCipherUri(cipher)), [cipher]);
|
||||
const iconStackRef = useRef<HTMLSpanElement | null>(null);
|
||||
const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false));
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [shouldLoad, setShouldLoad] = useState(() => !host);
|
||||
const markIconError = () => {
|
||||
if (host) failedIconHosts.add(host);
|
||||
setErrored(true);
|
||||
};
|
||||
const syncCachedIconState = (img: HTMLImageElement | null) => {
|
||||
if (!img || !img.complete) return;
|
||||
if (img.naturalWidth > 0) {
|
||||
setLoaded(true);
|
||||
return;
|
||||
const hideFallback = () => {
|
||||
const stack = iconStackRef.current;
|
||||
if (stack) {
|
||||
const fallback = stack.querySelector('.list-icon-fallback') as HTMLElement | null;
|
||||
if (fallback) fallback.style.display = 'none';
|
||||
}
|
||||
markIconError();
|
||||
};
|
||||
const handleImgRef = (img: HTMLImageElement | null) => {
|
||||
if (!img || !img.complete) return;
|
||||
if (img.naturalWidth > 0) hideFallback();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setErrored(host ? failedIconHosts.has(host) : false);
|
||||
setLoaded(false);
|
||||
setShouldLoad(!host);
|
||||
// Reset fallback visibility so it shows while loading the new icon
|
||||
const fallback = iconStackRef.current?.querySelector('.list-icon-fallback') as HTMLElement | null;
|
||||
if (fallback) fallback.style.display = '';
|
||||
}, [host]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!host || errored || shouldLoad) return;
|
||||
const node = iconStackRef.current;
|
||||
if (!node) return;
|
||||
if (typeof IntersectionObserver !== 'function') {
|
||||
setShouldLoad(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting && entry.intersectionRatio <= 0) continue;
|
||||
if (!cancelled) setShouldLoad(true);
|
||||
observer.disconnect();
|
||||
break;
|
||||
}
|
||||
},
|
||||
{ rootMargin: ICON_LOAD_ROOT_MARGIN }
|
||||
);
|
||||
|
||||
observer.observe(node);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [host, errored, shouldLoad]);
|
||||
|
||||
if (host && !errored) {
|
||||
return (
|
||||
<span className="list-icon-stack">
|
||||
<span className={`list-icon-fallback ${loaded ? 'hidden' : ''}`}>
|
||||
<span className="list-icon-stack" ref={iconStackRef}>
|
||||
<span className="list-icon-fallback">
|
||||
<Globe size={18} />
|
||||
</span>
|
||||
<img
|
||||
className={`list-icon ${loaded ? 'loaded' : ''}`}
|
||||
src={websiteIconUrl(host)}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer"
|
||||
ref={syncCachedIconState}
|
||||
onLoad={() => setLoaded(true)}
|
||||
onError={markIconError}
|
||||
/>
|
||||
{shouldLoad && (
|
||||
<img
|
||||
className="list-icon loaded"
|
||||
src={websiteIconUrl(host)}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
referrerPolicy="no-referrer"
|
||||
ref={handleImgRef}
|
||||
onLoad={hideFallback}
|
||||
onError={markIconError}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user