birdclaw 0.8.3 → 0.8.5

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.
Files changed (284) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +10 -8
  3. package/bin/birdclaw.mjs +4 -66
  4. package/dist/cli/birdclaw.js +28164 -0
  5. package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
  6. package/dist/client/assets/ProfilePreview-DQX6_xc3.js +1 -0
  7. package/dist/client/assets/SavedTimelineView-BhSiVyR1.js +1 -0
  8. package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
  9. package/dist/client/assets/TimelineRouteFrame-9NDLnXzH.js +1 -0
  10. package/dist/client/assets/blocks-CFp_njvG.js +1 -0
  11. package/dist/client/assets/bookmarks-LbYOsoKf.js +1 -0
  12. package/dist/client/assets/chunk-C0phanGV.js +1 -0
  13. package/dist/client/assets/circle-check-C_Vq3ElD.js +1 -0
  14. package/dist/client/assets/client-stream-contracts-sR4xMQxY.js +5 -0
  15. package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
  16. package/dist/client/assets/data-sources-euTr6T_L.js +1 -0
  17. package/dist/client/assets/discuss-BWKu_p7B.js +1 -0
  18. package/dist/client/assets/dms-SLXMtC4v.js +1 -0
  19. package/dist/client/assets/inbox-BybSI5Vf.js +1 -0
  20. package/dist/client/assets/index-DZVisyna.js +31 -0
  21. package/dist/client/assets/index-bpFrQ5yG.css +1 -0
  22. package/dist/client/assets/likes-dyh9p8y_.js +1 -0
  23. package/dist/client/assets/links-CIwqK_dX.js +1 -0
  24. package/dist/client/assets/mapbox-B4zqX7r0.js +2 -0
  25. package/dist/client/assets/mapbox-gl-DYNcLcll.js +3215 -0
  26. package/dist/client/assets/mentions-BoVVAIT7.js +1 -0
  27. package/dist/client/assets/network-map-zRysIY_b.js +2 -0
  28. package/dist/client/assets/profile-analyze-C_yDeX6-.js +1 -0
  29. package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
  30. package/dist/client/assets/profiles._handle-DdIVPqU-.js +1 -0
  31. package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
  32. package/dist/client/assets/rate-limits-BWPmPOSh.js +1 -0
  33. package/dist/client/assets/react-dom-DHl6Ug7F.js +1 -0
  34. package/dist/client/assets/react-nBylq1U5.js +1 -0
  35. package/dist/client/assets/repeat-2-xCwHM9hD.js +1 -0
  36. package/dist/client/assets/routes-CiLuYpZr.js +1 -0
  37. package/dist/client/assets/schemas-I9r6F6ya.js +64 -0
  38. package/dist/client/assets/search-Dq5kYsxA.js +1 -0
  39. package/dist/client/assets/sparkles-CQVlvHsZ.js +1 -0
  40. package/dist/client/assets/styles-B8s3tt3r.css +2 -0
  41. package/dist/client/assets/today-BoO64YoY.js +1 -0
  42. package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
  43. package/dist/client/assets/useTimelineRouteData-BaO3Rna1.js +1 -0
  44. package/dist/client/assets/users-CHbKlqUr.js +1 -0
  45. package/dist/server/assets/AvatarChip-zUDAEmjL.js +105 -0
  46. package/dist/server/assets/FeedState-ntW0Q2GB.js +214 -0
  47. package/dist/server/assets/ProfileAnalysisStream-L7bs6IHH.js +198 -0
  48. package/dist/server/assets/ProfilePreview-Cg-AOHBE.js +751 -0
  49. package/dist/server/assets/SavedTimelineView-BtCq1dHk.js +64 -0
  50. package/dist/server/assets/SmartTimestamp-DfwE1NGf.js +45 -0
  51. package/dist/server/assets/SyncNowButton-BOn_IBXS.js +76 -0
  52. package/dist/server/assets/TimelineRouteFrame-CjMIh63G.js +85 -0
  53. package/dist/server/assets/TweetRichText-DDywESRF.js +74 -0
  54. package/dist/server/assets/_tanstack-start-manifest_v-aLWUdbAc.js +148 -0
  55. package/dist/server/assets/account-selection-CTdFGWRk.js +44 -0
  56. package/dist/server/assets/blocks-C1mgGTB3.js +299 -0
  57. package/dist/server/assets/blocks-DSgQghqP.js +17 -0
  58. package/dist/server/assets/bookmarks-D_YkmCK3.js +14 -0
  59. package/dist/server/assets/client-stream-contracts-Bq6kMmhh.js +769 -0
  60. package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
  61. package/dist/server/assets/discuss-C1QMBBSk.js +338 -0
  62. package/dist/server/assets/discuss-DAq6ehL9.js +15 -0
  63. package/dist/server/assets/dms-BuLj4bp8.js +368 -0
  64. package/dist/server/assets/dms-DAyQrVM2.js +240 -0
  65. package/dist/server/assets/empty-plugin-adapters-DzrEkdBF.js +5 -0
  66. package/dist/server/assets/inbox-CkGOaW1q.js +214 -0
  67. package/dist/server/assets/inbox-DpWd1EzC.js +146 -0
  68. package/dist/server/assets/likes-BMm2JWYI.js +14 -0
  69. package/dist/server/assets/links-Da4tjWtI.js +468 -0
  70. package/dist/server/assets/links-dqXb35YM.js +240 -0
  71. package/dist/server/assets/mentions-CEyEcOFw.js +26 -0
  72. package/dist/server/assets/network-map-D3CzRcC4.js +224 -0
  73. package/dist/server/assets/network-map-D5DoLOwR.js +582 -0
  74. package/dist/server/assets/profile-analyze-CGCj4VFd.js +90 -0
  75. package/dist/server/assets/profile-analyze-DH1TSBkT.js +9 -0
  76. package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
  77. package/dist/server/assets/profiles._handle-B5V-huoo.js +12 -0
  78. package/dist/server/assets/profiles._handle-CJ6rgzt8.js +184 -0
  79. package/dist/server/assets/query-client-BdauHBgd.js +923 -0
  80. package/dist/server/assets/rate-limits-VAr2qzYD.js +262 -0
  81. package/dist/server/assets/route-search-BUlQ5lGM.js +117 -0
  82. package/dist/server/assets/router-C4cz9ypd.js +17026 -0
  83. package/dist/server/assets/routes-B9aikOdB.js +26 -0
  84. package/dist/server/assets/start-FaDOTmto.js +4 -0
  85. package/dist/server/assets/today-B_TA16QG.js +303 -0
  86. package/dist/server/assets/today-JkxZXdBt.js +17 -0
  87. package/dist/server/assets/ui-qbmcMSSX.js +137 -0
  88. package/{src/components/useDebouncedValue.ts → dist/server/assets/useDebouncedValue-DOnA6Xcy.js} +4 -4
  89. package/dist/server/assets/useTimelineRouteData-BLXvMGb2.js +1101 -0
  90. package/dist/server/server.js +5520 -0
  91. package/package.json +26 -34
  92. package/playwright.config.ts +0 -35
  93. package/scripts/browser-perf.mjs +0 -427
  94. package/scripts/build-docs-site.mjs +0 -967
  95. package/scripts/docs-site-assets.mjs +0 -311
  96. package/scripts/run-vitest.mjs +0 -21
  97. package/scripts/sanitize-node-options.mjs +0 -23
  98. package/scripts/start-test-server.mjs +0 -42
  99. package/src/cli/command-context.ts +0 -17
  100. package/src/cli/register-analysis.ts +0 -500
  101. package/src/cli/register-compose.ts +0 -40
  102. package/src/cli/register-graph.ts +0 -132
  103. package/src/cli/register-inbox.ts +0 -41
  104. package/src/cli/register-storage.ts +0 -106
  105. package/src/cli-moderation.ts +0 -210
  106. package/src/cli.ts +0 -1616
  107. package/src/components/AccountSwitcher.tsx +0 -178
  108. package/src/components/AppNav.tsx +0 -118
  109. package/src/components/AvatarChip.tsx +0 -58
  110. package/src/components/AvatarPreload.ts +0 -149
  111. package/src/components/BrandMark.tsx +0 -67
  112. package/src/components/ConversationThread.tsx +0 -131
  113. package/src/components/DmWorkspace.tsx +0 -286
  114. package/src/components/EmbeddedTweetCard.tsx +0 -55
  115. package/src/components/FeedState.tsx +0 -147
  116. package/src/components/FloatingPreview.tsx +0 -382
  117. package/src/components/InboxCard.tsx +0 -152
  118. package/src/components/LinkPreviewCard.tsx +0 -292
  119. package/src/components/MarkdownCitations.tsx +0 -680
  120. package/src/components/MarkdownViewer.tsx +0 -111
  121. package/src/components/ProfileAnalysisClient.ts +0 -191
  122. package/src/components/ProfileAnalysisStream.tsx +0 -259
  123. package/src/components/ProfilePreview.tsx +0 -131
  124. package/src/components/SavedTimelineView.tsx +0 -155
  125. package/src/components/SmartTimestamp.tsx +0 -72
  126. package/src/components/SyncNowButton.tsx +0 -140
  127. package/src/components/ThemeSlider.tsx +0 -80
  128. package/src/components/TimelineCard.tsx +0 -533
  129. package/src/components/TimelineRouteFrame.tsx +0 -186
  130. package/src/components/TweetArticleCard.tsx +0 -66
  131. package/src/components/TweetMediaGrid.tsx +0 -185
  132. package/src/components/TweetRichText.tsx +0 -159
  133. package/src/components/account-selection.ts +0 -64
  134. package/src/components/links-controller.ts +0 -162
  135. package/src/components/links-model.ts +0 -198
  136. package/src/components/network-map-controller.ts +0 -84
  137. package/src/components/network-map-model.ts +0 -255
  138. package/src/components/useTimelineRouteData.ts +0 -193
  139. package/src/lib/account-sync-job.ts +0 -666
  140. package/src/lib/actions-transport.ts +0 -300
  141. package/src/lib/analysis-runtime.ts +0 -238
  142. package/src/lib/api-client.ts +0 -220
  143. package/src/lib/api-contracts.ts +0 -328
  144. package/src/lib/archive-finder.ts +0 -185
  145. package/src/lib/archive-import-plan.ts +0 -102
  146. package/src/lib/archive-import.ts +0 -2695
  147. package/src/lib/authored-live.ts +0 -1027
  148. package/src/lib/avatar-cache.ts +0 -349
  149. package/src/lib/backup.ts +0 -2652
  150. package/src/lib/bird-actions.ts +0 -294
  151. package/src/lib/bird-command.ts +0 -130
  152. package/src/lib/bird.ts +0 -1140
  153. package/src/lib/blocklist.ts +0 -136
  154. package/src/lib/blocks-write.ts +0 -171
  155. package/src/lib/blocks.ts +0 -387
  156. package/src/lib/bookmark-sync-job.ts +0 -536
  157. package/src/lib/config.ts +0 -224
  158. package/src/lib/conversation-surface.ts +0 -205
  159. package/src/lib/data-sources.ts +0 -219
  160. package/src/lib/database-metrics.ts +0 -88
  161. package/src/lib/database-migrations.ts +0 -34
  162. package/src/lib/database-schema.ts +0 -312
  163. package/src/lib/database-writer.ts +0 -69
  164. package/src/lib/db.ts +0 -889
  165. package/src/lib/dm-read-model.ts +0 -533
  166. package/src/lib/dms-live.ts +0 -870
  167. package/src/lib/effect-runtime.ts +0 -45
  168. package/src/lib/follow-graph.ts +0 -1087
  169. package/src/lib/geocoding.ts +0 -296
  170. package/src/lib/http-effect.ts +0 -222
  171. package/src/lib/identity-search-index.ts +0 -369
  172. package/src/lib/import-repository.ts +0 -138
  173. package/src/lib/inbox.ts +0 -242
  174. package/src/lib/link-index.ts +0 -641
  175. package/src/lib/link-insights.ts +0 -860
  176. package/src/lib/link-preview-metadata.ts +0 -754
  177. package/src/lib/live-sync-engine.ts +0 -209
  178. package/src/lib/live-transport-gateway.ts +0 -128
  179. package/src/lib/location.ts +0 -137
  180. package/src/lib/media-fetch.ts +0 -860
  181. package/src/lib/media-includes.ts +0 -165
  182. package/src/lib/mention-threads-live.ts +0 -830
  183. package/src/lib/mentions-export.ts +0 -147
  184. package/src/lib/mentions-live.ts +0 -1114
  185. package/src/lib/moderation-target.ts +0 -227
  186. package/src/lib/moderation-write.ts +0 -131
  187. package/src/lib/mutes-write.ts +0 -171
  188. package/src/lib/mutes.ts +0 -84
  189. package/src/lib/ndjson-stream.ts +0 -106
  190. package/src/lib/network-map.ts +0 -382
  191. package/src/lib/openai-response-runtime.ts +0 -251
  192. package/src/lib/openai.ts +0 -117
  193. package/src/lib/paginated-sync.ts +0 -93
  194. package/src/lib/period-digest.ts +0 -1364
  195. package/src/lib/present.ts +0 -118
  196. package/src/lib/profile-affiliation-hydration.ts +0 -228
  197. package/src/lib/profile-affiliations.ts +0 -262
  198. package/src/lib/profile-analysis.ts +0 -1198
  199. package/src/lib/profile-bio-entities.ts +0 -318
  200. package/src/lib/profile-history.ts +0 -164
  201. package/src/lib/profile-hydration.ts +0 -180
  202. package/src/lib/profile-replies.ts +0 -77
  203. package/src/lib/profile-resolver.ts +0 -650
  204. package/src/lib/queries.ts +0 -6
  205. package/src/lib/query-actions.ts +0 -437
  206. package/src/lib/query-client.tsx +0 -47
  207. package/src/lib/query-read-model-shared.ts +0 -52
  208. package/src/lib/query-read-models.ts +0 -5
  209. package/src/lib/query-resource.ts +0 -41
  210. package/src/lib/query-status.ts +0 -164
  211. package/src/lib/research.ts +0 -652
  212. package/src/lib/runtime-services.ts +0 -20
  213. package/src/lib/search-discussion.ts +0 -812
  214. package/src/lib/seed.ts +0 -613
  215. package/src/lib/server-runtime-services.ts +0 -30
  216. package/src/lib/sqlite.ts +0 -183
  217. package/src/lib/streaming-ingestion.ts +0 -240
  218. package/src/lib/sync-cache.ts +0 -70
  219. package/src/lib/sync-plan.ts +0 -175
  220. package/src/lib/theme-transition.ts +0 -113
  221. package/src/lib/theme.tsx +0 -165
  222. package/src/lib/timeline-collections-live.ts +0 -407
  223. package/src/lib/timeline-live.ts +0 -302
  224. package/src/lib/timeline-read-model.ts +0 -1191
  225. package/src/lib/tweet-account-edges.ts +0 -48
  226. package/src/lib/tweet-lookup.ts +0 -46
  227. package/src/lib/tweet-render.ts +0 -430
  228. package/src/lib/tweet-repository.ts +0 -156
  229. package/src/lib/tweet-search-live.ts +0 -462
  230. package/src/lib/types.ts +0 -723
  231. package/src/lib/ui.ts +0 -529
  232. package/src/lib/url-expansion-store.ts +0 -110
  233. package/src/lib/url-expansion.ts +0 -343
  234. package/src/lib/url-safety.ts +0 -220
  235. package/src/lib/web-sync.ts +0 -533
  236. package/src/lib/whois.ts +0 -1080
  237. package/src/lib/x-profile.ts +0 -365
  238. package/src/lib/x-web.ts +0 -199
  239. package/src/lib/xurl-rate-limits.ts +0 -267
  240. package/src/lib/xurl.ts +0 -1677
  241. package/src/routeTree.gen.ts +0 -723
  242. package/src/router.tsx +0 -20
  243. package/src/routes/__root.tsx +0 -82
  244. package/src/routes/api/action.tsx +0 -147
  245. package/src/routes/api/avatar.tsx +0 -50
  246. package/src/routes/api/blocks.tsx +0 -35
  247. package/src/routes/api/conversation.tsx +0 -45
  248. package/src/routes/api/data-sources.tsx +0 -24
  249. package/src/routes/api/inbox.tsx +0 -43
  250. package/src/routes/api/link-insights.tsx +0 -85
  251. package/src/routes/api/link-preview.tsx +0 -47
  252. package/src/routes/api/network-map.tsx +0 -55
  253. package/src/routes/api/period-digest.tsx +0 -101
  254. package/src/routes/api/profile-analysis.tsx +0 -97
  255. package/src/routes/api/profile-hydrate.tsx +0 -54
  256. package/src/routes/api/query.tsx +0 -121
  257. package/src/routes/api/search-discussion.tsx +0 -113
  258. package/src/routes/api/status.tsx +0 -30
  259. package/src/routes/api/sync.tsx +0 -108
  260. package/src/routes/api/xurl-rate-limits.tsx +0 -24
  261. package/src/routes/blocks.tsx +0 -343
  262. package/src/routes/bookmarks.tsx +0 -18
  263. package/src/routes/data-sources.tsx +0 -249
  264. package/src/routes/discuss.tsx +0 -419
  265. package/src/routes/dms.tsx +0 -415
  266. package/src/routes/inbox.tsx +0 -232
  267. package/src/routes/index.tsx +0 -32
  268. package/src/routes/likes.tsx +0 -18
  269. package/src/routes/links.tsx +0 -631
  270. package/src/routes/mentions.tsx +0 -32
  271. package/src/routes/network-map.tsx +0 -732
  272. package/src/routes/profile-analyze.tsx +0 -112
  273. package/src/routes/profiles.$handle.tsx +0 -228
  274. package/src/routes/rate-limits.tsx +0 -305
  275. package/src/routes/today.tsx +0 -511
  276. package/src/styles.css +0 -257
  277. package/tsconfig.json +0 -29
  278. package/vite.config.ts +0 -29
  279. /package/{public → dist/client}/birdclaw-mark.png +0 -0
  280. /package/{public → dist/client}/favicon.ico +0 -0
  281. /package/{public → dist/client}/logo192.png +0 -0
  282. /package/{public → dist/client}/logo512.png +0 -0
  283. /package/{public → dist/client}/manifest.json +0 -0
  284. /package/{public → dist/client}/robots.txt +0 -0
@@ -0,0 +1,1101 @@
1
+ import { $ as feedRowBodyClass, B as embeddedCardClass, Dt as pageSubtitleClass, Et as pageHeaderRowClass, G as embeddedCardNameClass, H as embeddedCardHandleClass, Ht as searchFieldInputClass, J as feedActionButtonClass, Ot as pageTitleClass, Q as feedRowActionsClass, Tt as pageHeaderClass, U as embeddedCardHeaderClass, Ut as searchFieldShellClass, V as embeddedCardCopyClass, Vt as searchFieldIconClass, W as embeddedCardLabelClass, X as feedActionIconWrapClass, Y as feedActionIconClass, Z as feedClass, _n as tweetMediaTileClass, at as feedRowStatePillActiveClass, ct as feedRowTextClass, dt as linkPreviewCardClass, et as feedRowClass, ft as linkPreviewDescClass, gn as tweetMediaGridClass, it as feedRowNameClass, lt as feedRowTimestampClass, m as cx, mt as linkPreviewTitleClass, nt as feedRowHandleClass, ot as feedRowStatePillClass, pt as linkPreviewHostClass, rt as feedRowHeaderClass, st as feedRowStatePillOpenClass, tt as feedRowDotClass, z as embeddedCardBodyClass } from "./ui-qbmcMSSX.js";
2
+ import { D as tweetConversationResponseSchema, a as fetchQueryEnvelope, d as runEffectPromise, i as fetchJsonEffect, n as queryKeys, o as fetchQueryResponse, r as fetchJson, s as postAction, x as linkPreviewResponseSchema } from "./query-client-BdauHBgd.js";
3
+ import { r as formatCompactNumber, t as AvatarChip } from "./AvatarChip-zUDAEmjL.js";
4
+ import { r as useSelectedAccountId } from "./account-selection-CTdFGWRk.js";
5
+ import { a as TweetSkeletonRows, n as FeedError, o as BirdclawEmpty, r as FeedLoading, s as BirdclawLoading, t as FeedEmpty } from "./FeedState-ntW0Q2GB.js";
6
+ import { d as isTweetArticleUrlEntity, f as normalizeTweetUrlEntityRangeForText, s as safeHttpUrl, t as ProfilePreview } from "./ProfilePreview-Cg-AOHBE.js";
7
+ import { t as SmartTimestamp } from "./SmartTimestamp-DfwE1NGf.js";
8
+ import { t as useDebouncedValue } from "./useDebouncedValue-DOnA6Xcy.js";
9
+ import { t as TweetRichText } from "./TweetRichText-DDywESRF.js";
10
+ import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
11
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
12
+ import { BookOpen, BookmarkCheck, CheckCircle2, Circle, ExternalLink, Heart, Image, MessageCircle, Repeat2, Search, UserSearch, X } from "lucide-react";
13
+ import { queryOptions, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
14
+ //#region src/lib/conversation-surface.ts
15
+ var ConversationSurfaceContext = createContext(null);
16
+ function conversationQueryOptions(tweetId) {
17
+ return queryOptions({
18
+ queryKey: [...queryKeys.conversations, tweetId],
19
+ queryFn: () => runEffectPromise(fetchJsonEffect(`/api/conversation?tweetId=${encodeURIComponent(tweetId)}`, void 0, tweetConversationResponseSchema, "Conversation unavailable")).then((data) => data.items),
20
+ staleTime: Number.POSITIVE_INFINITY
21
+ });
22
+ }
23
+ function ConversationSurfaceScope({ children }) {
24
+ const [expandedSurfaceId, setExpandedSurfaceId] = useState(null);
25
+ const value = useMemo(() => ({
26
+ expandedSurfaceId,
27
+ setExpandedSurfaceId
28
+ }), [expandedSurfaceId]);
29
+ return createElement(ConversationSurfaceContext.Provider, { value }, children);
30
+ }
31
+ function useConversationSurface(surfaceId, tweetId = surfaceId) {
32
+ const scope = useContext(ConversationSurfaceContext);
33
+ const [localExpandedSurfaceId, setLocalExpandedSurfaceId] = useState(null);
34
+ const expandedSurfaceId = scope?.expandedSurfaceId ?? localExpandedSurfaceId;
35
+ const setExpandedSurfaceId = scope?.setExpandedSurfaceId ?? setLocalExpandedSurfaceId;
36
+ const isOpen = expandedSurfaceId === surfaceId;
37
+ const queryClient = useQueryClient();
38
+ const query = useQuery({
39
+ ...conversationQueryOptions(tweetId),
40
+ enabled: isOpen
41
+ });
42
+ const toggle = useCallback(() => {
43
+ setExpandedSurfaceId(isOpen ? null : surfaceId);
44
+ }, [
45
+ isOpen,
46
+ setExpandedSurfaceId,
47
+ surfaceId
48
+ ]);
49
+ const prefetch = useCallback(() => {
50
+ queryClient.prefetchQuery(conversationQueryOptions(tweetId));
51
+ }, [queryClient, tweetId]);
52
+ const status = query.isError ? "error" : query.isFetching ? "loading" : query.data ? "ready" : "idle";
53
+ return {
54
+ error: query.error instanceof Error ? query.error.message : null,
55
+ isOpen,
56
+ items: query.data ?? [],
57
+ loading: query.isFetching,
58
+ prefetch,
59
+ status,
60
+ toggle
61
+ };
62
+ }
63
+ //#endregion
64
+ //#region src/components/TweetArticleCard.tsx
65
+ function safeArticleImageUrl(value) {
66
+ const url = safeHttpUrl(value);
67
+ if (!url) return null;
68
+ try {
69
+ return new URL(url).hostname === "pbs.twimg.com" ? url : null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+ function TweetArticleCard({ article }) {
75
+ const href = safeHttpUrl(article.url);
76
+ if (!href) return null;
77
+ const imageUrl = safeArticleImageUrl(article.coverImageUrl);
78
+ return /* @__PURE__ */ jsxs("a", {
79
+ "aria-label": `Read article: ${article.title}`,
80
+ className: linkPreviewCardClass,
81
+ "data-perf": "tweet-article-card",
82
+ href,
83
+ rel: "noreferrer",
84
+ target: "_blank",
85
+ children: [/* @__PURE__ */ jsxs("div", {
86
+ className: "flex min-w-0 flex-1 flex-col justify-center gap-1 px-3.5 py-3",
87
+ children: [
88
+ /* @__PURE__ */ jsxs("div", {
89
+ className: "flex min-w-0 items-center gap-2",
90
+ children: [
91
+ /* @__PURE__ */ jsx(BookOpen, {
92
+ "aria-hidden": "true",
93
+ className: "size-3.5 shrink-0 text-[var(--ink-soft)]",
94
+ strokeWidth: 1.8
95
+ }),
96
+ /* @__PURE__ */ jsx("span", {
97
+ className: linkPreviewHostClass,
98
+ children: "Article on X"
99
+ }),
100
+ /* @__PURE__ */ jsx(ExternalLink, {
101
+ "aria-hidden": "true",
102
+ className: "size-3.5 shrink-0 text-[var(--ink-soft)] opacity-0 transition-opacity group-hover/link-preview:opacity-100",
103
+ strokeWidth: 1.8
104
+ })
105
+ ]
106
+ }),
107
+ /* @__PURE__ */ jsx("span", {
108
+ className: linkPreviewTitleClass,
109
+ children: article.title
110
+ }),
111
+ article.previewText ? /* @__PURE__ */ jsx("span", {
112
+ className: linkPreviewDescClass,
113
+ children: article.previewText
114
+ }) : null
115
+ ]
116
+ }), imageUrl ? /* @__PURE__ */ jsx("div", {
117
+ className: "flex aspect-[1.45] w-40 shrink-0 overflow-hidden border-l border-[var(--line)] bg-[var(--bg-soft)] max-[720px]:w-28",
118
+ children: /* @__PURE__ */ jsx("img", {
119
+ alt: "",
120
+ className: "size-full object-cover transition-transform duration-200 group-hover/link-preview:scale-[1.03]",
121
+ loading: "lazy",
122
+ src: imageUrl
123
+ })
124
+ }) : null]
125
+ });
126
+ }
127
+ //#endregion
128
+ //#region src/components/TweetMediaGrid.tsx
129
+ function TweetMediaGrid({ items }) {
130
+ const [selectedIndex, setSelectedIndex] = useState(null);
131
+ if (items.length === 0) return null;
132
+ const visibleItems = items.slice(0, 4);
133
+ const selectedItem = selectedIndex === null ? null : visibleItems[selectedIndex] ?? null;
134
+ const selectedVideoUrl = selectedItem?.type === "video" || selectedItem?.type === "gif" ? selectedItem.variants?.[0]?.url ?? playableVideoUrl(selectedItem.url) : null;
135
+ const singleImage = visibleItems.length === 1 && visibleItems[0]?.type === "image" ? visibleItems[0] : null;
136
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [singleImage ? /* @__PURE__ */ jsx("button", {
137
+ "aria-label": "Open tweet media 1",
138
+ className: cx("tweet-media-single mt-2 max-w-full overflow-hidden rounded-2xl border border-[var(--line)] bg-[var(--bg-active)] p-0 text-left", singleImage.width && singleImage.height ? "block" : "inline-block align-top"),
139
+ onClick: (event) => {
140
+ event.stopPropagation();
141
+ setSelectedIndex(0);
142
+ },
143
+ style: singleImageStyle(singleImage),
144
+ type: "button",
145
+ children: /* @__PURE__ */ jsx("img", {
146
+ alt: singleImage.altText ?? "Tweet media 1",
147
+ className: cx("tweet-media-image block max-h-[720px] max-w-full", singleImage.width && singleImage.height ? "size-full object-cover" : "h-auto w-auto object-contain"),
148
+ height: singleImage.height,
149
+ loading: "lazy",
150
+ src: singleImage.thumbnailUrl ?? singleImage.url,
151
+ width: singleImage.width
152
+ })
153
+ }) : /* @__PURE__ */ jsx("div", {
154
+ className: tweetMediaGridClass(Math.min(items.length, 4)),
155
+ children: visibleItems.map((item, index) => /* @__PURE__ */ jsx("button", {
156
+ "aria-label": `Open tweet media ${String(index + 1)}`,
157
+ className: tweetMediaTileClass(index, Math.min(items.length, 4)),
158
+ onClick: (event) => {
159
+ event.stopPropagation();
160
+ setSelectedIndex(index);
161
+ },
162
+ style: visibleItems.length === 1 && item.width && item.height ? { aspectRatio: `${String(item.width)} / ${String(item.height)}` } : void 0,
163
+ type: "button",
164
+ children: item.type === "image" ? /* @__PURE__ */ jsx("img", {
165
+ alt: item.altText ?? `Tweet media ${String(index + 1)}`,
166
+ className: "tweet-media-image block size-full object-contain",
167
+ loading: "lazy",
168
+ src: item.thumbnailUrl ?? item.url
169
+ }) : /* @__PURE__ */ jsx("span", {
170
+ className: "tweet-media-fallback grid min-h-40 place-items-center font-semibold text-[var(--ink-soft)]",
171
+ children: item.type === "video" ? "Video" : item.type === "gif" ? "GIF" : "Media"
172
+ })
173
+ }, item.url + String(index)))
174
+ }), selectedItem ? /* @__PURE__ */ jsxs("div", {
175
+ "aria-modal": "true",
176
+ className: "fixed inset-0 z-50 flex items-center justify-center bg-black/85 p-4",
177
+ onClick: (event) => {
178
+ event.stopPropagation();
179
+ setSelectedIndex(null);
180
+ },
181
+ role: "dialog",
182
+ children: [/* @__PURE__ */ jsx("button", {
183
+ "aria-label": "Close media viewer",
184
+ className: "absolute right-4 top-4 grid size-10 place-items-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20",
185
+ onClick: (event) => {
186
+ event.stopPropagation();
187
+ setSelectedIndex(null);
188
+ },
189
+ type: "button",
190
+ children: /* @__PURE__ */ jsx(X, {
191
+ className: "size-5",
192
+ strokeWidth: 1.8
193
+ })
194
+ }), selectedItem.type === "image" ? /* @__PURE__ */ jsx("img", {
195
+ alt: selectedItem.altText ?? "Tweet media",
196
+ className: "max-h-[92vh] max-w-[92vw] object-contain",
197
+ onClick: (event) => event.stopPropagation(),
198
+ src: selectedItem.url
199
+ }) : selectedVideoUrl ? /* @__PURE__ */ jsx("video", {
200
+ autoPlay: selectedItem.type === "gif",
201
+ className: "max-h-[92vh] max-w-[92vw]",
202
+ controls: true,
203
+ loop: selectedItem.type === "gif",
204
+ muted: selectedItem.type === "gif",
205
+ onClick: (event) => event.stopPropagation(),
206
+ playsInline: true,
207
+ poster: selectedItem.thumbnailUrl,
208
+ src: selectedVideoUrl
209
+ }) : /* @__PURE__ */ jsxs("div", {
210
+ className: "grid min-h-64 min-w-80 place-items-center gap-3 rounded-2xl border border-white/20 bg-black p-6 text-white",
211
+ onClick: (event) => event.stopPropagation(),
212
+ children: [/* @__PURE__ */ jsx("span", { children: selectedItem.type === "video" ? "Video" : selectedItem.type === "gif" ? "GIF" : "Media" }), /* @__PURE__ */ jsx("a", {
213
+ className: "rounded-full bg-white/10 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-white/20",
214
+ href: selectedItem.url,
215
+ rel: "noreferrer",
216
+ target: "_blank",
217
+ children: "Open media"
218
+ })]
219
+ })]
220
+ }) : null] });
221
+ }
222
+ function singleImageStyle(item) {
223
+ if (!item.width || !item.height) return void 0;
224
+ const width = Math.min(item.width, Math.round(item.width / item.height * 720));
225
+ return {
226
+ aspectRatio: `${String(item.width)} / ${String(item.height)}`,
227
+ width: `${String(width)}px`
228
+ };
229
+ }
230
+ function playableVideoUrl(url) {
231
+ try {
232
+ const parsed = new URL(url);
233
+ if (parsed.hostname === "video.twimg.com") return url;
234
+ return /\.(?:mp4|m3u8)(?:$|[?#])/i.test(parsed.pathname) ? url : void 0;
235
+ } catch {
236
+ return /\.(?:mp4|m3u8)(?:$|[?#])/i.test(url) ? url : void 0;
237
+ }
238
+ }
239
+ //#endregion
240
+ //#region src/components/ConversationThread.tsx
241
+ function ConversationThread({ anchorId, error, items, loading }) {
242
+ if (loading) return /* @__PURE__ */ jsx("section", {
243
+ className: "mt-3 rounded-2xl border border-[var(--line)] bg-[var(--bg-card)]",
244
+ children: /* @__PURE__ */ jsx(BirdclawLoading, {
245
+ detail: "Finding archived replies around this post",
246
+ label: "Loading conversation"
247
+ })
248
+ });
249
+ if (error) return /* @__PURE__ */ jsx("section", {
250
+ className: "mt-3 rounded-2xl border border-[var(--alert)] bg-[var(--alert-soft)] px-4 py-3 text-[14px] text-[var(--alert)]",
251
+ children: error
252
+ });
253
+ if (items.length <= 1) return /* @__PURE__ */ jsx("section", {
254
+ className: "mt-3 rounded-2xl border border-[var(--line)]",
255
+ children: /* @__PURE__ */ jsx(BirdclawEmpty, {
256
+ detail: "This post has no other archived replies locally.",
257
+ label: "No thread context yet"
258
+ })
259
+ });
260
+ return /* @__PURE__ */ jsxs("section", {
261
+ "aria-label": "Conversation",
262
+ className: "mt-3 overflow-hidden rounded-2xl border border-[var(--line)] bg-[var(--bg-card)]",
263
+ children: [/* @__PURE__ */ jsxs("div", {
264
+ className: "flex items-center gap-2 border-b border-[var(--line)] px-4 py-2.5 text-[13px] font-bold text-[var(--ink)]",
265
+ children: [/* @__PURE__ */ jsx(MessageCircle, {
266
+ className: feedActionIconClass,
267
+ strokeWidth: 1.8
268
+ }), /* @__PURE__ */ jsxs("span", { children: [items.length, " tweets in conversation"] })]
269
+ }), /* @__PURE__ */ jsx("div", {
270
+ className: "flex flex-col",
271
+ children: items.map((tweet, index) => {
272
+ const isAnchor = tweet.id === anchorId;
273
+ return /* @__PURE__ */ jsxs("div", {
274
+ className: cx("flex gap-3 px-4 py-3", index > 0 && "border-t border-[var(--line)]", isAnchor && "bg-[var(--accent-soft)]"),
275
+ children: [/* @__PURE__ */ jsxs("div", {
276
+ className: "flex flex-col items-center",
277
+ children: [/* @__PURE__ */ jsx(AvatarChip, {
278
+ avatarUrl: tweet.author.avatarUrl,
279
+ hue: tweet.author.avatarHue,
280
+ name: tweet.author.displayName,
281
+ profileId: tweet.author.id,
282
+ size: "small"
283
+ }), index < items.length - 1 ? /* @__PURE__ */ jsx("span", { className: "mt-2 w-px flex-1 bg-[var(--line)]" }) : null]
284
+ }), /* @__PURE__ */ jsxs("div", {
285
+ className: "min-w-0 flex-1",
286
+ children: [
287
+ /* @__PURE__ */ jsxs("header", {
288
+ className: "flex min-w-0 items-center gap-1.5 text-[14px]",
289
+ children: [
290
+ /* @__PURE__ */ jsx(ProfilePreview, {
291
+ profile: tweet.author,
292
+ children: /* @__PURE__ */ jsxs("span", {
293
+ className: "flex min-w-0 items-center gap-1.5",
294
+ children: [/* @__PURE__ */ jsx("span", {
295
+ className: feedRowNameClass,
296
+ children: tweet.author.displayName
297
+ }), /* @__PURE__ */ jsxs("span", {
298
+ className: feedRowHandleClass,
299
+ children: ["@", tweet.author.handle]
300
+ })]
301
+ })
302
+ }),
303
+ /* @__PURE__ */ jsx("span", {
304
+ className: "text-[var(--ink-soft)]",
305
+ children: "·"
306
+ }),
307
+ /* @__PURE__ */ jsx(SmartTimestamp, {
308
+ className: feedRowTimestampClass,
309
+ value: tweet.createdAt
310
+ }),
311
+ isAnchor ? /* @__PURE__ */ jsx("span", {
312
+ className: "ml-auto rounded-full bg-[var(--accent)] px-2 py-0.5 text-[11px] font-bold text-white",
313
+ children: "selected"
314
+ }) : null
315
+ ]
316
+ }),
317
+ /* @__PURE__ */ jsx(TweetRichText, {
318
+ className: "mt-1 whitespace-pre-wrap break-words text-[14px] leading-[1.45] text-[var(--ink)] [overflow-wrap:anywhere]",
319
+ entities: tweet.entities,
320
+ text: tweet.text
321
+ }),
322
+ /* @__PURE__ */ jsx(TweetMediaGrid, { items: tweet.media }),
323
+ tweet.entities.article ? /* @__PURE__ */ jsx(TweetArticleCard, { article: tweet.entities.article }) : null
324
+ ]
325
+ })]
326
+ }, tweet.id);
327
+ })
328
+ })]
329
+ });
330
+ }
331
+ //#endregion
332
+ //#region src/components/EmbeddedTweetCard.tsx
333
+ function EmbeddedTweetCard({ item, label }) {
334
+ return /* @__PURE__ */ jsxs("section", {
335
+ className: embeddedCardBodyClass,
336
+ children: [
337
+ /* @__PURE__ */ jsx("p", {
338
+ className: embeddedCardLabelClass,
339
+ children: label
340
+ }),
341
+ /* @__PURE__ */ jsxs("header", {
342
+ className: embeddedCardHeaderClass,
343
+ children: [
344
+ /* @__PURE__ */ jsx(ProfilePreview, {
345
+ profile: item.author,
346
+ children: /* @__PURE__ */ jsxs("span", {
347
+ className: "flex min-w-0 items-center gap-1.5",
348
+ children: [/* @__PURE__ */ jsx("span", {
349
+ className: embeddedCardNameClass,
350
+ children: item.author.displayName
351
+ }), /* @__PURE__ */ jsxs("span", {
352
+ className: embeddedCardHandleClass,
353
+ children: ["@", item.author.handle]
354
+ })]
355
+ })
356
+ }),
357
+ /* @__PURE__ */ jsx("span", {
358
+ className: "text-[var(--ink-soft)]",
359
+ children: "·"
360
+ }),
361
+ /* @__PURE__ */ jsx(SmartTimestamp, {
362
+ className: feedRowTimestampClass,
363
+ value: item.createdAt
364
+ })
365
+ ]
366
+ }),
367
+ /* @__PURE__ */ jsx(TweetRichText, {
368
+ className: embeddedCardCopyClass,
369
+ entities: item.entities,
370
+ text: item.text
371
+ }),
372
+ /* @__PURE__ */ jsx(TweetMediaGrid, { items: item.media }),
373
+ item.entities.article ? /* @__PURE__ */ jsx(TweetArticleCard, { article: item.entities.article }) : null
374
+ ]
375
+ });
376
+ }
377
+ //#endregion
378
+ //#region src/components/LinkPreviewCard.tsx
379
+ var MAX_CONCURRENT_PREVIEW_FETCHES = 2;
380
+ var activePreviewFetches = 0;
381
+ var queuedPreviewFetches = [];
382
+ function needsHydration(preview) {
383
+ const targetUrl = preview.expandedUrl || "";
384
+ if (preview.imageUrl && preview.siteName && preview.title && isDirectImageUrl(targetUrl)) return false;
385
+ return !preview.imageUrl || !preview.title || !preview.description || preview.title === preview.displayUrl || preview.description === preview.displayUrl;
386
+ }
387
+ function runQueuedPreviewFetches() {
388
+ while (activePreviewFetches < MAX_CONCURRENT_PREVIEW_FETCHES && queuedPreviewFetches.length > 0) queuedPreviewFetches.shift()?.();
389
+ }
390
+ function schedulePreviewFetch(task) {
391
+ return new Promise((resolve, reject) => {
392
+ queuedPreviewFetches.push(() => {
393
+ activePreviewFetches += 1;
394
+ task().then(resolve).catch(reject).finally(() => {
395
+ activePreviewFetches = Math.max(0, activePreviewFetches - 1);
396
+ runQueuedPreviewFetches();
397
+ });
398
+ });
399
+ runQueuedPreviewFetches();
400
+ });
401
+ }
402
+ function linkPreviewQueryOptions(targetUrl) {
403
+ const params = new URLSearchParams({ url: targetUrl });
404
+ return queryOptions({
405
+ queryKey: [...queryKeys.linkPreviews, targetUrl],
406
+ queryFn: () => schedulePreviewFetch(() => fetchJson(`/api/link-preview?${params.toString()}`, void 0, linkPreviewResponseSchema, "Link preview unavailable").then((data) => data.preview)),
407
+ staleTime: 30 * 6e4
408
+ });
409
+ }
410
+ function displayHost(url, fallback) {
411
+ try {
412
+ return new URL(url).hostname.replace(/^www\./, "");
413
+ } catch {
414
+ return fallback;
415
+ }
416
+ }
417
+ function isDirectImageUrl(url) {
418
+ try {
419
+ const parsed = new URL(url);
420
+ return parsed.protocol === "https:" && parsed.hostname === "pbs.twimg.com" && (parsed.pathname.startsWith("/media/") || parsed.pathname.startsWith("/amplify_video_thumb/"));
421
+ } catch {
422
+ return false;
423
+ }
424
+ }
425
+ function safePreviewImageUrl(url) {
426
+ if (!url) return null;
427
+ try {
428
+ const parsed = new URL(url);
429
+ if (parsed.protocol === "https:" && parsed.hostname === "pbs.twimg.com" && (parsed.pathname.startsWith("/media/") || parsed.pathname.startsWith("/amplify_video_thumb/"))) return parsed.toString();
430
+ return null;
431
+ } catch {
432
+ return null;
433
+ }
434
+ }
435
+ function LinkPreviewCard({ entry, index }) {
436
+ const targetUrl = safeHttpUrl(entry.expandedUrl || entry.url);
437
+ const previewUrl = targetUrl ?? "";
438
+ const displayUrl = entry.displayUrl || (targetUrl ? displayHost(targetUrl, targetUrl) : "");
439
+ const directImageUrl = targetUrl && isDirectImageUrl(targetUrl) ? targetUrl : null;
440
+ const initialPreview = useMemo(() => ({
441
+ expandedUrl: previewUrl,
442
+ displayUrl,
443
+ title: entry.title ?? (directImageUrl ? displayHost(previewUrl, displayUrl) : void 0),
444
+ description: entry.description ?? (directImageUrl ? displayUrl : void 0),
445
+ imageUrl: entry.imageUrl ?? directImageUrl,
446
+ siteName: entry.siteName ?? (directImageUrl ? displayHost(previewUrl, displayUrl) : void 0)
447
+ }), [
448
+ directImageUrl,
449
+ displayUrl,
450
+ entry.description,
451
+ entry.imageUrl,
452
+ entry.siteName,
453
+ entry.title,
454
+ previewUrl
455
+ ]);
456
+ const [imageFailed, setImageFailed] = useState(false);
457
+ const [canHydrate, setCanHydrate] = useState(false);
458
+ const [hydrationReady, setHydrationReady] = useState(false);
459
+ const cardRef = useRef(null);
460
+ const shouldHydrate = Boolean(targetUrl && needsHydration(initialPreview));
461
+ const previewQuery = useQuery({
462
+ ...linkPreviewQueryOptions(targetUrl ?? ""),
463
+ enabled: shouldHydrate && canHydrate && hydrationReady
464
+ });
465
+ const preview = useMemo(() => {
466
+ const metadata = previewQuery.data;
467
+ if (!metadata) return initialPreview;
468
+ return {
469
+ expandedUrl: safeHttpUrl(metadata.url) ?? initialPreview.expandedUrl,
470
+ displayUrl: initialPreview.displayUrl,
471
+ title: metadata.title ?? initialPreview.title,
472
+ description: metadata.description ?? initialPreview.description,
473
+ imageUrl: metadata.imageUrl ?? initialPreview.imageUrl,
474
+ siteName: metadata.siteName ?? initialPreview.siteName
475
+ };
476
+ }, [initialPreview, previewQuery.data]);
477
+ useEffect(() => {
478
+ setImageFailed(false);
479
+ setCanHydrate(false);
480
+ setHydrationReady(false);
481
+ }, [initialPreview]);
482
+ useEffect(() => {
483
+ if (!targetUrl) return;
484
+ if (!shouldHydrate) return;
485
+ const node = cardRef.current;
486
+ if (!node || typeof IntersectionObserver === "undefined") {
487
+ setCanHydrate(true);
488
+ return;
489
+ }
490
+ const observer = new IntersectionObserver((entries) => {
491
+ if (entries.some((entry) => entry.isIntersecting)) {
492
+ setCanHydrate(true);
493
+ observer.disconnect();
494
+ }
495
+ }, { rootMargin: "320px 0px" });
496
+ observer.observe(node);
497
+ return () => observer.disconnect();
498
+ }, [shouldHydrate, targetUrl]);
499
+ useEffect(() => {
500
+ if (!targetUrl || !canHydrate || !shouldHydrate) return;
501
+ const timer = window.setTimeout(() => {
502
+ setHydrationReady(true);
503
+ }, 100);
504
+ return () => window.clearTimeout(timer);
505
+ }, [
506
+ canHydrate,
507
+ shouldHydrate,
508
+ targetUrl
509
+ ]);
510
+ if (!targetUrl) return null;
511
+ const title = preview.title || entry.displayUrl;
512
+ const description = preview.description && preview.description !== title ? preview.description : preview.siteName || displayHost(preview.expandedUrl, entry.displayUrl);
513
+ const host = preview.siteName || displayHost(preview.expandedUrl, entry.displayUrl);
514
+ const imageUrl = safePreviewImageUrl(preview.imageUrl);
515
+ const showImage = Boolean(imageUrl && !imageFailed);
516
+ return /* @__PURE__ */ jsxs("a", {
517
+ className: linkPreviewCardClass,
518
+ "data-perf": "link-preview-card",
519
+ href: safeHttpUrl(preview.expandedUrl) ?? targetUrl,
520
+ ref: cardRef,
521
+ rel: "noreferrer",
522
+ target: "_blank",
523
+ children: [/* @__PURE__ */ jsxs("div", {
524
+ className: "flex min-w-0 flex-1 flex-col justify-center gap-1 px-3.5 py-3",
525
+ children: [
526
+ /* @__PURE__ */ jsxs("div", {
527
+ className: "flex min-w-0 items-center gap-2",
528
+ children: [/* @__PURE__ */ jsx("span", {
529
+ className: linkPreviewHostClass,
530
+ children: host
531
+ }), /* @__PURE__ */ jsx(ExternalLink, {
532
+ "aria-hidden": "true",
533
+ className: "size-3.5 shrink-0 text-[var(--ink-soft)] opacity-0 transition-opacity group-hover/link-preview:opacity-100",
534
+ strokeWidth: 1.8
535
+ })]
536
+ }),
537
+ /* @__PURE__ */ jsx("span", {
538
+ className: linkPreviewTitleClass,
539
+ children: title
540
+ }),
541
+ /* @__PURE__ */ jsx("span", {
542
+ className: linkPreviewDescClass,
543
+ children: description
544
+ }),
545
+ /* @__PURE__ */ jsx("span", {
546
+ className: cx(linkPreviewHostClass, "text-[12px]"),
547
+ children: entry.displayUrl
548
+ })
549
+ ]
550
+ }), /* @__PURE__ */ jsx("div", {
551
+ className: "flex aspect-[1.45] w-40 shrink-0 items-center justify-center overflow-hidden border-l border-[var(--line)] bg-[var(--bg-soft)] max-[720px]:w-28",
552
+ children: showImage ? /* @__PURE__ */ jsx("img", {
553
+ alt: title,
554
+ className: "size-full object-cover transition-transform duration-200 group-hover/link-preview:scale-[1.03]",
555
+ loading: "lazy",
556
+ onError: () => setImageFailed(true),
557
+ src: imageUrl ?? ""
558
+ }) : /* @__PURE__ */ jsx(Image, {
559
+ "aria-hidden": "true",
560
+ className: "size-8 text-[var(--ink-soft)]",
561
+ strokeWidth: 1.7
562
+ })
563
+ })]
564
+ }, `${entry.expandedUrl}-${String(index)}`);
565
+ }
566
+ //#endregion
567
+ //#region src/components/TimelineCard.tsx
568
+ function comparableUrl(value) {
569
+ if (!value) return null;
570
+ try {
571
+ const parsed = new URL(value);
572
+ return `${parsed.protocol}//${parsed.hostname}${parsed.pathname}`;
573
+ } catch {
574
+ return value.split("?")[0] ?? value;
575
+ }
576
+ }
577
+ function getMediaUrlSet(media) {
578
+ const urls = /* @__PURE__ */ new Set();
579
+ for (const item of media) for (const url of [item.url, item.thumbnailUrl]) {
580
+ const comparable = comparableUrl(url);
581
+ if (comparable) urls.add(comparable);
582
+ }
583
+ return urls;
584
+ }
585
+ function isMediaUrlEntity(entry, mediaUrls, tweetId) {
586
+ if (mediaUrls.size > 0 && isOwnStatusMediaUrl(entry.expandedUrl, tweetId)) return true;
587
+ for (const url of [
588
+ entry.url,
589
+ entry.expandedUrl,
590
+ entry.displayUrl
591
+ ]) {
592
+ const comparable = comparableUrl(url);
593
+ if (comparable && mediaUrls.has(comparable)) return true;
594
+ }
595
+ return false;
596
+ }
597
+ function isShortUrl(value) {
598
+ if (!value) return false;
599
+ try {
600
+ const candidate = value.includes("://") ? value : `https://${value}`;
601
+ return new URL(candidate).hostname.replace(/^www\./, "") === "t.co";
602
+ } catch {
603
+ return false;
604
+ }
605
+ }
606
+ function isUnresolvedShortUrlEntity(entry) {
607
+ if (isShortUrl(entry.expandedUrl)) return true;
608
+ if (entry.expandedUrl) return false;
609
+ if (isShortUrl(entry.displayUrl)) return true;
610
+ return !entry.displayUrl && isShortUrl(entry.url);
611
+ }
612
+ function unresolvedShortUrlRanges(text, entities) {
613
+ return (entities.urls ?? []).filter(isUnresolvedShortUrlEntity).map((entry) => normalizeTweetUrlEntityRangeForText(text, entry));
614
+ }
615
+ function textOutsideRanges(text, ranges) {
616
+ let cursor = 0;
617
+ let output = "";
618
+ for (const range of [...ranges].sort((left, right) => left.start - right.start)) {
619
+ if (range.start < cursor || range.end <= range.start || range.end > text.length) continue;
620
+ output += text.slice(cursor, range.start);
621
+ cursor = range.end;
622
+ }
623
+ output += text.slice(cursor);
624
+ return output;
625
+ }
626
+ function shouldHideUnresolvedShortUrls(text, entities, mediaUrls) {
627
+ if (mediaUrls.size === 0) return false;
628
+ const ranges = unresolvedShortUrlRanges(text, entities);
629
+ if (ranges.length === 0) return false;
630
+ return textOutsideRanges(text, ranges).trim().length === 0;
631
+ }
632
+ function isOwnStatusMediaUrl(value, tweetId) {
633
+ if (!value) return false;
634
+ try {
635
+ const parsed = new URL(value);
636
+ const host = parsed.hostname.replace(/^www\./, "");
637
+ if (host !== "x.com" && host !== "twitter.com") return false;
638
+ const segments = parsed.pathname.split("/").filter(Boolean);
639
+ const statusIndex = segments.indexOf("status");
640
+ if (statusIndex < 0 || segments[statusIndex + 1] !== tweetId) return false;
641
+ const mediaSegment = segments[statusIndex + 2];
642
+ return mediaSegment === "photo" || mediaSegment === "video";
643
+ } catch {
644
+ return false;
645
+ }
646
+ }
647
+ function getVisibleEntities(entities, media, tweetId, text) {
648
+ const mediaUrls = getMediaUrlSet(media);
649
+ if (mediaUrls.size === 0) return entities;
650
+ const hideUnresolvedShortUrls = shouldHideUnresolvedShortUrls(text, entities, mediaUrls);
651
+ return {
652
+ ...entities,
653
+ urls: (entities.urls ?? []).filter((entry) => !isMediaUrlEntity(entry, mediaUrls, tweetId) && !(hideUnresolvedShortUrls && isUnresolvedShortUrlEntity(entry)))
654
+ };
655
+ }
656
+ function getHiddenMediaUrlRanges(entities, media, tweetId, text) {
657
+ const mediaUrls = getMediaUrlSet(media);
658
+ if (mediaUrls.size === 0) return [];
659
+ const hideUnresolvedShortUrls = shouldHideUnresolvedShortUrls(text, entities, mediaUrls);
660
+ return (entities.urls ?? []).filter((entry) => isMediaUrlEntity(entry, mediaUrls, tweetId) || hideUnresolvedShortUrls && isUnresolvedShortUrlEntity(entry)).map((entry) => normalizeTweetUrlEntityRangeForText(text, entry));
661
+ }
662
+ function getVisibleUrlCards(entities, quotedTweetId) {
663
+ return (entities.urls ?? []).filter((entry) => {
664
+ if (isUnresolvedShortUrlEntity(entry)) return false;
665
+ if (entities.article && isTweetArticleUrlEntity(entry, entities.article)) return false;
666
+ if (!quotedTweetId) return true;
667
+ return !entry.expandedUrl.includes(quotedTweetId);
668
+ });
669
+ }
670
+ function isInteractiveTarget(target) {
671
+ return target instanceof Element && Boolean(target.closest("a,button,input,textarea,select,[role='button']"));
672
+ }
673
+ function TweetPresentation({ tweet, hiddenUrlRanges, visibleUrlCards, replyToTweet, quotedTweet }) {
674
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
675
+ /* @__PURE__ */ jsx(TweetRichText, {
676
+ className: feedRowTextClass,
677
+ entities: tweet.entities,
678
+ hiddenUrlRanges,
679
+ text: tweet.text
680
+ }),
681
+ /* @__PURE__ */ jsx(TweetMediaGrid, { items: tweet.media }),
682
+ tweet.entities.article ? /* @__PURE__ */ jsx(TweetArticleCard, { article: tweet.entities.article }) : null,
683
+ replyToTweet ? /* @__PURE__ */ jsx("div", {
684
+ className: embeddedCardClass,
685
+ children: /* @__PURE__ */ jsx(EmbeddedTweetCard, {
686
+ item: replyToTweet,
687
+ label: "In reply to"
688
+ })
689
+ }) : null,
690
+ quotedTweet ? /* @__PURE__ */ jsx("div", {
691
+ className: embeddedCardClass,
692
+ children: /* @__PURE__ */ jsx(EmbeddedTweetCard, {
693
+ item: quotedTweet,
694
+ label: "Quoted tweet"
695
+ })
696
+ }) : null,
697
+ visibleUrlCards.map((entry, index) => /* @__PURE__ */ jsx(LinkPreviewCard, {
698
+ entry,
699
+ index
700
+ }, `${entry.expandedUrl}-${String(index)}`))
701
+ ] });
702
+ }
703
+ function TimelineCard({ item, onReply, showReplyControls = true }) {
704
+ const canReply = showReplyControls && item.kind !== "like" && item.kind !== "bookmark";
705
+ const displayTweet = item.retweetedTweet ?? item;
706
+ const displayTweetId = displayTweet.id;
707
+ const interactionTweetId = item.retweetedTweet && displayTweetId === `${item.id}:retweeted` ? item.id : displayTweetId;
708
+ const displayAuthor = displayTweet.author;
709
+ const conversation = useConversationSurface(item.id, interactionTweetId);
710
+ const visibleEntities = getVisibleEntities(displayTweet.entities, displayTweet.media, displayTweet.id, displayTweet.text);
711
+ const hiddenMediaUrlRanges = getHiddenMediaUrlRanges(displayTweet.entities, displayTweet.media, displayTweet.id, displayTweet.text);
712
+ const visibleUrlCards = getVisibleUrlCards(visibleEntities, item.retweetedTweet ? null : item.quotedTweet?.id ?? null);
713
+ const displayMediaCount = item.retweetedTweet ? displayTweet.mediaCount ?? displayTweet.media.length : item.mediaCount;
714
+ const displayIsReplied = displayTweet.isReplied ?? item.isReplied;
715
+ const displayLikeCount = displayTweet.likeCount ?? item.likeCount;
716
+ const displayBookmarked = displayTweet.bookmarked ?? item.bookmarked;
717
+ const displayLiked = displayTweet.liked ?? item.liked;
718
+ const showLikeIndicator = displayLiked || displayLikeCount > 0;
719
+ const showMediaIndicator = displayMediaCount > 0;
720
+ const hasConversation = Boolean(item.retweetedTweet ? displayTweet.replyToId : item.replyToTweet || item.replyToId);
721
+ return /* @__PURE__ */ jsxs("article", {
722
+ className: cx(feedRowClass, "cursor-pointer [content-visibility:auto] [contain-intrinsic-size:auto_280px]"),
723
+ "data-perf": "timeline-card",
724
+ onFocus: conversation.prefetch,
725
+ onMouseEnter: conversation.prefetch,
726
+ onClick: (event) => {
727
+ if (isInteractiveTarget(event.target)) return;
728
+ conversation.toggle();
729
+ },
730
+ children: [/* @__PURE__ */ jsx(AvatarChip, {
731
+ avatarUrl: displayAuthor.avatarUrl,
732
+ hue: displayAuthor.avatarHue,
733
+ name: displayAuthor.displayName,
734
+ profileId: displayAuthor.id
735
+ }), /* @__PURE__ */ jsxs("div", {
736
+ className: feedRowBodyClass,
737
+ children: [
738
+ item.retweetedTweet ? /* @__PURE__ */ jsxs("div", {
739
+ className: "inline-flex items-center gap-2 text-[13px] font-medium text-[var(--ink-soft)]",
740
+ children: [/* @__PURE__ */ jsx(Repeat2, {
741
+ className: "size-4",
742
+ strokeWidth: 1.8
743
+ }), /* @__PURE__ */ jsx(ProfilePreview, {
744
+ profile: item.author,
745
+ children: /* @__PURE__ */ jsxs("span", { children: [item.author.displayName, " reposted"] })
746
+ })]
747
+ }) : null,
748
+ /* @__PURE__ */ jsxs("header", {
749
+ className: feedRowHeaderClass,
750
+ children: [
751
+ /* @__PURE__ */ jsx(ProfilePreview, {
752
+ profile: displayAuthor,
753
+ children: /* @__PURE__ */ jsxs("span", {
754
+ className: "flex min-w-0 items-center gap-1.5",
755
+ children: [/* @__PURE__ */ jsx("span", {
756
+ className: feedRowNameClass,
757
+ children: displayAuthor.displayName
758
+ }), /* @__PURE__ */ jsxs("span", {
759
+ className: feedRowHandleClass,
760
+ children: ["@", displayAuthor.handle]
761
+ })]
762
+ })
763
+ }),
764
+ /* @__PURE__ */ jsx("span", {
765
+ className: feedRowDotClass,
766
+ children: "·"
767
+ }),
768
+ /* @__PURE__ */ jsx(SmartTimestamp, {
769
+ className: feedRowTimestampClass,
770
+ value: displayTweet.createdAt
771
+ }),
772
+ canReply || hasConversation ? /* @__PURE__ */ jsxs("span", {
773
+ className: "ml-auto inline-flex items-center gap-1",
774
+ children: [hasConversation ? /* @__PURE__ */ jsxs("span", {
775
+ "aria-label": "Part of a conversation",
776
+ className: cx(feedRowStatePillClass, feedRowStatePillActiveClass),
777
+ title: "Part of a conversation",
778
+ children: [/* @__PURE__ */ jsx(MessageCircle, {
779
+ className: "size-3.5",
780
+ strokeWidth: 2
781
+ }), "thread"]
782
+ }) : null, canReply ? /* @__PURE__ */ jsxs("span", {
783
+ "aria-label": displayIsReplied ? "We replied" : "Reply open",
784
+ className: cx(feedRowStatePillClass, displayIsReplied ? feedRowStatePillActiveClass : feedRowStatePillOpenClass),
785
+ title: displayIsReplied ? "We replied" : "Reply open",
786
+ children: [displayIsReplied ? /* @__PURE__ */ jsx(CheckCircle2, {
787
+ className: "size-3.5",
788
+ strokeWidth: 2
789
+ }) : /* @__PURE__ */ jsx(Circle, {
790
+ className: "size-3",
791
+ strokeWidth: 2.2
792
+ }), displayIsReplied ? "replied" : "open"]
793
+ }) : null]
794
+ }) : null
795
+ ]
796
+ }),
797
+ /* @__PURE__ */ jsx(TweetPresentation, {
798
+ hiddenUrlRanges: hiddenMediaUrlRanges,
799
+ quotedTweet: item.retweetedTweet ? null : item.quotedTweet,
800
+ replyToTweet: item.retweetedTweet ? null : item.replyToTweet,
801
+ tweet: displayTweet,
802
+ visibleUrlCards
803
+ }),
804
+ /* @__PURE__ */ jsx("footer", {
805
+ className: feedRowActionsClass,
806
+ children: /* @__PURE__ */ jsxs("div", {
807
+ className: "flex items-center gap-3 text-[13px] text-[var(--ink-soft)]",
808
+ children: [
809
+ /* @__PURE__ */ jsxs("button", {
810
+ "aria-expanded": conversation.isOpen,
811
+ "aria-label": conversation.isOpen ? "Hide conversation" : "Show conversation",
812
+ className: feedActionButtonClass,
813
+ onClick: (event) => {
814
+ event.stopPropagation();
815
+ conversation.toggle();
816
+ },
817
+ type: "button",
818
+ children: [/* @__PURE__ */ jsx("span", {
819
+ className: feedActionIconWrapClass,
820
+ children: /* @__PURE__ */ jsx(MessageCircle, {
821
+ className: feedActionIconClass,
822
+ strokeWidth: 1.7
823
+ })
824
+ }), /* @__PURE__ */ jsx("span", {
825
+ className: "text-[13px]",
826
+ children: conversation.isOpen ? "Hide thread" : "Thread"
827
+ })]
828
+ }),
829
+ canReply ? /* @__PURE__ */ jsxs("button", {
830
+ className: feedActionButtonClass,
831
+ onClick: (event) => {
832
+ event.stopPropagation();
833
+ onReply(interactionTweetId);
834
+ },
835
+ type: "button",
836
+ "aria-label": "Reply",
837
+ children: [/* @__PURE__ */ jsx("span", {
838
+ className: feedActionIconWrapClass,
839
+ children: /* @__PURE__ */ jsx(MessageCircle, {
840
+ className: feedActionIconClass,
841
+ strokeWidth: 1.7
842
+ })
843
+ }), /* @__PURE__ */ jsx("span", {
844
+ className: "text-[13px]",
845
+ children: "Reply"
846
+ })]
847
+ }) : null,
848
+ /* @__PURE__ */ jsxs("a", {
849
+ "aria-label": `Analyse @${displayAuthor.handle}`,
850
+ className: feedActionButtonClass,
851
+ href: `/profiles/${encodeURIComponent(displayAuthor.handle)}`,
852
+ onClick: (event) => {
853
+ event.stopPropagation();
854
+ },
855
+ title: `Analyse @${displayAuthor.handle}`,
856
+ children: [/* @__PURE__ */ jsx("span", {
857
+ className: feedActionIconWrapClass,
858
+ children: /* @__PURE__ */ jsx(UserSearch, {
859
+ className: feedActionIconClass,
860
+ strokeWidth: 1.7
861
+ })
862
+ }), /* @__PURE__ */ jsx("span", {
863
+ className: "text-[13px]",
864
+ children: "Analyse"
865
+ })]
866
+ }),
867
+ showLikeIndicator ? /* @__PURE__ */ jsxs("span", {
868
+ "aria-label": `${formatCompactNumber(displayLikeCount)} likes`,
869
+ className: cx("inline-flex items-center gap-1 px-2 py-1 text-[13px]", displayLiked && "text-[var(--like)]"),
870
+ title: `${formatCompactNumber(displayLikeCount)} likes`,
871
+ children: [/* @__PURE__ */ jsx(Heart, {
872
+ className: feedActionIconClass,
873
+ strokeWidth: 1.7,
874
+ fill: displayLiked ? "currentColor" : "none"
875
+ }), /* @__PURE__ */ jsx("span", { children: formatCompactNumber(displayLikeCount) })]
876
+ }) : null,
877
+ displayBookmarked ? /* @__PURE__ */ jsx("span", {
878
+ "aria-label": "Bookmarked",
879
+ className: "inline-flex items-center px-2 py-1",
880
+ title: "Bookmarked",
881
+ children: /* @__PURE__ */ jsx(BookmarkCheck, {
882
+ className: feedActionIconClass,
883
+ strokeWidth: 1.7
884
+ })
885
+ }) : null,
886
+ showMediaIndicator ? /* @__PURE__ */ jsxs("span", {
887
+ "aria-label": `${String(displayMediaCount)} media attachments`,
888
+ className: "inline-flex items-center gap-1 px-2 py-1 text-[13px]",
889
+ title: `${String(displayMediaCount)} media attachments`,
890
+ children: [/* @__PURE__ */ jsx(Image, {
891
+ className: feedActionIconClass,
892
+ strokeWidth: 1.7
893
+ }), /* @__PURE__ */ jsx("span", { children: displayMediaCount })]
894
+ }) : null
895
+ ]
896
+ })
897
+ }),
898
+ conversation.isOpen ? /* @__PURE__ */ jsx(ConversationThread, {
899
+ anchorId: interactionTweetId,
900
+ error: conversation.error,
901
+ items: conversation.items,
902
+ loading: conversation.loading
903
+ }) : null
904
+ ]
905
+ })]
906
+ });
907
+ }
908
+ //#endregion
909
+ //#region src/components/TimelineFeedShell.tsx
910
+ function TimelineFeedHeader({ title, subtitles, action, controls }) {
911
+ return /* @__PURE__ */ jsxs("header", {
912
+ className: pageHeaderClass,
913
+ children: [/* @__PURE__ */ jsxs("div", {
914
+ className: pageHeaderRowClass,
915
+ children: [/* @__PURE__ */ jsxs("div", {
916
+ className: "flex min-w-0 flex-col",
917
+ children: [/* @__PURE__ */ jsx("h1", {
918
+ className: pageTitleClass,
919
+ children: title
920
+ }), subtitles]
921
+ }), action]
922
+ }), controls]
923
+ });
924
+ }
925
+ function TimelineHeaderSubtitle({ children }) {
926
+ return /* @__PURE__ */ jsx("p", {
927
+ className: pageSubtitleClass,
928
+ children
929
+ });
930
+ }
931
+ function TimelineSearchField({ value, onChange, placeholder }) {
932
+ return /* @__PURE__ */ jsx("div", {
933
+ className: "px-4 pb-3",
934
+ children: /* @__PURE__ */ jsxs("label", {
935
+ className: searchFieldShellClass,
936
+ children: [/* @__PURE__ */ jsx(Search, {
937
+ className: searchFieldIconClass,
938
+ strokeWidth: 2
939
+ }), /* @__PURE__ */ jsx("input", {
940
+ className: searchFieldInputClass,
941
+ onChange: (event) => onChange(event.target.value),
942
+ placeholder,
943
+ value
944
+ })]
945
+ })
946
+ });
947
+ }
948
+ function TimelineFeedShell({ header, notice, loading, loadingLabel, loadingDetail, error, errorTitle, onRetry, empty, emptyLabel, emptyDetail, children, hasMore, loadingMore, onLoadMore }) {
949
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
950
+ header,
951
+ notice,
952
+ /* @__PURE__ */ jsx(ConversationSurfaceScope, { children: /* @__PURE__ */ jsxs("section", {
953
+ className: feedClass,
954
+ children: [
955
+ loading ? /* @__PURE__ */ jsx(FeedLoading, {
956
+ detail: loadingDetail,
957
+ label: loadingLabel,
958
+ children: /* @__PURE__ */ jsx(TweetSkeletonRows, {})
959
+ }) : error ? /* @__PURE__ */ jsx(FeedError, {
960
+ action: /* @__PURE__ */ jsx("button", {
961
+ className: "rounded-full bg-[var(--accent)] px-4 py-1.5 text-[14px] font-bold text-white",
962
+ onClick: onRetry,
963
+ type: "button",
964
+ children: "Retry"
965
+ }),
966
+ message: error,
967
+ title: errorTitle
968
+ }) : empty ? /* @__PURE__ */ jsx(FeedEmpty, {
969
+ detail: emptyDetail,
970
+ label: emptyLabel
971
+ }) : null,
972
+ children,
973
+ !loading && !error && hasMore ? /* @__PURE__ */ jsx("div", {
974
+ className: "flex justify-center py-4",
975
+ children: /* @__PURE__ */ jsx("button", {
976
+ className: "rounded-full bg-[var(--accent)] px-5 py-1.5 text-[14px] font-bold text-white disabled:opacity-60",
977
+ disabled: loadingMore,
978
+ onClick: onLoadMore,
979
+ type: "button",
980
+ children: loadingMore ? "Loading…" : "Load more"
981
+ })
982
+ }) : null
983
+ ]
984
+ }) })
985
+ ] });
986
+ }
987
+ //#endregion
988
+ //#region src/components/useTimelineRouteData.ts
989
+ var PAGE_SIZE = 50;
990
+ var TIMELINE_STALE_TIME_MS = 5 * 6e4;
991
+ function buildTimelineQueryUrl({ resource, search, replyFilter, likedOnly, bookmarkedOnly, selectedAccountId, pageParam }) {
992
+ const params = new URLSearchParams({
993
+ resource,
994
+ limit: String(PAGE_SIZE)
995
+ });
996
+ if (selectedAccountId) params.set("account", selectedAccountId);
997
+ if (replyFilter) params.set("replyFilter", replyFilter);
998
+ if (likedOnly) params.set("liked", "true");
999
+ if (bookmarkedOnly) params.set("bookmarked", "true");
1000
+ if (search.trim()) params.set("search", search.trim());
1001
+ if (pageParam) {
1002
+ params.set("until", pageParam.until);
1003
+ params.set("untilId", pageParam.untilId);
1004
+ }
1005
+ params.sort();
1006
+ const base = typeof window === "undefined" ? "http://birdclaw.local" : window.location.origin;
1007
+ return new URL(`/api/query?${params.toString()}`, base).toString();
1008
+ }
1009
+ function useTimelineRouteData({ resource, search, errorFallback, replyFilter, likedOnly = false, bookmarkedOnly = false }) {
1010
+ const queryClient = useQueryClient();
1011
+ const meta = useQuery({
1012
+ queryKey: queryKeys.status,
1013
+ queryFn: ({ signal }) => fetchQueryEnvelope({ signal })
1014
+ }).data ?? null;
1015
+ const selectedAccountId = useSelectedAccountId(meta?.accounts);
1016
+ const debouncedSearch = useDebouncedValue(search, 180);
1017
+ const timelineQueryKey = [...queryKeys.timelines, {
1018
+ resource,
1019
+ search: debouncedSearch,
1020
+ replyFilter: replyFilter ?? "all",
1021
+ likedOnly,
1022
+ bookmarkedOnly,
1023
+ selectedAccountId: selectedAccountId ?? null
1024
+ }];
1025
+ const timelineQuery = useInfiniteQuery({
1026
+ queryKey: timelineQueryKey,
1027
+ initialPageParam: void 0,
1028
+ queryFn: ({ pageParam, signal }) => fetchQueryResponse(buildTimelineQueryUrl({
1029
+ resource,
1030
+ search: debouncedSearch,
1031
+ replyFilter,
1032
+ likedOnly,
1033
+ bookmarkedOnly,
1034
+ selectedAccountId,
1035
+ pageParam
1036
+ }), { signal }),
1037
+ getNextPageParam: (lastPage) => {
1038
+ if (lastPage.resource === "dms") return void 0;
1039
+ const items = lastPage.items;
1040
+ const lastItem = items.at(-1);
1041
+ return items.length >= PAGE_SIZE && lastItem ? {
1042
+ until: lastItem.createdAt,
1043
+ untilId: lastItem.id
1044
+ } : void 0;
1045
+ },
1046
+ staleTime: TIMELINE_STALE_TIME_MS
1047
+ });
1048
+ const items = useMemo(() => {
1049
+ const seen = /* @__PURE__ */ new Set();
1050
+ const merged = [];
1051
+ for (const page of timelineQuery.data?.pages ?? []) {
1052
+ if (page.resource === "dms") continue;
1053
+ for (const item of page.items) {
1054
+ if (seen.has(item.id)) continue;
1055
+ seen.add(item.id);
1056
+ merged.push(item);
1057
+ }
1058
+ }
1059
+ return merged;
1060
+ }, [timelineQuery.data]);
1061
+ const replyMutation = useMutation({
1062
+ mutationFn: ({ tweetId, text }) => postAction({
1063
+ kind: "replyTweet",
1064
+ accountId: selectedAccountId ?? "acct_primary",
1065
+ tweetId,
1066
+ text
1067
+ }),
1068
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: timelineQueryKey })
1069
+ });
1070
+ function retry() {
1071
+ timelineQuery.refetch();
1072
+ }
1073
+ function refreshLocalView() {
1074
+ Promise.all([queryClient.invalidateQueries({ queryKey: queryKeys.timelines }), queryClient.invalidateQueries({ queryKey: queryKeys.status })]);
1075
+ }
1076
+ async function replyToTweet(tweetId) {
1077
+ const text = window.prompt("Reply text");
1078
+ if (!text?.trim()) return;
1079
+ await replyMutation.mutateAsync({
1080
+ tweetId,
1081
+ text: text.trim()
1082
+ }).catch(() => {});
1083
+ }
1084
+ const queryError = timelineQuery.error;
1085
+ return {
1086
+ meta,
1087
+ items,
1088
+ loading: timelineQuery.isPending,
1089
+ error: queryError ? queryError instanceof Error ? queryError.message : errorFallback : null,
1090
+ replyError: replyMutation.error ? replyMutation.error instanceof Error ? replyMutation.error.message : "Reply failed" : null,
1091
+ retry,
1092
+ refreshLocalView,
1093
+ replyToTweet,
1094
+ selectedAccountId,
1095
+ hasMore: timelineQuery.hasNextPage,
1096
+ loadingMore: timelineQuery.isFetchingNextPage,
1097
+ loadMore: () => timelineQuery.fetchNextPage().then(() => void 0)
1098
+ };
1099
+ }
1100
+ //#endregion
1101
+ export { TimelineSearchField as a, TimelineHeaderSubtitle as i, TimelineFeedHeader as n, TimelineCard as o, TimelineFeedShell as r, useTimelineRouteData as t };