birdclaw 0.5.1 → 0.7.0
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 +92 -1
- package/README.md +75 -5
- package/package.json +8 -2
- package/scripts/browser-perf.mjs +1 -0
- package/scripts/start-test-server.mjs +16 -3
- package/src/cli.ts +812 -37
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +37 -7
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/DmWorkspace.tsx +18 -8
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +818 -0
- package/src/components/ProfileAnalysisStream.tsx +428 -0
- package/src/components/ProfilePreview.tsx +120 -9
- package/src/components/SavedTimelineView.tsx +30 -8
- package/src/components/SyncNowButton.tsx +60 -25
- package/src/components/ThemeSlider.tsx +55 -50
- package/src/components/TimelineCard.tsx +240 -92
- package/src/components/TimelineRouteFrame.tsx +38 -8
- package/src/components/TweetMediaGrid.tsx +87 -38
- package/src/components/TweetRichText.tsx +45 -17
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +97 -13
- package/src/lib/account-sync-job.ts +666 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +128 -53
- package/src/lib/archive-finder.ts +78 -63
- package/src/lib/archive-import.ts +1593 -1291
- package/src/lib/authored-live.ts +262 -204
- package/src/lib/avatar-cache.ts +208 -43
- package/src/lib/backup.ts +1536 -954
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +582 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/config.ts +35 -2
- package/src/lib/conversation-surface.ts +79 -48
- package/src/lib/data-sources.ts +219 -0
- package/src/lib/db.ts +95 -4
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/geocoding.ts +296 -0
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/location.ts +137 -0
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +445 -288
- package/src/lib/mentions-live.ts +549 -354
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/network-map.ts +382 -0
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +1317 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-analysis.ts +1272 -0
- package/src/lib/profile-bio-entities.ts +1 -1
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +983 -203
- package/src/lib/research.ts +165 -120
- package/src/lib/search-discussion.ts +1016 -0
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +325 -51
- package/src/lib/tweet-account-edges.ts +2 -0
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/tweet-render.ts +141 -1
- package/src/lib/tweet-search-live.ts +565 -0
- package/src/lib/types.ts +75 -3
- package/src/lib/ui.ts +31 -8
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +222 -149
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl-rate-limits.ts +267 -0
- package/src/lib/xurl.ts +1185 -405
- package/src/routeTree.gen.ts +273 -0
- package/src/routes/__root.tsx +24 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/data-sources.tsx +24 -0
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/network-map.tsx +55 -0
- package/src/routes/api/period-digest.tsx +133 -0
- package/src/routes/api/profile-analysis.tsx +152 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +80 -55
- package/src/routes/api/search-discussion.tsx +169 -0
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +75 -29
- package/src/routes/api/xurl-rate-limits.tsx +24 -0
- package/src/routes/data-sources.tsx +255 -0
- package/src/routes/discuss.tsx +419 -0
- package/src/routes/dms.tsx +95 -28
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/network-map.tsx +1035 -0
- package/src/routes/profile-analyze.tsx +112 -0
- package/src/routes/profiles.$handle.tsx +228 -0
- package/src/routes/rate-limits.tsx +309 -0
- package/src/routes/today.tsx +455 -0
- package/src/styles.css +22 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Bookmark,
|
|
3
2
|
BookmarkCheck,
|
|
4
3
|
CheckCircle2,
|
|
5
4
|
Circle,
|
|
6
5
|
Heart,
|
|
6
|
+
Image,
|
|
7
7
|
MessageCircle,
|
|
8
8
|
Repeat2,
|
|
9
|
+
UserSearch,
|
|
9
10
|
} from "lucide-react";
|
|
10
11
|
import { formatCompactNumber, formatShortTimestamp } from "#/lib/present";
|
|
12
|
+
import { normalizeTweetUrlEntityRangeForText } from "#/lib/tweet-render";
|
|
11
13
|
import type {
|
|
12
14
|
TimelineItem,
|
|
13
15
|
TweetEntities,
|
|
@@ -21,8 +23,6 @@ import {
|
|
|
21
23
|
feedActionButtonClass,
|
|
22
24
|
feedActionIconClass,
|
|
23
25
|
feedActionIconWrapClass,
|
|
24
|
-
feedActionIconWrapLikeClass,
|
|
25
|
-
feedActionLikeClass,
|
|
26
26
|
feedRowActionsClass,
|
|
27
27
|
feedRowBodyClass,
|
|
28
28
|
feedRowClass,
|
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
feedRowStatePillOpenClass,
|
|
36
36
|
feedRowTextClass,
|
|
37
37
|
feedRowTimestampClass,
|
|
38
|
-
mutedDotClass,
|
|
39
38
|
} from "#/lib/ui";
|
|
40
39
|
import { AvatarChip } from "./AvatarChip";
|
|
41
40
|
import { ConversationThread } from "./ConversationThread";
|
|
@@ -83,6 +82,64 @@ function isMediaUrlEntity(
|
|
|
83
82
|
return false;
|
|
84
83
|
}
|
|
85
84
|
|
|
85
|
+
function isShortUrl(value: string | null | undefined) {
|
|
86
|
+
if (!value) return false;
|
|
87
|
+
try {
|
|
88
|
+
const candidate = value.includes("://") ? value : `https://${value}`;
|
|
89
|
+
const parsed = new URL(candidate);
|
|
90
|
+
return parsed.hostname.replace(/^www\./, "") === "t.co";
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isUnresolvedShortUrlEntity(entry: TweetUrlEntity) {
|
|
97
|
+
if (isShortUrl(entry.expandedUrl)) return true;
|
|
98
|
+
if (entry.expandedUrl) return false;
|
|
99
|
+
if (isShortUrl(entry.displayUrl)) return true;
|
|
100
|
+
return !entry.displayUrl && isShortUrl(entry.url);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function unresolvedShortUrlRanges(text: string, entities: TweetEntities) {
|
|
104
|
+
return (entities.urls ?? [])
|
|
105
|
+
.filter(isUnresolvedShortUrlEntity)
|
|
106
|
+
.map((entry) => normalizeTweetUrlEntityRangeForText(text, entry));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function textOutsideRanges(
|
|
110
|
+
text: string,
|
|
111
|
+
ranges: Array<{ start: number; end: number }>,
|
|
112
|
+
) {
|
|
113
|
+
let cursor = 0;
|
|
114
|
+
let output = "";
|
|
115
|
+
for (const range of [...ranges].sort(
|
|
116
|
+
(left, right) => left.start - right.start,
|
|
117
|
+
)) {
|
|
118
|
+
if (
|
|
119
|
+
range.start < cursor ||
|
|
120
|
+
range.end <= range.start ||
|
|
121
|
+
range.end > text.length
|
|
122
|
+
) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
output += text.slice(cursor, range.start);
|
|
126
|
+
cursor = range.end;
|
|
127
|
+
}
|
|
128
|
+
output += text.slice(cursor);
|
|
129
|
+
return output;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function shouldHideUnresolvedShortUrls(
|
|
133
|
+
text: string,
|
|
134
|
+
entities: TweetEntities,
|
|
135
|
+
mediaUrls: Set<string>,
|
|
136
|
+
) {
|
|
137
|
+
if (mediaUrls.size === 0) return false;
|
|
138
|
+
const ranges = unresolvedShortUrlRanges(text, entities);
|
|
139
|
+
if (ranges.length === 0) return false;
|
|
140
|
+
return textOutsideRanges(text, ranges).trim().length === 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
86
143
|
function isOwnStatusMediaUrl(
|
|
87
144
|
value: string | null | undefined,
|
|
88
145
|
tweetId: string,
|
|
@@ -108,13 +165,21 @@ function getVisibleEntities(
|
|
|
108
165
|
entities: TweetEntities,
|
|
109
166
|
media: TweetMediaItem[],
|
|
110
167
|
tweetId: string,
|
|
168
|
+
text: string,
|
|
111
169
|
) {
|
|
112
170
|
const mediaUrls = getMediaUrlSet(media);
|
|
113
171
|
if (mediaUrls.size === 0) return entities;
|
|
172
|
+
const hideUnresolvedShortUrls = shouldHideUnresolvedShortUrls(
|
|
173
|
+
text,
|
|
174
|
+
entities,
|
|
175
|
+
mediaUrls,
|
|
176
|
+
);
|
|
114
177
|
return {
|
|
115
178
|
...entities,
|
|
116
179
|
urls: (entities.urls ?? []).filter(
|
|
117
|
-
(entry) =>
|
|
180
|
+
(entry) =>
|
|
181
|
+
!isMediaUrlEntity(entry, mediaUrls, tweetId) &&
|
|
182
|
+
!(hideUnresolvedShortUrls && isUnresolvedShortUrlEntity(entry)),
|
|
118
183
|
),
|
|
119
184
|
};
|
|
120
185
|
}
|
|
@@ -123,19 +188,32 @@ function getHiddenMediaUrlRanges(
|
|
|
123
188
|
entities: TweetEntities,
|
|
124
189
|
media: TweetMediaItem[],
|
|
125
190
|
tweetId: string,
|
|
191
|
+
text: string,
|
|
126
192
|
) {
|
|
127
193
|
const mediaUrls = getMediaUrlSet(media);
|
|
128
194
|
if (mediaUrls.size === 0) return [];
|
|
195
|
+
const hideUnresolvedShortUrls = shouldHideUnresolvedShortUrls(
|
|
196
|
+
text,
|
|
197
|
+
entities,
|
|
198
|
+
mediaUrls,
|
|
199
|
+
);
|
|
129
200
|
return (entities.urls ?? [])
|
|
130
|
-
.filter(
|
|
131
|
-
|
|
201
|
+
.filter(
|
|
202
|
+
(entry) =>
|
|
203
|
+
isMediaUrlEntity(entry, mediaUrls, tweetId) ||
|
|
204
|
+
(hideUnresolvedShortUrls && isUnresolvedShortUrlEntity(entry)),
|
|
205
|
+
)
|
|
206
|
+
.map((entry) => normalizeTweetUrlEntityRangeForText(text, entry));
|
|
132
207
|
}
|
|
133
208
|
|
|
134
|
-
function getVisibleUrlCards(
|
|
135
|
-
|
|
209
|
+
function getVisibleUrlCards(
|
|
210
|
+
entities: TweetEntities,
|
|
211
|
+
quotedTweetId: string | null,
|
|
212
|
+
) {
|
|
136
213
|
return (entities.urls ?? []).filter((entry) => {
|
|
137
|
-
if (
|
|
138
|
-
|
|
214
|
+
if (isUnresolvedShortUrlEntity(entry)) return false;
|
|
215
|
+
if (!quotedTweetId) return true;
|
|
216
|
+
return !entry.expandedUrl.includes(quotedTweetId);
|
|
139
217
|
});
|
|
140
218
|
}
|
|
141
219
|
|
|
@@ -157,18 +235,44 @@ export function TimelineCard({
|
|
|
157
235
|
}) {
|
|
158
236
|
const canReply =
|
|
159
237
|
showReplyControls && item.kind !== "like" && item.kind !== "bookmark";
|
|
160
|
-
const
|
|
238
|
+
const displayTweet = item.retweetedTweet ?? item;
|
|
239
|
+
const displayTweetId = displayTweet.id;
|
|
240
|
+
const interactionTweetId =
|
|
241
|
+
item.retweetedTweet && displayTweetId === `${item.id}:retweeted`
|
|
242
|
+
? item.id
|
|
243
|
+
: displayTweetId;
|
|
244
|
+
const displayAuthor = displayTweet.author;
|
|
245
|
+
const conversation = useConversationSurface(item.id, interactionTweetId);
|
|
161
246
|
const visibleEntities = getVisibleEntities(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
247
|
+
displayTweet.entities,
|
|
248
|
+
displayTweet.media,
|
|
249
|
+
displayTweet.id,
|
|
250
|
+
displayTweet.text,
|
|
165
251
|
);
|
|
166
252
|
const hiddenMediaUrlRanges = getHiddenMediaUrlRanges(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
253
|
+
displayTweet.entities,
|
|
254
|
+
displayTweet.media,
|
|
255
|
+
displayTweet.id,
|
|
256
|
+
displayTweet.text,
|
|
257
|
+
);
|
|
258
|
+
const visibleUrlCards = getVisibleUrlCards(
|
|
259
|
+
visibleEntities,
|
|
260
|
+
item.retweetedTweet ? null : (item.quotedTweet?.id ?? null),
|
|
261
|
+
);
|
|
262
|
+
const displayMediaCount = item.retweetedTweet
|
|
263
|
+
? (displayTweet.mediaCount ?? displayTweet.media.length)
|
|
264
|
+
: item.mediaCount;
|
|
265
|
+
const displayIsReplied = displayTweet.isReplied ?? item.isReplied;
|
|
266
|
+
const displayLikeCount = displayTweet.likeCount ?? item.likeCount;
|
|
267
|
+
const displayBookmarked = displayTweet.bookmarked ?? item.bookmarked;
|
|
268
|
+
const displayLiked = displayTweet.liked ?? item.liked;
|
|
269
|
+
const showLikeIndicator = displayLiked || displayLikeCount > 0;
|
|
270
|
+
const showMediaIndicator = displayMediaCount > 0;
|
|
271
|
+
const hasConversation = Boolean(
|
|
272
|
+
item.retweetedTweet
|
|
273
|
+
? displayTweet.replyToId
|
|
274
|
+
: item.replyToTweet || item.replyToId,
|
|
170
275
|
);
|
|
171
|
-
const hasConversation = Boolean(item.replyToTweet || item.replyToId);
|
|
172
276
|
|
|
173
277
|
return (
|
|
174
278
|
<article
|
|
@@ -182,24 +286,34 @@ export function TimelineCard({
|
|
|
182
286
|
}}
|
|
183
287
|
>
|
|
184
288
|
<AvatarChip
|
|
185
|
-
avatarUrl={
|
|
186
|
-
hue={
|
|
187
|
-
name={
|
|
188
|
-
profileId={
|
|
289
|
+
avatarUrl={displayAuthor.avatarUrl}
|
|
290
|
+
hue={displayAuthor.avatarHue}
|
|
291
|
+
name={displayAuthor.displayName}
|
|
292
|
+
profileId={displayAuthor.id}
|
|
189
293
|
/>
|
|
190
294
|
<div className={feedRowBodyClass}>
|
|
295
|
+
{item.retweetedTweet ? (
|
|
296
|
+
<div className="inline-flex items-center gap-2 text-[13px] font-medium text-[var(--ink-soft)]">
|
|
297
|
+
<Repeat2 className="size-4" strokeWidth={1.8} />
|
|
298
|
+
<ProfilePreview profile={item.author}>
|
|
299
|
+
<span>{item.author.displayName} reposted</span>
|
|
300
|
+
</ProfilePreview>
|
|
301
|
+
</div>
|
|
302
|
+
) : null}
|
|
191
303
|
<header className={feedRowHeaderClass}>
|
|
192
|
-
<ProfilePreview profile={
|
|
304
|
+
<ProfilePreview profile={displayAuthor}>
|
|
193
305
|
<span className="flex min-w-0 items-center gap-1.5">
|
|
194
306
|
<span className={feedRowNameClass}>
|
|
195
|
-
{
|
|
307
|
+
{displayAuthor.displayName}
|
|
308
|
+
</span>
|
|
309
|
+
<span className={feedRowHandleClass}>
|
|
310
|
+
@{displayAuthor.handle}
|
|
196
311
|
</span>
|
|
197
|
-
<span className={feedRowHandleClass}>@{item.author.handle}</span>
|
|
198
312
|
</span>
|
|
199
313
|
</ProfilePreview>
|
|
200
314
|
<span className={feedRowDotClass}>·</span>
|
|
201
315
|
<span className={feedRowTimestampClass}>
|
|
202
|
-
{formatShortTimestamp(
|
|
316
|
+
{formatShortTimestamp(displayTweet.createdAt)}
|
|
203
317
|
</span>
|
|
204
318
|
{canReply || hasConversation ? (
|
|
205
319
|
<span className="ml-auto inline-flex items-center gap-1">
|
|
@@ -218,50 +332,77 @@ export function TimelineCard({
|
|
|
218
332
|
) : null}
|
|
219
333
|
{canReply ? (
|
|
220
334
|
<span
|
|
221
|
-
aria-label={
|
|
335
|
+
aria-label={displayIsReplied ? "We replied" : "Reply open"}
|
|
222
336
|
className={cx(
|
|
223
337
|
feedRowStatePillClass,
|
|
224
|
-
|
|
338
|
+
displayIsReplied
|
|
225
339
|
? feedRowStatePillActiveClass
|
|
226
340
|
: feedRowStatePillOpenClass,
|
|
227
341
|
)}
|
|
228
|
-
title={
|
|
342
|
+
title={displayIsReplied ? "We replied" : "Reply open"}
|
|
229
343
|
>
|
|
230
|
-
{
|
|
344
|
+
{displayIsReplied ? (
|
|
231
345
|
<CheckCircle2 className="size-3.5" strokeWidth={2} />
|
|
232
346
|
) : (
|
|
233
347
|
<Circle className="size-3" strokeWidth={2.2} />
|
|
234
348
|
)}
|
|
235
|
-
{
|
|
349
|
+
{displayIsReplied ? "replied" : "open"}
|
|
236
350
|
</span>
|
|
237
351
|
) : null}
|
|
238
352
|
</span>
|
|
239
353
|
) : null}
|
|
240
354
|
</header>
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
355
|
+
{item.retweetedTweet ? (
|
|
356
|
+
<>
|
|
357
|
+
<TweetRichText
|
|
358
|
+
className={feedRowTextClass}
|
|
359
|
+
entities={displayTweet.entities}
|
|
360
|
+
hiddenUrlRanges={hiddenMediaUrlRanges}
|
|
361
|
+
text={displayTweet.text}
|
|
362
|
+
/>
|
|
363
|
+
<TweetMediaGrid items={displayTweet.media} />
|
|
364
|
+
{visibleUrlCards.map((entry, index) => (
|
|
365
|
+
<LinkPreviewCard
|
|
366
|
+
key={`${entry.expandedUrl}-${String(index)}`}
|
|
367
|
+
entry={entry}
|
|
368
|
+
index={index}
|
|
369
|
+
/>
|
|
370
|
+
))}
|
|
371
|
+
</>
|
|
372
|
+
) : (
|
|
373
|
+
<>
|
|
374
|
+
<TweetRichText
|
|
375
|
+
className={feedRowTextClass}
|
|
376
|
+
entities={item.entities}
|
|
377
|
+
hiddenUrlRanges={hiddenMediaUrlRanges}
|
|
378
|
+
text={item.text}
|
|
379
|
+
/>
|
|
380
|
+
<TweetMediaGrid items={item.media} />
|
|
381
|
+
{item.replyToTweet ? (
|
|
382
|
+
<div className={embeddedCardClass}>
|
|
383
|
+
<EmbeddedTweetCard
|
|
384
|
+
item={item.replyToTweet}
|
|
385
|
+
label="In reply to"
|
|
386
|
+
/>
|
|
387
|
+
</div>
|
|
388
|
+
) : null}
|
|
389
|
+
{item.quotedTweet ? (
|
|
390
|
+
<div className={embeddedCardClass}>
|
|
391
|
+
<EmbeddedTweetCard
|
|
392
|
+
item={item.quotedTweet}
|
|
393
|
+
label="Quoted tweet"
|
|
394
|
+
/>
|
|
395
|
+
</div>
|
|
396
|
+
) : null}
|
|
397
|
+
{visibleUrlCards.map((entry, index) => (
|
|
398
|
+
<LinkPreviewCard
|
|
399
|
+
key={`${entry.expandedUrl}-${String(index)}`}
|
|
400
|
+
entry={entry}
|
|
401
|
+
index={index}
|
|
402
|
+
/>
|
|
403
|
+
))}
|
|
404
|
+
</>
|
|
405
|
+
)}
|
|
265
406
|
<footer className={feedRowActionsClass}>
|
|
266
407
|
<div className="flex items-center gap-3 text-[13px] text-[var(--ink-soft)]">
|
|
267
408
|
<button
|
|
@@ -291,7 +432,7 @@ export function TimelineCard({
|
|
|
291
432
|
className={feedActionButtonClass}
|
|
292
433
|
onClick={(event) => {
|
|
293
434
|
event.stopPropagation();
|
|
294
|
-
onReply(
|
|
435
|
+
onReply(interactionTweetId);
|
|
295
436
|
}}
|
|
296
437
|
type="button"
|
|
297
438
|
aria-label="Reply"
|
|
@@ -305,57 +446,64 @@ export function TimelineCard({
|
|
|
305
446
|
<span className="text-[13px]">Reply</span>
|
|
306
447
|
</button>
|
|
307
448
|
) : null}
|
|
308
|
-
<
|
|
449
|
+
<a
|
|
450
|
+
aria-label={`Analyse @${displayAuthor.handle}`}
|
|
451
|
+
className={feedActionButtonClass}
|
|
452
|
+
href={`/profiles/${encodeURIComponent(displayAuthor.handle)}`}
|
|
453
|
+
onClick={(event) => {
|
|
454
|
+
event.stopPropagation();
|
|
455
|
+
}}
|
|
456
|
+
title={`Analyse @${displayAuthor.handle}`}
|
|
457
|
+
>
|
|
309
458
|
<span className={feedActionIconWrapClass}>
|
|
310
|
-
<
|
|
459
|
+
<UserSearch className={feedActionIconClass} strokeWidth={1.7} />
|
|
311
460
|
</span>
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
feedActionButtonClass,
|
|
316
|
-
feedActionLikeClass,
|
|
317
|
-
"cursor-default",
|
|
318
|
-
item.liked && "text-[var(--like)]",
|
|
319
|
-
)}
|
|
320
|
-
>
|
|
461
|
+
<span className="text-[13px]">Analyse</span>
|
|
462
|
+
</a>
|
|
463
|
+
{showLikeIndicator ? (
|
|
321
464
|
<span
|
|
465
|
+
aria-label={`${formatCompactNumber(displayLikeCount)} likes`}
|
|
322
466
|
className={cx(
|
|
323
|
-
|
|
324
|
-
|
|
467
|
+
"inline-flex items-center gap-1 px-2 py-1 text-[13px]",
|
|
468
|
+
displayLiked && "text-[var(--like)]",
|
|
325
469
|
)}
|
|
470
|
+
title={`${formatCompactNumber(displayLikeCount)} likes`}
|
|
326
471
|
>
|
|
327
472
|
<Heart
|
|
328
473
|
className={feedActionIconClass}
|
|
329
474
|
strokeWidth={1.7}
|
|
330
|
-
fill={
|
|
475
|
+
fill={displayLiked ? "currentColor" : "none"}
|
|
331
476
|
/>
|
|
477
|
+
<span>{formatCompactNumber(displayLikeCount)}</span>
|
|
332
478
|
</span>
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
)}
|
|
479
|
+
) : null}
|
|
480
|
+
{displayBookmarked ? (
|
|
481
|
+
<span
|
|
482
|
+
aria-label="Bookmarked"
|
|
483
|
+
className="inline-flex items-center px-2 py-1"
|
|
484
|
+
title="Bookmarked"
|
|
485
|
+
>
|
|
486
|
+
<BookmarkCheck
|
|
487
|
+
className={feedActionIconClass}
|
|
488
|
+
strokeWidth={1.7}
|
|
489
|
+
/>
|
|
345
490
|
</span>
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
491
|
+
) : null}
|
|
492
|
+
{showMediaIndicator ? (
|
|
493
|
+
<span
|
|
494
|
+
aria-label={`${String(displayMediaCount)} media attachments`}
|
|
495
|
+
className="inline-flex items-center gap-1 px-2 py-1 text-[13px]"
|
|
496
|
+
title={`${String(displayMediaCount)} media attachments`}
|
|
497
|
+
>
|
|
498
|
+
<Image className={feedActionIconClass} strokeWidth={1.7} />
|
|
499
|
+
<span>{displayMediaCount}</span>
|
|
500
|
+
</span>
|
|
501
|
+
) : null}
|
|
354
502
|
</div>
|
|
355
503
|
</footer>
|
|
356
504
|
{conversation.isOpen ? (
|
|
357
505
|
<ConversationThread
|
|
358
|
-
anchorId={
|
|
506
|
+
anchorId={interactionTweetId}
|
|
359
507
|
error={conversation.error}
|
|
360
508
|
items={conversation.items}
|
|
361
509
|
loading={conversation.loading}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
tabButtonClass,
|
|
26
26
|
tabButtonIndicatorClass,
|
|
27
27
|
tabStripClass,
|
|
28
|
+
timestampClass,
|
|
28
29
|
} from "#/lib/ui";
|
|
29
30
|
import { useTimelineRouteData } from "./useTimelineRouteData";
|
|
30
31
|
|
|
@@ -68,13 +69,24 @@ export function TimelineRouteFrame({
|
|
|
68
69
|
const [replyFilter, setReplyFilter] =
|
|
69
70
|
useState<ReplyFilter>(initialReplyFilter);
|
|
70
71
|
const [search, setSearch] = useState("");
|
|
71
|
-
const {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
const {
|
|
73
|
+
meta,
|
|
74
|
+
items,
|
|
75
|
+
loading,
|
|
76
|
+
error,
|
|
77
|
+
replyError,
|
|
78
|
+
retry,
|
|
79
|
+
refreshLocalView,
|
|
80
|
+
replyToTweet,
|
|
81
|
+
hasMore,
|
|
82
|
+
loadingMore,
|
|
83
|
+
loadMore,
|
|
84
|
+
} = useTimelineRouteData({
|
|
85
|
+
resource,
|
|
86
|
+
replyFilter,
|
|
87
|
+
search,
|
|
88
|
+
errorFallback,
|
|
89
|
+
});
|
|
78
90
|
const subtitleText = useMemo(() => subtitle(meta), [meta, subtitle]);
|
|
79
91
|
|
|
80
92
|
return (
|
|
@@ -86,10 +98,11 @@ export function TimelineRouteFrame({
|
|
|
86
98
|
<p className={pageSubtitleClass}>{subtitleText}</p>
|
|
87
99
|
</div>
|
|
88
100
|
<SyncNowButton
|
|
89
|
-
accounts={
|
|
101
|
+
accounts={meta?.accounts}
|
|
90
102
|
kind={syncKind}
|
|
91
103
|
label={syncLabel}
|
|
92
104
|
onSynced={refreshLocalView}
|
|
105
|
+
showAccountPicker
|
|
93
106
|
/>
|
|
94
107
|
</div>
|
|
95
108
|
<div className="px-4 pb-3">
|
|
@@ -123,6 +136,11 @@ export function TimelineRouteFrame({
|
|
|
123
136
|
})}
|
|
124
137
|
</div>
|
|
125
138
|
</header>
|
|
139
|
+
{replyError ? (
|
|
140
|
+
<p className={cx(timestampClass, "px-4 py-2 text-red-500")}>
|
|
141
|
+
{replyError}
|
|
142
|
+
</p>
|
|
143
|
+
) : null}
|
|
126
144
|
<ConversationSurfaceScope>
|
|
127
145
|
<section className={feedClass}>
|
|
128
146
|
{loading ? (
|
|
@@ -149,6 +167,18 @@ export function TimelineRouteFrame({
|
|
|
149
167
|
{items.map((item) => (
|
|
150
168
|
<TimelineCard key={item.id} item={item} onReply={replyToTweet} />
|
|
151
169
|
))}
|
|
170
|
+
{!loading && !error && hasMore ? (
|
|
171
|
+
<div className="flex justify-center py-4">
|
|
172
|
+
<button
|
|
173
|
+
className="rounded-full bg-[var(--accent)] px-5 py-1.5 text-[14px] font-bold text-white disabled:opacity-60"
|
|
174
|
+
disabled={loadingMore}
|
|
175
|
+
onClick={loadMore}
|
|
176
|
+
type="button"
|
|
177
|
+
>
|
|
178
|
+
{loadingMore ? "Loading…" : "Load more"}
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
) : null}
|
|
152
182
|
</section>
|
|
153
183
|
</ConversationSurfaceScope>
|
|
154
184
|
</>
|