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,51 +1,77 @@
|
|
|
1
1
|
import { RefreshCw } from "lucide-react";
|
|
2
|
-
import {
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
3
|
import { postSync } from "#/lib/api-client";
|
|
4
4
|
import type { AccountRecord } from "#/lib/types";
|
|
5
5
|
import { cx, selectFieldClass } from "#/lib/ui";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
WebSyncKind,
|
|
8
|
+
WebSyncOptions,
|
|
9
|
+
WebSyncResponse,
|
|
10
|
+
} from "#/lib/web-sync";
|
|
11
|
+
import {
|
|
12
|
+
defaultAccountId as getDefaultAccountId,
|
|
13
|
+
setStoredAccountId,
|
|
14
|
+
useSelectedAccountId,
|
|
15
|
+
} from "./account-selection";
|
|
7
16
|
|
|
8
17
|
interface SyncNowButtonProps {
|
|
9
18
|
kind: WebSyncKind;
|
|
10
19
|
label: string;
|
|
11
20
|
accounts?: AccountRecord[];
|
|
12
21
|
onSynced: (result: WebSyncResponse) => void;
|
|
22
|
+
showAccountPicker?: boolean;
|
|
23
|
+
syncOptions?: WebSyncOptions;
|
|
13
24
|
}
|
|
14
25
|
|
|
15
26
|
export function SyncNowButton({
|
|
16
27
|
kind,
|
|
17
28
|
label,
|
|
18
|
-
accounts
|
|
29
|
+
accounts,
|
|
19
30
|
onSynced,
|
|
31
|
+
showAccountPicker = false,
|
|
32
|
+
syncOptions,
|
|
20
33
|
}: SyncNowButtonProps) {
|
|
21
34
|
const [syncing, setSyncing] = useState(false);
|
|
22
35
|
const [message, setMessage] = useState<string | null>(null);
|
|
23
36
|
const [error, setError] = useState<string | null>(null);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
>();
|
|
37
|
+
const accountList = accounts ?? [];
|
|
38
|
+
const globalAccountId = useSelectedAccountId(accounts);
|
|
27
39
|
const defaultAccountId = useMemo(
|
|
28
|
-
() => accounts
|
|
40
|
+
() => getDefaultAccountId(accounts),
|
|
29
41
|
[accounts],
|
|
30
42
|
);
|
|
31
|
-
const accountId =
|
|
43
|
+
const accountId = globalAccountId ?? defaultAccountId;
|
|
44
|
+
const accountAwareSync = kind !== "dms";
|
|
45
|
+
const waitingForAccount =
|
|
46
|
+
accountAwareSync &&
|
|
47
|
+
accounts === undefined &&
|
|
48
|
+
(showAccountPicker || kind !== "timeline");
|
|
49
|
+
const birdOnlyWrongAccount =
|
|
50
|
+
!accountAwareSync &&
|
|
51
|
+
accountId !== undefined &&
|
|
52
|
+
defaultAccountId !== undefined &&
|
|
53
|
+
accountId !== defaultAccountId;
|
|
54
|
+
const disabled = syncing || waitingForAccount || birdOnlyWrongAccount;
|
|
55
|
+
const statusMessage = birdOnlyWrongAccount
|
|
56
|
+
? "Switch to default to sync"
|
|
57
|
+
: waitingForAccount
|
|
58
|
+
? "Loading account"
|
|
59
|
+
: (error ?? message ?? "");
|
|
32
60
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
if (!accountId || !accounts.some((account) => account.id === accountId)) {
|
|
39
|
-
setSelectedAccountId(defaultAccountId);
|
|
40
|
-
}
|
|
41
|
-
}, [accountId, accounts, defaultAccountId]);
|
|
61
|
+
function selectAccount(accountId: string) {
|
|
62
|
+
setStoredAccountId(accountId);
|
|
63
|
+
}
|
|
42
64
|
|
|
43
65
|
async function syncNow() {
|
|
44
66
|
setSyncing(true);
|
|
45
67
|
setError(null);
|
|
46
68
|
setMessage(null);
|
|
47
69
|
try {
|
|
48
|
-
const data = await postSync(
|
|
70
|
+
const data = await postSync(
|
|
71
|
+
kind,
|
|
72
|
+
accountAwareSync ? accountId : undefined,
|
|
73
|
+
syncOptions,
|
|
74
|
+
);
|
|
49
75
|
if (!data.ok) throw new Error(data.summary);
|
|
50
76
|
setMessage(data.summary);
|
|
51
77
|
onSynced(data);
|
|
@@ -58,15 +84,15 @@ export function SyncNowButton({
|
|
|
58
84
|
|
|
59
85
|
return (
|
|
60
86
|
<div className="flex shrink-0 items-center gap-2">
|
|
61
|
-
{
|
|
87
|
+
{showAccountPicker && accountAwareSync && accountList.length > 1 ? (
|
|
62
88
|
<select
|
|
63
89
|
aria-label="Sync account"
|
|
64
90
|
className={cx(selectFieldClass, "h-9 w-[132px]")}
|
|
65
91
|
disabled={syncing}
|
|
66
|
-
onChange={(event) =>
|
|
92
|
+
onChange={(event) => selectAccount(event.target.value)}
|
|
67
93
|
value={accountId ?? ""}
|
|
68
94
|
>
|
|
69
|
-
{
|
|
95
|
+
{accountList.map((account) => (
|
|
70
96
|
<option key={account.id} value={account.id}>
|
|
71
97
|
{account.handle}
|
|
72
98
|
</option>
|
|
@@ -76,11 +102,20 @@ export function SyncNowButton({
|
|
|
76
102
|
<button
|
|
77
103
|
type="button"
|
|
78
104
|
className={cx(
|
|
79
|
-
"inline-flex h-9 shrink-0 items-center gap-1.5 rounded-full border border-[var(--line)] bg-[var(--bg)] px-3 text-[13px] font-semibold text-[var(--ink)] transition-[background,border-color,color,transform] duration-150 hover:border-[color:color-mix(in_srgb,var(--accent)_45%,var(--line))] hover:bg-[var(--accent-soft)] hover:text-[var(--accent)] active:scale-[0.98] disabled:
|
|
105
|
+
"inline-flex h-9 shrink-0 items-center gap-1.5 rounded-full border border-[var(--line)] bg-[var(--bg)] px-3 text-[13px] font-semibold text-[var(--ink)] transition-[background,border-color,color,transform] duration-150 hover:border-[color:color-mix(in_srgb,var(--accent)_45%,var(--line))] hover:bg-[var(--accent-soft)] hover:text-[var(--accent)] active:scale-[0.98] disabled:opacity-65",
|
|
80
106
|
syncing && "text-[var(--ink-soft)]",
|
|
107
|
+
birdOnlyWrongAccount
|
|
108
|
+
? "disabled:cursor-not-allowed"
|
|
109
|
+
: "disabled:cursor-wait",
|
|
81
110
|
)}
|
|
82
|
-
aria-label={
|
|
83
|
-
|
|
111
|
+
aria-label={
|
|
112
|
+
birdOnlyWrongAccount
|
|
113
|
+
? `${label}: default account only`
|
|
114
|
+
: syncing
|
|
115
|
+
? `${label}: syncing`
|
|
116
|
+
: label
|
|
117
|
+
}
|
|
118
|
+
disabled={disabled}
|
|
84
119
|
onClick={syncNow}
|
|
85
120
|
>
|
|
86
121
|
<RefreshCw
|
|
@@ -98,7 +133,7 @@ export function SyncNowButton({
|
|
|
98
133
|
)}
|
|
99
134
|
role="status"
|
|
100
135
|
>
|
|
101
|
-
{
|
|
136
|
+
{statusMessage}
|
|
102
137
|
</span>
|
|
103
138
|
</div>
|
|
104
139
|
);
|
|
@@ -17,59 +17,64 @@ const THEME_OPTIONS = [
|
|
|
17
17
|
label: string;
|
|
18
18
|
}>;
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<fieldset
|
|
25
|
-
className="theme-slider-shell m-0 flex justify-center border-0 px-2 py-1 min-[1100px]:justify-start min-[1100px]:px-3"
|
|
26
|
-
aria-label="Theme selector"
|
|
27
|
-
>
|
|
28
|
-
<div className="theme-slider flex flex-col items-center gap-1.5 min-[1100px]:flex-row">
|
|
29
|
-
{THEME_OPTIONS.map((option, index) => {
|
|
30
|
-
const Icon = option.icon;
|
|
31
|
-
const isActive = option.key === theme || (index === 0 && !theme);
|
|
20
|
+
function themeIndex(theme: ThemeValue) {
|
|
21
|
+
return THEME_OPTIONS.findIndex((option) => option.key === theme);
|
|
22
|
+
}
|
|
32
23
|
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
export function ThemeSlider({ compact = false }: { compact?: boolean }) {
|
|
25
|
+
const { isReady, theme, setTheme } = useTheme();
|
|
26
|
+
const activeIndex = Math.max(themeIndex(theme), 0);
|
|
27
|
+
const activeOption = THEME_OPTIONS[activeIndex];
|
|
28
|
+
const nextOption = THEME_OPTIONS[(activeIndex + 1) % THEME_OPTIONS.length];
|
|
29
|
+
const Icon = activeOption.icon;
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
|
|
32
|
+
const context: ThemeTransitionContext = {
|
|
33
|
+
element: event.currentTarget,
|
|
34
|
+
pointerClientX: event.clientX,
|
|
35
|
+
pointerClientY: event.clientY,
|
|
36
|
+
};
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
startThemeTransition({
|
|
39
|
+
nextTheme: nextOption.key,
|
|
40
|
+
currentTheme: theme,
|
|
41
|
+
setTheme,
|
|
42
|
+
context,
|
|
43
|
+
});
|
|
44
|
+
};
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
className={cx(
|
|
49
|
+
"theme-toggle-shell flex justify-center",
|
|
50
|
+
compact
|
|
51
|
+
? "shrink-0"
|
|
52
|
+
: "px-1 py-1 min-[1100px]:justify-start min-[1100px]:px-2",
|
|
53
|
+
)}
|
|
54
|
+
title={`${activeOption.label}; click for ${nextOption.label}`}
|
|
55
|
+
>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
className={cx(
|
|
59
|
+
"theme-toggle-button inline-flex items-center justify-center rounded-full border bg-transparent text-[var(--ink-soft)] transition-[background,border-color,color,transform,box-shadow] duration-150 hover:border-[color:color-mix(in_srgb,var(--accent)_38%,var(--line))] hover:bg-[var(--bg-hover)] hover:text-[var(--ink)] active:scale-95 disabled:cursor-default disabled:opacity-55 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:color-mix(in_srgb,var(--accent)_54%,transparent)]",
|
|
60
|
+
compact
|
|
61
|
+
? "size-8 border-transparent"
|
|
62
|
+
: "size-11 border-[var(--line)] shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--line)_44%,transparent)]",
|
|
63
|
+
)}
|
|
64
|
+
onClick={handleClick}
|
|
65
|
+
aria-label={`Theme: ${activeOption.label}. Switch to ${nextOption.label}.`}
|
|
66
|
+
data-testid="theme-toggle"
|
|
67
|
+
disabled={!isReady}
|
|
68
|
+
>
|
|
69
|
+
<Icon
|
|
70
|
+
className={cx(
|
|
71
|
+
"theme-toggle-icon",
|
|
72
|
+
compact ? "size-[17px]" : "size-[19px]",
|
|
73
|
+
)}
|
|
74
|
+
strokeWidth={2}
|
|
75
|
+
aria-hidden="true"
|
|
76
|
+
/>
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
74
79
|
);
|
|
75
80
|
}
|