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
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import { Bt as profilePreviewTriggerClass, Ft as profilePreviewClass, It as profilePreviewHandleClass, Lt as profilePreviewHeaderClass, Nt as profilePreviewBioClass, Pt as profilePreviewCardClass, Rt as profilePreviewMetaClass, hn as tweetLinkClass, m as cx, zt as profilePreviewNameClass } from "./ui-qbmcMSSX.js";
|
|
2
|
+
import { n as avatarPath, r as formatCompactNumber, t as AvatarChip } from "./AvatarChip-zUDAEmjL.js";
|
|
3
|
+
import { Fragment, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/lib/tweet-render.ts
|
|
6
|
+
var RAW_URL_PATTERN = /https?:\/\/[^\s<>"'`]+/g;
|
|
7
|
+
var TRAILING_URL_PUNCTUATION = /[),.;:!?]+$/;
|
|
8
|
+
function displayUrlForLink(url) {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = new URL(url);
|
|
11
|
+
const host = parsed.hostname.replace(/^www\./, "");
|
|
12
|
+
const suffix = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
13
|
+
return suffix === "/" ? host : `${host}${suffix}`;
|
|
14
|
+
} catch {
|
|
15
|
+
return url;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function comparableUrl(value) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = new URL(value);
|
|
21
|
+
return `${parsed.protocol}//${parsed.hostname.replace(/^www\./, "")}${parsed.pathname}`;
|
|
22
|
+
} catch {
|
|
23
|
+
return value.split("?")[0] ?? value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isTweetArticleUrlEntity(entry, article) {
|
|
27
|
+
if (comparableUrl(entry.expandedUrl) === comparableUrl(article.url)) return true;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = new URL(entry.expandedUrl);
|
|
30
|
+
const host = parsed.hostname.replace(/^www\./, "");
|
|
31
|
+
return (host === "x.com" || host === "twitter.com") && parsed.pathname.startsWith("/i/article/");
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function asRecord(value) {
|
|
37
|
+
return value && typeof value === "object" ? value : {};
|
|
38
|
+
}
|
|
39
|
+
function tweetEntitiesFromXurl(raw) {
|
|
40
|
+
const entities = asRecord(raw);
|
|
41
|
+
const rawMentions = Array.isArray(entities.mentions) ? entities.mentions : [];
|
|
42
|
+
const rawUrls = Array.isArray(entities.urls) ? entities.urls : [];
|
|
43
|
+
const rawHashtags = Array.isArray(entities.hashtags) ? entities.hashtags : [];
|
|
44
|
+
const rawArticle = asRecord(entities.article);
|
|
45
|
+
const articleTitle = String(rawArticle.title ?? "").trim();
|
|
46
|
+
const articleUrl = String(rawArticle.url ?? "").trim();
|
|
47
|
+
return {
|
|
48
|
+
...rawMentions.length ? { mentions: rawMentions.map((mention) => {
|
|
49
|
+
const value = asRecord(mention);
|
|
50
|
+
return {
|
|
51
|
+
username: String(value.username ?? ""),
|
|
52
|
+
id: typeof value.id === "string" ? String(value.id) : void 0,
|
|
53
|
+
start: Number(value.start ?? 0),
|
|
54
|
+
end: Number(value.end ?? 0)
|
|
55
|
+
};
|
|
56
|
+
}) } : {},
|
|
57
|
+
...rawUrls.length ? { urls: rawUrls.map((url) => {
|
|
58
|
+
const value = asRecord(url);
|
|
59
|
+
const expandedUrl = String(value.expandedUrl ?? value.expanded_url ?? value.url ?? "");
|
|
60
|
+
return {
|
|
61
|
+
url: String(value.url ?? ""),
|
|
62
|
+
expandedUrl,
|
|
63
|
+
displayUrl: String(value.displayUrl ?? value.display_url ?? expandedUrl ?? value.url ?? ""),
|
|
64
|
+
start: Number(value.start ?? 0),
|
|
65
|
+
end: Number(value.end ?? 0),
|
|
66
|
+
...typeof value.title === "string" ? { title: value.title } : {},
|
|
67
|
+
...typeof value.description === "string" || value.description === null ? { description: value.description } : {},
|
|
68
|
+
...typeof value.imageUrl === "string" ? { imageUrl: value.imageUrl } : typeof value.image_url === "string" ? { imageUrl: value.image_url } : {},
|
|
69
|
+
...typeof value.siteName === "string" ? { siteName: value.siteName } : typeof value.site_name === "string" ? { siteName: value.site_name } : {}
|
|
70
|
+
};
|
|
71
|
+
}) } : {},
|
|
72
|
+
...rawHashtags.length ? { hashtags: rawHashtags.map((hashtag) => {
|
|
73
|
+
const value = asRecord(hashtag);
|
|
74
|
+
return {
|
|
75
|
+
tag: String(value.tag ?? value.text ?? ""),
|
|
76
|
+
start: Number(value.start ?? 0),
|
|
77
|
+
end: Number(value.end ?? 0)
|
|
78
|
+
};
|
|
79
|
+
}) } : {},
|
|
80
|
+
...articleTitle && articleUrl ? { article: {
|
|
81
|
+
title: articleTitle,
|
|
82
|
+
url: articleUrl,
|
|
83
|
+
...typeof (rawArticle.previewText ?? rawArticle.preview_text) === "string" && String(rawArticle.previewText ?? rawArticle.preview_text).trim() ? { previewText: String(rawArticle.previewText ?? rawArticle.preview_text).trim() } : {},
|
|
84
|
+
...typeof (rawArticle.coverImageUrl ?? rawArticle.cover_image_url) === "string" && String(rawArticle.coverImageUrl ?? rawArticle.cover_image_url).trim() ? { coverImageUrl: String(rawArticle.coverImageUrl ?? rawArticle.cover_image_url).trim() } : {}
|
|
85
|
+
} } : {}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function profileDescriptionEntitiesFromXurl(raw) {
|
|
89
|
+
return tweetEntitiesFromXurl(asRecord(raw).description);
|
|
90
|
+
}
|
|
91
|
+
function spansOverlap(leftStart, leftEnd, rightStart, rightEnd) {
|
|
92
|
+
return leftStart < rightEnd && rightStart < leftEnd;
|
|
93
|
+
}
|
|
94
|
+
function stringIndexFromCodePointIndex(text, index) {
|
|
95
|
+
if (index <= 0) return 0;
|
|
96
|
+
let stringIndex = 0;
|
|
97
|
+
let codePointIndex = 0;
|
|
98
|
+
for (const character of text) {
|
|
99
|
+
if (codePointIndex >= index) break;
|
|
100
|
+
stringIndex += character.length;
|
|
101
|
+
codePointIndex += 1;
|
|
102
|
+
}
|
|
103
|
+
return stringIndex;
|
|
104
|
+
}
|
|
105
|
+
function segmentTextMatches(text, segment, start, end) {
|
|
106
|
+
const slice = text.slice(start, end);
|
|
107
|
+
if (segment.kind === "mention") return slice.toLowerCase() === `@${segment.username}`.toLowerCase();
|
|
108
|
+
if (segment.kind === "hashtag") return slice.toLowerCase() === `#${segment.tag}`.toLowerCase();
|
|
109
|
+
return slice === segment.url;
|
|
110
|
+
}
|
|
111
|
+
function normalizeSegmentTextRange(text, segment) {
|
|
112
|
+
if (segment.start >= 0 && segment.end > segment.start && segment.end <= text.length && segmentTextMatches(text, segment, segment.start, segment.end)) return segment;
|
|
113
|
+
const start = stringIndexFromCodePointIndex(text, segment.start);
|
|
114
|
+
const end = stringIndexFromCodePointIndex(text, segment.end);
|
|
115
|
+
if (start >= 0 && end > start && end <= text.length && segmentTextMatches(text, segment, start, end)) return {
|
|
116
|
+
...segment,
|
|
117
|
+
start,
|
|
118
|
+
end
|
|
119
|
+
};
|
|
120
|
+
return segment;
|
|
121
|
+
}
|
|
122
|
+
function normalizeTweetUrlEntityRangeForText(text, entry) {
|
|
123
|
+
const normalized = normalizeSegmentTextRange(text, {
|
|
124
|
+
...entry,
|
|
125
|
+
kind: "url"
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
start: normalized.start,
|
|
129
|
+
end: normalized.end
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function enrichFallbackUrlEntities(text, entities, resolveExpansion) {
|
|
133
|
+
const enrichedExistingUrls = (entities.urls ?? []).map((entry) => {
|
|
134
|
+
const expansion = resolveExpansion?.(entry.url);
|
|
135
|
+
if (!expansion) return entry;
|
|
136
|
+
const expandedUrl = expansion.expandedUrl || entry.expandedUrl;
|
|
137
|
+
return {
|
|
138
|
+
...entry,
|
|
139
|
+
expandedUrl,
|
|
140
|
+
displayUrl: expansion.displayUrl || displayUrlForLink(expandedUrl),
|
|
141
|
+
...expansion.title ? { title: expansion.title } : {},
|
|
142
|
+
...expansion && "description" in expansion ? { description: expansion.description ?? null } : {},
|
|
143
|
+
...expansion.imageUrl ? { imageUrl: expansion.imageUrl } : {},
|
|
144
|
+
...expansion.siteName ? { siteName: expansion.siteName } : {}
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
const fallbackUrls = [];
|
|
148
|
+
for (const match of text.matchAll(RAW_URL_PATTERN)) {
|
|
149
|
+
const url = match[0].replace(TRAILING_URL_PUNCTUATION, "");
|
|
150
|
+
const start = match.index ?? 0;
|
|
151
|
+
const end = start + url.length;
|
|
152
|
+
if (!url) continue;
|
|
153
|
+
if (enrichedExistingUrls.some((entry) => spansOverlap(start, end, entry.start, entry.end))) continue;
|
|
154
|
+
const expansion = resolveExpansion?.(url);
|
|
155
|
+
const expandedUrl = expansion?.expandedUrl || url;
|
|
156
|
+
fallbackUrls.push({
|
|
157
|
+
url,
|
|
158
|
+
expandedUrl,
|
|
159
|
+
displayUrl: expansion?.displayUrl || displayUrlForLink(expandedUrl),
|
|
160
|
+
start,
|
|
161
|
+
end,
|
|
162
|
+
...expansion?.title ? { title: expansion.title } : {},
|
|
163
|
+
...expansion && "description" in expansion ? { description: expansion.description ?? null } : {},
|
|
164
|
+
...expansion?.imageUrl ? { imageUrl: expansion.imageUrl } : {},
|
|
165
|
+
...expansion?.siteName ? { siteName: expansion.siteName } : {}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (fallbackUrls.length === 0) return {
|
|
169
|
+
...entities,
|
|
170
|
+
urls: enrichedExistingUrls
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
...entities,
|
|
174
|
+
urls: [...enrichedExistingUrls, ...fallbackUrls].sort((left, right) => left.start - right.start)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function collectTweetSegments(entities) {
|
|
178
|
+
return [
|
|
179
|
+
...entities.mentions?.map((entry) => ({
|
|
180
|
+
...entry,
|
|
181
|
+
kind: "mention"
|
|
182
|
+
})) ?? [],
|
|
183
|
+
...entities.urls?.map((entry) => ({
|
|
184
|
+
...entry,
|
|
185
|
+
kind: "url"
|
|
186
|
+
})) ?? [],
|
|
187
|
+
...entities.hashtags?.map((entry) => ({
|
|
188
|
+
...entry,
|
|
189
|
+
kind: "hashtag"
|
|
190
|
+
})) ?? []
|
|
191
|
+
].sort((left, right) => left.start - right.start);
|
|
192
|
+
}
|
|
193
|
+
function collectTweetSegmentsForText(text, entities) {
|
|
194
|
+
return collectTweetSegments(entities).map((segment) => normalizeSegmentTextRange(text, segment)).sort((left, right) => left.start - right.start);
|
|
195
|
+
}
|
|
196
|
+
function renderTweetText(text, entities, renderSegment) {
|
|
197
|
+
const segments = collectTweetSegmentsForText(text, entities);
|
|
198
|
+
let cursor = 0;
|
|
199
|
+
let output = "";
|
|
200
|
+
for (const segment of segments) {
|
|
201
|
+
if (segment.start < cursor || segment.end <= segment.start || segment.end > text.length) continue;
|
|
202
|
+
output += text.slice(cursor, segment.start);
|
|
203
|
+
const fallback = text.slice(segment.start, segment.end);
|
|
204
|
+
output += renderSegment(segment, fallback);
|
|
205
|
+
cursor = segment.end;
|
|
206
|
+
}
|
|
207
|
+
output += text.slice(cursor);
|
|
208
|
+
return output;
|
|
209
|
+
}
|
|
210
|
+
function renderTweetPlainText(text, entities) {
|
|
211
|
+
return renderTweetText(text, entities, (segment, fallback) => {
|
|
212
|
+
if (segment.kind === "url") return segment.expandedUrl;
|
|
213
|
+
if (segment.kind === "mention") return `@${segment.username}`;
|
|
214
|
+
if (segment.kind === "hashtag") return `#${segment.tag}`;
|
|
215
|
+
return fallback;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/lib/url-safety.ts
|
|
220
|
+
var PRIVATE_IPV4_RANGES = [
|
|
221
|
+
["0.0.0.0", 8],
|
|
222
|
+
["10.0.0.0", 8],
|
|
223
|
+
["100.64.0.0", 10],
|
|
224
|
+
["127.0.0.0", 8],
|
|
225
|
+
["169.254.0.0", 16],
|
|
226
|
+
["172.16.0.0", 12],
|
|
227
|
+
["192.0.0.0", 24],
|
|
228
|
+
["192.0.2.0", 24],
|
|
229
|
+
["192.168.0.0", 16],
|
|
230
|
+
["198.18.0.0", 15],
|
|
231
|
+
["198.51.100.0", 24],
|
|
232
|
+
["203.0.113.0", 24],
|
|
233
|
+
["224.0.0.0", 4],
|
|
234
|
+
["240.0.0.0", 4]
|
|
235
|
+
];
|
|
236
|
+
function ipv4ToNumber(value) {
|
|
237
|
+
const parts = value.split(".").map((part) => Number(part));
|
|
238
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part))) return null;
|
|
239
|
+
if (parts.some((part) => part < 0 || part > 255)) return null;
|
|
240
|
+
return (parts[0] ?? 0) * 256 ** 3 + (parts[1] ?? 0) * 256 ** 2 + (parts[2] ?? 0) * 256 + (parts[3] ?? 0);
|
|
241
|
+
}
|
|
242
|
+
function isIpv4InRange(address, range, prefix) {
|
|
243
|
+
const addressNumber = ipv4ToNumber(address);
|
|
244
|
+
const rangeNumber = ipv4ToNumber(range);
|
|
245
|
+
if (addressNumber === null || rangeNumber === null) return false;
|
|
246
|
+
const mask = prefix === 0 ? 0 : 4294967295 << 32 - prefix >>> 0;
|
|
247
|
+
return (addressNumber & mask) === (rangeNumber & mask);
|
|
248
|
+
}
|
|
249
|
+
function isPrivateIpv4(address) {
|
|
250
|
+
return PRIVATE_IPV4_RANGES.some(([range, prefix]) => isIpv4InRange(address, range, prefix));
|
|
251
|
+
}
|
|
252
|
+
function normalizeIpv6(address) {
|
|
253
|
+
return address.toLowerCase().replace(/^\[|\]$/g, "");
|
|
254
|
+
}
|
|
255
|
+
function parseIpv6Parts(address) {
|
|
256
|
+
let normalized = normalizeIpv6(address);
|
|
257
|
+
if (!normalized.includes(":")) return null;
|
|
258
|
+
if (normalized.includes(".")) {
|
|
259
|
+
const lastColon = normalized.lastIndexOf(":");
|
|
260
|
+
const addressNumber = ipv4ToNumber(normalized.slice(lastColon + 1));
|
|
261
|
+
if (addressNumber === null) return null;
|
|
262
|
+
normalized = `${normalized.slice(0, lastColon + 1)}${(addressNumber >>> 16 & 65535).toString(16)}:${(addressNumber & 65535).toString(16)}`;
|
|
263
|
+
}
|
|
264
|
+
const halves = normalized.split("::");
|
|
265
|
+
if (halves.length > 2) return null;
|
|
266
|
+
const parseGroups = (value) => value === "" ? [] : value.split(":").map((part) => Number.parseInt(part, 16));
|
|
267
|
+
const left = parseGroups(halves[0] ?? "");
|
|
268
|
+
const right = halves.length === 2 ? parseGroups(halves[1] ?? "") : [];
|
|
269
|
+
const missingGroups = 8 - left.length - right.length;
|
|
270
|
+
if (halves.length === 1 && missingGroups !== 0 || halves.length === 2 && missingGroups < 0 || ![...left, ...right].every((part) => Number.isInteger(part) && part >= 0 && part <= 65535)) return null;
|
|
271
|
+
return [
|
|
272
|
+
...left,
|
|
273
|
+
...Array.from({ length: missingGroups }, () => 0),
|
|
274
|
+
...right
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
function ipv4FromHexPair(parts) {
|
|
278
|
+
if (parts.length !== 2) return null;
|
|
279
|
+
const high = Number.parseInt(parts[0] ?? "", 16);
|
|
280
|
+
const low = Number.parseInt(parts[1] ?? "", 16);
|
|
281
|
+
if (![high, low].every((part) => Number.isInteger(part) && part >= 0 && part <= 65535)) return null;
|
|
282
|
+
return [
|
|
283
|
+
high >> 8 & 255,
|
|
284
|
+
high & 255,
|
|
285
|
+
low >> 8 & 255,
|
|
286
|
+
low & 255
|
|
287
|
+
].join(".");
|
|
288
|
+
}
|
|
289
|
+
function ipv4FromIpv6Parts(parts) {
|
|
290
|
+
const tailIpv4 = () => [
|
|
291
|
+
parts[6] >> 8 & 255,
|
|
292
|
+
parts[6] & 255,
|
|
293
|
+
parts[7] >> 8 & 255,
|
|
294
|
+
parts[7] & 255
|
|
295
|
+
].join(".");
|
|
296
|
+
const hasZeroPrefix = (length) => parts.slice(0, length).every((part) => part === 0);
|
|
297
|
+
if (hasZeroPrefix(5) && parts[5] === 65535) return tailIpv4();
|
|
298
|
+
if (hasZeroPrefix(4) && parts[4] === 65535 && parts[5] === 0) return tailIpv4();
|
|
299
|
+
if (hasZeroPrefix(6)) return tailIpv4();
|
|
300
|
+
if (parts[0] === 100 && parts[1] === 65435 && parts[2] === 0) return tailIpv4();
|
|
301
|
+
if (parts[0] === 100 && parts[1] === 65435 && parts[2] === 1) return tailIpv4();
|
|
302
|
+
if (parts[0] === 8194) return [
|
|
303
|
+
parts[1] >> 8 & 255,
|
|
304
|
+
parts[1] & 255,
|
|
305
|
+
parts[2] >> 8 & 255,
|
|
306
|
+
parts[2] & 255
|
|
307
|
+
].join(".");
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
function ipv4FromIpv6Suffix(address) {
|
|
311
|
+
const normalized = normalizeIpv6(address);
|
|
312
|
+
const parts = parseIpv6Parts(normalized);
|
|
313
|
+
if (parts) return ipv4FromIpv6Parts(parts);
|
|
314
|
+
const prefix = [
|
|
315
|
+
"::ffff:",
|
|
316
|
+
"64:ff9b::",
|
|
317
|
+
"64:ff9b:1::",
|
|
318
|
+
"::"
|
|
319
|
+
].find((value) => normalized.startsWith(value));
|
|
320
|
+
if (!prefix) return null;
|
|
321
|
+
const suffix = normalized.slice(prefix.length);
|
|
322
|
+
if (ipv4ToNumber(suffix) !== null) return suffix;
|
|
323
|
+
return ipv4FromHexPair(suffix.split(":"));
|
|
324
|
+
}
|
|
325
|
+
function isPrivateIpv6(address) {
|
|
326
|
+
const normalized = normalizeIpv6(address);
|
|
327
|
+
const parts = parseIpv6Parts(normalized);
|
|
328
|
+
const mappedIpv4 = parts ? ipv4FromIpv6Parts(parts) : ipv4FromIpv6Suffix(normalized);
|
|
329
|
+
if (mappedIpv4) return isPrivateIpv4(mappedIpv4);
|
|
330
|
+
if (parts) {
|
|
331
|
+
const first = parts[0] ?? 0;
|
|
332
|
+
return parts.every((part) => part === 0) || parts.slice(0, 7).every((part) => part === 0) || (first & 65024) === 64512 || (first & 65472) === 65152 || (first & 65472) === 65216 || (first & 65280) === 65280;
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
function isBlockedAddress(address) {
|
|
337
|
+
const normalized = address.replace(/^\[|\]$/g, "");
|
|
338
|
+
if (ipv4ToNumber(normalized) !== null) return isPrivateIpv4(normalized);
|
|
339
|
+
if (normalized.includes(":")) return isPrivateIpv6(normalized);
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
function isLocalHostname(hostname) {
|
|
343
|
+
const normalized = hostname.toLowerCase().replace(/\.$/, "");
|
|
344
|
+
return normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal") || normalized.endsWith(".test");
|
|
345
|
+
}
|
|
346
|
+
function assertSafePreviewUrl(url) {
|
|
347
|
+
const parsed = new URL(url);
|
|
348
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("Link preview URL must use http or https");
|
|
349
|
+
if (parsed.username || parsed.password) throw new Error("Link preview URL must not include credentials");
|
|
350
|
+
if (isLocalHostname(parsed.hostname) || isBlockedAddress(parsed.hostname)) throw new Error("Link preview URL points to a private host");
|
|
351
|
+
return parsed;
|
|
352
|
+
}
|
|
353
|
+
function safeHttpUrl(value) {
|
|
354
|
+
if (!value) return null;
|
|
355
|
+
try {
|
|
356
|
+
const parsed = new URL(value);
|
|
357
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
358
|
+
return parsed.toString();
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
var __test__ = {
|
|
364
|
+
assertSafePreviewUrl,
|
|
365
|
+
ipv4FromIpv6Suffix,
|
|
366
|
+
ipv4ToNumber,
|
|
367
|
+
isBlockedAddress,
|
|
368
|
+
isIpv4InRange,
|
|
369
|
+
isPrivateIpv6,
|
|
370
|
+
parseIpv6Parts,
|
|
371
|
+
safeHttpUrl
|
|
372
|
+
};
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/components/AvatarPreload.ts
|
|
375
|
+
var MAX_CONCURRENT_PRELOADS = 4;
|
|
376
|
+
var IDLE_TIMEOUT_MS = 2500;
|
|
377
|
+
var PRELOAD_ROOT_MARGIN = "800px 0px";
|
|
378
|
+
var queuedSources = [];
|
|
379
|
+
var knownSources = /* @__PURE__ */ new Set();
|
|
380
|
+
var activeImages = /* @__PURE__ */ new Set();
|
|
381
|
+
var activePreloads = 0;
|
|
382
|
+
var listeningForLoad = false;
|
|
383
|
+
var idleId = null;
|
|
384
|
+
var fallbackTimerId = null;
|
|
385
|
+
function finishPreload(image) {
|
|
386
|
+
if (!activeImages.delete(image)) return;
|
|
387
|
+
activePreloads -= 1;
|
|
388
|
+
drainPreloads();
|
|
389
|
+
}
|
|
390
|
+
function drainPreloads() {
|
|
391
|
+
while (activePreloads < MAX_CONCURRENT_PRELOADS && queuedSources.length > 0) {
|
|
392
|
+
const src = queuedSources.shift();
|
|
393
|
+
if (!src) return;
|
|
394
|
+
const image = new Image();
|
|
395
|
+
activePreloads += 1;
|
|
396
|
+
activeImages.add(image);
|
|
397
|
+
image.decoding = "async";
|
|
398
|
+
image.onerror = () => finishPreload(image);
|
|
399
|
+
image.onload = () => {
|
|
400
|
+
if (typeof image.decode !== "function") {
|
|
401
|
+
finishPreload(image);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
image.decode().catch(() => void 0).finally(() => finishPreload(image));
|
|
405
|
+
};
|
|
406
|
+
image.src = src;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function runIdlePreloads() {
|
|
410
|
+
idleId = null;
|
|
411
|
+
fallbackTimerId = null;
|
|
412
|
+
drainPreloads();
|
|
413
|
+
}
|
|
414
|
+
function scheduleIdlePreloads() {
|
|
415
|
+
if (queuedSources.length === 0 || idleId !== null || fallbackTimerId !== null) return;
|
|
416
|
+
const requestIdle = window.requestIdleCallback?.bind(window);
|
|
417
|
+
if (requestIdle) {
|
|
418
|
+
idleId = requestIdle(runIdlePreloads, { timeout: IDLE_TIMEOUT_MS });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
fallbackTimerId = window.setTimeout(runIdlePreloads, 0);
|
|
422
|
+
}
|
|
423
|
+
function handleWindowLoad() {
|
|
424
|
+
listeningForLoad = false;
|
|
425
|
+
scheduleIdlePreloads();
|
|
426
|
+
}
|
|
427
|
+
function scheduleAfterPageLoad() {
|
|
428
|
+
if (document.readyState === "complete") {
|
|
429
|
+
scheduleIdlePreloads();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (listeningForLoad) return;
|
|
433
|
+
listeningForLoad = true;
|
|
434
|
+
window.addEventListener("load", handleWindowLoad, { once: true });
|
|
435
|
+
}
|
|
436
|
+
function queueAvatarPreload(profileId, avatarUrl) {
|
|
437
|
+
if (!profileId || !avatarUrl || typeof window === "undefined") return;
|
|
438
|
+
const src = avatarPath(profileId, avatarUrl);
|
|
439
|
+
if (knownSources.has(src)) return;
|
|
440
|
+
knownSources.add(src);
|
|
441
|
+
queuedSources.push(src);
|
|
442
|
+
scheduleAfterPageLoad();
|
|
443
|
+
}
|
|
444
|
+
function useAvatarPreload(reference, profileId, avatarUrl) {
|
|
445
|
+
useEffect(() => {
|
|
446
|
+
if (!profileId || !avatarUrl) return;
|
|
447
|
+
const node = reference.current;
|
|
448
|
+
if (!node || typeof IntersectionObserver === "undefined") {
|
|
449
|
+
queueAvatarPreload(profileId, avatarUrl);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const observer = new IntersectionObserver((entries) => {
|
|
453
|
+
if (!entries.some((entry) => entry.isIntersecting)) return;
|
|
454
|
+
observer.disconnect();
|
|
455
|
+
queueAvatarPreload(profileId, avatarUrl);
|
|
456
|
+
}, { rootMargin: PRELOAD_ROOT_MARGIN });
|
|
457
|
+
observer.observe(node);
|
|
458
|
+
return () => observer.disconnect();
|
|
459
|
+
}, [
|
|
460
|
+
avatarUrl,
|
|
461
|
+
profileId,
|
|
462
|
+
reference
|
|
463
|
+
]);
|
|
464
|
+
}
|
|
465
|
+
//#endregion
|
|
466
|
+
//#region src/components/FloatingPreview.tsx
|
|
467
|
+
var DEFAULT_GAP = 10;
|
|
468
|
+
var DEFAULT_GUTTER = 12;
|
|
469
|
+
var DEFAULT_CLOSE_DELAY_MS = 120;
|
|
470
|
+
var initialPosition = {
|
|
471
|
+
top: 0,
|
|
472
|
+
left: 0,
|
|
473
|
+
maxWidth: 0,
|
|
474
|
+
maxHeight: 0,
|
|
475
|
+
side: "bottom",
|
|
476
|
+
measured: false,
|
|
477
|
+
ready: false
|
|
478
|
+
};
|
|
479
|
+
function edgeRect(rects, side) {
|
|
480
|
+
return rects.reduce((best, rect) => {
|
|
481
|
+
if (side === "bottom") {
|
|
482
|
+
if (rect.bottom > best.bottom) return rect;
|
|
483
|
+
if (rect.bottom === best.bottom && rect.width > best.width) return rect;
|
|
484
|
+
return best;
|
|
485
|
+
}
|
|
486
|
+
if (rect.top < best.top) return rect;
|
|
487
|
+
if (rect.top === best.top && rect.width > best.width) return rect;
|
|
488
|
+
return best;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
function clamp(value, minimum, maximum) {
|
|
492
|
+
if (maximum < minimum) return minimum;
|
|
493
|
+
return Math.min(Math.max(value, minimum), maximum);
|
|
494
|
+
}
|
|
495
|
+
function calculatePreviewPosition({ referenceRects, floatingWidth, floatingHeight, viewport, gap = DEFAULT_GAP, gutter = DEFAULT_GUTTER }) {
|
|
496
|
+
const referenceTop = Math.min(...referenceRects.map((rect) => rect.top));
|
|
497
|
+
const referenceBottom = Math.max(...referenceRects.map((rect) => rect.bottom));
|
|
498
|
+
const availableBelow = Math.max(0, viewport.bottom - gutter - referenceBottom - gap);
|
|
499
|
+
const availableAbove = Math.max(0, referenceTop - viewport.top - gutter - gap);
|
|
500
|
+
const side = availableBelow >= floatingHeight || availableBelow >= availableAbove ? "bottom" : "top";
|
|
501
|
+
const availableHeight = side === "bottom" ? availableBelow : availableAbove;
|
|
502
|
+
const renderedHeight = Math.min(floatingHeight, availableHeight);
|
|
503
|
+
const anchor = edgeRect(referenceRects, side);
|
|
504
|
+
const maxWidth = Math.max(0, viewport.width - gutter * 2);
|
|
505
|
+
const renderedWidth = Math.min(floatingWidth, maxWidth);
|
|
506
|
+
const minimumLeft = viewport.left + gutter;
|
|
507
|
+
const maximumLeft = viewport.right - gutter - renderedWidth;
|
|
508
|
+
const left = clamp(anchor.left + anchor.width / 2 - renderedWidth / 2, minimumLeft, maximumLeft);
|
|
509
|
+
return {
|
|
510
|
+
top: side === "bottom" ? referenceBottom + gap : referenceTop - gap - renderedHeight,
|
|
511
|
+
left,
|
|
512
|
+
maxWidth,
|
|
513
|
+
maxHeight: availableHeight,
|
|
514
|
+
side,
|
|
515
|
+
measured: true,
|
|
516
|
+
ready: true
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function viewportRect() {
|
|
520
|
+
const viewport = window.visualViewport;
|
|
521
|
+
const left = viewport?.offsetLeft ?? 0;
|
|
522
|
+
const top = viewport?.offsetTop ?? 0;
|
|
523
|
+
const width = viewport?.width ?? window.innerWidth;
|
|
524
|
+
const height = viewport?.height ?? window.innerHeight;
|
|
525
|
+
return {
|
|
526
|
+
top,
|
|
527
|
+
right: left + width,
|
|
528
|
+
bottom: top + height,
|
|
529
|
+
left,
|
|
530
|
+
width,
|
|
531
|
+
height
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function elementRects(element) {
|
|
535
|
+
const fragments = Array.from(element.getClientRects()).filter((rect) => rect.width > 0 || rect.height > 0);
|
|
536
|
+
return fragments.length > 0 ? fragments : [element.getBoundingClientRect()];
|
|
537
|
+
}
|
|
538
|
+
function referenceIntersectsViewport(rects, viewport) {
|
|
539
|
+
return rects.some((rect) => rect.right >= viewport.left && rect.left <= viewport.right && rect.bottom >= viewport.top && rect.top <= viewport.bottom);
|
|
540
|
+
}
|
|
541
|
+
function samePosition(left, right) {
|
|
542
|
+
return left.top === right.top && left.left === right.left && left.maxWidth === right.maxWidth && left.maxHeight === right.maxHeight && left.side === right.side && left.measured === right.measured && left.ready === right.ready;
|
|
543
|
+
}
|
|
544
|
+
function floatingStyle(position) {
|
|
545
|
+
return {
|
|
546
|
+
position: "fixed",
|
|
547
|
+
top: position.top,
|
|
548
|
+
left: position.left,
|
|
549
|
+
maxWidth: position.measured ? position.maxWidth : void 0,
|
|
550
|
+
maxHeight: position.measured ? position.maxHeight : void 0,
|
|
551
|
+
visibility: position.ready ? "visible" : "hidden"
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function containsTarget(target, ...elements) {
|
|
555
|
+
return target instanceof Node && elements.some((element) => element?.contains(target));
|
|
556
|
+
}
|
|
557
|
+
function useFloatingPreview(options = {}) {
|
|
558
|
+
const closeDelayMs = options.closeDelayMs ?? DEFAULT_CLOSE_DELAY_MS;
|
|
559
|
+
const [open, setOpen] = useState(false);
|
|
560
|
+
const [position, setPosition] = useState(initialPosition);
|
|
561
|
+
const floatingId = useId();
|
|
562
|
+
const referenceRef = useRef(null);
|
|
563
|
+
const floatingRef = useRef(null);
|
|
564
|
+
const closeTimerRef = useRef(null);
|
|
565
|
+
const clearCloseTimer = useCallback(() => {
|
|
566
|
+
if (closeTimerRef.current === null) return;
|
|
567
|
+
window.clearTimeout(closeTimerRef.current);
|
|
568
|
+
closeTimerRef.current = null;
|
|
569
|
+
}, []);
|
|
570
|
+
const openPreview = useCallback(() => {
|
|
571
|
+
clearCloseTimer();
|
|
572
|
+
if (!open) setPosition(initialPosition);
|
|
573
|
+
setOpen(true);
|
|
574
|
+
}, [clearCloseTimer, open]);
|
|
575
|
+
const closePreview = useCallback(() => {
|
|
576
|
+
clearCloseTimer();
|
|
577
|
+
setOpen(false);
|
|
578
|
+
}, [clearCloseTimer]);
|
|
579
|
+
const scheduleClose = useCallback(() => {
|
|
580
|
+
clearCloseTimer();
|
|
581
|
+
closeTimerRef.current = window.setTimeout(() => {
|
|
582
|
+
closeTimerRef.current = null;
|
|
583
|
+
const reference = referenceRef.current;
|
|
584
|
+
const floating = floatingRef.current;
|
|
585
|
+
const focusInside = containsTarget(document.activeElement, reference, floating);
|
|
586
|
+
const pointerInside = Boolean(reference?.matches(":hover")) || Boolean(floating?.matches(":hover"));
|
|
587
|
+
if (!focusInside && !pointerInside) setOpen(false);
|
|
588
|
+
}, closeDelayMs);
|
|
589
|
+
}, [clearCloseTimer, closeDelayMs]);
|
|
590
|
+
const handleBlur = useCallback((event) => {
|
|
591
|
+
if (containsTarget(event.relatedTarget, referenceRef.current, floatingRef.current)) return;
|
|
592
|
+
scheduleClose();
|
|
593
|
+
}, [scheduleClose]);
|
|
594
|
+
const handleKeyDown = useCallback((event) => {
|
|
595
|
+
if (event.key !== "Escape") return;
|
|
596
|
+
closePreview();
|
|
597
|
+
referenceRef.current?.querySelector("a,button")?.focus();
|
|
598
|
+
}, [closePreview]);
|
|
599
|
+
const updatePlacement = useCallback(() => {
|
|
600
|
+
const reference = referenceRef.current;
|
|
601
|
+
const floating = floatingRef.current;
|
|
602
|
+
if (!reference || !floating) return;
|
|
603
|
+
const viewport = viewportRect();
|
|
604
|
+
const referenceRects = elementRects(reference);
|
|
605
|
+
if (!referenceIntersectsViewport(referenceRects, viewport)) {
|
|
606
|
+
setPosition((current) => current.ready ? {
|
|
607
|
+
...current,
|
|
608
|
+
ready: false
|
|
609
|
+
} : current);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const floatingRect = floating.getBoundingClientRect();
|
|
613
|
+
const content = floating.querySelector("[data-floating-preview-content]");
|
|
614
|
+
const style = window.getComputedStyle(floating);
|
|
615
|
+
const verticalChrome = [
|
|
616
|
+
style.paddingTop,
|
|
617
|
+
style.paddingBottom,
|
|
618
|
+
style.borderTopWidth,
|
|
619
|
+
style.borderBottomWidth
|
|
620
|
+
].reduce((total, value) => total + (Number.parseFloat(value) || 0), 0);
|
|
621
|
+
const naturalHeight = Math.max(floatingRect.height, (content?.getBoundingClientRect().height ?? 0) + verticalChrome);
|
|
622
|
+
const next = calculatePreviewPosition({
|
|
623
|
+
referenceRects,
|
|
624
|
+
floatingWidth: floatingRect.width,
|
|
625
|
+
floatingHeight: naturalHeight,
|
|
626
|
+
viewport
|
|
627
|
+
});
|
|
628
|
+
if (floatingRect.width > next.maxWidth + .5) next.ready = false;
|
|
629
|
+
setPosition((current) => samePosition(current, next) ? current : next);
|
|
630
|
+
}, []);
|
|
631
|
+
useLayoutEffect(() => {
|
|
632
|
+
if (!open) return;
|
|
633
|
+
updatePlacement();
|
|
634
|
+
let frame = 0;
|
|
635
|
+
const trackLayout = () => {
|
|
636
|
+
updatePlacement();
|
|
637
|
+
frame = window.requestAnimationFrame(trackLayout);
|
|
638
|
+
};
|
|
639
|
+
frame = window.requestAnimationFrame(trackLayout);
|
|
640
|
+
const viewport = window.visualViewport;
|
|
641
|
+
window.addEventListener("resize", updatePlacement);
|
|
642
|
+
window.addEventListener("scroll", updatePlacement, true);
|
|
643
|
+
viewport?.addEventListener("resize", updatePlacement);
|
|
644
|
+
viewport?.addEventListener("scroll", updatePlacement);
|
|
645
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(updatePlacement);
|
|
646
|
+
if (referenceRef.current) observer?.observe(referenceRef.current);
|
|
647
|
+
if (floatingRef.current) observer?.observe(floatingRef.current);
|
|
648
|
+
return () => {
|
|
649
|
+
window.cancelAnimationFrame(frame);
|
|
650
|
+
window.removeEventListener("resize", updatePlacement);
|
|
651
|
+
window.removeEventListener("scroll", updatePlacement, true);
|
|
652
|
+
viewport?.removeEventListener("resize", updatePlacement);
|
|
653
|
+
viewport?.removeEventListener("scroll", updatePlacement);
|
|
654
|
+
observer?.disconnect();
|
|
655
|
+
};
|
|
656
|
+
}, [open, updatePlacement]);
|
|
657
|
+
useEffect(() => () => clearCloseTimer(), [clearCloseTimer]);
|
|
658
|
+
const interactionProps = {
|
|
659
|
+
onPointerEnter: (_event) => openPreview(),
|
|
660
|
+
onPointerLeave: (_event) => scheduleClose(),
|
|
661
|
+
onFocus: (_event) => openPreview(),
|
|
662
|
+
onBlur: handleBlur,
|
|
663
|
+
onKeyDown: handleKeyDown
|
|
664
|
+
};
|
|
665
|
+
return {
|
|
666
|
+
open,
|
|
667
|
+
referenceRef,
|
|
668
|
+
floatingRef,
|
|
669
|
+
floatingStyle: floatingStyle(position),
|
|
670
|
+
referenceProps: interactionProps,
|
|
671
|
+
floatingProps: interactionProps,
|
|
672
|
+
floatingId,
|
|
673
|
+
closePreview
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
//#endregion
|
|
677
|
+
//#region src/components/ProfilePreview.tsx
|
|
678
|
+
function ProfilePreviewBio({ profile }) {
|
|
679
|
+
const segments = collectTweetSegmentsForText(profile.bio, profileDescriptionEntitiesFromXurl(profile.entities));
|
|
680
|
+
let cursor = 0;
|
|
681
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
682
|
+
className: profilePreviewBioClass,
|
|
683
|
+
children: [segments.map((segment, index) => {
|
|
684
|
+
if (segment.kind !== "url" || segment.start < cursor || segment.end <= segment.start || segment.end > profile.bio.length) return null;
|
|
685
|
+
const prefix = profile.bio.slice(cursor, segment.start);
|
|
686
|
+
cursor = segment.end;
|
|
687
|
+
const href = safeHttpUrl(segment.expandedUrl);
|
|
688
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [prefix, href ? /* @__PURE__ */ jsx("a", {
|
|
689
|
+
className: tweetLinkClass,
|
|
690
|
+
href,
|
|
691
|
+
rel: "noreferrer",
|
|
692
|
+
target: "_blank",
|
|
693
|
+
children: segment.expandedUrl
|
|
694
|
+
}) : profile.bio.slice(segment.start, segment.end)] }, `${segment.url}-${String(index)}`);
|
|
695
|
+
}), profile.bio.slice(cursor)]
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
function ProfilePreview({ profile, children, className = "" }) {
|
|
699
|
+
const preview = useFloatingPreview();
|
|
700
|
+
useAvatarPreload(preview.referenceRef, profile.id, profile.avatarUrl);
|
|
701
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
702
|
+
ref: preview.referenceRef,
|
|
703
|
+
className: cx(profilePreviewClass, className),
|
|
704
|
+
...preview.referenceProps,
|
|
705
|
+
children: [/* @__PURE__ */ jsx("a", {
|
|
706
|
+
"aria-controls": preview.open ? preview.floatingId : void 0,
|
|
707
|
+
"aria-expanded": preview.open,
|
|
708
|
+
className: profilePreviewTriggerClass,
|
|
709
|
+
href: `/profiles/${encodeURIComponent(profile.handle)}`,
|
|
710
|
+
children
|
|
711
|
+
}), preview.open ? /* @__PURE__ */ jsx("span", {
|
|
712
|
+
"aria-label": `${profile.displayName} profile preview`,
|
|
713
|
+
id: preview.floatingId,
|
|
714
|
+
ref: preview.floatingRef,
|
|
715
|
+
className: profilePreviewCardClass,
|
|
716
|
+
role: "group",
|
|
717
|
+
style: preview.floatingStyle,
|
|
718
|
+
...preview.floatingProps,
|
|
719
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
720
|
+
className: "grid gap-2",
|
|
721
|
+
"data-floating-preview-content": true,
|
|
722
|
+
children: [
|
|
723
|
+
/* @__PURE__ */ jsxs("span", {
|
|
724
|
+
className: profilePreviewHeaderClass,
|
|
725
|
+
children: [/* @__PURE__ */ jsx(AvatarChip, {
|
|
726
|
+
avatarUrl: profile.avatarUrl,
|
|
727
|
+
hue: profile.avatarHue,
|
|
728
|
+
name: profile.displayName,
|
|
729
|
+
profileId: profile.id
|
|
730
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
731
|
+
className: "flex min-w-0 flex-col",
|
|
732
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
733
|
+
className: profilePreviewNameClass,
|
|
734
|
+
children: profile.displayName
|
|
735
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
736
|
+
className: profilePreviewHandleClass,
|
|
737
|
+
children: ["@", profile.handle]
|
|
738
|
+
})]
|
|
739
|
+
})]
|
|
740
|
+
}),
|
|
741
|
+
profile.bio ? /* @__PURE__ */ jsx(ProfilePreviewBio, { profile }) : null,
|
|
742
|
+
/* @__PURE__ */ jsxs("span", {
|
|
743
|
+
className: profilePreviewMetaClass,
|
|
744
|
+
children: [formatCompactNumber(profile.followersCount), " followers"]
|
|
745
|
+
})
|
|
746
|
+
]
|
|
747
|
+
})
|
|
748
|
+
}) : null]
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
//#endregion
|
|
752
|
+
export { assertSafePreviewUrl as a, collectTweetSegmentsForText as c, isTweetArticleUrlEntity as d, normalizeTweetUrlEntityRangeForText as f, tweetEntitiesFromXurl as h, __test__ as i, displayUrlForLink as l, renderTweetPlainText as m, useFloatingPreview as n, isBlockedAddress as o, profileDescriptionEntitiesFromXurl as p, useAvatarPreload as r, safeHttpUrl as s, ProfilePreview as t, enrichFallbackUrlEntities as u };
|