birdclaw 0.4.1 → 0.5.1
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 +52 -0
- package/README.md +113 -7
- package/bin/birdclaw.mjs +50 -11
- package/package.json +30 -28
- package/playwright.config.ts +1 -0
- package/public/birdclaw-mark.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +2 -2
- package/scripts/browser-perf.mjs +399 -0
- package/scripts/build-docs-site.mjs +940 -0
- package/scripts/docs-site-assets.mjs +311 -0
- package/scripts/run-vitest.mjs +21 -0
- package/scripts/sanitize-node-options.mjs +23 -0
- package/scripts/start-test-server.mjs +29 -0
- package/src/cli.ts +496 -19
- package/src/components/AppNav.tsx +66 -29
- package/src/components/AvatarChip.tsx +10 -5
- package/src/components/BrandMark.tsx +67 -0
- package/src/components/ConversationThread.tsx +126 -0
- package/src/components/DmWorkspace.tsx +118 -105
- package/src/components/EmbeddedTweetCard.tsx +20 -14
- package/src/components/FeedState.tsx +147 -0
- package/src/components/InboxCard.tsx +104 -90
- package/src/components/LinkPreviewCard.tsx +270 -0
- package/src/components/ProfilePreview.tsx +8 -3
- package/src/components/SavedTimelineView.tsx +89 -71
- package/src/components/SyncNowButton.tsx +105 -0
- package/src/components/ThemeSlider.tsx +10 -59
- package/src/components/TimelineCard.tsx +326 -86
- package/src/components/TimelineRouteFrame.tsx +156 -0
- package/src/components/TweetMediaGrid.tsx +120 -23
- package/src/components/TweetRichText.tsx +19 -4
- package/src/components/useTimelineRouteData.ts +137 -0
- package/src/lib/api-client.ts +229 -0
- package/src/lib/archive-finder.ts +24 -20
- package/src/lib/archive-import.ts +1582 -67
- package/src/lib/authored-live.ts +1074 -0
- package/src/lib/backup.ts +316 -14
- package/src/lib/bird-actions.ts +1 -10
- package/src/lib/bird-command.ts +57 -0
- package/src/lib/bird.ts +89 -5
- package/src/lib/config.ts +1 -1
- package/src/lib/conversation-surface.ts +174 -0
- package/src/lib/db.ts +193 -4
- package/src/lib/follow-graph.ts +1053 -0
- package/src/lib/link-index.ts +11 -98
- package/src/lib/link-insights.ts +834 -0
- package/src/lib/link-preview-metadata.ts +334 -0
- package/src/lib/media-fetch.ts +787 -0
- package/src/lib/media-includes.ts +165 -0
- package/src/lib/mention-threads-live.ts +535 -43
- package/src/lib/mentions-live.ts +623 -19
- package/src/lib/moderation-target.ts +6 -0
- package/src/lib/profile-hydration.ts +1 -1
- package/src/lib/profile-resolver.ts +115 -1
- package/src/lib/queries.ts +326 -35
- package/src/lib/seed.ts +127 -8
- package/src/lib/timeline-collections-live.ts +145 -22
- package/src/lib/timeline-live.ts +10 -15
- package/src/lib/tweet-account-edges.ts +9 -2
- package/src/lib/tweet-render.ts +97 -0
- package/src/lib/types.ts +185 -2
- package/src/lib/ui.ts +383 -147
- package/src/lib/url-expansion-store.ts +110 -0
- package/src/lib/url-expansion.ts +20 -3
- package/src/lib/web-sync.ts +443 -0
- package/src/lib/x-profile.ts +7 -2
- package/src/lib/xurl.ts +296 -12
- package/src/routeTree.gen.ts +126 -0
- package/src/routes/__root.tsx +7 -3
- package/src/routes/api/conversation.tsx +34 -0
- package/src/routes/api/link-insights.tsx +79 -0
- package/src/routes/api/link-preview.tsx +43 -0
- package/src/routes/api/profile-hydrate.tsx +51 -0
- package/src/routes/api/sync.tsx +59 -0
- package/src/routes/blocks.tsx +111 -86
- package/src/routes/dms.tsx +172 -87
- package/src/routes/inbox.tsx +98 -86
- package/src/routes/index.tsx +22 -115
- package/src/routes/links.tsx +928 -0
- package/src/routes/mentions.tsx +22 -115
- package/src/styles.css +169 -43
- package/vite.config.ts +8 -0
package/src/routeTree.gen.ts
CHANGED
|
@@ -10,15 +10,21 @@
|
|
|
10
10
|
|
|
11
11
|
import { Route as rootRouteImport } from './routes/__root'
|
|
12
12
|
import { Route as MentionsRouteImport } from './routes/mentions'
|
|
13
|
+
import { Route as LinksRouteImport } from './routes/links'
|
|
13
14
|
import { Route as LikesRouteImport } from './routes/likes'
|
|
14
15
|
import { Route as InboxRouteImport } from './routes/inbox'
|
|
15
16
|
import { Route as DmsRouteImport } from './routes/dms'
|
|
16
17
|
import { Route as BookmarksRouteImport } from './routes/bookmarks'
|
|
17
18
|
import { Route as BlocksRouteImport } from './routes/blocks'
|
|
18
19
|
import { Route as IndexRouteImport } from './routes/index'
|
|
20
|
+
import { Route as ApiSyncRouteImport } from './routes/api/sync'
|
|
19
21
|
import { Route as ApiStatusRouteImport } from './routes/api/status'
|
|
20
22
|
import { Route as ApiQueryRouteImport } from './routes/api/query'
|
|
23
|
+
import { Route as ApiProfileHydrateRouteImport } from './routes/api/profile-hydrate'
|
|
24
|
+
import { Route as ApiLinkPreviewRouteImport } from './routes/api/link-preview'
|
|
25
|
+
import { Route as ApiLinkInsightsRouteImport } from './routes/api/link-insights'
|
|
21
26
|
import { Route as ApiInboxRouteImport } from './routes/api/inbox'
|
|
27
|
+
import { Route as ApiConversationRouteImport } from './routes/api/conversation'
|
|
22
28
|
import { Route as ApiBlocksRouteImport } from './routes/api/blocks'
|
|
23
29
|
import { Route as ApiAvatarRouteImport } from './routes/api/avatar'
|
|
24
30
|
import { Route as ApiActionRouteImport } from './routes/api/action'
|
|
@@ -28,6 +34,11 @@ const MentionsRoute = MentionsRouteImport.update({
|
|
|
28
34
|
path: '/mentions',
|
|
29
35
|
getParentRoute: () => rootRouteImport,
|
|
30
36
|
} as any)
|
|
37
|
+
const LinksRoute = LinksRouteImport.update({
|
|
38
|
+
id: '/links',
|
|
39
|
+
path: '/links',
|
|
40
|
+
getParentRoute: () => rootRouteImport,
|
|
41
|
+
} as any)
|
|
31
42
|
const LikesRoute = LikesRouteImport.update({
|
|
32
43
|
id: '/likes',
|
|
33
44
|
path: '/likes',
|
|
@@ -58,6 +69,11 @@ const IndexRoute = IndexRouteImport.update({
|
|
|
58
69
|
path: '/',
|
|
59
70
|
getParentRoute: () => rootRouteImport,
|
|
60
71
|
} as any)
|
|
72
|
+
const ApiSyncRoute = ApiSyncRouteImport.update({
|
|
73
|
+
id: '/api/sync',
|
|
74
|
+
path: '/api/sync',
|
|
75
|
+
getParentRoute: () => rootRouteImport,
|
|
76
|
+
} as any)
|
|
61
77
|
const ApiStatusRoute = ApiStatusRouteImport.update({
|
|
62
78
|
id: '/api/status',
|
|
63
79
|
path: '/api/status',
|
|
@@ -68,11 +84,31 @@ const ApiQueryRoute = ApiQueryRouteImport.update({
|
|
|
68
84
|
path: '/api/query',
|
|
69
85
|
getParentRoute: () => rootRouteImport,
|
|
70
86
|
} as any)
|
|
87
|
+
const ApiProfileHydrateRoute = ApiProfileHydrateRouteImport.update({
|
|
88
|
+
id: '/api/profile-hydrate',
|
|
89
|
+
path: '/api/profile-hydrate',
|
|
90
|
+
getParentRoute: () => rootRouteImport,
|
|
91
|
+
} as any)
|
|
92
|
+
const ApiLinkPreviewRoute = ApiLinkPreviewRouteImport.update({
|
|
93
|
+
id: '/api/link-preview',
|
|
94
|
+
path: '/api/link-preview',
|
|
95
|
+
getParentRoute: () => rootRouteImport,
|
|
96
|
+
} as any)
|
|
97
|
+
const ApiLinkInsightsRoute = ApiLinkInsightsRouteImport.update({
|
|
98
|
+
id: '/api/link-insights',
|
|
99
|
+
path: '/api/link-insights',
|
|
100
|
+
getParentRoute: () => rootRouteImport,
|
|
101
|
+
} as any)
|
|
71
102
|
const ApiInboxRoute = ApiInboxRouteImport.update({
|
|
72
103
|
id: '/api/inbox',
|
|
73
104
|
path: '/api/inbox',
|
|
74
105
|
getParentRoute: () => rootRouteImport,
|
|
75
106
|
} as any)
|
|
107
|
+
const ApiConversationRoute = ApiConversationRouteImport.update({
|
|
108
|
+
id: '/api/conversation',
|
|
109
|
+
path: '/api/conversation',
|
|
110
|
+
getParentRoute: () => rootRouteImport,
|
|
111
|
+
} as any)
|
|
76
112
|
const ApiBlocksRoute = ApiBlocksRouteImport.update({
|
|
77
113
|
id: '/api/blocks',
|
|
78
114
|
path: '/api/blocks',
|
|
@@ -96,13 +132,19 @@ export interface FileRoutesByFullPath {
|
|
|
96
132
|
'/dms': typeof DmsRoute
|
|
97
133
|
'/inbox': typeof InboxRoute
|
|
98
134
|
'/likes': typeof LikesRoute
|
|
135
|
+
'/links': typeof LinksRoute
|
|
99
136
|
'/mentions': typeof MentionsRoute
|
|
100
137
|
'/api/action': typeof ApiActionRoute
|
|
101
138
|
'/api/avatar': typeof ApiAvatarRoute
|
|
102
139
|
'/api/blocks': typeof ApiBlocksRoute
|
|
140
|
+
'/api/conversation': typeof ApiConversationRoute
|
|
103
141
|
'/api/inbox': typeof ApiInboxRoute
|
|
142
|
+
'/api/link-insights': typeof ApiLinkInsightsRoute
|
|
143
|
+
'/api/link-preview': typeof ApiLinkPreviewRoute
|
|
144
|
+
'/api/profile-hydrate': typeof ApiProfileHydrateRoute
|
|
104
145
|
'/api/query': typeof ApiQueryRoute
|
|
105
146
|
'/api/status': typeof ApiStatusRoute
|
|
147
|
+
'/api/sync': typeof ApiSyncRoute
|
|
106
148
|
}
|
|
107
149
|
export interface FileRoutesByTo {
|
|
108
150
|
'/': typeof IndexRoute
|
|
@@ -111,13 +153,19 @@ export interface FileRoutesByTo {
|
|
|
111
153
|
'/dms': typeof DmsRoute
|
|
112
154
|
'/inbox': typeof InboxRoute
|
|
113
155
|
'/likes': typeof LikesRoute
|
|
156
|
+
'/links': typeof LinksRoute
|
|
114
157
|
'/mentions': typeof MentionsRoute
|
|
115
158
|
'/api/action': typeof ApiActionRoute
|
|
116
159
|
'/api/avatar': typeof ApiAvatarRoute
|
|
117
160
|
'/api/blocks': typeof ApiBlocksRoute
|
|
161
|
+
'/api/conversation': typeof ApiConversationRoute
|
|
118
162
|
'/api/inbox': typeof ApiInboxRoute
|
|
163
|
+
'/api/link-insights': typeof ApiLinkInsightsRoute
|
|
164
|
+
'/api/link-preview': typeof ApiLinkPreviewRoute
|
|
165
|
+
'/api/profile-hydrate': typeof ApiProfileHydrateRoute
|
|
119
166
|
'/api/query': typeof ApiQueryRoute
|
|
120
167
|
'/api/status': typeof ApiStatusRoute
|
|
168
|
+
'/api/sync': typeof ApiSyncRoute
|
|
121
169
|
}
|
|
122
170
|
export interface FileRoutesById {
|
|
123
171
|
__root__: typeof rootRouteImport
|
|
@@ -127,13 +175,19 @@ export interface FileRoutesById {
|
|
|
127
175
|
'/dms': typeof DmsRoute
|
|
128
176
|
'/inbox': typeof InboxRoute
|
|
129
177
|
'/likes': typeof LikesRoute
|
|
178
|
+
'/links': typeof LinksRoute
|
|
130
179
|
'/mentions': typeof MentionsRoute
|
|
131
180
|
'/api/action': typeof ApiActionRoute
|
|
132
181
|
'/api/avatar': typeof ApiAvatarRoute
|
|
133
182
|
'/api/blocks': typeof ApiBlocksRoute
|
|
183
|
+
'/api/conversation': typeof ApiConversationRoute
|
|
134
184
|
'/api/inbox': typeof ApiInboxRoute
|
|
185
|
+
'/api/link-insights': typeof ApiLinkInsightsRoute
|
|
186
|
+
'/api/link-preview': typeof ApiLinkPreviewRoute
|
|
187
|
+
'/api/profile-hydrate': typeof ApiProfileHydrateRoute
|
|
135
188
|
'/api/query': typeof ApiQueryRoute
|
|
136
189
|
'/api/status': typeof ApiStatusRoute
|
|
190
|
+
'/api/sync': typeof ApiSyncRoute
|
|
137
191
|
}
|
|
138
192
|
export interface FileRouteTypes {
|
|
139
193
|
fileRoutesByFullPath: FileRoutesByFullPath
|
|
@@ -144,13 +198,19 @@ export interface FileRouteTypes {
|
|
|
144
198
|
| '/dms'
|
|
145
199
|
| '/inbox'
|
|
146
200
|
| '/likes'
|
|
201
|
+
| '/links'
|
|
147
202
|
| '/mentions'
|
|
148
203
|
| '/api/action'
|
|
149
204
|
| '/api/avatar'
|
|
150
205
|
| '/api/blocks'
|
|
206
|
+
| '/api/conversation'
|
|
151
207
|
| '/api/inbox'
|
|
208
|
+
| '/api/link-insights'
|
|
209
|
+
| '/api/link-preview'
|
|
210
|
+
| '/api/profile-hydrate'
|
|
152
211
|
| '/api/query'
|
|
153
212
|
| '/api/status'
|
|
213
|
+
| '/api/sync'
|
|
154
214
|
fileRoutesByTo: FileRoutesByTo
|
|
155
215
|
to:
|
|
156
216
|
| '/'
|
|
@@ -159,13 +219,19 @@ export interface FileRouteTypes {
|
|
|
159
219
|
| '/dms'
|
|
160
220
|
| '/inbox'
|
|
161
221
|
| '/likes'
|
|
222
|
+
| '/links'
|
|
162
223
|
| '/mentions'
|
|
163
224
|
| '/api/action'
|
|
164
225
|
| '/api/avatar'
|
|
165
226
|
| '/api/blocks'
|
|
227
|
+
| '/api/conversation'
|
|
166
228
|
| '/api/inbox'
|
|
229
|
+
| '/api/link-insights'
|
|
230
|
+
| '/api/link-preview'
|
|
231
|
+
| '/api/profile-hydrate'
|
|
167
232
|
| '/api/query'
|
|
168
233
|
| '/api/status'
|
|
234
|
+
| '/api/sync'
|
|
169
235
|
id:
|
|
170
236
|
| '__root__'
|
|
171
237
|
| '/'
|
|
@@ -174,13 +240,19 @@ export interface FileRouteTypes {
|
|
|
174
240
|
| '/dms'
|
|
175
241
|
| '/inbox'
|
|
176
242
|
| '/likes'
|
|
243
|
+
| '/links'
|
|
177
244
|
| '/mentions'
|
|
178
245
|
| '/api/action'
|
|
179
246
|
| '/api/avatar'
|
|
180
247
|
| '/api/blocks'
|
|
248
|
+
| '/api/conversation'
|
|
181
249
|
| '/api/inbox'
|
|
250
|
+
| '/api/link-insights'
|
|
251
|
+
| '/api/link-preview'
|
|
252
|
+
| '/api/profile-hydrate'
|
|
182
253
|
| '/api/query'
|
|
183
254
|
| '/api/status'
|
|
255
|
+
| '/api/sync'
|
|
184
256
|
fileRoutesById: FileRoutesById
|
|
185
257
|
}
|
|
186
258
|
export interface RootRouteChildren {
|
|
@@ -190,13 +262,19 @@ export interface RootRouteChildren {
|
|
|
190
262
|
DmsRoute: typeof DmsRoute
|
|
191
263
|
InboxRoute: typeof InboxRoute
|
|
192
264
|
LikesRoute: typeof LikesRoute
|
|
265
|
+
LinksRoute: typeof LinksRoute
|
|
193
266
|
MentionsRoute: typeof MentionsRoute
|
|
194
267
|
ApiActionRoute: typeof ApiActionRoute
|
|
195
268
|
ApiAvatarRoute: typeof ApiAvatarRoute
|
|
196
269
|
ApiBlocksRoute: typeof ApiBlocksRoute
|
|
270
|
+
ApiConversationRoute: typeof ApiConversationRoute
|
|
197
271
|
ApiInboxRoute: typeof ApiInboxRoute
|
|
272
|
+
ApiLinkInsightsRoute: typeof ApiLinkInsightsRoute
|
|
273
|
+
ApiLinkPreviewRoute: typeof ApiLinkPreviewRoute
|
|
274
|
+
ApiProfileHydrateRoute: typeof ApiProfileHydrateRoute
|
|
198
275
|
ApiQueryRoute: typeof ApiQueryRoute
|
|
199
276
|
ApiStatusRoute: typeof ApiStatusRoute
|
|
277
|
+
ApiSyncRoute: typeof ApiSyncRoute
|
|
200
278
|
}
|
|
201
279
|
|
|
202
280
|
declare module '@tanstack/react-router' {
|
|
@@ -208,6 +286,13 @@ declare module '@tanstack/react-router' {
|
|
|
208
286
|
preLoaderRoute: typeof MentionsRouteImport
|
|
209
287
|
parentRoute: typeof rootRouteImport
|
|
210
288
|
}
|
|
289
|
+
'/links': {
|
|
290
|
+
id: '/links'
|
|
291
|
+
path: '/links'
|
|
292
|
+
fullPath: '/links'
|
|
293
|
+
preLoaderRoute: typeof LinksRouteImport
|
|
294
|
+
parentRoute: typeof rootRouteImport
|
|
295
|
+
}
|
|
211
296
|
'/likes': {
|
|
212
297
|
id: '/likes'
|
|
213
298
|
path: '/likes'
|
|
@@ -250,6 +335,13 @@ declare module '@tanstack/react-router' {
|
|
|
250
335
|
preLoaderRoute: typeof IndexRouteImport
|
|
251
336
|
parentRoute: typeof rootRouteImport
|
|
252
337
|
}
|
|
338
|
+
'/api/sync': {
|
|
339
|
+
id: '/api/sync'
|
|
340
|
+
path: '/api/sync'
|
|
341
|
+
fullPath: '/api/sync'
|
|
342
|
+
preLoaderRoute: typeof ApiSyncRouteImport
|
|
343
|
+
parentRoute: typeof rootRouteImport
|
|
344
|
+
}
|
|
253
345
|
'/api/status': {
|
|
254
346
|
id: '/api/status'
|
|
255
347
|
path: '/api/status'
|
|
@@ -264,6 +356,27 @@ declare module '@tanstack/react-router' {
|
|
|
264
356
|
preLoaderRoute: typeof ApiQueryRouteImport
|
|
265
357
|
parentRoute: typeof rootRouteImport
|
|
266
358
|
}
|
|
359
|
+
'/api/profile-hydrate': {
|
|
360
|
+
id: '/api/profile-hydrate'
|
|
361
|
+
path: '/api/profile-hydrate'
|
|
362
|
+
fullPath: '/api/profile-hydrate'
|
|
363
|
+
preLoaderRoute: typeof ApiProfileHydrateRouteImport
|
|
364
|
+
parentRoute: typeof rootRouteImport
|
|
365
|
+
}
|
|
366
|
+
'/api/link-preview': {
|
|
367
|
+
id: '/api/link-preview'
|
|
368
|
+
path: '/api/link-preview'
|
|
369
|
+
fullPath: '/api/link-preview'
|
|
370
|
+
preLoaderRoute: typeof ApiLinkPreviewRouteImport
|
|
371
|
+
parentRoute: typeof rootRouteImport
|
|
372
|
+
}
|
|
373
|
+
'/api/link-insights': {
|
|
374
|
+
id: '/api/link-insights'
|
|
375
|
+
path: '/api/link-insights'
|
|
376
|
+
fullPath: '/api/link-insights'
|
|
377
|
+
preLoaderRoute: typeof ApiLinkInsightsRouteImport
|
|
378
|
+
parentRoute: typeof rootRouteImport
|
|
379
|
+
}
|
|
267
380
|
'/api/inbox': {
|
|
268
381
|
id: '/api/inbox'
|
|
269
382
|
path: '/api/inbox'
|
|
@@ -271,6 +384,13 @@ declare module '@tanstack/react-router' {
|
|
|
271
384
|
preLoaderRoute: typeof ApiInboxRouteImport
|
|
272
385
|
parentRoute: typeof rootRouteImport
|
|
273
386
|
}
|
|
387
|
+
'/api/conversation': {
|
|
388
|
+
id: '/api/conversation'
|
|
389
|
+
path: '/api/conversation'
|
|
390
|
+
fullPath: '/api/conversation'
|
|
391
|
+
preLoaderRoute: typeof ApiConversationRouteImport
|
|
392
|
+
parentRoute: typeof rootRouteImport
|
|
393
|
+
}
|
|
274
394
|
'/api/blocks': {
|
|
275
395
|
id: '/api/blocks'
|
|
276
396
|
path: '/api/blocks'
|
|
@@ -302,13 +422,19 @@ const rootRouteChildren: RootRouteChildren = {
|
|
|
302
422
|
DmsRoute: DmsRoute,
|
|
303
423
|
InboxRoute: InboxRoute,
|
|
304
424
|
LikesRoute: LikesRoute,
|
|
425
|
+
LinksRoute: LinksRoute,
|
|
305
426
|
MentionsRoute: MentionsRoute,
|
|
306
427
|
ApiActionRoute: ApiActionRoute,
|
|
307
428
|
ApiAvatarRoute: ApiAvatarRoute,
|
|
308
429
|
ApiBlocksRoute: ApiBlocksRoute,
|
|
430
|
+
ApiConversationRoute: ApiConversationRoute,
|
|
309
431
|
ApiInboxRoute: ApiInboxRoute,
|
|
432
|
+
ApiLinkInsightsRoute: ApiLinkInsightsRoute,
|
|
433
|
+
ApiLinkPreviewRoute: ApiLinkPreviewRoute,
|
|
434
|
+
ApiProfileHydrateRoute: ApiProfileHydrateRoute,
|
|
310
435
|
ApiQueryRoute: ApiQueryRoute,
|
|
311
436
|
ApiStatusRoute: ApiStatusRoute,
|
|
437
|
+
ApiSyncRoute: ApiSyncRoute,
|
|
312
438
|
}
|
|
313
439
|
export const routeTree = rootRouteImport
|
|
314
440
|
._addFileChildren(rootRouteChildren)
|
package/src/routes/__root.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
5
|
import { AppNav } from "#/components/AppNav";
|
|
6
6
|
import { ThemeProvider, themeScript } from "#/lib/theme";
|
|
7
|
-
import { bodyClass,
|
|
7
|
+
import { bodyClass, mainColumnClass, siteShellClass } from "#/lib/ui";
|
|
8
8
|
|
|
9
9
|
import appCss from "../styles.css?url";
|
|
10
10
|
|
|
@@ -34,7 +34,11 @@ export const Route = createRootRoute({
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
function NotFoundView() {
|
|
37
|
-
return
|
|
37
|
+
return (
|
|
38
|
+
<main className={mainColumnClass}>
|
|
39
|
+
<div className="px-4 py-10 text-[var(--ink-soft)]">Not Found</div>
|
|
40
|
+
</main>
|
|
41
|
+
);
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
function RootDocument({ children }: { children: ReactNode }) {
|
|
@@ -48,7 +52,7 @@ function RootDocument({ children }: { children: ReactNode }) {
|
|
|
48
52
|
<ThemeProvider>
|
|
49
53
|
<div className={siteShellClass}>
|
|
50
54
|
<AppNav />
|
|
51
|
-
{children}
|
|
55
|
+
<main className={mainColumnClass}>{children}</main>
|
|
52
56
|
</div>
|
|
53
57
|
</ThemeProvider>
|
|
54
58
|
<TanStackDevtools
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { maybeAutoUpdateBackup } from "#/lib/backup";
|
|
3
|
+
import { getTweetConversation } from "#/lib/queries";
|
|
4
|
+
|
|
5
|
+
function json(data: unknown, status = 200) {
|
|
6
|
+
return new Response(JSON.stringify(data), {
|
|
7
|
+
status,
|
|
8
|
+
headers: {
|
|
9
|
+
"content-type": "application/json",
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Route = createFileRoute("/api/conversation")({
|
|
15
|
+
server: {
|
|
16
|
+
handlers: {
|
|
17
|
+
GET: async ({ request }) => {
|
|
18
|
+
await maybeAutoUpdateBackup();
|
|
19
|
+
const url = new URL(request.url);
|
|
20
|
+
const tweetId = url.searchParams.get("tweetId")?.trim();
|
|
21
|
+
if (!tweetId) {
|
|
22
|
+
return json({ ok: false, error: "Missing tweetId" }, 400);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const conversation = getTweetConversation(tweetId);
|
|
26
|
+
if (!conversation) {
|
|
27
|
+
return json({ ok: false, error: "Tweet not found" }, 404);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return json({ ok: true, ...conversation });
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { maybeAutoUpdateBackup } from "#/lib/backup";
|
|
3
|
+
import { getNativeDb } from "#/lib/db";
|
|
4
|
+
import { getLinkInsights } from "#/lib/link-insights";
|
|
5
|
+
import type {
|
|
6
|
+
LinkInsightKind,
|
|
7
|
+
LinkInsightRange,
|
|
8
|
+
LinkInsightSort,
|
|
9
|
+
LinkInsightSource,
|
|
10
|
+
} from "#/lib/types";
|
|
11
|
+
|
|
12
|
+
function json(data: unknown) {
|
|
13
|
+
return new Response(JSON.stringify(data), {
|
|
14
|
+
headers: {
|
|
15
|
+
"content-type": "application/json",
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseKind(value: string | null): LinkInsightKind {
|
|
21
|
+
return value === "videos" ? "videos" : "links";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseRange(value: string | null): LinkInsightRange {
|
|
25
|
+
if (
|
|
26
|
+
value === "today" ||
|
|
27
|
+
value === "week" ||
|
|
28
|
+
value === "month" ||
|
|
29
|
+
value === "year" ||
|
|
30
|
+
value === "all"
|
|
31
|
+
) {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
return "week";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseSource(value: string | null): LinkInsightSource {
|
|
38
|
+
if (value === "tweet" || value === "dm") {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
return "all";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseSort(value: string | null): LinkInsightSort {
|
|
45
|
+
if (value === "recent" || value === "comments") {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
return "rank";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseNumber(value: string | null) {
|
|
52
|
+
if (!value) return undefined;
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const Route = createFileRoute("/api/link-insights")({
|
|
58
|
+
server: {
|
|
59
|
+
handlers: {
|
|
60
|
+
GET: async ({ request }) => {
|
|
61
|
+
await maybeAutoUpdateBackup();
|
|
62
|
+
getNativeDb();
|
|
63
|
+
const url = new URL(request.url);
|
|
64
|
+
return json(
|
|
65
|
+
getLinkInsights({
|
|
66
|
+
kind: parseKind(url.searchParams.get("kind")),
|
|
67
|
+
range: parseRange(url.searchParams.get("range")),
|
|
68
|
+
sort: parseSort(url.searchParams.get("sort")),
|
|
69
|
+
source: parseSource(url.searchParams.get("source")),
|
|
70
|
+
since: url.searchParams.get("since") ?? undefined,
|
|
71
|
+
until: url.searchParams.get("until") ?? undefined,
|
|
72
|
+
limit: parseNumber(url.searchParams.get("limit")),
|
|
73
|
+
commentsLimit: parseNumber(url.searchParams.get("commentsLimit")),
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { getOrFetchLinkPreview } from "#/lib/link-preview-metadata";
|
|
3
|
+
|
|
4
|
+
function json(data: unknown, init?: ResponseInit) {
|
|
5
|
+
return new Response(JSON.stringify(data), {
|
|
6
|
+
...init,
|
|
7
|
+
headers: {
|
|
8
|
+
"content-type": "application/json",
|
|
9
|
+
...init?.headers,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseUrl(value: string | null) {
|
|
15
|
+
if (!value) return null;
|
|
16
|
+
try {
|
|
17
|
+
const parsed = new URL(value);
|
|
18
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return parsed.toString();
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Route = createFileRoute("/api/link-preview")({
|
|
28
|
+
server: {
|
|
29
|
+
handlers: {
|
|
30
|
+
GET: async ({ request }) => {
|
|
31
|
+
const url = new URL(request.url);
|
|
32
|
+
const previewUrl = parseUrl(url.searchParams.get("url"));
|
|
33
|
+
const shortUrl = parseUrl(url.searchParams.get("shortUrl"));
|
|
34
|
+
if (!previewUrl) {
|
|
35
|
+
return json({ ok: false, message: "Missing url" }, { status: 400 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const preview = await getOrFetchLinkPreview(previewUrl, { shortUrl });
|
|
39
|
+
return json({ ok: true, preview });
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { resolveProfilesForHandles } from "#/lib/profile-resolver";
|
|
3
|
+
|
|
4
|
+
function json(data: unknown, init?: ResponseInit) {
|
|
5
|
+
return new Response(JSON.stringify(data), {
|
|
6
|
+
...init,
|
|
7
|
+
headers: {
|
|
8
|
+
"content-type": "application/json",
|
|
9
|
+
...init?.headers,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseHandles(url: URL) {
|
|
15
|
+
const rawValues = [
|
|
16
|
+
...url.searchParams.getAll("handle"),
|
|
17
|
+
...(url.searchParams.get("handles")?.split(",") ?? []),
|
|
18
|
+
];
|
|
19
|
+
return Array.from(
|
|
20
|
+
new Set(
|
|
21
|
+
rawValues
|
|
22
|
+
.map((value) => value.trim().replace(/^@/, ""))
|
|
23
|
+
.filter((value) => /^[A-Za-z0-9_]{1,15}$/.test(value)),
|
|
24
|
+
),
|
|
25
|
+
).slice(0, 50);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Route = createFileRoute("/api/profile-hydrate")({
|
|
29
|
+
server: {
|
|
30
|
+
handlers: {
|
|
31
|
+
GET: async ({ request }) => {
|
|
32
|
+
const url = new URL(request.url);
|
|
33
|
+
const handles = parseHandles(url);
|
|
34
|
+
if (handles.length === 0) {
|
|
35
|
+
return json(
|
|
36
|
+
{ ok: false, message: "Missing handles" },
|
|
37
|
+
{ status: 400 },
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const results = await resolveProfilesForHandles(handles);
|
|
42
|
+
return json({
|
|
43
|
+
ok: true,
|
|
44
|
+
results,
|
|
45
|
+
hydratedProfiles: results.filter((result) => result.status === "hit")
|
|
46
|
+
.length,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { getWebSyncJob, parseWebSyncKind, startWebSync } from "#/lib/web-sync";
|
|
3
|
+
|
|
4
|
+
function json(data: unknown, init?: ResponseInit) {
|
|
5
|
+
return new Response(JSON.stringify(data), {
|
|
6
|
+
...init,
|
|
7
|
+
headers: {
|
|
8
|
+
"content-type": "application/json",
|
|
9
|
+
...init?.headers,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseAccountId(value: unknown) {
|
|
15
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Route = createFileRoute("/api/sync")({
|
|
19
|
+
server: {
|
|
20
|
+
handlers: {
|
|
21
|
+
GET: ({ request }) => {
|
|
22
|
+
const url = new URL(request.url);
|
|
23
|
+
const id = url.searchParams.get("id");
|
|
24
|
+
if (!id) {
|
|
25
|
+
return json(
|
|
26
|
+
{ ok: false, message: "Missing sync job id" },
|
|
27
|
+
{ status: 400 },
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const job = getWebSyncJob(id);
|
|
32
|
+
if (!job) {
|
|
33
|
+
return json(
|
|
34
|
+
{ ok: false, message: "Sync job not found" },
|
|
35
|
+
{ status: 404 },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return json(job);
|
|
40
|
+
},
|
|
41
|
+
POST: async ({ request }) => {
|
|
42
|
+
const body = (await request.json().catch(() => ({}))) as Record<
|
|
43
|
+
string,
|
|
44
|
+
unknown
|
|
45
|
+
>;
|
|
46
|
+
const kind = parseWebSyncKind(body.kind);
|
|
47
|
+
if (!kind) {
|
|
48
|
+
return json(
|
|
49
|
+
{ ok: false, message: "Unknown sync kind" },
|
|
50
|
+
{ status: 400 },
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const job = startWebSync(kind, parseAccountId(body.accountId));
|
|
55
|
+
return json(job, { status: job.inProgress ? 202 : 200 });
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|