birdclaw 0.8.2 → 0.8.3
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 +16 -0
- package/package.json +2 -1
- package/src/cli/command-context.ts +17 -0
- package/src/cli/register-analysis.ts +500 -0
- package/src/cli/register-compose.ts +40 -0
- package/src/cli/register-graph.ts +132 -0
- package/src/cli/register-inbox.ts +41 -0
- package/src/cli/register-storage.ts +106 -0
- package/src/cli.ts +30 -750
- package/src/components/AccountSwitcher.tsx +7 -15
- package/src/components/AvatarChip.tsx +1 -1
- package/src/components/AvatarPreload.ts +149 -0
- package/src/components/MarkdownCitations.tsx +680 -0
- package/src/components/MarkdownViewer.tsx +8 -674
- package/src/components/ProfileAnalysisClient.ts +191 -0
- package/src/components/ProfileAnalysisStream.tsx +16 -185
- package/src/components/ProfilePreview.tsx +2 -0
- package/src/components/links-controller.ts +162 -0
- package/src/components/links-model.ts +198 -0
- package/src/components/network-map-controller.ts +84 -0
- package/src/components/network-map-model.ts +255 -0
- package/src/components/useTimelineRouteData.ts +105 -235
- package/src/lib/analysis-runtime.ts +238 -0
- package/src/lib/api-client.ts +16 -215
- package/src/lib/api-contracts.ts +328 -0
- package/src/lib/archive-import-plan.ts +102 -0
- package/src/lib/archive-import.ts +170 -239
- package/src/lib/authored-live.ts +75 -120
- package/src/lib/backup.ts +335 -424
- package/src/lib/blocks-write.ts +30 -26
- package/src/lib/blocks.ts +18 -20
- package/src/lib/database-metrics.ts +88 -0
- package/src/lib/database-migrations.ts +34 -0
- package/src/lib/database-schema.ts +312 -0
- package/src/lib/database-writer.ts +69 -0
- package/src/lib/db.ts +84 -330
- package/src/lib/dm-read-model.ts +533 -0
- package/src/lib/dms-live.ts +34 -97
- package/src/lib/follow-graph.ts +17 -27
- package/src/lib/import-repository.ts +138 -0
- package/src/lib/inbox.ts +2 -1
- package/src/lib/live-sync-engine.ts +209 -0
- package/src/lib/live-transport-gateway.ts +128 -0
- package/src/lib/mention-threads-live.ts +90 -177
- package/src/lib/mentions-export.ts +1 -1
- package/src/lib/mentions-live.ts +57 -181
- package/src/lib/moderation-target.ts +15 -4
- package/src/lib/moderation-write.ts +1 -1
- package/src/lib/mutes-write.ts +30 -26
- package/src/lib/openai-response-runtime.ts +251 -0
- package/src/lib/paginated-sync.ts +93 -0
- package/src/lib/period-digest.ts +116 -304
- package/src/lib/profile-analysis.ts +36 -110
- package/src/lib/queries.ts +6 -2381
- package/src/lib/query-actions.ts +437 -0
- package/src/lib/query-client.tsx +47 -0
- package/src/lib/query-read-model-shared.ts +52 -0
- package/src/lib/query-read-models.ts +5 -0
- package/src/lib/query-resource.ts +41 -0
- package/src/lib/query-status.ts +164 -0
- package/src/lib/research.ts +1 -1
- package/src/lib/runtime-services.ts +20 -0
- package/src/lib/search-discussion.ts +75 -279
- package/src/lib/server-runtime-services.ts +30 -0
- package/src/lib/sqlite.ts +48 -12
- package/src/lib/streaming-ingestion.ts +240 -0
- package/src/lib/sync-cache.ts +6 -1
- package/src/lib/sync-plan.ts +175 -0
- package/src/lib/timeline-collections-live.ts +83 -257
- package/src/lib/timeline-live.ts +86 -236
- package/src/lib/timeline-read-model.ts +1191 -0
- package/src/lib/tweet-repository.ts +156 -0
- package/src/lib/tweet-search-live.ts +63 -167
- package/src/lib/web-sync.ts +67 -50
- package/src/lib/whois.ts +2 -1
- package/src/routes/__root.tsx +11 -8
- package/src/routes/api/action.tsx +1 -1
- package/src/routes/api/conversation.tsx +1 -1
- package/src/routes/api/query.tsx +32 -26
- package/src/routes/api/status.tsx +6 -4
- package/src/routes/api/sync.tsx +5 -2
- package/src/routes/blocks.tsx +97 -131
- package/src/routes/data-sources.tsx +17 -25
- package/src/routes/dms.tsx +167 -184
- package/src/routes/inbox.tsx +63 -57
- package/src/routes/links.tsx +31 -394
- package/src/routes/network-map.tsx +41 -344
- package/src/routes/rate-limits.tsx +17 -21
- package/src/lib/client-cache.ts +0 -109
package/src/lib/whois.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { fetchProfileBioEntities } from "./profile-bio-entities";
|
|
|
9
9
|
import { fetchProfileSnapshots } from "./profile-history";
|
|
10
10
|
import { resolveProfilesForIdsEffect } from "./profile-resolver";
|
|
11
11
|
import type { ProfileResolveResult } from "./profile-resolver";
|
|
12
|
-
import { listDmConversations
|
|
12
|
+
import { listDmConversations } from "./dm-read-model";
|
|
13
|
+
import { listTimelineItems } from "./timeline-read-model";
|
|
13
14
|
import { expandUrlsFromTextsEffect } from "./url-expansion";
|
|
14
15
|
import type {
|
|
15
16
|
DmConversationItem,
|
package/src/routes/__root.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "@tanstack/react-router";
|
|
7
7
|
import type { ReactNode } from "react";
|
|
8
8
|
import { AppNav } from "#/components/AppNav";
|
|
9
|
+
import { BirdclawQueryProvider } from "#/lib/query-client";
|
|
9
10
|
import { ThemeProvider, themeScript } from "#/lib/theme";
|
|
10
11
|
import {
|
|
11
12
|
bodyClass,
|
|
@@ -64,14 +65,16 @@ function RootDocument({ children }: { children: ReactNode }) {
|
|
|
64
65
|
<script suppressHydrationWarning>{themeScript}</script>
|
|
65
66
|
</head>
|
|
66
67
|
<body className={bodyClass}>
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
{
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
<BirdclawQueryProvider>
|
|
69
|
+
<ThemeProvider>
|
|
70
|
+
<div className={wideMode ? siteShellDmClass : siteShellClass}>
|
|
71
|
+
<AppNav compact={wideMode} />
|
|
72
|
+
<main className={wideMode ? mainColumnDmClass : mainColumnClass}>
|
|
73
|
+
{children}
|
|
74
|
+
</main>
|
|
75
|
+
</div>
|
|
76
|
+
</ThemeProvider>
|
|
77
|
+
</BirdclawQueryProvider>
|
|
75
78
|
<Scripts />
|
|
76
79
|
</body>
|
|
77
80
|
</html>
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
runRouteEffect,
|
|
6
6
|
sensitiveRequestErrorResponse,
|
|
7
7
|
} from "#/lib/http-effect";
|
|
8
|
-
import { getTweetConversation } from "#/lib/
|
|
8
|
+
import { getTweetConversation } from "#/lib/timeline-read-model";
|
|
9
9
|
|
|
10
10
|
function json(data: unknown, status = 200) {
|
|
11
11
|
return new Response(JSON.stringify(data), {
|
package/src/routes/api/query.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
|
+
import { queryResponseSchema } from "#/lib/api-contracts";
|
|
3
4
|
import { maybeAutoUpdateBackupEffect } from "#/lib/backup";
|
|
4
5
|
import {
|
|
5
6
|
jsonResponse,
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
runRouteEffect,
|
|
8
9
|
sensitiveRequestErrorResponse,
|
|
9
10
|
} from "#/lib/http-effect";
|
|
10
|
-
import { queryResource } from "#/lib/
|
|
11
|
+
import { queryResource } from "#/lib/query-resource";
|
|
11
12
|
import type {
|
|
12
13
|
DmQuery,
|
|
13
14
|
ReplyFilter,
|
|
@@ -78,35 +79,40 @@ export const Route = createFileRoute("/api/query")({
|
|
|
78
79
|
|
|
79
80
|
if (resource === "dms") {
|
|
80
81
|
return jsonResponse(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
url.searchParams.get("
|
|
100
|
-
|
|
82
|
+
queryResponseSchema.parse(
|
|
83
|
+
queryResource("dms", {
|
|
84
|
+
...baseFilters,
|
|
85
|
+
participant:
|
|
86
|
+
url.searchParams.get("participant") ?? undefined,
|
|
87
|
+
minFollowers: parseOptionalNumber(
|
|
88
|
+
url.searchParams.get("minFollowers"),
|
|
89
|
+
),
|
|
90
|
+
maxFollowers: parseOptionalNumber(
|
|
91
|
+
url.searchParams.get("maxFollowers"),
|
|
92
|
+
),
|
|
93
|
+
minInfluenceScore: parseOptionalNumber(
|
|
94
|
+
url.searchParams.get("minInfluenceScore"),
|
|
95
|
+
),
|
|
96
|
+
maxInfluenceScore: parseOptionalNumber(
|
|
97
|
+
url.searchParams.get("maxInfluenceScore"),
|
|
98
|
+
),
|
|
99
|
+
sort: parseDmSort(url.searchParams.get("sort")),
|
|
100
|
+
inbox: parseDmInbox(url.searchParams.get("inbox")),
|
|
101
|
+
conversationId:
|
|
102
|
+
url.searchParams.get("conversationId") ?? undefined,
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
101
105
|
);
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
return jsonResponse(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
queryResponseSchema.parse(
|
|
110
|
+
queryResource(resource, {
|
|
111
|
+
...baseFilters,
|
|
112
|
+
resource,
|
|
113
|
+
untilId: url.searchParams.get("untilId") ?? undefined,
|
|
114
|
+
}),
|
|
115
|
+
),
|
|
110
116
|
);
|
|
111
117
|
}),
|
|
112
118
|
),
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
|
+
import { queryEnvelopeSchema } from "#/lib/api-contracts";
|
|
3
4
|
import { maybeAutoUpdateBackupEffect } from "#/lib/backup";
|
|
4
5
|
import {
|
|
5
6
|
jsonResponse,
|
|
6
7
|
runRouteEffect,
|
|
7
8
|
sensitiveRequestErrorResponse,
|
|
8
9
|
} from "#/lib/http-effect";
|
|
9
|
-
import { getQueryEnvelopeEffect } from "#/lib/
|
|
10
|
+
import { getQueryEnvelopeEffect } from "#/lib/query-status";
|
|
10
11
|
|
|
11
12
|
export const Route = createFileRoute("/api/status")({
|
|
12
13
|
server: {
|
|
@@ -18,9 +19,10 @@ export const Route = createFileRoute("/api/status")({
|
|
|
18
19
|
if (denied) return denied;
|
|
19
20
|
|
|
20
21
|
yield* maybeAutoUpdateBackupEffect();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
);
|
|
22
|
+
const envelope = yield* getQueryEnvelopeEffect({
|
|
23
|
+
includeArchives: false,
|
|
24
|
+
});
|
|
25
|
+
return jsonResponse(queryEnvelopeSchema.parse(envelope));
|
|
24
26
|
}),
|
|
25
27
|
),
|
|
26
28
|
},
|
package/src/routes/api/sync.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
|
+
import { webSyncJobSchema } from "#/lib/api-contracts";
|
|
3
4
|
import {
|
|
4
5
|
jsonResponse,
|
|
5
6
|
requestJsonEffect,
|
|
@@ -72,7 +73,7 @@ export const Route = createFileRoute("/api/sync")({
|
|
|
72
73
|
);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
return jsonResponse(job);
|
|
76
|
+
return jsonResponse(webSyncJobSchema.parse(job));
|
|
76
77
|
},
|
|
77
78
|
POST: ({ request }) =>
|
|
78
79
|
runRouteEffect(
|
|
@@ -97,7 +98,9 @@ export const Route = createFileRoute("/api/sync")({
|
|
|
97
98
|
parseAccountId(body.accountId),
|
|
98
99
|
parseSyncOptions(kind, body),
|
|
99
100
|
);
|
|
100
|
-
return jsonResponse(job, {
|
|
101
|
+
return jsonResponse(webSyncJobSchema.parse(job), {
|
|
102
|
+
status: job.inProgress ? 202 : 200,
|
|
103
|
+
});
|
|
101
104
|
}),
|
|
102
105
|
),
|
|
103
106
|
},
|
package/src/routes/blocks.tsx
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import {
|
|
3
|
+
keepPreviousData,
|
|
4
|
+
useQuery,
|
|
5
|
+
useQueryClient,
|
|
6
|
+
} from "@tanstack/react-query";
|
|
2
7
|
import { useEffect, useMemo, useState } from "react";
|
|
3
8
|
import { AvatarChip } from "#/components/AvatarChip";
|
|
9
|
+
import { useDebouncedValue } from "#/components/useDebouncedValue";
|
|
10
|
+
import { fetchQueryEnvelope, postAction } from "#/lib/api-client";
|
|
4
11
|
import { formatCompactNumber } from "#/lib/present";
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
BlockListResponse,
|
|
8
|
-
BlockSearchItem,
|
|
9
|
-
QueryEnvelope,
|
|
10
|
-
} from "#/lib/types";
|
|
12
|
+
import { queryKeys } from "#/lib/query-client";
|
|
13
|
+
import type { BlockListResponse } from "#/lib/types";
|
|
11
14
|
import {
|
|
12
15
|
blockRowBodyClass,
|
|
13
16
|
blockRowClass,
|
|
@@ -35,128 +38,96 @@ export const Route = createFileRoute("/blocks")({
|
|
|
35
38
|
});
|
|
36
39
|
|
|
37
40
|
function BlocksRoute() {
|
|
38
|
-
const
|
|
41
|
+
const queryClient = useQueryClient();
|
|
39
42
|
const [accountId, setAccountId] = useState<string>("acct_primary");
|
|
40
43
|
const [search, setSearch] = useState("");
|
|
41
|
-
const [items, setItems] = useState<BlockItem[]>([]);
|
|
42
|
-
const [matches, setMatches] = useState<BlockSearchItem[]>([]);
|
|
43
|
-
const [refreshTick, setRefreshTick] = useState(0);
|
|
44
44
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
45
|
-
const [isSyncing, setIsSyncing] = useState(false);
|
|
46
45
|
const [message, setMessage] = useState("");
|
|
47
|
-
const [
|
|
46
|
+
const [actionError, setActionError] = useState("");
|
|
47
|
+
const statusQuery = useQuery({
|
|
48
|
+
queryKey: queryKeys.status,
|
|
49
|
+
queryFn: ({ signal }) => fetchQueryEnvelope({ signal }),
|
|
50
|
+
});
|
|
51
|
+
const meta = statusQuery.data ?? null;
|
|
52
|
+
const debouncedSearch = useDebouncedValue(search, 180);
|
|
48
53
|
const hasAccountId = accountId.trim().length > 0;
|
|
49
54
|
const isReady = Boolean(meta);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.catch((error: unknown) => {
|
|
62
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
setError(
|
|
66
|
-
error instanceof Error
|
|
67
|
-
? error.message
|
|
68
|
-
: "Unable to load blocklist status",
|
|
69
|
-
);
|
|
55
|
+
const blocksQueryKey = [
|
|
56
|
+
...queryKeys.blocks,
|
|
57
|
+
{ accountId, search: debouncedSearch },
|
|
58
|
+
] as const;
|
|
59
|
+
const blocksQuery = useQuery({
|
|
60
|
+
queryKey: blocksQueryKey,
|
|
61
|
+
enabled: hasAccountId,
|
|
62
|
+
queryFn: async ({ signal }) => {
|
|
63
|
+
const params = new URLSearchParams({
|
|
64
|
+
account: accountId,
|
|
65
|
+
limit: "12",
|
|
70
66
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
const controller = new AbortController();
|
|
79
|
-
const params = new URLSearchParams({
|
|
80
|
-
account: accountId,
|
|
81
|
-
limit: "12",
|
|
82
|
-
refresh: String(refreshTick),
|
|
83
|
-
});
|
|
84
|
-
if (search.trim()) {
|
|
85
|
-
params.set("search", search.trim());
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
fetch(`/api/blocks?${params.toString()}`, { signal: controller.signal })
|
|
89
|
-
.then((response) => response.json())
|
|
90
|
-
.then((data: BlockListResponse) => {
|
|
91
|
-
setItems(data.items);
|
|
92
|
-
setMatches(data.matches);
|
|
93
|
-
setError("");
|
|
94
|
-
})
|
|
95
|
-
.catch((error: unknown) => {
|
|
96
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
setError(
|
|
100
|
-
error instanceof Error ? error.message : "Unable to load blocklist",
|
|
101
|
-
);
|
|
67
|
+
if (debouncedSearch.trim()) {
|
|
68
|
+
params.set("search", debouncedSearch.trim());
|
|
69
|
+
}
|
|
70
|
+
const response = await fetch(`/api/blocks?${params.toString()}`, {
|
|
71
|
+
signal,
|
|
102
72
|
});
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Blocklist request failed (${String(response.status)})`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return (await response.json()) as BlockListResponse;
|
|
79
|
+
},
|
|
80
|
+
placeholderData: keepPreviousData,
|
|
81
|
+
staleTime: 5 * 60_000,
|
|
82
|
+
});
|
|
83
|
+
const items = blocksQuery.data?.items ?? [];
|
|
84
|
+
const matches = blocksQuery.data?.matches ?? [];
|
|
85
|
+
const blockSyncQuery = useQuery({
|
|
86
|
+
queryKey: [...queryKeys.blockSync, accountId],
|
|
87
|
+
enabled: hasAccountId,
|
|
88
|
+
retry: false,
|
|
89
|
+
queryFn: async () => {
|
|
90
|
+
const data = (await postAction({
|
|
121
91
|
kind: "syncBlocks",
|
|
122
92
|
accountId,
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
},
|
|
147
|
-
)
|
|
148
|
-
.catch((error: unknown) => {
|
|
149
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
setError(error instanceof Error ? error.message : "Block sync failed");
|
|
153
|
-
})
|
|
154
|
-
.finally(() => setIsSyncing(false));
|
|
93
|
+
})) as {
|
|
94
|
+
ok?: boolean;
|
|
95
|
+
syncedCount?: number;
|
|
96
|
+
transport?: { ok?: boolean; output?: string };
|
|
97
|
+
};
|
|
98
|
+
if (data.ok === false || data.transport?.ok === false) {
|
|
99
|
+
throw new Error(data.transport?.output ?? "Block sync failed");
|
|
100
|
+
}
|
|
101
|
+
await queryClient.invalidateQueries({ queryKey: queryKeys.blocks });
|
|
102
|
+
return data;
|
|
103
|
+
},
|
|
104
|
+
staleTime: 5 * 60_000,
|
|
105
|
+
});
|
|
106
|
+
const isSyncing = blockSyncQuery.isFetching;
|
|
107
|
+
const queryError =
|
|
108
|
+
statusQuery.error ?? blocksQuery.error ?? blockSyncQuery.error ?? null;
|
|
109
|
+
const error =
|
|
110
|
+
actionError ||
|
|
111
|
+
(queryError instanceof Error
|
|
112
|
+
? queryError.message
|
|
113
|
+
: queryError
|
|
114
|
+
? "Unable to load blocklist"
|
|
115
|
+
: "");
|
|
155
116
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!meta?.accounts.length) return;
|
|
119
|
+
if (meta.accounts.some((account) => account.id === accountId)) return;
|
|
120
|
+
setAccountId(meta.accounts[0]?.id ?? "acct_primary");
|
|
121
|
+
}, [accountId, meta]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const data = blockSyncQuery.data;
|
|
125
|
+
if (!data || data.transport?.output?.includes("disabled")) return;
|
|
126
|
+
setMessage(
|
|
127
|
+
data.transport?.output ??
|
|
128
|
+
`Synced ${String(data.syncedCount ?? 0)} remote blocks`,
|
|
129
|
+
);
|
|
130
|
+
}, [blockSyncQuery.data]);
|
|
160
131
|
|
|
161
132
|
const subtitle = useMemo(() => {
|
|
162
133
|
if (!meta) {
|
|
@@ -177,26 +148,21 @@ function BlocksRoute() {
|
|
|
177
148
|
if (!normalized) return;
|
|
178
149
|
|
|
179
150
|
setIsSubmitting(true);
|
|
180
|
-
|
|
151
|
+
setActionError("");
|
|
181
152
|
setMessage("");
|
|
182
153
|
|
|
183
154
|
try {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
accountId,
|
|
190
|
-
query: normalized,
|
|
191
|
-
}),
|
|
192
|
-
});
|
|
193
|
-
const data = (await response.json()) as {
|
|
155
|
+
const data = (await postAction({
|
|
156
|
+
kind,
|
|
157
|
+
accountId,
|
|
158
|
+
query: normalized,
|
|
159
|
+
})) as {
|
|
194
160
|
ok?: boolean;
|
|
195
161
|
profile?: { handle?: string };
|
|
196
162
|
transport?: { ok?: boolean; output?: string };
|
|
197
163
|
};
|
|
198
164
|
if (data.ok === false || data.transport?.ok === false) {
|
|
199
|
-
|
|
165
|
+
setActionError(data.transport?.output ?? "Blocklist action failed");
|
|
200
166
|
return;
|
|
201
167
|
}
|
|
202
168
|
|
|
@@ -205,9 +171,9 @@ function BlocksRoute() {
|
|
|
205
171
|
data.profile?.handle ?? normalized.replace(/^@/, "")
|
|
206
172
|
} · ${data.transport?.output ?? "local"}`,
|
|
207
173
|
);
|
|
208
|
-
|
|
174
|
+
await queryClient.invalidateQueries({ queryKey: queryKeys.blocks });
|
|
209
175
|
} catch (submitError) {
|
|
210
|
-
|
|
176
|
+
setActionError(
|
|
211
177
|
submitError instanceof Error
|
|
212
178
|
? submitError.message
|
|
213
179
|
: "Blocklist action failed",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
3
|
import {
|
|
3
4
|
AlertTriangle,
|
|
4
5
|
CheckCircle2,
|
|
@@ -8,7 +9,8 @@ import {
|
|
|
8
9
|
TerminalSquare,
|
|
9
10
|
XCircle,
|
|
10
11
|
} from "lucide-react";
|
|
11
|
-
import {
|
|
12
|
+
import { useMemo } from "react";
|
|
13
|
+
import { queryKeys } from "#/lib/query-client";
|
|
12
14
|
import type {
|
|
13
15
|
LiveDataSourceCapability,
|
|
14
16
|
LiveDataSourceKind,
|
|
@@ -169,11 +171,14 @@ function CapabilityRow({
|
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
function DataSourcesRoute() {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
const dataSourcesQuery = useQuery({
|
|
175
|
+
queryKey: queryKeys.dataSources,
|
|
176
|
+
queryFn: fetchDataSources,
|
|
177
|
+
staleTime: 5 * 60_000,
|
|
178
|
+
});
|
|
179
|
+
const snapshot = dataSourcesQuery.data ?? null;
|
|
180
|
+
const loading = dataSourcesQuery.isFetching;
|
|
181
|
+
const error = dataSourcesQuery.error;
|
|
177
182
|
const sourcesByKind = useMemo(
|
|
178
183
|
() =>
|
|
179
184
|
new Map(
|
|
@@ -182,23 +187,6 @@ function DataSourcesRoute() {
|
|
|
182
187
|
[snapshot],
|
|
183
188
|
);
|
|
184
189
|
|
|
185
|
-
const load = useCallback(() => {
|
|
186
|
-
setLoading(true);
|
|
187
|
-
setError(null);
|
|
188
|
-
fetchDataSources()
|
|
189
|
-
.then(setSnapshot)
|
|
190
|
-
.catch((cause: unknown) => {
|
|
191
|
-
setError(
|
|
192
|
-
cause instanceof Error ? cause.message : "Data sources unavailable",
|
|
193
|
-
);
|
|
194
|
-
})
|
|
195
|
-
.finally(() => setLoading(false));
|
|
196
|
-
}, []);
|
|
197
|
-
|
|
198
|
-
useEffect(() => {
|
|
199
|
-
load();
|
|
200
|
-
}, [load]);
|
|
201
|
-
|
|
202
190
|
return (
|
|
203
191
|
<section className="flex min-h-screen flex-col">
|
|
204
192
|
<header className={pageHeaderClass}>
|
|
@@ -213,7 +201,7 @@ function DataSourcesRoute() {
|
|
|
213
201
|
<button
|
|
214
202
|
className={secondaryButtonClass}
|
|
215
203
|
type="button"
|
|
216
|
-
onClick={
|
|
204
|
+
onClick={() => void dataSourcesQuery.refetch()}
|
|
217
205
|
disabled={loading}
|
|
218
206
|
>
|
|
219
207
|
<RefreshCw className={cx("size-4", loading && "animate-spin")} />
|
|
@@ -222,7 +210,11 @@ function DataSourcesRoute() {
|
|
|
222
210
|
</div>
|
|
223
211
|
</div>
|
|
224
212
|
</header>
|
|
225
|
-
{error ?
|
|
213
|
+
{error ? (
|
|
214
|
+
<div className={errorCopyClass}>
|
|
215
|
+
{error instanceof Error ? error.message : "Data sources unavailable"}
|
|
216
|
+
</div>
|
|
217
|
+
) : null}
|
|
226
218
|
{snapshot ? (
|
|
227
219
|
<>
|
|
228
220
|
<div className="grid border-t border-[var(--line)]">
|