birdclaw 0.8.3 → 0.8.4
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 +17 -0
- package/README.md +10 -8
- package/bin/birdclaw.mjs +4 -66
- package/dist/cli/birdclaw.js +28165 -0
- package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
- package/dist/client/assets/ProfilePreview-BejAuYsJ.js +1 -0
- package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +1 -0
- package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
- package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +1 -0
- package/dist/client/assets/blocks-7UDKQp8H.js +1 -0
- package/dist/client/assets/bookmarks-CbBkMvXk.js +1 -0
- package/dist/client/assets/chunk-C0phanGV.js +1 -0
- package/dist/client/assets/circle-check-C_Vq3ElD.js +1 -0
- package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +5 -0
- package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
- package/dist/client/assets/data-sources-Cm2bpNvg.js +1 -0
- package/dist/client/assets/discuss-DTjaOugw.js +1 -0
- package/dist/client/assets/dms-CGd6Eogq.js +1 -0
- package/dist/client/assets/inbox-BHlGuOy2.js +1 -0
- package/dist/client/assets/index-CEIG7S6s.js +31 -0
- package/dist/client/assets/index-bpFrQ5yG.css +1 -0
- package/dist/client/assets/likes-cLrcAQem.js +1 -0
- package/dist/client/assets/links-CM9QO1LC.js +1 -0
- package/dist/client/assets/mapbox-BxTlqXt9.js +2 -0
- package/dist/client/assets/mapbox-gl-dgf_hX3a.js +3215 -0
- package/dist/client/assets/mentions-CxKCgzIf.js +1 -0
- package/dist/client/assets/network-map-B5a3whqj.js +2 -0
- package/dist/client/assets/profile-analyze-CRtNEwlb.js +1 -0
- package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
- package/dist/client/assets/profiles._handle-C82_HoKX.js +1 -0
- package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
- package/dist/client/assets/rate-limits-ScaEwdVK.js +1 -0
- package/dist/client/assets/react-dom-DHl6Ug7F.js +1 -0
- package/dist/client/assets/react-nBylq1U5.js +1 -0
- package/dist/client/assets/repeat-2-xCwHM9hD.js +1 -0
- package/dist/client/assets/routes-q7ZaW4En.js +1 -0
- package/dist/client/assets/schemas-I9r6F6ya.js +64 -0
- package/dist/client/assets/search-Dq5kYsxA.js +1 -0
- package/dist/client/assets/sparkles-CQVlvHsZ.js +1 -0
- package/dist/client/assets/styles-B8s3tt3r.css +2 -0
- package/dist/client/assets/today-CeG1O7Py.js +1 -0
- package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
- package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +1 -0
- package/dist/client/assets/users-CHbKlqUr.js +1 -0
- package/dist/server/assets/AvatarChip-zUDAEmjL.js +105 -0
- package/dist/server/assets/FeedState-ntW0Q2GB.js +214 -0
- package/dist/server/assets/ProfileAnalysisStream-BFvhVdhn.js +198 -0
- package/dist/server/assets/ProfilePreview-C6gAor_j.js +752 -0
- package/dist/server/assets/SavedTimelineView-BG1jnBp1.js +64 -0
- package/dist/server/assets/SmartTimestamp-DfwE1NGf.js +45 -0
- package/dist/server/assets/SyncNowButton-BOn_IBXS.js +76 -0
- package/dist/server/assets/TimelineRouteFrame-CU7ilM7U.js +85 -0
- package/dist/server/assets/TweetRichText-DhCVv1-6.js +74 -0
- package/dist/server/assets/_tanstack-start-manifest_v-C-ZRnG_n.js +148 -0
- package/dist/server/assets/account-selection-CTdFGWRk.js +44 -0
- package/dist/server/assets/blocks-C1mgGTB3.js +299 -0
- package/dist/server/assets/blocks-DSgQghqP.js +17 -0
- package/dist/server/assets/bookmarks-BJN8tykY.js +14 -0
- package/dist/server/assets/client-stream-contracts-C2xbWVjm.js +769 -0
- package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
- package/dist/server/assets/discuss-CJ1dau1j.js +15 -0
- package/dist/server/assets/discuss-QTo08NCN.js +338 -0
- package/dist/server/assets/dms-BuLj4bp8.js +368 -0
- package/dist/server/assets/dms-DAyQrVM2.js +240 -0
- package/dist/server/assets/empty-plugin-adapters-DzrEkdBF.js +5 -0
- package/dist/server/assets/inbox-CkGOaW1q.js +214 -0
- package/dist/server/assets/inbox-DpWd1EzC.js +146 -0
- package/dist/server/assets/likes-DBOAtsJU.js +14 -0
- package/dist/server/assets/links-DAhSjynu.js +240 -0
- package/dist/server/assets/links-DmH5lxHO.js +468 -0
- package/dist/server/assets/mentions-BH2MzKtB.js +26 -0
- package/dist/server/assets/network-map-D3CzRcC4.js +224 -0
- package/dist/server/assets/network-map-D5DoLOwR.js +582 -0
- package/dist/server/assets/profile-analyze-BP65PED0.js +90 -0
- package/dist/server/assets/profile-analyze-C1NoM0tV.js +9 -0
- package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
- package/dist/server/assets/profiles._handle-DjwlN_ac.js +184 -0
- package/dist/server/assets/profiles._handle-DsLNj2Ca.js +12 -0
- package/dist/server/assets/query-client-BdauHBgd.js +923 -0
- package/dist/server/assets/rate-limits-VAr2qzYD.js +262 -0
- package/dist/server/assets/route-search-BUlQ5lGM.js +117 -0
- package/dist/server/assets/router-DqkwSPiM.js +17026 -0
- package/dist/server/assets/routes-A9c9ZhwN.js +26 -0
- package/dist/server/assets/start-FaDOTmto.js +4 -0
- package/dist/server/assets/today-B5hVM57Z.js +17 -0
- package/dist/server/assets/today-fvGiZ7ud.js +303 -0
- package/dist/server/assets/ui-qbmcMSSX.js +137 -0
- package/{src/components/useDebouncedValue.ts → dist/server/assets/useDebouncedValue-DOnA6Xcy.js} +4 -4
- package/dist/server/assets/useTimelineRouteData-27cxynxa.js +1101 -0
- package/dist/server/server.js +5520 -0
- package/package.json +26 -34
- package/playwright.config.ts +0 -35
- package/scripts/browser-perf.mjs +0 -427
- package/scripts/build-docs-site.mjs +0 -967
- package/scripts/docs-site-assets.mjs +0 -311
- package/scripts/run-vitest.mjs +0 -21
- package/scripts/sanitize-node-options.mjs +0 -23
- package/scripts/start-test-server.mjs +0 -42
- package/src/cli/command-context.ts +0 -17
- package/src/cli/register-analysis.ts +0 -500
- package/src/cli/register-compose.ts +0 -40
- package/src/cli/register-graph.ts +0 -132
- package/src/cli/register-inbox.ts +0 -41
- package/src/cli/register-storage.ts +0 -106
- package/src/cli-moderation.ts +0 -210
- package/src/cli.ts +0 -1616
- package/src/components/AccountSwitcher.tsx +0 -178
- package/src/components/AppNav.tsx +0 -118
- package/src/components/AvatarChip.tsx +0 -58
- package/src/components/AvatarPreload.ts +0 -149
- package/src/components/BrandMark.tsx +0 -67
- package/src/components/ConversationThread.tsx +0 -131
- package/src/components/DmWorkspace.tsx +0 -286
- package/src/components/EmbeddedTweetCard.tsx +0 -55
- package/src/components/FeedState.tsx +0 -147
- package/src/components/FloatingPreview.tsx +0 -382
- package/src/components/InboxCard.tsx +0 -152
- package/src/components/LinkPreviewCard.tsx +0 -292
- package/src/components/MarkdownCitations.tsx +0 -680
- package/src/components/MarkdownViewer.tsx +0 -111
- package/src/components/ProfileAnalysisClient.ts +0 -191
- package/src/components/ProfileAnalysisStream.tsx +0 -259
- package/src/components/ProfilePreview.tsx +0 -131
- package/src/components/SavedTimelineView.tsx +0 -155
- package/src/components/SmartTimestamp.tsx +0 -72
- package/src/components/SyncNowButton.tsx +0 -140
- package/src/components/ThemeSlider.tsx +0 -80
- package/src/components/TimelineCard.tsx +0 -533
- package/src/components/TimelineRouteFrame.tsx +0 -186
- package/src/components/TweetArticleCard.tsx +0 -66
- package/src/components/TweetMediaGrid.tsx +0 -185
- package/src/components/TweetRichText.tsx +0 -159
- package/src/components/account-selection.ts +0 -64
- package/src/components/links-controller.ts +0 -162
- package/src/components/links-model.ts +0 -198
- package/src/components/network-map-controller.ts +0 -84
- package/src/components/network-map-model.ts +0 -255
- package/src/components/useTimelineRouteData.ts +0 -193
- package/src/lib/account-sync-job.ts +0 -666
- package/src/lib/actions-transport.ts +0 -300
- package/src/lib/analysis-runtime.ts +0 -238
- package/src/lib/api-client.ts +0 -220
- package/src/lib/api-contracts.ts +0 -328
- package/src/lib/archive-finder.ts +0 -185
- package/src/lib/archive-import-plan.ts +0 -102
- package/src/lib/archive-import.ts +0 -2695
- package/src/lib/authored-live.ts +0 -1027
- package/src/lib/avatar-cache.ts +0 -349
- package/src/lib/backup.ts +0 -2652
- package/src/lib/bird-actions.ts +0 -294
- package/src/lib/bird-command.ts +0 -130
- package/src/lib/bird.ts +0 -1140
- package/src/lib/blocklist.ts +0 -136
- package/src/lib/blocks-write.ts +0 -171
- package/src/lib/blocks.ts +0 -387
- package/src/lib/bookmark-sync-job.ts +0 -536
- package/src/lib/config.ts +0 -224
- package/src/lib/conversation-surface.ts +0 -205
- package/src/lib/data-sources.ts +0 -219
- package/src/lib/database-metrics.ts +0 -88
- package/src/lib/database-migrations.ts +0 -34
- package/src/lib/database-schema.ts +0 -312
- package/src/lib/database-writer.ts +0 -69
- package/src/lib/db.ts +0 -889
- package/src/lib/dm-read-model.ts +0 -533
- package/src/lib/dms-live.ts +0 -870
- package/src/lib/effect-runtime.ts +0 -45
- package/src/lib/follow-graph.ts +0 -1087
- package/src/lib/geocoding.ts +0 -296
- package/src/lib/http-effect.ts +0 -222
- package/src/lib/identity-search-index.ts +0 -369
- package/src/lib/import-repository.ts +0 -138
- package/src/lib/inbox.ts +0 -242
- package/src/lib/link-index.ts +0 -641
- package/src/lib/link-insights.ts +0 -860
- package/src/lib/link-preview-metadata.ts +0 -754
- package/src/lib/live-sync-engine.ts +0 -209
- package/src/lib/live-transport-gateway.ts +0 -128
- package/src/lib/location.ts +0 -137
- package/src/lib/media-fetch.ts +0 -860
- package/src/lib/media-includes.ts +0 -165
- package/src/lib/mention-threads-live.ts +0 -830
- package/src/lib/mentions-export.ts +0 -147
- package/src/lib/mentions-live.ts +0 -1114
- package/src/lib/moderation-target.ts +0 -227
- package/src/lib/moderation-write.ts +0 -131
- package/src/lib/mutes-write.ts +0 -171
- package/src/lib/mutes.ts +0 -84
- package/src/lib/ndjson-stream.ts +0 -106
- package/src/lib/network-map.ts +0 -382
- package/src/lib/openai-response-runtime.ts +0 -251
- package/src/lib/openai.ts +0 -117
- package/src/lib/paginated-sync.ts +0 -93
- package/src/lib/period-digest.ts +0 -1364
- package/src/lib/present.ts +0 -118
- package/src/lib/profile-affiliation-hydration.ts +0 -228
- package/src/lib/profile-affiliations.ts +0 -262
- package/src/lib/profile-analysis.ts +0 -1198
- package/src/lib/profile-bio-entities.ts +0 -318
- package/src/lib/profile-history.ts +0 -164
- package/src/lib/profile-hydration.ts +0 -180
- package/src/lib/profile-replies.ts +0 -77
- package/src/lib/profile-resolver.ts +0 -650
- package/src/lib/queries.ts +0 -6
- package/src/lib/query-actions.ts +0 -437
- package/src/lib/query-client.tsx +0 -47
- package/src/lib/query-read-model-shared.ts +0 -52
- package/src/lib/query-read-models.ts +0 -5
- package/src/lib/query-resource.ts +0 -41
- package/src/lib/query-status.ts +0 -164
- package/src/lib/research.ts +0 -652
- package/src/lib/runtime-services.ts +0 -20
- package/src/lib/search-discussion.ts +0 -812
- package/src/lib/seed.ts +0 -613
- package/src/lib/server-runtime-services.ts +0 -30
- package/src/lib/sqlite.ts +0 -183
- package/src/lib/streaming-ingestion.ts +0 -240
- package/src/lib/sync-cache.ts +0 -70
- package/src/lib/sync-plan.ts +0 -175
- package/src/lib/theme-transition.ts +0 -113
- package/src/lib/theme.tsx +0 -165
- package/src/lib/timeline-collections-live.ts +0 -407
- package/src/lib/timeline-live.ts +0 -302
- package/src/lib/timeline-read-model.ts +0 -1191
- package/src/lib/tweet-account-edges.ts +0 -48
- package/src/lib/tweet-lookup.ts +0 -46
- package/src/lib/tweet-render.ts +0 -430
- package/src/lib/tweet-repository.ts +0 -156
- package/src/lib/tweet-search-live.ts +0 -462
- package/src/lib/types.ts +0 -723
- package/src/lib/ui.ts +0 -529
- package/src/lib/url-expansion-store.ts +0 -110
- package/src/lib/url-expansion.ts +0 -343
- package/src/lib/url-safety.ts +0 -220
- package/src/lib/web-sync.ts +0 -533
- package/src/lib/whois.ts +0 -1080
- package/src/lib/x-profile.ts +0 -365
- package/src/lib/x-web.ts +0 -199
- package/src/lib/xurl-rate-limits.ts +0 -267
- package/src/lib/xurl.ts +0 -1677
- package/src/routeTree.gen.ts +0 -723
- package/src/router.tsx +0 -20
- package/src/routes/__root.tsx +0 -82
- package/src/routes/api/action.tsx +0 -147
- package/src/routes/api/avatar.tsx +0 -50
- package/src/routes/api/blocks.tsx +0 -35
- package/src/routes/api/conversation.tsx +0 -45
- package/src/routes/api/data-sources.tsx +0 -24
- package/src/routes/api/inbox.tsx +0 -43
- package/src/routes/api/link-insights.tsx +0 -85
- package/src/routes/api/link-preview.tsx +0 -47
- package/src/routes/api/network-map.tsx +0 -55
- package/src/routes/api/period-digest.tsx +0 -101
- package/src/routes/api/profile-analysis.tsx +0 -97
- package/src/routes/api/profile-hydrate.tsx +0 -54
- package/src/routes/api/query.tsx +0 -121
- package/src/routes/api/search-discussion.tsx +0 -113
- package/src/routes/api/status.tsx +0 -30
- package/src/routes/api/sync.tsx +0 -108
- package/src/routes/api/xurl-rate-limits.tsx +0 -24
- package/src/routes/blocks.tsx +0 -343
- package/src/routes/bookmarks.tsx +0 -18
- package/src/routes/data-sources.tsx +0 -249
- package/src/routes/discuss.tsx +0 -419
- package/src/routes/dms.tsx +0 -415
- package/src/routes/inbox.tsx +0 -232
- package/src/routes/index.tsx +0 -32
- package/src/routes/likes.tsx +0 -18
- package/src/routes/links.tsx +0 -631
- package/src/routes/mentions.tsx +0 -32
- package/src/routes/network-map.tsx +0 -732
- package/src/routes/profile-analyze.tsx +0 -112
- package/src/routes/profiles.$handle.tsx +0 -228
- package/src/routes/rate-limits.tsx +0 -305
- package/src/routes/today.tsx +0 -511
- package/src/styles.css +0 -257
- package/tsconfig.json +0 -29
- package/vite.config.ts +0 -29
- /package/{public → dist/client}/birdclaw-mark.png +0 -0
- /package/{public → dist/client}/favicon.ico +0 -0
- /package/{public → dist/client}/logo192.png +0 -0
- /package/{public → dist/client}/logo512.png +0 -0
- /package/{public → dist/client}/manifest.json +0 -0
- /package/{public → dist/client}/robots.txt +0 -0
package/src/routes/links.tsx
DELETED
|
@@ -1,631 +0,0 @@
|
|
|
1
|
-
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
-
import {
|
|
3
|
-
ChevronDown,
|
|
4
|
-
ChevronUp,
|
|
5
|
-
ExternalLink,
|
|
6
|
-
MessageCircle,
|
|
7
|
-
Play,
|
|
8
|
-
Repeat2,
|
|
9
|
-
Search,
|
|
10
|
-
Users,
|
|
11
|
-
} from "lucide-react";
|
|
12
|
-
import { useState } from "react";
|
|
13
|
-
import { AvatarChip } from "#/components/AvatarChip";
|
|
14
|
-
import {
|
|
15
|
-
FeedEmpty,
|
|
16
|
-
FeedError,
|
|
17
|
-
FeedLoading,
|
|
18
|
-
LinkSkeletonRows,
|
|
19
|
-
} from "#/components/FeedState";
|
|
20
|
-
import { ProfilePreview } from "#/components/ProfilePreview";
|
|
21
|
-
import { SmartTimestamp } from "#/components/SmartTimestamp";
|
|
22
|
-
import { useLinksController } from "#/components/links-controller";
|
|
23
|
-
import { formatCompactNumber } from "#/lib/present";
|
|
24
|
-
import type {
|
|
25
|
-
LinkInsightItem,
|
|
26
|
-
LinkInsightMention,
|
|
27
|
-
ProfileRecord,
|
|
28
|
-
} from "#/lib/types";
|
|
29
|
-
import {
|
|
30
|
-
cx,
|
|
31
|
-
pageHeaderClass,
|
|
32
|
-
pageHeaderRowClass,
|
|
33
|
-
pageSubtitleClass,
|
|
34
|
-
pageTitleClass,
|
|
35
|
-
searchFieldIconClass,
|
|
36
|
-
searchFieldInputClass,
|
|
37
|
-
searchFieldShellClass,
|
|
38
|
-
secondaryButtonClass,
|
|
39
|
-
segmentActiveClass,
|
|
40
|
-
segmentClass,
|
|
41
|
-
segmentedClass,
|
|
42
|
-
} from "#/lib/ui";
|
|
43
|
-
|
|
44
|
-
export const Route = createFileRoute("/links")({
|
|
45
|
-
component: LinksRoute,
|
|
46
|
-
});
|
|
47
|
-
|
|
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";
|
|
62
|
-
|
|
63
|
-
function ProfilePill({
|
|
64
|
-
profile,
|
|
65
|
-
className,
|
|
66
|
-
}: {
|
|
67
|
-
profile: ProfileRecord | null | undefined;
|
|
68
|
-
className?: string;
|
|
69
|
-
}) {
|
|
70
|
-
if (!profile) {
|
|
71
|
-
return (
|
|
72
|
-
<span className={cx("text-[var(--ink-soft)]", className)}>unknown</span>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
return (
|
|
76
|
-
<ProfilePreview profile={profile}>
|
|
77
|
-
<span
|
|
78
|
-
className={cx("font-bold text-[var(--ink)] hover:underline", className)}
|
|
79
|
-
>
|
|
80
|
-
@{profile.handle}
|
|
81
|
-
</span>
|
|
82
|
-
</ProfilePreview>
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function SharerStrip({ item }: { item: LinkInsightItem }) {
|
|
87
|
-
const itemSharers = item.sharers ?? [];
|
|
88
|
-
const sharers =
|
|
89
|
-
itemSharers.length > 0
|
|
90
|
-
? itemSharers
|
|
91
|
-
: item.topSharer
|
|
92
|
-
? [item.topSharer]
|
|
93
|
-
: [];
|
|
94
|
-
const visible = sharers.slice(0, 5);
|
|
95
|
-
const hidden = Math.max(0, item.uniqueSharers - visible.length);
|
|
96
|
-
|
|
97
|
-
if (visible.length === 0) {
|
|
98
|
-
return <span>top unknown</span>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<div className="group/sharers relative z-10 inline-flex items-center gap-1 hover:z-50 focus-within:z-50">
|
|
103
|
-
<div className="flex -space-x-2">
|
|
104
|
-
{visible.slice(0, 4).map((profile) => (
|
|
105
|
-
<a
|
|
106
|
-
aria-label={`Open @${profile.handle}`}
|
|
107
|
-
className="inline-flex rounded-full bg-[var(--bg)] ring-2 ring-[var(--bg)]"
|
|
108
|
-
href={`https://x.com/${profile.handle}`}
|
|
109
|
-
key={profile.id}
|
|
110
|
-
rel="noreferrer"
|
|
111
|
-
target="_blank"
|
|
112
|
-
>
|
|
113
|
-
<AvatarChip
|
|
114
|
-
avatarUrl={profile.avatarUrl}
|
|
115
|
-
hue={profile.avatarHue}
|
|
116
|
-
name={profile.displayName}
|
|
117
|
-
profileId={profile.id}
|
|
118
|
-
size="small"
|
|
119
|
-
/>
|
|
120
|
-
</a>
|
|
121
|
-
))}
|
|
122
|
-
</div>
|
|
123
|
-
<span className="inline-flex items-center gap-1 text-[13px] text-[var(--ink-soft)]">
|
|
124
|
-
<span>top</span>
|
|
125
|
-
<a
|
|
126
|
-
className="font-medium text-[var(--ink-soft)] hover:underline"
|
|
127
|
-
href={`https://x.com/${visible[0]?.handle ?? ""}`}
|
|
128
|
-
rel="noreferrer"
|
|
129
|
-
target="_blank"
|
|
130
|
-
>
|
|
131
|
-
@{visible[0]?.handle}
|
|
132
|
-
</a>
|
|
133
|
-
</span>
|
|
134
|
-
{hidden > 0 ? (
|
|
135
|
-
<span className="text-[13px] text-[var(--ink-soft)]">
|
|
136
|
-
+{formatCompactNumber(hidden)}
|
|
137
|
-
</span>
|
|
138
|
-
) : null}
|
|
139
|
-
<div className="pointer-events-none absolute left-0 top-[calc(100%+8px)] z-[80] grid w-[280px] translate-y-1 gap-2 rounded-2xl border border-[var(--line)] bg-[var(--bg-elevated)] p-3 opacity-0 shadow-[0_8px_28px_var(--shadow-strong)] transition-all duration-150 group-hover/sharers:pointer-events-auto group-hover/sharers:translate-y-0 group-hover/sharers:opacity-100 group-focus-within/sharers:pointer-events-auto group-focus-within/sharers:translate-y-0 group-focus-within/sharers:opacity-100">
|
|
140
|
-
<div className="flex items-center gap-2 text-[13px] font-bold text-[var(--ink)]">
|
|
141
|
-
<Users className="size-4" />
|
|
142
|
-
<span>{formatCompactNumber(item.uniqueSharers)} sharers</span>
|
|
143
|
-
</div>
|
|
144
|
-
<div className="grid gap-2">
|
|
145
|
-
{visible.map((profile) => (
|
|
146
|
-
<a
|
|
147
|
-
className="flex min-w-0 items-center gap-2 rounded-xl p-1 transition-colors hover:bg-[var(--bg-hover)]"
|
|
148
|
-
href={`https://x.com/${profile.handle}`}
|
|
149
|
-
key={profile.id}
|
|
150
|
-
rel="noreferrer"
|
|
151
|
-
target="_blank"
|
|
152
|
-
>
|
|
153
|
-
<AvatarChip
|
|
154
|
-
avatarUrl={profile.avatarUrl}
|
|
155
|
-
hue={profile.avatarHue}
|
|
156
|
-
name={profile.displayName}
|
|
157
|
-
profileId={profile.id}
|
|
158
|
-
size="small"
|
|
159
|
-
/>
|
|
160
|
-
<span className="min-w-0">
|
|
161
|
-
<span className="block truncate text-[14px] font-bold text-[var(--ink)]">
|
|
162
|
-
{profile.displayName}
|
|
163
|
-
</span>
|
|
164
|
-
<span className="block truncate text-[13px] text-[var(--ink-soft)]">
|
|
165
|
-
@{profile.handle} /{" "}
|
|
166
|
-
{formatCompactNumber(profile.followersCount)} followers
|
|
167
|
-
</span>
|
|
168
|
-
</span>
|
|
169
|
-
</a>
|
|
170
|
-
))}
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function SourceActions({
|
|
178
|
-
item,
|
|
179
|
-
mention,
|
|
180
|
-
}: {
|
|
181
|
-
item: LinkInsightItem;
|
|
182
|
-
mention: LinkInsightMention;
|
|
183
|
-
}) {
|
|
184
|
-
const links = [
|
|
185
|
-
mention.sourceUrl
|
|
186
|
-
? {
|
|
187
|
-
href: mention.sourceUrl,
|
|
188
|
-
label: mention.sourceKind === "dm" ? "Open DM" : "Open tweet",
|
|
189
|
-
}
|
|
190
|
-
: null,
|
|
191
|
-
mention.contentTweetUrl
|
|
192
|
-
? { href: mention.contentTweetUrl, label: "Open source tweet" }
|
|
193
|
-
: null,
|
|
194
|
-
{ href: item.url, label: "Open link" },
|
|
195
|
-
].filter(Boolean) as Array<{ href: string; label: string }>;
|
|
196
|
-
|
|
197
|
-
return (
|
|
198
|
-
<div className="flex flex-wrap items-center gap-3 text-[13px]">
|
|
199
|
-
{links.map((link) => (
|
|
200
|
-
<a
|
|
201
|
-
key={`${link.label}:${link.href}`}
|
|
202
|
-
className="inline-flex items-center gap-1 font-semibold text-[var(--accent)] hover:underline"
|
|
203
|
-
href={link.href}
|
|
204
|
-
rel="noreferrer"
|
|
205
|
-
target="_blank"
|
|
206
|
-
>
|
|
207
|
-
<span>{link.label}</span>
|
|
208
|
-
<ExternalLink className="size-3.5" />
|
|
209
|
-
</a>
|
|
210
|
-
))}
|
|
211
|
-
</div>
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function MentionCard({
|
|
216
|
-
item,
|
|
217
|
-
mention,
|
|
218
|
-
}: {
|
|
219
|
-
item: LinkInsightItem;
|
|
220
|
-
mention: LinkInsightMention;
|
|
221
|
-
}) {
|
|
222
|
-
const image = mediaImage(mention.media);
|
|
223
|
-
const href = mentionHref(mention, item);
|
|
224
|
-
const contentAuthor =
|
|
225
|
-
mention.contentAuthor &&
|
|
226
|
-
!isSameProfile(mention.sharedBy, mention.contentAuthor)
|
|
227
|
-
? mention.contentAuthor
|
|
228
|
-
: null;
|
|
229
|
-
|
|
230
|
-
return (
|
|
231
|
-
<article className="grid gap-2 rounded-2xl border border-[var(--line)] bg-[var(--bg)] px-3 py-2.5">
|
|
232
|
-
<div className="flex min-w-0 items-center gap-2 text-[13px] text-[var(--ink-soft)]">
|
|
233
|
-
<ProfilePill profile={mention.sharedBy} />
|
|
234
|
-
<span>{mention.sourceLabel ?? mention.sourceKind}</span>
|
|
235
|
-
<span>/</span>
|
|
236
|
-
<a
|
|
237
|
-
className="hover:underline"
|
|
238
|
-
href={href}
|
|
239
|
-
rel="noreferrer"
|
|
240
|
-
target="_blank"
|
|
241
|
-
>
|
|
242
|
-
<SmartTimestamp value={mention.createdAt} />
|
|
243
|
-
</a>
|
|
244
|
-
</div>
|
|
245
|
-
<p className="m-0 whitespace-pre-wrap text-[14px] leading-[1.45] text-[var(--ink)] [overflow-wrap:anywhere]">
|
|
246
|
-
{mentionCopy(mention)}
|
|
247
|
-
</p>
|
|
248
|
-
{contentAuthor ? (
|
|
249
|
-
<div className="text-[13px] text-[var(--ink-soft)]">
|
|
250
|
-
source by <ProfilePill profile={contentAuthor} />
|
|
251
|
-
</div>
|
|
252
|
-
) : null}
|
|
253
|
-
{image ? (
|
|
254
|
-
<a
|
|
255
|
-
className="block max-w-[220px] overflow-hidden rounded-2xl border border-[var(--line)] bg-[var(--bg-active)]"
|
|
256
|
-
href={href}
|
|
257
|
-
rel="noreferrer"
|
|
258
|
-
target="_blank"
|
|
259
|
-
>
|
|
260
|
-
<img
|
|
261
|
-
alt={image.altText || "Mention media"}
|
|
262
|
-
className="aspect-video h-auto w-full object-cover"
|
|
263
|
-
src={image.thumbnailUrl || image.url}
|
|
264
|
-
/>
|
|
265
|
-
</a>
|
|
266
|
-
) : null}
|
|
267
|
-
<SourceActions item={item} mention={mention} />
|
|
268
|
-
</article>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function PureShareCluster({
|
|
273
|
-
item,
|
|
274
|
-
mentions,
|
|
275
|
-
}: {
|
|
276
|
-
item: LinkInsightItem;
|
|
277
|
-
mentions: LinkInsightMention[];
|
|
278
|
-
}) {
|
|
279
|
-
const profiles = mentions
|
|
280
|
-
.map((mention) => mention.sharedBy)
|
|
281
|
-
.filter(Boolean) as ProfileRecord[];
|
|
282
|
-
const visible = profiles.slice(0, 6);
|
|
283
|
-
const firstSource = mentions.find((mention) => mention.sourceUrl)?.sourceUrl;
|
|
284
|
-
|
|
285
|
-
return (
|
|
286
|
-
<div className="flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-[var(--line)] bg-[var(--bg-hover)] px-3 py-2.5">
|
|
287
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
288
|
-
<div className="grid size-8 shrink-0 place-items-center rounded-full bg-[var(--bg-active)] text-[var(--ink-soft)]">
|
|
289
|
-
<Repeat2 className="size-4" />
|
|
290
|
-
</div>
|
|
291
|
-
<div className="min-w-0">
|
|
292
|
-
<div className="text-[14px] font-bold text-[var(--ink)]">
|
|
293
|
-
{formatCompactNumber(pureShareCount(item))} shares without comment
|
|
294
|
-
</div>
|
|
295
|
-
<div className="flex min-w-0 flex-wrap items-center gap-1 text-[13px] text-[var(--ink-soft)]">
|
|
296
|
-
{visible.map((profile) => (
|
|
297
|
-
<ProfilePill key={profile.id} profile={profile} />
|
|
298
|
-
))}
|
|
299
|
-
{pureShareCount(item) > visible.length ? (
|
|
300
|
-
<span>
|
|
301
|
-
+{formatCompactNumber(pureShareCount(item) - visible.length)}{" "}
|
|
302
|
-
more
|
|
303
|
-
</span>
|
|
304
|
-
) : null}
|
|
305
|
-
</div>
|
|
306
|
-
</div>
|
|
307
|
-
</div>
|
|
308
|
-
{firstSource ? (
|
|
309
|
-
<a
|
|
310
|
-
className="inline-flex items-center gap-1 text-[13px] font-semibold text-[var(--accent)] hover:underline"
|
|
311
|
-
href={firstSource}
|
|
312
|
-
rel="noreferrer"
|
|
313
|
-
target="_blank"
|
|
314
|
-
>
|
|
315
|
-
<span>Open first source</span>
|
|
316
|
-
<ExternalLink className="size-3.5" />
|
|
317
|
-
</a>
|
|
318
|
-
) : null}
|
|
319
|
-
</div>
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function CommentsSection({ item }: { item: LinkInsightItem }) {
|
|
324
|
-
const [expanded, setExpanded] = useState(false);
|
|
325
|
-
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COMMENTS);
|
|
326
|
-
const comments = item.mentions.filter((mention) => mention.hasComment);
|
|
327
|
-
const pureShares = item.mentions.filter(
|
|
328
|
-
(mention) => mention.isPureShare || !mention.hasComment,
|
|
329
|
-
);
|
|
330
|
-
const itemCommentCount = commentCount(item);
|
|
331
|
-
const itemPureShareCount = pureShareCount(item);
|
|
332
|
-
const totalDiscussion = itemCommentCount + itemPureShareCount;
|
|
333
|
-
const visibleComments = comments.slice(0, visibleCount);
|
|
334
|
-
const remainingLocalComments = Math.max(0, comments.length - visibleCount);
|
|
335
|
-
const remainingComments = Math.max(
|
|
336
|
-
0,
|
|
337
|
-
itemCommentCount - visibleComments.length,
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
if (totalDiscussion === 0) {
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return (
|
|
345
|
-
<div className="mt-2 grid gap-2 border-t border-[var(--line)] pt-2">
|
|
346
|
-
<button
|
|
347
|
-
className="inline-flex w-fit items-center gap-1 rounded-full border-0 bg-transparent px-2 py-1 text-[13px] font-bold text-[var(--accent)] transition-colors hover:bg-[var(--accent-soft)]"
|
|
348
|
-
onClick={() => setExpanded((value) => !value)}
|
|
349
|
-
type="button"
|
|
350
|
-
>
|
|
351
|
-
<MessageCircle className="size-4" />
|
|
352
|
-
<span>
|
|
353
|
-
{expanded ? "Hide" : "Show"} {formatCompactNumber(itemCommentCount)}{" "}
|
|
354
|
-
comments
|
|
355
|
-
{itemPureShareCount > 0
|
|
356
|
-
? ` / ${formatCompactNumber(itemPureShareCount)} shares`
|
|
357
|
-
: ""}
|
|
358
|
-
</span>
|
|
359
|
-
{expanded ? (
|
|
360
|
-
<ChevronUp className="size-4" />
|
|
361
|
-
) : (
|
|
362
|
-
<ChevronDown className="size-4" />
|
|
363
|
-
)}
|
|
364
|
-
</button>
|
|
365
|
-
{expanded ? (
|
|
366
|
-
<div className="grid gap-2">
|
|
367
|
-
{visibleComments.map((mention) => (
|
|
368
|
-
<MentionCard key={mention.id} item={item} mention={mention} />
|
|
369
|
-
))}
|
|
370
|
-
{remainingLocalComments > 0 ? (
|
|
371
|
-
<button
|
|
372
|
-
className={cx(secondaryButtonClass, "w-fit")}
|
|
373
|
-
onClick={() =>
|
|
374
|
-
setVisibleCount((value) => value + MORE_COMMENTS_BATCH)
|
|
375
|
-
}
|
|
376
|
-
type="button"
|
|
377
|
-
>
|
|
378
|
-
Show{" "}
|
|
379
|
-
{formatCompactNumber(
|
|
380
|
-
Math.min(MORE_COMMENTS_BATCH, remainingLocalComments),
|
|
381
|
-
)}{" "}
|
|
382
|
-
more
|
|
383
|
-
</button>
|
|
384
|
-
) : null}
|
|
385
|
-
{remainingComments > 0 && remainingLocalComments === 0 ? (
|
|
386
|
-
<div className="rounded-2xl border border-[var(--line)] bg-[var(--bg-hover)] px-3 py-2 text-[13px] text-[var(--ink-soft)]">
|
|
387
|
-
{formatCompactNumber(remainingComments)} more mentions in archive.
|
|
388
|
-
Narrow the range or raise the API limit to inspect them.
|
|
389
|
-
</div>
|
|
390
|
-
) : null}
|
|
391
|
-
{itemPureShareCount > 0 ? (
|
|
392
|
-
<PureShareCluster item={item} mentions={pureShares} />
|
|
393
|
-
) : null}
|
|
394
|
-
</div>
|
|
395
|
-
) : null}
|
|
396
|
-
</div>
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function VideoPreview({ item }: { item: LinkInsightItem }) {
|
|
401
|
-
if (item.kind !== "videos") {
|
|
402
|
-
return null;
|
|
403
|
-
}
|
|
404
|
-
const image = mediaImage(item.mentions.flatMap((mention) => mention.media));
|
|
405
|
-
const thumbnailUrl =
|
|
406
|
-
image?.thumbnailUrl || image?.url || youtubeThumbnailUrl(item.url);
|
|
407
|
-
|
|
408
|
-
return (
|
|
409
|
-
<a
|
|
410
|
-
className="mt-2 flex max-w-md items-center gap-3 overflow-hidden rounded-2xl border border-[var(--line)] bg-[var(--bg-hover)] transition-colors hover:bg-[var(--bg-active)]"
|
|
411
|
-
href={item.url}
|
|
412
|
-
rel="noreferrer"
|
|
413
|
-
target="_blank"
|
|
414
|
-
>
|
|
415
|
-
<div className="relative grid aspect-video w-36 shrink-0 place-items-center overflow-hidden bg-[var(--bg-active)] text-[var(--ink-soft)]">
|
|
416
|
-
{thumbnailUrl ? (
|
|
417
|
-
<img
|
|
418
|
-
alt={image?.altText || itemTitle(item)}
|
|
419
|
-
className="h-full w-full object-cover transition-transform duration-150 hover:scale-105"
|
|
420
|
-
src={thumbnailUrl}
|
|
421
|
-
/>
|
|
422
|
-
) : (
|
|
423
|
-
<Play className="size-6" />
|
|
424
|
-
)}
|
|
425
|
-
<div className="absolute inset-0 grid place-items-center bg-black/20">
|
|
426
|
-
<span className="grid size-9 place-items-center rounded-full bg-black/70 text-white shadow-[0_4px_18px_rgba(0,0,0,0.35)]">
|
|
427
|
-
<Play className="ml-0.5 size-4 fill-current" />
|
|
428
|
-
</span>
|
|
429
|
-
</div>
|
|
430
|
-
</div>
|
|
431
|
-
<div className="min-w-0 py-2 pr-3">
|
|
432
|
-
<div className="truncate text-[13px] font-bold text-[var(--ink)]">
|
|
433
|
-
{itemTitle(item)}
|
|
434
|
-
</div>
|
|
435
|
-
<div className="truncate text-[13px] text-[var(--ink-soft)]">
|
|
436
|
-
{item.displayUrl}
|
|
437
|
-
</div>
|
|
438
|
-
</div>
|
|
439
|
-
</a>
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function LinkInsightRow({
|
|
444
|
-
index,
|
|
445
|
-
item,
|
|
446
|
-
}: {
|
|
447
|
-
index: number;
|
|
448
|
-
item: LinkInsightItem;
|
|
449
|
-
}) {
|
|
450
|
-
return (
|
|
451
|
-
<article
|
|
452
|
-
className="flex flex-col gap-2 border-b border-[var(--line)] px-4 py-3 transition-colors duration-150 hover:bg-[var(--bg-hover)]"
|
|
453
|
-
data-perf="link-insight-row"
|
|
454
|
-
>
|
|
455
|
-
<div className="flex items-start gap-3">
|
|
456
|
-
<div className="grid size-9 shrink-0 place-items-center rounded-full bg-[var(--bg-active)] text-[14px] font-bold text-[var(--ink-soft)]">
|
|
457
|
-
{index + 1}
|
|
458
|
-
</div>
|
|
459
|
-
<div className="min-w-0 flex-1">
|
|
460
|
-
<a
|
|
461
|
-
className="block break-words text-[15px] font-bold text-[var(--ink)] [overflow-wrap:anywhere] hover:underline"
|
|
462
|
-
href={item.url}
|
|
463
|
-
rel="noreferrer"
|
|
464
|
-
target="_blank"
|
|
465
|
-
>
|
|
466
|
-
{itemTitle(item)}
|
|
467
|
-
</a>
|
|
468
|
-
<p className="mt-0.5 truncate text-[13px] text-[var(--ink-soft)]">
|
|
469
|
-
{itemSubtitle(item)}
|
|
470
|
-
</p>
|
|
471
|
-
<div className="mt-1 flex flex-wrap items-center gap-2 text-[13px] text-[var(--ink-soft)]">
|
|
472
|
-
<span>{item.host}</span>
|
|
473
|
-
<span>/</span>
|
|
474
|
-
<span>{formatCompactNumber(item.shareCount)} shares</span>
|
|
475
|
-
<span>/</span>
|
|
476
|
-
<span>{formatCompactNumber(item.uniqueSharers)} sharers</span>
|
|
477
|
-
<span>/</span>
|
|
478
|
-
<span>{formatCompactNumber(commentCount(item))} comments</span>
|
|
479
|
-
<span>/</span>
|
|
480
|
-
<SharerStrip item={item} />
|
|
481
|
-
<span>/</span>
|
|
482
|
-
<SmartTimestamp value={item.lastSeenAt} />
|
|
483
|
-
</div>
|
|
484
|
-
<VideoPreview item={item} />
|
|
485
|
-
</div>
|
|
486
|
-
</div>
|
|
487
|
-
<CommentsSection item={item} />
|
|
488
|
-
</article>
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function LinksRoute() {
|
|
493
|
-
const {
|
|
494
|
-
kind,
|
|
495
|
-
setKind,
|
|
496
|
-
range,
|
|
497
|
-
setRange,
|
|
498
|
-
source,
|
|
499
|
-
setSource,
|
|
500
|
-
sort,
|
|
501
|
-
setSort,
|
|
502
|
-
search,
|
|
503
|
-
setSearch,
|
|
504
|
-
items,
|
|
505
|
-
subtitle,
|
|
506
|
-
loading,
|
|
507
|
-
error,
|
|
508
|
-
retry,
|
|
509
|
-
} = useLinksController();
|
|
510
|
-
|
|
511
|
-
return (
|
|
512
|
-
<>
|
|
513
|
-
<header className={pageHeaderClass}>
|
|
514
|
-
<div className={pageHeaderRowClass}>
|
|
515
|
-
<div className="flex min-w-0 flex-col">
|
|
516
|
-
<h1 className={pageTitleClass}>Links</h1>
|
|
517
|
-
<p className={pageSubtitleClass}>{subtitle}</p>
|
|
518
|
-
</div>
|
|
519
|
-
</div>
|
|
520
|
-
<div className="flex flex-wrap items-center gap-2 px-4 pb-3">
|
|
521
|
-
<label className={cx(searchFieldShellClass, "min-w-[180px] flex-1")}>
|
|
522
|
-
<Search className={searchFieldIconClass} strokeWidth={2} />
|
|
523
|
-
<input
|
|
524
|
-
className={searchFieldInputClass}
|
|
525
|
-
onChange={(event) => setSearch(event.target.value)}
|
|
526
|
-
placeholder="Search links"
|
|
527
|
-
value={search}
|
|
528
|
-
/>
|
|
529
|
-
</label>
|
|
530
|
-
<div className={segmentedClass}>
|
|
531
|
-
{(["links", "videos"] as const).map((value) => (
|
|
532
|
-
<button
|
|
533
|
-
key={value}
|
|
534
|
-
className={cx(
|
|
535
|
-
segmentClass,
|
|
536
|
-
kind === value && segmentActiveClass,
|
|
537
|
-
)}
|
|
538
|
-
onClick={() => setKind(value)}
|
|
539
|
-
type="button"
|
|
540
|
-
>
|
|
541
|
-
{value}
|
|
542
|
-
</button>
|
|
543
|
-
))}
|
|
544
|
-
</div>
|
|
545
|
-
<div className={segmentedClass}>
|
|
546
|
-
{ranges.map((entry) => (
|
|
547
|
-
<button
|
|
548
|
-
key={entry.value}
|
|
549
|
-
className={cx(
|
|
550
|
-
segmentClass,
|
|
551
|
-
range === entry.value && segmentActiveClass,
|
|
552
|
-
)}
|
|
553
|
-
onClick={() => setRange(entry.value)}
|
|
554
|
-
type="button"
|
|
555
|
-
>
|
|
556
|
-
{entry.label}
|
|
557
|
-
</button>
|
|
558
|
-
))}
|
|
559
|
-
</div>
|
|
560
|
-
<div className={segmentedClass}>
|
|
561
|
-
{(["all", "tweet", "dm"] as const).map((value) => (
|
|
562
|
-
<button
|
|
563
|
-
key={value}
|
|
564
|
-
className={cx(
|
|
565
|
-
segmentClass,
|
|
566
|
-
source === value && segmentActiveClass,
|
|
567
|
-
)}
|
|
568
|
-
onClick={() => setSource(value)}
|
|
569
|
-
type="button"
|
|
570
|
-
>
|
|
571
|
-
{value}
|
|
572
|
-
</button>
|
|
573
|
-
))}
|
|
574
|
-
</div>
|
|
575
|
-
<div className={segmentedClass}>
|
|
576
|
-
{(["rank", "recent", "comments"] as const).map((value) => (
|
|
577
|
-
<button
|
|
578
|
-
key={value}
|
|
579
|
-
className={cx(
|
|
580
|
-
segmentClass,
|
|
581
|
-
sort === value && segmentActiveClass,
|
|
582
|
-
)}
|
|
583
|
-
onClick={() => setSort(value)}
|
|
584
|
-
type="button"
|
|
585
|
-
>
|
|
586
|
-
{value}
|
|
587
|
-
</button>
|
|
588
|
-
))}
|
|
589
|
-
</div>
|
|
590
|
-
</div>
|
|
591
|
-
</header>
|
|
592
|
-
|
|
593
|
-
<section className="flex flex-col">
|
|
594
|
-
{loading ? (
|
|
595
|
-
<FeedLoading
|
|
596
|
-
detail="Ranking URLs and collecting local discussion"
|
|
597
|
-
label="Loading links"
|
|
598
|
-
>
|
|
599
|
-
<LinkSkeletonRows />
|
|
600
|
-
</FeedLoading>
|
|
601
|
-
) : error ? (
|
|
602
|
-
<FeedError
|
|
603
|
-
action={
|
|
604
|
-
<button
|
|
605
|
-
className="rounded-full bg-[var(--accent)] px-4 py-1.5 text-[14px] font-bold text-white"
|
|
606
|
-
onClick={() => void retry()}
|
|
607
|
-
type="button"
|
|
608
|
-
>
|
|
609
|
-
Retry
|
|
610
|
-
</button>
|
|
611
|
-
}
|
|
612
|
-
message={error}
|
|
613
|
-
title="Could not load links"
|
|
614
|
-
/>
|
|
615
|
-
) : items.length === 0 ? (
|
|
616
|
-
<FeedEmpty
|
|
617
|
-
detail="Try a different range, source, or search term."
|
|
618
|
-
label="No links in this window"
|
|
619
|
-
/>
|
|
620
|
-
) : null}
|
|
621
|
-
{items.map((item, index) => (
|
|
622
|
-
<LinkInsightRow
|
|
623
|
-
index={index}
|
|
624
|
-
item={item}
|
|
625
|
-
key={`${kind}:${range}:${source}:${item.id}`}
|
|
626
|
-
/>
|
|
627
|
-
))}
|
|
628
|
-
</section>
|
|
629
|
-
</>
|
|
630
|
-
);
|
|
631
|
-
}
|
package/src/routes/mentions.tsx
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
-
import { TimelineRouteFrame } from "#/components/TimelineRouteFrame";
|
|
3
|
-
import type { QueryEnvelope } from "#/lib/types";
|
|
4
|
-
|
|
5
|
-
export const Route = createFileRoute("/mentions")({
|
|
6
|
-
component: MentionsRoute,
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
function mentionsSubtitle(meta: QueryEnvelope | null) {
|
|
10
|
-
if (!meta) return "Loading mentions...";
|
|
11
|
-
return `${String(meta.stats.mentions)} mention/reply items in local store`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function MentionsRoute() {
|
|
15
|
-
return (
|
|
16
|
-
<TimelineRouteFrame
|
|
17
|
-
emptyDetail="Try All, search less narrowly, or sync mentions."
|
|
18
|
-
emptyLabel="No mentions in this view"
|
|
19
|
-
errorFallback="Mentions unavailable"
|
|
20
|
-
errorTitle="Could not load mentions"
|
|
21
|
-
initialReplyFilter="unreplied"
|
|
22
|
-
loadingDetail="Checking local mentions and reply context"
|
|
23
|
-
loadingLabel="Loading mentions"
|
|
24
|
-
resource="mentions"
|
|
25
|
-
searchPlaceholder="Search mentions"
|
|
26
|
-
subtitle={mentionsSubtitle}
|
|
27
|
-
syncKind="mentions"
|
|
28
|
-
syncLabel="Sync mentions"
|
|
29
|
-
title="Mentions"
|
|
30
|
-
/>
|
|
31
|
-
);
|
|
32
|
-
}
|