Skip to content

Commit 073bb80

Browse files
authored
Merge pull request #50 from thedamod/fix/link-ingestion-resilience
Fix link import card lifecycle
2 parents ca45032 + 0102cca commit 073bb80

2 files changed

Lines changed: 143 additions & 39 deletions

File tree

apps/web/src/components/files/explorer.tsx

Lines changed: 129 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,31 +1412,109 @@ function normalizeFilePageIcon(icon: string | null | undefined) {
14121412
return trimmed.slice(0, 8);
14131413
}
14141414

1415-
function getLinkResourceThumbnailUrl(file: FileRecord) {
1415+
interface LinkResourceCardMetadata {
1416+
faviconUrl: string | null;
1417+
imageUrl: string | null;
1418+
title: string | null;
1419+
}
1420+
1421+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
1422+
return typeof value === "object" && value !== null && !Array.isArray(value);
1423+
}
1424+
1425+
function readNonEmptyString(value: unknown) {
1426+
return typeof value === "string" && value.trim().length > 0
1427+
? value.trim()
1428+
: null;
1429+
}
1430+
1431+
function getLinkResourceCardMetadata(
1432+
file: FileRecord
1433+
): LinkResourceCardMetadata | null {
14161434
const metadata = file.metadata;
1417-
const link =
1418-
metadata &&
1419-
typeof metadata === "object" &&
1420-
!Array.isArray(metadata) &&
1421-
metadata.link &&
1422-
typeof metadata.link === "object" &&
1423-
!Array.isArray(metadata.link)
1424-
? (metadata.link as Record<string, unknown>)
1425-
: null;
1435+
const link = isObjectRecord(metadata) && isObjectRecord(metadata.link)
1436+
? metadata.link
1437+
: null;
14261438
if (!link) {
14271439
return null;
14281440
}
14291441

1430-
const snapshot =
1431-
link.snapshot && typeof link.snapshot === "object"
1432-
? (link.snapshot as Record<string, unknown>)
1433-
: null;
1442+
const sourceUrl = readNonEmptyString(link.sourceUrl);
1443+
if (!sourceUrl) {
1444+
return null;
1445+
}
1446+
1447+
const snapshot = isObjectRecord(link.snapshot) ? link.snapshot : null;
14341448
const imageUrl =
1435-
(typeof snapshot?.imageUrl === "string" && snapshot.imageUrl.trim()) ||
1436-
(typeof link.imageUrl === "string" && link.imageUrl.trim()) ||
1449+
readNonEmptyString(snapshot?.imageUrl) ??
1450+
readNonEmptyString(link.imageUrl) ??
14371451
null;
1452+
const pageIcon = normalizeFilePageIcon(file.page?.icon);
1453+
const faviconUrl =
1454+
readNonEmptyString(link.favicon) ??
1455+
(pageIcon && isRenderableIconUrl(pageIcon)
1456+
? pageIcon
1457+
: new URL("/favicon.ico", sourceUrl).toString());
14381458

1439-
return imageUrl;
1459+
return {
1460+
faviconUrl,
1461+
imageUrl,
1462+
title: readNonEmptyString(link.title),
1463+
};
1464+
}
1465+
1466+
function LinkResourceThumbnailFallback({
1467+
faviconUrl,
1468+
loading = false,
1469+
}: {
1470+
faviconUrl: string | null;
1471+
loading?: boolean;
1472+
}) {
1473+
return (
1474+
<div className="relative flex h-full w-full items-center justify-center overflow-hidden rounded-md border border-border/60 bg-muted/70">
1475+
<LinkSimple
1476+
aria-hidden="true"
1477+
className="size-8 text-muted-foreground/60"
1478+
/>
1479+
{faviconUrl ? (
1480+
<img
1481+
alt=""
1482+
className="absolute max-h-10 max-w-10 object-contain"
1483+
height={40}
1484+
onError={(event) => {
1485+
event.currentTarget.style.display = "none";
1486+
}}
1487+
referrerPolicy="no-referrer"
1488+
src={faviconUrl}
1489+
width={40}
1490+
/>
1491+
) : null}
1492+
{loading ? (
1493+
<Spinner className="absolute right-2 bottom-2 size-4 text-muted-foreground" />
1494+
) : null}
1495+
</div>
1496+
);
1497+
}
1498+
1499+
function LinkResourceTitleIcon({ faviconUrl }: { faviconUrl: string | null }) {
1500+
return (
1501+
<span className="relative inline-flex size-4 shrink-0 items-center justify-center overflow-hidden rounded-[3px] bg-muted text-muted-foreground">
1502+
<LinkSimple aria-hidden="true" className="size-3" />
1503+
{faviconUrl ? (
1504+
<img
1505+
alt=""
1506+
className="absolute inset-0 size-full object-contain"
1507+
height={16}
1508+
onError={(event) => {
1509+
event.currentTarget.style.display = "none";
1510+
}}
1511+
referrerPolicy="no-referrer"
1512+
src={faviconUrl}
1513+
width={16}
1514+
/>
1515+
) : null}
1516+
</span>
1517+
);
14401518
}
14411519

14421520
function isRenderableIconUrl(icon: string) {
@@ -1542,23 +1620,11 @@ function PendingLinkImportCard({ item }: { item: PendingLinkImport }) {
15421620
style={{ width: 160 }}
15431621
>
15441622
<CardContent className="px-0 pt-0">
1545-
<div className="relative flex h-28 items-center justify-center overflow-hidden rounded-md border border-border/60 bg-muted/70">
1546-
<LinkSimple
1547-
aria-hidden="true"
1548-
className="size-8 text-muted-foreground/60"
1623+
<div className="h-28">
1624+
<LinkResourceThumbnailFallback
1625+
faviconUrl={item.faviconUrl}
1626+
loading
15491627
/>
1550-
<img
1551-
alt=""
1552-
className="absolute max-h-10 max-w-10 object-contain"
1553-
height={40}
1554-
onError={(event) => {
1555-
event.currentTarget.style.display = "none";
1556-
}}
1557-
referrerPolicy="no-referrer"
1558-
src={item.faviconUrl}
1559-
width={40}
1560-
/>
1561-
<Spinner className="absolute right-2 bottom-2 size-4 text-muted-foreground" />
15621628
</div>
15631629
<div className="mt-2 flex min-w-0 items-center gap-2">
15641630
<LinkSimple
@@ -1797,6 +1863,9 @@ export function FileExplorer({
17971863
const importLinkIntentVersion = useFilesUiStore(
17981864
(state) => state.intentVersion.importLink
17991865
);
1866+
const consumedImportLinkIntentVersion = useFilesUiStore(
1867+
(state) => state.consumedIntentVersion.importLink
1868+
);
18001869
const uploadFileIntentVersion = useFilesUiStore(
18011870
(state) => state.intentVersion.uploadFile
18021871
);
@@ -1829,7 +1898,6 @@ export function FileExplorer({
18291898
deleteSelection: 0,
18301899
focusSearch: 0,
18311900
goParent: 0,
1832-
importLink: 0,
18331901
moveSelectionUp: 0,
18341902
newNote: 0,
18351903
openSelection: 0,
@@ -3424,8 +3492,8 @@ export function FileExplorer({
34243492
openCreateNoteDialog(currentFolderId);
34253493
}
34263494

3427-
if (importLinkIntentVersion > processed.importLink) {
3428-
processed.importLink = importLinkIntentVersion;
3495+
if (importLinkIntentVersion > consumedImportLinkIntentVersion) {
3496+
filesUiActions.consumeIntent("importLink", importLinkIntentVersion);
34293497
openImportLinkDialog(currentFolderId);
34303498
}
34313499

@@ -3452,6 +3520,7 @@ export function FileExplorer({
34523520
}
34533521
}, [
34543522
createFolderIntentVersion,
3523+
consumedImportLinkIntentVersion,
34553524
currentFolderId,
34563525
focusSearchIntentVersion,
34573526
importLinkIntentVersion,
@@ -6913,8 +6982,14 @@ export function FileExplorer({
69136982
const fileKind = detectFileKind(file);
69146983
const fileCardType =
69156984
fileKind === "sheet" ? "document" : fileKind;
6985+
const linkCardMetadata =
6986+
getLinkResourceCardMetadata(file);
69166987
const linkThumbnailUrl =
6917-
getLinkResourceThumbnailUrl(file);
6988+
linkCardMetadata?.imageUrl ?? null;
6989+
const fileCardName = linkCardMetadata
6990+
? (linkCardMetadata.title ??
6991+
file.name.replace(/\.mdx?$/i, ""))
6992+
: file.name;
69186993
const searchResult = searchResultByFileId.get(
69196994
file.id
69206995
);
@@ -7041,7 +7116,7 @@ export function FileExplorer({
70417116
}
70427117
matchMeta={searchMatchMeta}
70437118
matchSnippet={searchResult?.snippet}
7044-
name={file.name}
7119+
name={fileCardName}
70457120
previewContent={
70467121
isImage ? (
70477122
<img
@@ -7074,6 +7149,13 @@ export function FileExplorer({
70747149
hoveredPreviewFileId === file.id
70757150
}
70767151
/>
7152+
) : linkCardMetadata &&
7153+
!linkThumbnailUrl ? (
7154+
<LinkResourceThumbnailFallback
7155+
faviconUrl={
7156+
linkCardMetadata.faviconUrl
7157+
}
7158+
/>
70777159
) : isMarkdown && !linkThumbnailUrl ? (
70787160
<MarkdownThumbnail
70797161
className="h-full w-full"
@@ -7088,7 +7170,15 @@ export function FileExplorer({
70887170
}
70897171
previewUrl={linkThumbnailUrl ?? undefined}
70907172
titleIcon={
7091-
normalizeFilePageIcon(file.page?.icon)
7173+
linkCardMetadata ? (
7174+
<LinkResourceTitleIcon
7175+
faviconUrl={
7176+
linkCardMetadata.faviconUrl
7177+
}
7178+
/>
7179+
) : normalizeFilePageIcon(
7180+
file.page?.icon
7181+
)
70927182
? getFileVisualIcon(
70937183
file,
70947184
fileKind,

apps/web/src/stores/filesUiStore.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const INITIAL_INTENT_VERSION: FilesUiIntentVersion = {
3434
};
3535

3636
interface FilesUiState {
37+
consumedIntentVersion: FilesUiIntentVersion;
3738
intentVersion: FilesUiIntentVersion;
3839
sync: {
3940
version: number;
@@ -43,6 +44,7 @@ interface FilesUiState {
4344
}
4445

4546
export const useFilesUiStore = create<FilesUiState>()(() => ({
47+
consumedIntentVersion: INITIAL_INTENT_VERSION,
4648
intentVersion: INITIAL_INTENT_VERSION,
4749
sync: {
4850
version: 0,
@@ -52,6 +54,18 @@ export const useFilesUiStore = create<FilesUiState>()(() => ({
5254
}));
5355

5456
export const filesUiActions = {
57+
consumeIntent: (intent: FilesUiIntent, version: number) =>
58+
useFilesUiStore.setState((state) => {
59+
if (state.consumedIntentVersion[intent] >= version) {
60+
return state;
61+
}
62+
return {
63+
consumedIntentVersion: {
64+
...state.consumedIntentVersion,
65+
[intent]: version,
66+
},
67+
};
68+
}),
5569
emitIntent: (intent: FilesUiIntent) =>
5670
useFilesUiStore.setState((state) => ({
5771
intentVersion: {

0 commit comments

Comments
 (0)