birdclaw 0.8.2 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/package.json +2 -1
- package/src/cli/command-context.ts +17 -0
- package/src/cli/register-analysis.ts +500 -0
- package/src/cli/register-compose.ts +40 -0
- package/src/cli/register-graph.ts +132 -0
- package/src/cli/register-inbox.ts +41 -0
- package/src/cli/register-storage.ts +106 -0
- package/src/cli.ts +30 -750
- package/src/components/AccountSwitcher.tsx +7 -15
- package/src/components/AvatarChip.tsx +1 -1
- package/src/components/AvatarPreload.ts +149 -0
- package/src/components/MarkdownCitations.tsx +680 -0
- package/src/components/MarkdownViewer.tsx +8 -674
- package/src/components/ProfileAnalysisClient.ts +191 -0
- package/src/components/ProfileAnalysisStream.tsx +16 -185
- package/src/components/ProfilePreview.tsx +2 -0
- package/src/components/links-controller.ts +162 -0
- package/src/components/links-model.ts +198 -0
- package/src/components/network-map-controller.ts +84 -0
- package/src/components/network-map-model.ts +255 -0
- package/src/components/useTimelineRouteData.ts +105 -235
- package/src/lib/analysis-runtime.ts +238 -0
- package/src/lib/api-client.ts +16 -215
- package/src/lib/api-contracts.ts +328 -0
- package/src/lib/archive-import-plan.ts +102 -0
- package/src/lib/archive-import.ts +170 -239
- package/src/lib/authored-live.ts +75 -120
- package/src/lib/backup.ts +335 -424
- package/src/lib/blocks-write.ts +30 -26
- package/src/lib/blocks.ts +18 -20
- package/src/lib/database-metrics.ts +88 -0
- package/src/lib/database-migrations.ts +34 -0
- package/src/lib/database-schema.ts +312 -0
- package/src/lib/database-writer.ts +69 -0
- package/src/lib/db.ts +84 -330
- package/src/lib/dm-read-model.ts +533 -0
- package/src/lib/dms-live.ts +34 -97
- package/src/lib/follow-graph.ts +17 -27
- package/src/lib/import-repository.ts +138 -0
- package/src/lib/inbox.ts +2 -1
- package/src/lib/live-sync-engine.ts +209 -0
- package/src/lib/live-transport-gateway.ts +128 -0
- package/src/lib/mention-threads-live.ts +90 -177
- package/src/lib/mentions-export.ts +1 -1
- package/src/lib/mentions-live.ts +57 -181
- package/src/lib/moderation-target.ts +15 -4
- package/src/lib/moderation-write.ts +1 -1
- package/src/lib/mutes-write.ts +30 -26
- package/src/lib/openai-response-runtime.ts +251 -0
- package/src/lib/paginated-sync.ts +93 -0
- package/src/lib/period-digest.ts +116 -304
- package/src/lib/profile-analysis.ts +36 -110
- package/src/lib/queries.ts +6 -2381
- package/src/lib/query-actions.ts +437 -0
- package/src/lib/query-client.tsx +47 -0
- package/src/lib/query-read-model-shared.ts +52 -0
- package/src/lib/query-read-models.ts +5 -0
- package/src/lib/query-resource.ts +41 -0
- package/src/lib/query-status.ts +164 -0
- package/src/lib/research.ts +1 -1
- package/src/lib/runtime-services.ts +20 -0
- package/src/lib/search-discussion.ts +75 -279
- package/src/lib/server-runtime-services.ts +30 -0
- package/src/lib/sqlite.ts +48 -12
- package/src/lib/streaming-ingestion.ts +240 -0
- package/src/lib/sync-cache.ts +6 -1
- package/src/lib/sync-plan.ts +175 -0
- package/src/lib/timeline-collections-live.ts +83 -257
- package/src/lib/timeline-live.ts +86 -236
- package/src/lib/timeline-read-model.ts +1191 -0
- package/src/lib/tweet-repository.ts +156 -0
- package/src/lib/tweet-search-live.ts +63 -167
- package/src/lib/web-sync.ts +67 -50
- package/src/lib/whois.ts +2 -1
- package/src/routes/__root.tsx +11 -8
- package/src/routes/api/action.tsx +1 -1
- package/src/routes/api/conversation.tsx +1 -1
- package/src/routes/api/query.tsx +32 -26
- package/src/routes/api/status.tsx +6 -4
- package/src/routes/api/sync.tsx +5 -2
- package/src/routes/blocks.tsx +97 -131
- package/src/routes/data-sources.tsx +17 -25
- package/src/routes/dms.tsx +167 -184
- package/src/routes/inbox.tsx +63 -57
- package/src/routes/links.tsx +31 -394
- package/src/routes/network-map.tsx +41 -344
- package/src/routes/rate-limits.tsx +17 -21
- package/src/lib/client-cache.ts +0 -109
package/src/routes/links.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
Search,
|
|
10
10
|
Users,
|
|
11
11
|
} from "lucide-react";
|
|
12
|
-
import {
|
|
12
|
+
import { useState } from "react";
|
|
13
13
|
import { AvatarChip } from "#/components/AvatarChip";
|
|
14
14
|
import {
|
|
15
15
|
FeedEmpty,
|
|
@@ -19,22 +19,12 @@ import {
|
|
|
19
19
|
} from "#/components/FeedState";
|
|
20
20
|
import { ProfilePreview } from "#/components/ProfilePreview";
|
|
21
21
|
import { SmartTimestamp } from "#/components/SmartTimestamp";
|
|
22
|
-
import {
|
|
23
|
-
loadClientCache,
|
|
24
|
-
readClientCache,
|
|
25
|
-
writeClientCache,
|
|
26
|
-
} from "#/lib/client-cache";
|
|
22
|
+
import { useLinksController } from "#/components/links-controller";
|
|
27
23
|
import { formatCompactNumber } from "#/lib/present";
|
|
28
24
|
import type {
|
|
29
25
|
LinkInsightItem,
|
|
30
|
-
LinkInsightKind,
|
|
31
26
|
LinkInsightMention,
|
|
32
|
-
LinkInsightRange,
|
|
33
|
-
LinkInsightResponse,
|
|
34
|
-
LinkInsightSort,
|
|
35
|
-
LinkInsightSource,
|
|
36
27
|
ProfileRecord,
|
|
37
|
-
TweetMediaItem,
|
|
38
28
|
} from "#/lib/types";
|
|
39
29
|
import {
|
|
40
30
|
cx,
|
|
@@ -55,187 +45,20 @@ export const Route = createFileRoute("/links")({
|
|
|
55
45
|
component: LinksRoute,
|
|
56
46
|
});
|
|
57
47
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{ value: "month", label: "Month" },
|
|
73
|
-
{ value: "year", label: "Year" },
|
|
74
|
-
{ value: "all", label: "All" },
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
function itemTitle(item: LinkInsightItem) {
|
|
78
|
-
return item.title?.trim() || item.displayUrl;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function itemSubtitle(item: LinkInsightItem) {
|
|
82
|
-
const description = item.description?.trim();
|
|
83
|
-
if (description) {
|
|
84
|
-
return description;
|
|
85
|
-
}
|
|
86
|
-
return item.displayUrl.split("?")[0] || item.displayUrl;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function insightCacheKey(
|
|
90
|
-
kind: LinkInsightKind,
|
|
91
|
-
range: LinkInsightRange,
|
|
92
|
-
sort: LinkInsightSort,
|
|
93
|
-
source: LinkInsightSource,
|
|
94
|
-
refreshTick: number,
|
|
95
|
-
) {
|
|
96
|
-
return `${kind}:${range}:${sort}:${source}:${refreshTick}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function sharedInsightCacheKey(
|
|
100
|
-
kind: LinkInsightKind,
|
|
101
|
-
range: LinkInsightRange,
|
|
102
|
-
sort: LinkInsightSort,
|
|
103
|
-
source: LinkInsightSource,
|
|
104
|
-
) {
|
|
105
|
-
return `${LINK_INSIGHTS_CACHE_PREFIX}${kind}:${range}:${sort}:${source}`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function hydratedProfileCacheKey(handle: string) {
|
|
109
|
-
return `${LINK_PROFILE_HYDRATED_CACHE_PREFIX}${handle.toLowerCase()}`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function linkInsightsUrl(
|
|
113
|
-
kind: LinkInsightKind,
|
|
114
|
-
range: LinkInsightRange,
|
|
115
|
-
sort: LinkInsightSort,
|
|
116
|
-
source: LinkInsightSource,
|
|
117
|
-
refreshTick: number,
|
|
118
|
-
) {
|
|
119
|
-
const url = new URL("/api/link-insights", window.location.origin);
|
|
120
|
-
url.searchParams.set("kind", kind);
|
|
121
|
-
url.searchParams.set("range", range);
|
|
122
|
-
url.searchParams.set("sort", sort);
|
|
123
|
-
url.searchParams.set("source", source);
|
|
124
|
-
url.searchParams.set("limit", String(LINK_INSIGHTS_LIMIT));
|
|
125
|
-
url.searchParams.set("commentsLimit", String(LINK_INSIGHTS_COMMENTS_LIMIT));
|
|
126
|
-
url.searchParams.set("refresh", String(refreshTick));
|
|
127
|
-
return url;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function mentionHref(mention: LinkInsightMention, item: LinkInsightItem) {
|
|
131
|
-
return mention.sourceUrl || mention.contentTweetUrl || item.url;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function mentionCopy(mention: LinkInsightMention) {
|
|
135
|
-
return (
|
|
136
|
-
mention.commentText ||
|
|
137
|
-
mention.sharedContentText ||
|
|
138
|
-
mention.rawText ||
|
|
139
|
-
"Shared without comment"
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function isSameProfile(
|
|
144
|
-
left: ProfileRecord | null | undefined,
|
|
145
|
-
right: ProfileRecord | null | undefined,
|
|
146
|
-
) {
|
|
147
|
-
return Boolean(left && right && left.id === right.id);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function mediaImage(media: TweetMediaItem[]) {
|
|
151
|
-
return media.find((item) => item.thumbnailUrl || item.url) ?? null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function youtubeVideoId(rawUrl: string) {
|
|
155
|
-
try {
|
|
156
|
-
const url = new URL(rawUrl);
|
|
157
|
-
const host = url.hostname.toLowerCase().replace(/^www\./, "");
|
|
158
|
-
if (host === "youtu.be") {
|
|
159
|
-
return url.pathname.split("/").filter(Boolean)[0] ?? null;
|
|
160
|
-
}
|
|
161
|
-
if (!host.endsWith("youtube.com")) {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
if (url.pathname === "/watch") {
|
|
165
|
-
return url.searchParams.get("v");
|
|
166
|
-
}
|
|
167
|
-
const parts = url.pathname.split("/").filter(Boolean);
|
|
168
|
-
if (parts[0] === "embed" || parts[0] === "shorts" || parts[0] === "live") {
|
|
169
|
-
return parts[1] ?? null;
|
|
170
|
-
}
|
|
171
|
-
return null;
|
|
172
|
-
} catch {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function youtubeThumbnailUrl(rawUrl: string) {
|
|
178
|
-
const id = youtubeVideoId(rawUrl);
|
|
179
|
-
if (!id || !/^[\w-]{6,}$/.test(id)) {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
return `https://i.ytimg.com/vi/${encodeURIComponent(id)}/hqdefault.jpg`;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function commentCount(item: LinkInsightItem) {
|
|
186
|
-
return (
|
|
187
|
-
item.commentCount ??
|
|
188
|
-
item.mentions.filter((mention) => mention.hasComment).length
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function pureShareCount(item: LinkInsightItem) {
|
|
193
|
-
return (
|
|
194
|
-
item.pureShareCount ??
|
|
195
|
-
item.mentions.filter(
|
|
196
|
-
(mention) => mention.isPureShare || !mention.hasComment,
|
|
197
|
-
).length
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function isArchivePlaceholderProfile(profile: ProfileRecord) {
|
|
202
|
-
return (
|
|
203
|
-
/^id\d+$/i.test(profile.handle) &&
|
|
204
|
-
profile.displayName === profile.handle &&
|
|
205
|
-
profile.id === `profile_user_${profile.handle.slice(2)}`
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function profileNeedsHydration(profile: ProfileRecord | null | undefined) {
|
|
210
|
-
if (!profile?.handle || isArchivePlaceholderProfile(profile)) {
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
return !profile.avatarUrl || profile.followersCount === 0;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function collectProfilesForHydration(data: LinkInsightResponse | null) {
|
|
217
|
-
const handles = new Set<string>();
|
|
218
|
-
for (const item of data?.items ?? []) {
|
|
219
|
-
for (const profile of [
|
|
220
|
-
item.topSharer,
|
|
221
|
-
...item.sharers,
|
|
222
|
-
...item.mentions.flatMap((mention) => [
|
|
223
|
-
mention.sharedBy,
|
|
224
|
-
mention.contentAuthor,
|
|
225
|
-
mention.participant,
|
|
226
|
-
]),
|
|
227
|
-
]) {
|
|
228
|
-
if (!profile || !profileNeedsHydration(profile)) {
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
handles.add(profile.handle.replace(/^@/, ""));
|
|
232
|
-
if (handles.size >= PROFILE_HYDRATION_LIMIT) {
|
|
233
|
-
return [...handles];
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return [...handles];
|
|
238
|
-
}
|
|
48
|
+
import {
|
|
49
|
+
INITIAL_VISIBLE_COMMENTS,
|
|
50
|
+
MORE_COMMENTS_BATCH,
|
|
51
|
+
commentCount,
|
|
52
|
+
isSameProfile,
|
|
53
|
+
itemSubtitle,
|
|
54
|
+
itemTitle,
|
|
55
|
+
mediaImage,
|
|
56
|
+
mentionCopy,
|
|
57
|
+
mentionHref,
|
|
58
|
+
pureShareCount,
|
|
59
|
+
ranges,
|
|
60
|
+
youtubeThumbnailUrl,
|
|
61
|
+
} from "#/components/links-model";
|
|
239
62
|
|
|
240
63
|
function ProfilePill({
|
|
241
64
|
profile,
|
|
@@ -667,209 +490,23 @@ function LinkInsightRow({
|
|
|
667
490
|
}
|
|
668
491
|
|
|
669
492
|
function LinksRoute() {
|
|
670
|
-
const
|
|
671
|
-
const [range, setRange] = useState<LinkInsightRange>("week");
|
|
672
|
-
const [source, setSource] = useState<LinkInsightSource>("all");
|
|
673
|
-
const [sort, setSort] = useState<LinkInsightSort>("rank");
|
|
674
|
-
const [search, setSearch] = useState("");
|
|
675
|
-
const [refreshTick, setRefreshTick] = useState(0);
|
|
676
|
-
const initialCacheKey = insightCacheKey(kind, range, sort, source, 0);
|
|
677
|
-
const initialSharedData = readClientCache<LinkInsightResponse>(
|
|
678
|
-
sharedInsightCacheKey(kind, range, sort, source),
|
|
679
|
-
LINK_INSIGHTS_CACHE_MAX_AGE_MS,
|
|
680
|
-
);
|
|
681
|
-
const cacheRef = useRef(
|
|
682
|
-
new Map<string, LinkInsightResponse>(
|
|
683
|
-
initialSharedData ? [[initialCacheKey, initialSharedData]] : [],
|
|
684
|
-
),
|
|
685
|
-
);
|
|
686
|
-
const inFlightRef = useRef(new Set<string>());
|
|
687
|
-
const mountedRef = useRef(true);
|
|
688
|
-
const [errorByKey, setErrorByKey] = useState<Record<string, string>>({});
|
|
689
|
-
const [, bumpCacheVersion] = useState(0);
|
|
690
|
-
const currentCacheKey = insightCacheKey(
|
|
493
|
+
const {
|
|
691
494
|
kind,
|
|
495
|
+
setKind,
|
|
692
496
|
range,
|
|
693
|
-
|
|
497
|
+
setRange,
|
|
694
498
|
source,
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
const fetchInsights = useCallback(
|
|
708
|
-
(fetchKind: LinkInsightKind) => {
|
|
709
|
-
const key = insightCacheKey(fetchKind, range, sort, source, refreshTick);
|
|
710
|
-
if (cacheRef.current.has(key) || inFlightRef.current.has(key)) {
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
const sharedKey = sharedInsightCacheKey(fetchKind, range, sort, source);
|
|
714
|
-
if (refreshTick === 0) {
|
|
715
|
-
const cached = readClientCache<LinkInsightResponse>(
|
|
716
|
-
sharedKey,
|
|
717
|
-
LINK_INSIGHTS_CACHE_MAX_AGE_MS,
|
|
718
|
-
);
|
|
719
|
-
if (cached) {
|
|
720
|
-
cacheRef.current.set(key, cached);
|
|
721
|
-
bumpCacheVersion((value) => value + 1);
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
inFlightRef.current.add(key);
|
|
726
|
-
setErrorByKey((current) => {
|
|
727
|
-
const rest = { ...current };
|
|
728
|
-
delete rest[key];
|
|
729
|
-
return rest;
|
|
730
|
-
});
|
|
731
|
-
loadClientCache(
|
|
732
|
-
sharedKey,
|
|
733
|
-
() =>
|
|
734
|
-
fetch(
|
|
735
|
-
linkInsightsUrl(fetchKind, range, sort, source, refreshTick),
|
|
736
|
-
).then((response) => response.json() as Promise<LinkInsightResponse>),
|
|
737
|
-
{
|
|
738
|
-
force: refreshTick > 0,
|
|
739
|
-
maxAgeMs: LINK_INSIGHTS_CACHE_MAX_AGE_MS,
|
|
740
|
-
},
|
|
741
|
-
)
|
|
742
|
-
.then((response: LinkInsightResponse) => {
|
|
743
|
-
cacheRef.current.set(key, response);
|
|
744
|
-
if (mountedRef.current) {
|
|
745
|
-
bumpCacheVersion((value) => value + 1);
|
|
746
|
-
}
|
|
747
|
-
})
|
|
748
|
-
.catch((error: unknown) => {
|
|
749
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
if (!mountedRef.current) {
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
console.warn("Link insights failed", error);
|
|
756
|
-
setErrorByKey((current) => ({
|
|
757
|
-
...current,
|
|
758
|
-
[key]:
|
|
759
|
-
error instanceof Error
|
|
760
|
-
? error.message
|
|
761
|
-
: "Link insights unavailable",
|
|
762
|
-
}));
|
|
763
|
-
})
|
|
764
|
-
.finally(() => {
|
|
765
|
-
inFlightRef.current.delete(key);
|
|
766
|
-
});
|
|
767
|
-
},
|
|
768
|
-
[range, refreshTick, sort, source],
|
|
769
|
-
);
|
|
770
|
-
|
|
771
|
-
useEffect(() => {
|
|
772
|
-
fetchInsights(kind);
|
|
773
|
-
}, [fetchInsights, kind]);
|
|
774
|
-
|
|
775
|
-
useEffect(() => {
|
|
776
|
-
if (!data) {
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
const prefetchKind = kind === "links" ? "videos" : "links";
|
|
780
|
-
const timer = window.setTimeout(() => fetchInsights(prefetchKind), 250);
|
|
781
|
-
return () => window.clearTimeout(timer);
|
|
782
|
-
}, [data, fetchInsights, kind]);
|
|
783
|
-
|
|
784
|
-
useEffect(() => {
|
|
785
|
-
const handles = collectProfilesForHydration(data).filter(
|
|
786
|
-
(handle) =>
|
|
787
|
-
!readClientCache<boolean>(hydratedProfileCacheKey(handle)) &&
|
|
788
|
-
!hydratingLinkProfileHandles.has(handle.toLowerCase()),
|
|
789
|
-
);
|
|
790
|
-
if (handles.length === 0) {
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
const controller = new AbortController();
|
|
795
|
-
const url = new URL("/api/profile-hydrate", window.location.origin);
|
|
796
|
-
url.searchParams.set("handles", handles.join(","));
|
|
797
|
-
for (const handle of handles) {
|
|
798
|
-
hydratingLinkProfileHandles.add(handle.toLowerCase());
|
|
799
|
-
}
|
|
800
|
-
const finishHydration = (succeeded: boolean) => {
|
|
801
|
-
for (const handle of handles) {
|
|
802
|
-
const normalized = handle.toLowerCase();
|
|
803
|
-
hydratingLinkProfileHandles.delete(normalized);
|
|
804
|
-
if (succeeded) {
|
|
805
|
-
writeClientCache(hydratedProfileCacheKey(normalized), true);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
};
|
|
809
|
-
|
|
810
|
-
let idleId: number | null = null;
|
|
811
|
-
const runHydration = () => {
|
|
812
|
-
fetch(url, { signal: controller.signal })
|
|
813
|
-
.then((response) => response.json())
|
|
814
|
-
.then((response: { hydratedProfiles?: number }) => {
|
|
815
|
-
finishHydration(true);
|
|
816
|
-
if ((response.hydratedProfiles ?? 0) > 0) {
|
|
817
|
-
setRefreshTick((value) => value + 1);
|
|
818
|
-
}
|
|
819
|
-
})
|
|
820
|
-
.catch((error: unknown) => {
|
|
821
|
-
finishHydration(false);
|
|
822
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
console.warn("Profile hydration failed", error);
|
|
826
|
-
});
|
|
827
|
-
};
|
|
828
|
-
const timer = window.setTimeout(() => {
|
|
829
|
-
if ("requestIdleCallback" in window) {
|
|
830
|
-
idleId = window.requestIdleCallback(runHydration, { timeout: 2500 });
|
|
831
|
-
} else {
|
|
832
|
-
runHydration();
|
|
833
|
-
}
|
|
834
|
-
}, PROFILE_HYDRATION_DELAY_MS);
|
|
835
|
-
|
|
836
|
-
return () => {
|
|
837
|
-
controller.abort();
|
|
838
|
-
finishHydration(false);
|
|
839
|
-
window.clearTimeout(timer);
|
|
840
|
-
if (idleId !== null && "cancelIdleCallback" in window) {
|
|
841
|
-
window.cancelIdleCallback(idleId);
|
|
842
|
-
}
|
|
843
|
-
};
|
|
844
|
-
}, [data]);
|
|
845
|
-
|
|
846
|
-
const items = useMemo(() => {
|
|
847
|
-
const query = search.trim().toLowerCase();
|
|
848
|
-
const filtered = (data?.items ?? []).filter((item) => {
|
|
849
|
-
if (!query) {
|
|
850
|
-
return true;
|
|
851
|
-
}
|
|
852
|
-
return [
|
|
853
|
-
item.title,
|
|
854
|
-
item.description,
|
|
855
|
-
item.displayUrl,
|
|
856
|
-
item.host,
|
|
857
|
-
item.topSharer?.handle,
|
|
858
|
-
...item.mentions.map((mention) => mention.commentText),
|
|
859
|
-
]
|
|
860
|
-
.filter(Boolean)
|
|
861
|
-
.some((value) => String(value).toLowerCase().includes(query));
|
|
862
|
-
});
|
|
863
|
-
return filtered;
|
|
864
|
-
}, [data?.items, search]);
|
|
865
|
-
|
|
866
|
-
const subtitle = useMemo(() => {
|
|
867
|
-
if (!data) {
|
|
868
|
-
return "Loading link memory...";
|
|
869
|
-
}
|
|
870
|
-
const label = kind === "videos" ? "video URLs" : "URLs";
|
|
871
|
-
return `${formatCompactNumber(data.stats.occurrences)} ${label} across ${formatCompactNumber(data.stats.groups)} groups`;
|
|
872
|
-
}, [data, kind]);
|
|
499
|
+
setSource,
|
|
500
|
+
sort,
|
|
501
|
+
setSort,
|
|
502
|
+
search,
|
|
503
|
+
setSearch,
|
|
504
|
+
items,
|
|
505
|
+
subtitle,
|
|
506
|
+
loading,
|
|
507
|
+
error,
|
|
508
|
+
retry,
|
|
509
|
+
} = useLinksController();
|
|
873
510
|
|
|
874
511
|
return (
|
|
875
512
|
<>
|
|
@@ -966,7 +603,7 @@ function LinksRoute() {
|
|
|
966
603
|
action={
|
|
967
604
|
<button
|
|
968
605
|
className="rounded-full bg-[var(--accent)] px-4 py-1.5 text-[14px] font-bold text-white"
|
|
969
|
-
onClick={() =>
|
|
606
|
+
onClick={() => void retry()}
|
|
970
607
|
type="button"
|
|
971
608
|
>
|
|
972
609
|
Retry
|