botchan 0.1.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/LICENSE +21 -0
- package/README.md +237 -0
- package/dist/cli/index.mjs +2728 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/tui/index.mjs +1625 -0
- package/dist/tui/index.mjs.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,2728 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk10 from 'chalk';
|
|
3
|
+
import { encodeFunctionData, createWalletClient, http } from 'viem';
|
|
4
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
5
|
+
import { NULL_ADDRESS, NetClient, getChainRpcUrls } from '@net-protocol/core';
|
|
6
|
+
import { FeedRegistryClient, FeedClient, generatePostHash, parseCommentData } from '@net-protocol/feeds';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import React5, { useState, useCallback, useEffect, useRef } from 'react';
|
|
11
|
+
import { render, useStdout, Box, useInput, Text } from 'ink';
|
|
12
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
13
|
+
import { Command } from 'commander';
|
|
14
|
+
import { createRequire } from 'module';
|
|
15
|
+
import * as readline from 'readline';
|
|
16
|
+
|
|
17
|
+
var __defProp = Object.defineProperty;
|
|
18
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
19
|
+
var __esm = (fn, res) => function __init() {
|
|
20
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
21
|
+
};
|
|
22
|
+
var __export = (target, all) => {
|
|
23
|
+
for (var name in all)
|
|
24
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
25
|
+
};
|
|
26
|
+
function normalizeFeedName(feed) {
|
|
27
|
+
return feed.toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
function getChainId(optionValue) {
|
|
30
|
+
if (optionValue) {
|
|
31
|
+
return optionValue;
|
|
32
|
+
}
|
|
33
|
+
const envChainId = process.env.BOTCHAN_CHAIN_ID || process.env.NET_CHAIN_ID;
|
|
34
|
+
if (envChainId) {
|
|
35
|
+
return parseInt(envChainId, 10);
|
|
36
|
+
}
|
|
37
|
+
return DEFAULT_CHAIN_ID;
|
|
38
|
+
}
|
|
39
|
+
function getRpcUrl(optionValue) {
|
|
40
|
+
return optionValue || process.env.BOTCHAN_RPC_URL || process.env.NET_RPC_URL;
|
|
41
|
+
}
|
|
42
|
+
function parseReadOnlyOptions(options) {
|
|
43
|
+
return {
|
|
44
|
+
chainId: getChainId(options.chainId),
|
|
45
|
+
rpcUrl: getRpcUrl(options.rpcUrl)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function parseCommonOptions(options, supportsEncodeOnly = false) {
|
|
49
|
+
const privateKey = options.privateKey || process.env.BOTCHAN_PRIVATE_KEY || process.env.NET_PRIVATE_KEY || process.env.PRIVATE_KEY;
|
|
50
|
+
if (!privateKey) {
|
|
51
|
+
const encodeOnlyHint = supportsEncodeOnly ? ", or use --encode-only to output transaction data without submitting" : "";
|
|
52
|
+
console.error(
|
|
53
|
+
chalk10.red(
|
|
54
|
+
`Error: Private key is required. Provide via --private-key flag or BOTCHAN_PRIVATE_KEY/NET_PRIVATE_KEY environment variable${encodeOnlyHint}`
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
if (!privateKey.startsWith("0x") || privateKey.length !== 66) {
|
|
60
|
+
console.error(
|
|
61
|
+
chalk10.red(
|
|
62
|
+
"Error: Invalid private key format (must be 0x-prefixed, 66 characters)"
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
if (options.privateKey) {
|
|
68
|
+
console.warn(
|
|
69
|
+
chalk10.yellow(
|
|
70
|
+
"Warning: Private key provided via command line. Consider using BOTCHAN_PRIVATE_KEY environment variable instead."
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
privateKey,
|
|
76
|
+
chainId: getChainId(options.chainId),
|
|
77
|
+
rpcUrl: getRpcUrl(options.rpcUrl)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
var DEFAULT_CHAIN_ID;
|
|
81
|
+
var init_config = __esm({
|
|
82
|
+
"src/utils/config.ts"() {
|
|
83
|
+
DEFAULT_CHAIN_ID = 8453;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
function createWallet(privateKey, chainId, rpcUrl) {
|
|
87
|
+
const account = privateKeyToAccount(privateKey);
|
|
88
|
+
const rpcUrls = getChainRpcUrls({
|
|
89
|
+
chainId,
|
|
90
|
+
rpcUrl
|
|
91
|
+
});
|
|
92
|
+
return createWalletClient({
|
|
93
|
+
account,
|
|
94
|
+
transport: http(rpcUrls[0])
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async function executeTransaction(walletClient, txConfig) {
|
|
98
|
+
const hash = await walletClient.writeContract({
|
|
99
|
+
address: txConfig.to,
|
|
100
|
+
abi: txConfig.abi,
|
|
101
|
+
functionName: txConfig.functionName,
|
|
102
|
+
args: txConfig.args,
|
|
103
|
+
value: txConfig.value,
|
|
104
|
+
chain: null
|
|
105
|
+
});
|
|
106
|
+
return hash;
|
|
107
|
+
}
|
|
108
|
+
var init_wallet = __esm({
|
|
109
|
+
"src/utils/wallet.ts"() {
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
function truncateAddress(address) {
|
|
113
|
+
if (address.length <= 12) return address;
|
|
114
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
115
|
+
}
|
|
116
|
+
function formatTimestamp(timestamp) {
|
|
117
|
+
return new Date(Number(timestamp) * 1e3).toISOString().replace("T", " ").slice(0, 19);
|
|
118
|
+
}
|
|
119
|
+
function parseTopic(topic) {
|
|
120
|
+
const commentMatch = topic.match(/^(.+?):comments:/);
|
|
121
|
+
if (commentMatch) {
|
|
122
|
+
return { feedName: commentMatch[1], isComment: true };
|
|
123
|
+
}
|
|
124
|
+
return { feedName: topic, isComment: false };
|
|
125
|
+
}
|
|
126
|
+
function formatPost(post, index, options = {}) {
|
|
127
|
+
const { commentCount, showTopic } = options;
|
|
128
|
+
const timestamp = formatTimestamp(post.timestamp);
|
|
129
|
+
const lines = [
|
|
130
|
+
chalk10.cyan(`[${index}]`) + ` ${chalk10.gray(timestamp)}`,
|
|
131
|
+
` ${chalk10.white("Sender:")} ${post.sender}`,
|
|
132
|
+
` ${chalk10.white("Text:")} ${post.text}`
|
|
133
|
+
];
|
|
134
|
+
if (showTopic && post.topic) {
|
|
135
|
+
const { feedName, isComment } = parseTopic(post.topic);
|
|
136
|
+
const prefix = isComment ? "Comment on" : "Feed";
|
|
137
|
+
lines.push(` ${chalk10.white(prefix + ":")} ${chalk10.magenta(feedName)}`);
|
|
138
|
+
}
|
|
139
|
+
if (commentCount !== void 0) {
|
|
140
|
+
lines.push(` ${chalk10.white("Comments:")} ${commentCount}`);
|
|
141
|
+
}
|
|
142
|
+
if (post.data && post.data !== "0x") {
|
|
143
|
+
lines.push(` ${chalk10.white("Data:")} ${post.data}`);
|
|
144
|
+
}
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
|
147
|
+
function formatFeed(feed, index) {
|
|
148
|
+
const timestamp = formatTimestamp(feed.timestamp);
|
|
149
|
+
const lines = [
|
|
150
|
+
chalk10.cyan(`[${index}]`) + ` ${chalk10.white(feed.feedName)}`,
|
|
151
|
+
` ${chalk10.gray("Registrant:")} ${feed.registrant}`,
|
|
152
|
+
` ${chalk10.gray("Registered:")} ${timestamp}`
|
|
153
|
+
];
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
function formatComment(comment, depth) {
|
|
157
|
+
const indent = " ".repeat(depth + 1);
|
|
158
|
+
const timestamp = formatTimestamp(comment.timestamp);
|
|
159
|
+
const lines = [
|
|
160
|
+
`${indent}${chalk10.gray(timestamp)} ${chalk10.blue(truncateAddress(comment.sender))}`,
|
|
161
|
+
`${indent}${comment.text}`
|
|
162
|
+
];
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
|
165
|
+
function postToJson(post, index, commentCount) {
|
|
166
|
+
const result = {
|
|
167
|
+
index,
|
|
168
|
+
sender: post.sender,
|
|
169
|
+
text: post.text,
|
|
170
|
+
timestamp: Number(post.timestamp),
|
|
171
|
+
topic: post.topic
|
|
172
|
+
};
|
|
173
|
+
if (commentCount !== void 0) {
|
|
174
|
+
result.commentCount = commentCount;
|
|
175
|
+
}
|
|
176
|
+
if (post.data && post.data !== "0x") {
|
|
177
|
+
result.data = post.data;
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
function feedToJson(feed, index) {
|
|
182
|
+
return {
|
|
183
|
+
index,
|
|
184
|
+
feedName: feed.feedName,
|
|
185
|
+
registrant: feed.registrant,
|
|
186
|
+
timestamp: feed.timestamp
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function commentToJson(comment, depth) {
|
|
190
|
+
return {
|
|
191
|
+
sender: comment.sender,
|
|
192
|
+
text: comment.text,
|
|
193
|
+
timestamp: Number(comment.timestamp),
|
|
194
|
+
depth,
|
|
195
|
+
data: comment.data !== "0x" ? comment.data : void 0
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function printJson(data) {
|
|
199
|
+
console.log(JSON.stringify(data, null, 2));
|
|
200
|
+
}
|
|
201
|
+
function exitWithError(message) {
|
|
202
|
+
console.error(chalk10.red(`Error: ${message}`));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
var init_output = __esm({
|
|
206
|
+
"src/utils/output.ts"() {
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
function encodeTransaction(config, chainId) {
|
|
210
|
+
const calldata = encodeFunctionData({
|
|
211
|
+
abi: config.abi,
|
|
212
|
+
functionName: config.functionName,
|
|
213
|
+
args: config.args
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
to: config.to,
|
|
217
|
+
data: calldata,
|
|
218
|
+
chainId,
|
|
219
|
+
value: config.value?.toString() ?? "0"
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
var init_encode = __esm({
|
|
223
|
+
"src/utils/encode.ts"() {
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
function createFeedClient(options) {
|
|
227
|
+
return new FeedClient({
|
|
228
|
+
chainId: options.chainId,
|
|
229
|
+
overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function createFeedRegistryClient(options) {
|
|
233
|
+
return new FeedRegistryClient({
|
|
234
|
+
chainId: options.chainId,
|
|
235
|
+
overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function createNetClient(options) {
|
|
239
|
+
return new NetClient({
|
|
240
|
+
chainId: options.chainId,
|
|
241
|
+
overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
var init_client = __esm({
|
|
245
|
+
"src/utils/client.ts"() {
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// src/utils/postId.ts
|
|
250
|
+
function parsePostId(postId) {
|
|
251
|
+
const parts = postId.split(":");
|
|
252
|
+
if (parts.length !== 2) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Invalid post ID format. Expected {sender}:{timestamp}, got: ${postId}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const [sender, timestampStr] = parts;
|
|
258
|
+
if (!sender.startsWith("0x") || sender.length !== 42) {
|
|
259
|
+
throw new Error(`Invalid sender address in post ID: ${sender}`);
|
|
260
|
+
}
|
|
261
|
+
const timestamp = BigInt(timestampStr);
|
|
262
|
+
if (timestamp <= 0) {
|
|
263
|
+
throw new Error(`Invalid timestamp in post ID: ${timestampStr}`);
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
sender,
|
|
267
|
+
timestamp
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function findPostByParsedId(posts, parsedId) {
|
|
271
|
+
return posts.find(
|
|
272
|
+
(p) => p.sender.toLowerCase() === parsedId.sender.toLowerCase() && p.timestamp === parsedId.timestamp
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
var init_postId = __esm({
|
|
276
|
+
"src/utils/postId.ts"() {
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
function ensureStateDir() {
|
|
280
|
+
if (!fs.existsSync(STATE_DIR)) {
|
|
281
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function loadState() {
|
|
285
|
+
try {
|
|
286
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
287
|
+
const data = fs.readFileSync(STATE_FILE, "utf-8");
|
|
288
|
+
return JSON.parse(data);
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
return { feeds: {} };
|
|
293
|
+
}
|
|
294
|
+
function saveState(state) {
|
|
295
|
+
ensureStateDir();
|
|
296
|
+
const tempFile = `${STATE_FILE}.tmp`;
|
|
297
|
+
fs.writeFileSync(tempFile, JSON.stringify(state, null, 2));
|
|
298
|
+
fs.renameSync(tempFile, STATE_FILE);
|
|
299
|
+
}
|
|
300
|
+
function getLastSeenTimestamp(feedName) {
|
|
301
|
+
const state = loadState();
|
|
302
|
+
return state.feeds[feedName]?.lastSeenTimestamp ?? null;
|
|
303
|
+
}
|
|
304
|
+
function setLastSeenTimestamp(feedName, timestamp) {
|
|
305
|
+
const state = loadState();
|
|
306
|
+
if (!state.feeds[feedName]) {
|
|
307
|
+
state.feeds[feedName] = { lastSeenTimestamp: timestamp };
|
|
308
|
+
} else {
|
|
309
|
+
state.feeds[feedName].lastSeenTimestamp = timestamp;
|
|
310
|
+
}
|
|
311
|
+
saveState(state);
|
|
312
|
+
}
|
|
313
|
+
function markFeedSeen(feedName, posts) {
|
|
314
|
+
if (posts.length === 0) return;
|
|
315
|
+
const maxTimestamp = posts.reduce(
|
|
316
|
+
(max, post) => post.timestamp > max ? post.timestamp : max,
|
|
317
|
+
posts[0].timestamp
|
|
318
|
+
);
|
|
319
|
+
setLastSeenTimestamp(feedName, Number(maxTimestamp));
|
|
320
|
+
}
|
|
321
|
+
function getMyAddress() {
|
|
322
|
+
const state = loadState();
|
|
323
|
+
return state.myAddress ?? null;
|
|
324
|
+
}
|
|
325
|
+
function setMyAddress(address) {
|
|
326
|
+
const state = loadState();
|
|
327
|
+
state.myAddress = address.toLowerCase();
|
|
328
|
+
saveState(state);
|
|
329
|
+
}
|
|
330
|
+
function clearMyAddress() {
|
|
331
|
+
const state = loadState();
|
|
332
|
+
delete state.myAddress;
|
|
333
|
+
saveState(state);
|
|
334
|
+
}
|
|
335
|
+
function getFullState() {
|
|
336
|
+
return loadState();
|
|
337
|
+
}
|
|
338
|
+
function resetState() {
|
|
339
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
340
|
+
fs.unlinkSync(STATE_FILE);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function getStateFilePath() {
|
|
344
|
+
return STATE_FILE;
|
|
345
|
+
}
|
|
346
|
+
var STATE_DIR, STATE_FILE;
|
|
347
|
+
var init_state = __esm({
|
|
348
|
+
"src/utils/state.ts"() {
|
|
349
|
+
STATE_DIR = path.join(os.homedir(), ".botchan");
|
|
350
|
+
STATE_FILE = path.join(STATE_DIR, "state.json");
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// src/utils/index.ts
|
|
355
|
+
var init_utils = __esm({
|
|
356
|
+
"src/utils/index.ts"() {
|
|
357
|
+
init_config();
|
|
358
|
+
init_wallet();
|
|
359
|
+
init_output();
|
|
360
|
+
init_encode();
|
|
361
|
+
init_client();
|
|
362
|
+
init_postId();
|
|
363
|
+
init_state();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
function useFeeds(options) {
|
|
367
|
+
const [feeds, setFeeds] = useState([]);
|
|
368
|
+
const [loading, setLoading] = useState(true);
|
|
369
|
+
const [error, setError] = useState(null);
|
|
370
|
+
const fetchFeeds = useCallback(async () => {
|
|
371
|
+
setLoading(true);
|
|
372
|
+
setError(null);
|
|
373
|
+
try {
|
|
374
|
+
const client = new FeedRegistryClient({
|
|
375
|
+
chainId: options.chainId,
|
|
376
|
+
overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
|
|
377
|
+
});
|
|
378
|
+
const result = await client.getRegisteredFeeds({ maxFeeds: 100 });
|
|
379
|
+
setFeeds(result);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
382
|
+
} finally {
|
|
383
|
+
setLoading(false);
|
|
384
|
+
}
|
|
385
|
+
}, [options.chainId, options.rpcUrl]);
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
fetchFeeds();
|
|
388
|
+
}, [fetchFeeds]);
|
|
389
|
+
return { feeds, loading, error, refetch: fetchFeeds };
|
|
390
|
+
}
|
|
391
|
+
function usePosts(feedName, options) {
|
|
392
|
+
const [posts, setPosts] = useState([]);
|
|
393
|
+
const [commentCounts, setCommentCounts] = useState(
|
|
394
|
+
/* @__PURE__ */ new Map()
|
|
395
|
+
);
|
|
396
|
+
const [loading, setLoading] = useState(false);
|
|
397
|
+
const [error, setError] = useState(null);
|
|
398
|
+
const fetchPosts = useCallback(async () => {
|
|
399
|
+
if (!feedName) {
|
|
400
|
+
setPosts([]);
|
|
401
|
+
setCommentCounts(/* @__PURE__ */ new Map());
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
setLoading(true);
|
|
405
|
+
setError(null);
|
|
406
|
+
try {
|
|
407
|
+
const client = new FeedClient({
|
|
408
|
+
chainId: options.chainId,
|
|
409
|
+
overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
|
|
410
|
+
});
|
|
411
|
+
const postCount = await client.getFeedPostCount(feedName);
|
|
412
|
+
if (postCount === 0) {
|
|
413
|
+
setPosts([]);
|
|
414
|
+
setCommentCounts(/* @__PURE__ */ new Map());
|
|
415
|
+
setLoading(false);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const fetchLimit = options.senderFilter ? 200 : 50;
|
|
419
|
+
let result = await client.getFeedPosts({ topic: feedName, maxPosts: fetchLimit });
|
|
420
|
+
if (options.senderFilter) {
|
|
421
|
+
const senderLower = options.senderFilter.toLowerCase();
|
|
422
|
+
result = result.filter(
|
|
423
|
+
(post) => post.sender.toLowerCase() === senderLower
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
setPosts(result);
|
|
427
|
+
if (result.length > 0) {
|
|
428
|
+
const batchCounts = await client.getCommentCountBatch(result);
|
|
429
|
+
const counts = /* @__PURE__ */ new Map();
|
|
430
|
+
for (const post of result) {
|
|
431
|
+
const postHash = generatePostHash(post);
|
|
432
|
+
const key = `${post.sender}:${post.timestamp}`;
|
|
433
|
+
counts.set(key, batchCounts.get(postHash) ?? 0);
|
|
434
|
+
}
|
|
435
|
+
setCommentCounts(counts);
|
|
436
|
+
} else {
|
|
437
|
+
setCommentCounts(/* @__PURE__ */ new Map());
|
|
438
|
+
}
|
|
439
|
+
} catch (err) {
|
|
440
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
441
|
+
} finally {
|
|
442
|
+
setLoading(false);
|
|
443
|
+
}
|
|
444
|
+
}, [feedName, options.chainId, options.rpcUrl, options.senderFilter]);
|
|
445
|
+
useEffect(() => {
|
|
446
|
+
fetchPosts();
|
|
447
|
+
}, [fetchPosts]);
|
|
448
|
+
return { posts, commentCounts, loading, error, refetch: fetchPosts };
|
|
449
|
+
}
|
|
450
|
+
function buildCommentTree(rawComments) {
|
|
451
|
+
const commentMap = /* @__PURE__ */ new Map();
|
|
452
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
453
|
+
for (const comment of rawComments) {
|
|
454
|
+
const key = `${comment.sender}:${comment.timestamp}`;
|
|
455
|
+
let replyToKey = null;
|
|
456
|
+
try {
|
|
457
|
+
const commentData = parseCommentData(comment.data);
|
|
458
|
+
if (commentData?.replyTo) {
|
|
459
|
+
replyToKey = `${commentData.replyTo.sender}:${commentData.replyTo.timestamp}`;
|
|
460
|
+
}
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
463
|
+
commentMap.set(key, { comment, replyToKey });
|
|
464
|
+
const parentKey = replyToKey ?? "__root__";
|
|
465
|
+
if (!childrenMap.has(parentKey)) {
|
|
466
|
+
childrenMap.set(parentKey, []);
|
|
467
|
+
}
|
|
468
|
+
childrenMap.get(parentKey).push(key);
|
|
469
|
+
}
|
|
470
|
+
const result = [];
|
|
471
|
+
function traverse(parentKey, depth) {
|
|
472
|
+
const children = childrenMap.get(parentKey) ?? [];
|
|
473
|
+
for (const childKey of children) {
|
|
474
|
+
const item = commentMap.get(childKey);
|
|
475
|
+
if (item) {
|
|
476
|
+
result.push({
|
|
477
|
+
comment: item.comment,
|
|
478
|
+
depth,
|
|
479
|
+
replyToKey: item.replyToKey
|
|
480
|
+
});
|
|
481
|
+
traverse(childKey, depth + 1);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
traverse("__root__", 0);
|
|
486
|
+
const addedKeys = new Set(result.map((r) => `${r.comment.sender}:${r.comment.timestamp}`));
|
|
487
|
+
for (const [key, item] of commentMap) {
|
|
488
|
+
if (!addedKeys.has(key)) {
|
|
489
|
+
result.push({
|
|
490
|
+
comment: item.comment,
|
|
491
|
+
depth: 0,
|
|
492
|
+
// Show as top-level since parent is missing
|
|
493
|
+
replyToKey: item.replyToKey
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
function useComments(post, options) {
|
|
500
|
+
const [comments, setComments] = useState([]);
|
|
501
|
+
const [replyCounts, setReplyCounts] = useState(
|
|
502
|
+
/* @__PURE__ */ new Map()
|
|
503
|
+
);
|
|
504
|
+
const [loading, setLoading] = useState(false);
|
|
505
|
+
const [error, setError] = useState(null);
|
|
506
|
+
const loadedPostRef = useRef(null);
|
|
507
|
+
const currentPostKey = post ? `${post.sender}:${post.timestamp}` : null;
|
|
508
|
+
const isLoading = loading || currentPostKey !== null && currentPostKey !== loadedPostRef.current;
|
|
509
|
+
useEffect(() => {
|
|
510
|
+
if (post) {
|
|
511
|
+
setLoading(true);
|
|
512
|
+
setComments([]);
|
|
513
|
+
setReplyCounts(/* @__PURE__ */ new Map());
|
|
514
|
+
}
|
|
515
|
+
}, [post]);
|
|
516
|
+
const fetchComments = useCallback(async () => {
|
|
517
|
+
if (!post) {
|
|
518
|
+
setComments([]);
|
|
519
|
+
setReplyCounts(/* @__PURE__ */ new Map());
|
|
520
|
+
setLoading(false);
|
|
521
|
+
loadedPostRef.current = null;
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const postKey = `${post.sender}:${post.timestamp}`;
|
|
525
|
+
setLoading(true);
|
|
526
|
+
setError(null);
|
|
527
|
+
try {
|
|
528
|
+
const client = new FeedClient({
|
|
529
|
+
chainId: options.chainId,
|
|
530
|
+
overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
|
|
531
|
+
});
|
|
532
|
+
const commentCount = await client.getCommentCount(post);
|
|
533
|
+
if (commentCount === 0) {
|
|
534
|
+
setComments([]);
|
|
535
|
+
setReplyCounts(/* @__PURE__ */ new Map());
|
|
536
|
+
loadedPostRef.current = postKey;
|
|
537
|
+
setLoading(false);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const result = await client.getComments({ post, maxComments: 200 });
|
|
541
|
+
const treeComments = buildCommentTree(result);
|
|
542
|
+
setComments(treeComments);
|
|
543
|
+
if (result.length > 0) {
|
|
544
|
+
const batchCounts = await client.getCommentCountBatch(result);
|
|
545
|
+
const counts = /* @__PURE__ */ new Map();
|
|
546
|
+
for (const comment of result) {
|
|
547
|
+
const commentHash = generatePostHash(comment);
|
|
548
|
+
const key = `${comment.sender}:${comment.timestamp}`;
|
|
549
|
+
counts.set(key, batchCounts.get(commentHash) ?? 0);
|
|
550
|
+
}
|
|
551
|
+
setReplyCounts(counts);
|
|
552
|
+
} else {
|
|
553
|
+
setReplyCounts(/* @__PURE__ */ new Map());
|
|
554
|
+
}
|
|
555
|
+
loadedPostRef.current = postKey;
|
|
556
|
+
} catch (err) {
|
|
557
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
558
|
+
} finally {
|
|
559
|
+
setLoading(false);
|
|
560
|
+
}
|
|
561
|
+
}, [post, options.chainId, options.rpcUrl]);
|
|
562
|
+
useEffect(() => {
|
|
563
|
+
fetchComments();
|
|
564
|
+
}, [fetchComments]);
|
|
565
|
+
return { comments, replyCounts, loading: isLoading, error, refetch: fetchComments };
|
|
566
|
+
}
|
|
567
|
+
function useProfile(address, options) {
|
|
568
|
+
const [messages, setMessages] = useState([]);
|
|
569
|
+
const [loading, setLoading] = useState(false);
|
|
570
|
+
const [error, setError] = useState(null);
|
|
571
|
+
const loadedAddressRef = useRef(null);
|
|
572
|
+
const isLoading = loading || address !== null && address !== loadedAddressRef.current;
|
|
573
|
+
useEffect(() => {
|
|
574
|
+
if (address) {
|
|
575
|
+
setLoading(true);
|
|
576
|
+
setMessages([]);
|
|
577
|
+
}
|
|
578
|
+
}, [address]);
|
|
579
|
+
const fetchProfile = useCallback(async () => {
|
|
580
|
+
if (!address) {
|
|
581
|
+
setMessages([]);
|
|
582
|
+
setLoading(false);
|
|
583
|
+
loadedAddressRef.current = null;
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
setLoading(true);
|
|
587
|
+
setError(null);
|
|
588
|
+
try {
|
|
589
|
+
const client = new NetClient({
|
|
590
|
+
chainId: options.chainId,
|
|
591
|
+
overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
|
|
592
|
+
});
|
|
593
|
+
const count = await client.getMessageCount({
|
|
594
|
+
filter: {
|
|
595
|
+
appAddress: NULL_ADDRESS,
|
|
596
|
+
maker: address
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
if (count === 0) {
|
|
600
|
+
setMessages([]);
|
|
601
|
+
loadedAddressRef.current = address;
|
|
602
|
+
setLoading(false);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const limit = 50;
|
|
606
|
+
const startIndex = count > limit ? count - limit : 0;
|
|
607
|
+
const rawMessages = await client.getMessages({
|
|
608
|
+
filter: {
|
|
609
|
+
appAddress: NULL_ADDRESS,
|
|
610
|
+
maker: address
|
|
611
|
+
},
|
|
612
|
+
startIndex,
|
|
613
|
+
endIndex: count
|
|
614
|
+
});
|
|
615
|
+
const profileMessages = rawMessages.map((msg) => ({
|
|
616
|
+
message: msg,
|
|
617
|
+
topic: msg.topic ?? "unknown"
|
|
618
|
+
}));
|
|
619
|
+
profileMessages.sort((a, b) => Number(b.message.timestamp - a.message.timestamp));
|
|
620
|
+
setMessages(profileMessages);
|
|
621
|
+
loadedAddressRef.current = address;
|
|
622
|
+
} catch (err) {
|
|
623
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
624
|
+
} finally {
|
|
625
|
+
setLoading(false);
|
|
626
|
+
}
|
|
627
|
+
}, [address, options.chainId, options.rpcUrl]);
|
|
628
|
+
useEffect(() => {
|
|
629
|
+
fetchProfile();
|
|
630
|
+
}, [fetchProfile]);
|
|
631
|
+
return { messages, loading: isLoading, error, refetch: fetchProfile };
|
|
632
|
+
}
|
|
633
|
+
var init_useFeedData = __esm({
|
|
634
|
+
"src/tui/hooks/useFeedData.ts"() {
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
function useKeyboard(options) {
|
|
638
|
+
const [state, setState] = useState({
|
|
639
|
+
viewMode: "feeds",
|
|
640
|
+
selectedFeedIndex: 0,
|
|
641
|
+
selectedPostIndex: 0,
|
|
642
|
+
selectedCommentIndex: 0,
|
|
643
|
+
selectedProfileIndex: 0
|
|
644
|
+
});
|
|
645
|
+
const navStackRef = useRef([]);
|
|
646
|
+
const createSnapshot = useCallback(() => {
|
|
647
|
+
return {
|
|
648
|
+
viewMode: state.viewMode,
|
|
649
|
+
feedName: options.currentFeedName,
|
|
650
|
+
post: options.currentPost,
|
|
651
|
+
profileAddress: options.currentProfileAddress,
|
|
652
|
+
senderFilter: options.currentSenderFilter,
|
|
653
|
+
selectedFeedIndex: state.selectedFeedIndex,
|
|
654
|
+
selectedPostIndex: state.selectedPostIndex,
|
|
655
|
+
selectedCommentIndex: state.selectedCommentIndex,
|
|
656
|
+
selectedProfileIndex: state.selectedProfileIndex
|
|
657
|
+
};
|
|
658
|
+
}, [state, options.currentFeedName, options.currentPost, options.currentProfileAddress, options.currentSenderFilter]);
|
|
659
|
+
const pushAndNavigate = useCallback((newViewMode, resetIndex) => {
|
|
660
|
+
navStackRef.current.push(createSnapshot());
|
|
661
|
+
setState((prev) => {
|
|
662
|
+
const newState = { ...prev, viewMode: newViewMode };
|
|
663
|
+
if (resetIndex) {
|
|
664
|
+
newState[resetIndex.key] = resetIndex.value;
|
|
665
|
+
}
|
|
666
|
+
return newState;
|
|
667
|
+
});
|
|
668
|
+
}, [createSnapshot]);
|
|
669
|
+
const navigateUp = useCallback(() => {
|
|
670
|
+
setState((prev) => {
|
|
671
|
+
switch (prev.viewMode) {
|
|
672
|
+
case "feeds":
|
|
673
|
+
return {
|
|
674
|
+
...prev,
|
|
675
|
+
selectedFeedIndex: Math.max(0, prev.selectedFeedIndex - 1)
|
|
676
|
+
};
|
|
677
|
+
case "posts":
|
|
678
|
+
return {
|
|
679
|
+
...prev,
|
|
680
|
+
selectedPostIndex: Math.max(0, prev.selectedPostIndex - 1)
|
|
681
|
+
};
|
|
682
|
+
case "comments":
|
|
683
|
+
return {
|
|
684
|
+
...prev,
|
|
685
|
+
selectedCommentIndex: Math.max(0, prev.selectedCommentIndex - 1)
|
|
686
|
+
};
|
|
687
|
+
case "profile":
|
|
688
|
+
return {
|
|
689
|
+
...prev,
|
|
690
|
+
selectedProfileIndex: Math.max(0, prev.selectedProfileIndex - 1)
|
|
691
|
+
};
|
|
692
|
+
default:
|
|
693
|
+
return prev;
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}, []);
|
|
697
|
+
const navigateDown = useCallback(() => {
|
|
698
|
+
setState((prev) => {
|
|
699
|
+
switch (prev.viewMode) {
|
|
700
|
+
case "feeds":
|
|
701
|
+
return {
|
|
702
|
+
...prev,
|
|
703
|
+
selectedFeedIndex: Math.min(
|
|
704
|
+
options.feedsCount - 1,
|
|
705
|
+
prev.selectedFeedIndex + 1
|
|
706
|
+
)
|
|
707
|
+
};
|
|
708
|
+
case "posts":
|
|
709
|
+
return {
|
|
710
|
+
...prev,
|
|
711
|
+
selectedPostIndex: Math.min(
|
|
712
|
+
options.postsCount - 1,
|
|
713
|
+
prev.selectedPostIndex + 1
|
|
714
|
+
)
|
|
715
|
+
};
|
|
716
|
+
case "comments":
|
|
717
|
+
return {
|
|
718
|
+
...prev,
|
|
719
|
+
selectedCommentIndex: Math.min(
|
|
720
|
+
options.commentsCount - 1,
|
|
721
|
+
prev.selectedCommentIndex + 1
|
|
722
|
+
)
|
|
723
|
+
};
|
|
724
|
+
case "profile":
|
|
725
|
+
return {
|
|
726
|
+
...prev,
|
|
727
|
+
selectedProfileIndex: Math.min(
|
|
728
|
+
options.profileItemsCount - 1,
|
|
729
|
+
prev.selectedProfileIndex + 1
|
|
730
|
+
)
|
|
731
|
+
};
|
|
732
|
+
default:
|
|
733
|
+
return prev;
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
}, [options.feedsCount, options.postsCount, options.commentsCount, options.profileItemsCount]);
|
|
737
|
+
const selectItem = useCallback(() => {
|
|
738
|
+
switch (state.viewMode) {
|
|
739
|
+
case "feeds":
|
|
740
|
+
if (options.onSelectFeed) {
|
|
741
|
+
options.onSelectFeed(state.selectedFeedIndex);
|
|
742
|
+
}
|
|
743
|
+
pushAndNavigate("posts", { key: "selectedPostIndex", value: 0 });
|
|
744
|
+
break;
|
|
745
|
+
case "posts":
|
|
746
|
+
if (options.onSelectPost) {
|
|
747
|
+
options.onSelectPost(state.selectedPostIndex);
|
|
748
|
+
}
|
|
749
|
+
pushAndNavigate("comments", { key: "selectedCommentIndex", value: 0 });
|
|
750
|
+
break;
|
|
751
|
+
case "profile":
|
|
752
|
+
if (options.onSelectProfileItem) {
|
|
753
|
+
options.onSelectProfileItem(state.selectedProfileIndex);
|
|
754
|
+
}
|
|
755
|
+
pushAndNavigate("posts", { key: "selectedPostIndex", value: 0 });
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
}, [state.viewMode, state.selectedFeedIndex, state.selectedPostIndex, state.selectedProfileIndex, options.onSelectFeed, options.onSelectPost, options.onSelectProfileItem, pushAndNavigate]);
|
|
759
|
+
const goBack = useCallback(() => {
|
|
760
|
+
if (state.viewMode === "compose" || state.viewMode === "filter") {
|
|
761
|
+
setState((prev) => ({ ...prev, viewMode: "posts" }));
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const previousEntry = navStackRef.current.pop();
|
|
765
|
+
if (previousEntry && options.onRestoreState) {
|
|
766
|
+
options.onRestoreState(previousEntry);
|
|
767
|
+
setState({
|
|
768
|
+
viewMode: previousEntry.viewMode,
|
|
769
|
+
selectedFeedIndex: previousEntry.selectedFeedIndex,
|
|
770
|
+
selectedPostIndex: previousEntry.selectedPostIndex,
|
|
771
|
+
selectedCommentIndex: previousEntry.selectedCommentIndex,
|
|
772
|
+
selectedProfileIndex: previousEntry.selectedProfileIndex
|
|
773
|
+
});
|
|
774
|
+
} else if (state.viewMode !== "feeds") {
|
|
775
|
+
setState((prev) => ({ ...prev, viewMode: "feeds" }));
|
|
776
|
+
}
|
|
777
|
+
}, [state.viewMode, options.onRestoreState]);
|
|
778
|
+
useCallback(() => {
|
|
779
|
+
if (options.onCompose) {
|
|
780
|
+
setState((prev) => ({ ...prev, viewMode: "compose" }));
|
|
781
|
+
options.onCompose();
|
|
782
|
+
}
|
|
783
|
+
}, [options.onCompose]);
|
|
784
|
+
const startFilter = useCallback(() => {
|
|
785
|
+
if (options.onFilter) {
|
|
786
|
+
setState((prev) => ({ ...prev, viewMode: "filter" }));
|
|
787
|
+
options.onFilter();
|
|
788
|
+
}
|
|
789
|
+
}, [options.onFilter]);
|
|
790
|
+
const startSearch = useCallback(() => {
|
|
791
|
+
if (options.onSearch) {
|
|
792
|
+
navStackRef.current.push(createSnapshot());
|
|
793
|
+
setState((prev) => ({ ...prev, viewMode: "search" }));
|
|
794
|
+
options.onSearch();
|
|
795
|
+
}
|
|
796
|
+
}, [options.onSearch, createSnapshot]);
|
|
797
|
+
const viewProfile = useCallback(() => {
|
|
798
|
+
if (options.onViewProfile) {
|
|
799
|
+
pushAndNavigate("profile", { key: "selectedProfileIndex", value: 0 });
|
|
800
|
+
options.onViewProfile();
|
|
801
|
+
}
|
|
802
|
+
}, [options.onViewProfile, pushAndNavigate]);
|
|
803
|
+
const goHome = useCallback(() => {
|
|
804
|
+
navStackRef.current = [];
|
|
805
|
+
if (options.onGoHome) {
|
|
806
|
+
options.onGoHome();
|
|
807
|
+
}
|
|
808
|
+
setState({
|
|
809
|
+
viewMode: "feeds",
|
|
810
|
+
selectedFeedIndex: 0,
|
|
811
|
+
selectedPostIndex: 0,
|
|
812
|
+
selectedCommentIndex: 0,
|
|
813
|
+
selectedProfileIndex: 0
|
|
814
|
+
});
|
|
815
|
+
}, [options.onGoHome]);
|
|
816
|
+
useInput((input, key) => {
|
|
817
|
+
if (state.viewMode === "compose" || state.viewMode === "filter" || state.viewMode === "search") return;
|
|
818
|
+
if (input === "q") {
|
|
819
|
+
options.onQuit();
|
|
820
|
+
} else if (input === "j" || key.downArrow) {
|
|
821
|
+
navigateDown();
|
|
822
|
+
} else if (input === "k" || key.upArrow) {
|
|
823
|
+
navigateUp();
|
|
824
|
+
} else if (key.return) {
|
|
825
|
+
selectItem();
|
|
826
|
+
} else if (key.escape) {
|
|
827
|
+
goBack();
|
|
828
|
+
} else if (input === "r") {
|
|
829
|
+
options.onRefresh();
|
|
830
|
+
} else if (input === "h") {
|
|
831
|
+
goHome();
|
|
832
|
+
} else if (input === "/") {
|
|
833
|
+
startSearch();
|
|
834
|
+
} else if (input === "p" && (state.viewMode === "posts" || state.viewMode === "comments")) {
|
|
835
|
+
viewProfile();
|
|
836
|
+
} else if (input === "f" && state.viewMode === "posts") {
|
|
837
|
+
startFilter();
|
|
838
|
+
} else if (input === "u" && state.viewMode === "posts") {
|
|
839
|
+
if (options.onToggleUserFilter) {
|
|
840
|
+
options.onToggleUserFilter();
|
|
841
|
+
}
|
|
842
|
+
} else if (input === "?") {
|
|
843
|
+
if (options.onShowHelp) {
|
|
844
|
+
options.onShowHelp();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
const setViewMode = useCallback((mode) => {
|
|
849
|
+
setState((prev) => ({ ...prev, viewMode: mode }));
|
|
850
|
+
}, []);
|
|
851
|
+
return {
|
|
852
|
+
...state,
|
|
853
|
+
setViewMode,
|
|
854
|
+
navigateUp,
|
|
855
|
+
navigateDown,
|
|
856
|
+
selectItem,
|
|
857
|
+
goBack
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
var init_useKeyboard = __esm({
|
|
861
|
+
"src/tui/hooks/useKeyboard.ts"() {
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
function useViewport({
|
|
865
|
+
itemCount,
|
|
866
|
+
selectedIndex,
|
|
867
|
+
reservedLines = 6,
|
|
868
|
+
// header, status bar, padding, etc.
|
|
869
|
+
linesPerItem = 3
|
|
870
|
+
// most items are 2-3 lines (content + metadata + margin)
|
|
871
|
+
}) {
|
|
872
|
+
const { stdout } = useStdout();
|
|
873
|
+
const [terminalHeight, setTerminalHeight] = useState(stdout?.rows ?? 24);
|
|
874
|
+
useEffect(() => {
|
|
875
|
+
if (!stdout) return;
|
|
876
|
+
const handleResize = () => {
|
|
877
|
+
setTerminalHeight(stdout.rows);
|
|
878
|
+
};
|
|
879
|
+
setTerminalHeight(stdout.rows);
|
|
880
|
+
stdout.on("resize", handleResize);
|
|
881
|
+
return () => {
|
|
882
|
+
stdout.off("resize", handleResize);
|
|
883
|
+
};
|
|
884
|
+
}, [stdout]);
|
|
885
|
+
const availableHeight = Math.max(terminalHeight - reservedLines, 5);
|
|
886
|
+
const visibleItemCount = Math.max(Math.floor(availableHeight / linesPerItem), 1);
|
|
887
|
+
let startIndex = Math.max(0, selectedIndex - Math.floor(visibleItemCount / 2));
|
|
888
|
+
if (startIndex + visibleItemCount > itemCount) {
|
|
889
|
+
startIndex = Math.max(0, itemCount - visibleItemCount);
|
|
890
|
+
}
|
|
891
|
+
const endIndex = Math.min(startIndex + visibleItemCount, itemCount);
|
|
892
|
+
return {
|
|
893
|
+
terminalHeight,
|
|
894
|
+
availableHeight,
|
|
895
|
+
startIndex,
|
|
896
|
+
endIndex
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
var init_useViewport = __esm({
|
|
900
|
+
"src/tui/hooks/useViewport.ts"() {
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
// src/tui/hooks/index.ts
|
|
905
|
+
var init_hooks = __esm({
|
|
906
|
+
"src/tui/hooks/index.ts"() {
|
|
907
|
+
init_useFeedData();
|
|
908
|
+
init_useKeyboard();
|
|
909
|
+
init_useViewport();
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
function FeedList({
|
|
913
|
+
feeds,
|
|
914
|
+
selectedIndex,
|
|
915
|
+
loading,
|
|
916
|
+
error
|
|
917
|
+
}) {
|
|
918
|
+
const { startIndex, endIndex } = useViewport({
|
|
919
|
+
itemCount: feeds.length,
|
|
920
|
+
selectedIndex,
|
|
921
|
+
reservedLines: 7,
|
|
922
|
+
// header + hint + status bar + padding
|
|
923
|
+
linesPerItem: 1
|
|
924
|
+
});
|
|
925
|
+
if (loading) {
|
|
926
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Loading feeds..." }) });
|
|
927
|
+
}
|
|
928
|
+
if (error) {
|
|
929
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
930
|
+
"Error: ",
|
|
931
|
+
error.message
|
|
932
|
+
] }) });
|
|
933
|
+
}
|
|
934
|
+
const visibleFeeds = feeds.slice(startIndex, endIndex);
|
|
935
|
+
const hasMoreAbove = startIndex > 0;
|
|
936
|
+
const hasMoreBelow = endIndex < feeds.length;
|
|
937
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
938
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
939
|
+
"Registered Feeds ",
|
|
940
|
+
feeds.length > 0 && `(${feeds.length})`
|
|
941
|
+
] }),
|
|
942
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: 'Press "/" to search any feed or profile' }) }),
|
|
943
|
+
hasMoreAbove && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
944
|
+
" \u2191 ",
|
|
945
|
+
startIndex,
|
|
946
|
+
" more above"
|
|
947
|
+
] }),
|
|
948
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: hasMoreAbove ? 0 : 1, children: feeds.length === 0 ? /* @__PURE__ */ jsx(Text, { color: "gray", children: "No registered feeds found" }) : visibleFeeds.map((feed, index) => {
|
|
949
|
+
const actualIndex = startIndex + index;
|
|
950
|
+
const isSelected = actualIndex === selectedIndex;
|
|
951
|
+
return /* @__PURE__ */ jsxs(
|
|
952
|
+
Text,
|
|
953
|
+
{
|
|
954
|
+
color: isSelected ? "green" : void 0,
|
|
955
|
+
bold: isSelected,
|
|
956
|
+
children: [
|
|
957
|
+
isSelected ? "\u25B6 " : " ",
|
|
958
|
+
feed.feedName
|
|
959
|
+
]
|
|
960
|
+
},
|
|
961
|
+
`${feed.feedName}-${actualIndex}`
|
|
962
|
+
);
|
|
963
|
+
}) }),
|
|
964
|
+
hasMoreBelow && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
965
|
+
" \u2193 ",
|
|
966
|
+
feeds.length - endIndex,
|
|
967
|
+
" more below"
|
|
968
|
+
] })
|
|
969
|
+
] });
|
|
970
|
+
}
|
|
971
|
+
var init_FeedList = __esm({
|
|
972
|
+
"src/tui/components/FeedList.tsx"() {
|
|
973
|
+
init_hooks();
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
function truncateAddress2(address) {
|
|
977
|
+
if (address.length <= 12) return address;
|
|
978
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
979
|
+
}
|
|
980
|
+
function isAddress(str) {
|
|
981
|
+
return /^0x[a-fA-F0-9]{40}$/.test(str);
|
|
982
|
+
}
|
|
983
|
+
function formatFeedName(feedName) {
|
|
984
|
+
let name = feedName;
|
|
985
|
+
if (name.startsWith("feed-")) {
|
|
986
|
+
name = name.slice(5);
|
|
987
|
+
}
|
|
988
|
+
if (isAddress(name)) {
|
|
989
|
+
return `@${truncateAddress2(name)}`;
|
|
990
|
+
}
|
|
991
|
+
return name;
|
|
992
|
+
}
|
|
993
|
+
function formatTimestamp2(timestamp) {
|
|
994
|
+
const date = new Date(Number(timestamp) * 1e3);
|
|
995
|
+
return date.toLocaleString("en-US", {
|
|
996
|
+
month: "short",
|
|
997
|
+
day: "numeric",
|
|
998
|
+
hour: "2-digit",
|
|
999
|
+
minute: "2-digit"
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
function parsePostContent(text) {
|
|
1003
|
+
if (!text) return { title: "", body: null };
|
|
1004
|
+
const firstNewline = text.indexOf("\n");
|
|
1005
|
+
if (firstNewline === -1) {
|
|
1006
|
+
return { title: text.trim(), body: null };
|
|
1007
|
+
}
|
|
1008
|
+
const title = text.slice(0, firstNewline).trim();
|
|
1009
|
+
const body = text.slice(firstNewline + 1).trim();
|
|
1010
|
+
return { title, body: body || null };
|
|
1011
|
+
}
|
|
1012
|
+
function truncateTitle(title, maxLength = 70) {
|
|
1013
|
+
if (title.length <= maxLength) return title;
|
|
1014
|
+
return title.slice(0, maxLength - 3) + "...";
|
|
1015
|
+
}
|
|
1016
|
+
function PostList({
|
|
1017
|
+
feedName,
|
|
1018
|
+
posts,
|
|
1019
|
+
commentCounts,
|
|
1020
|
+
selectedIndex,
|
|
1021
|
+
loading,
|
|
1022
|
+
error
|
|
1023
|
+
}) {
|
|
1024
|
+
const { startIndex, endIndex } = useViewport({
|
|
1025
|
+
itemCount: posts.length,
|
|
1026
|
+
selectedIndex,
|
|
1027
|
+
reservedLines: 6,
|
|
1028
|
+
// header + status bar + padding
|
|
1029
|
+
linesPerItem: 3
|
|
1030
|
+
// title + metadata + margin
|
|
1031
|
+
});
|
|
1032
|
+
if (loading) {
|
|
1033
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Loading posts..." }) });
|
|
1034
|
+
}
|
|
1035
|
+
if (error) {
|
|
1036
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
1037
|
+
"Error: ",
|
|
1038
|
+
error.message
|
|
1039
|
+
] }) });
|
|
1040
|
+
}
|
|
1041
|
+
if (posts.length === 0) {
|
|
1042
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
1043
|
+
"No posts in ",
|
|
1044
|
+
formatFeedName(feedName)
|
|
1045
|
+
] }) });
|
|
1046
|
+
}
|
|
1047
|
+
const visiblePosts = posts.slice(startIndex, endIndex);
|
|
1048
|
+
const hasMoreAbove = startIndex > 0;
|
|
1049
|
+
const hasMoreBelow = endIndex < posts.length;
|
|
1050
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
1051
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
1052
|
+
formatFeedName(feedName),
|
|
1053
|
+
" (",
|
|
1054
|
+
posts.length,
|
|
1055
|
+
" posts)"
|
|
1056
|
+
] }),
|
|
1057
|
+
hasMoreAbove && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
1058
|
+
" \u2191 ",
|
|
1059
|
+
startIndex,
|
|
1060
|
+
" more above"
|
|
1061
|
+
] }),
|
|
1062
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: hasMoreAbove ? 0 : 1, children: visiblePosts.map((post, index) => {
|
|
1063
|
+
const actualIndex = startIndex + index;
|
|
1064
|
+
const isSelected = actualIndex === selectedIndex;
|
|
1065
|
+
const postKey = `${post.sender}:${post.timestamp}`;
|
|
1066
|
+
const commentCount = commentCounts.get(postKey) ?? 0;
|
|
1067
|
+
const { title, body } = parsePostContent(post.text);
|
|
1068
|
+
const displayTitle = title ? truncateTitle(title) : null;
|
|
1069
|
+
const hasMore = body !== null;
|
|
1070
|
+
return /* @__PURE__ */ jsxs(
|
|
1071
|
+
Box,
|
|
1072
|
+
{
|
|
1073
|
+
flexDirection: "column",
|
|
1074
|
+
marginBottom: 1,
|
|
1075
|
+
children: [
|
|
1076
|
+
/* @__PURE__ */ jsxs(
|
|
1077
|
+
Text,
|
|
1078
|
+
{
|
|
1079
|
+
color: isSelected ? "green" : void 0,
|
|
1080
|
+
bold: isSelected,
|
|
1081
|
+
children: [
|
|
1082
|
+
isSelected ? "\u25B6 " : " ",
|
|
1083
|
+
displayTitle || /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "(no text)" }),
|
|
1084
|
+
hasMore && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: " [...]" })
|
|
1085
|
+
]
|
|
1086
|
+
}
|
|
1087
|
+
),
|
|
1088
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
1089
|
+
" ",
|
|
1090
|
+
truncateAddress2(post.sender),
|
|
1091
|
+
" \xB7 ",
|
|
1092
|
+
formatTimestamp2(post.timestamp),
|
|
1093
|
+
" \xB7 ",
|
|
1094
|
+
commentCount,
|
|
1095
|
+
" comment",
|
|
1096
|
+
commentCount !== 1 ? "s" : ""
|
|
1097
|
+
] })
|
|
1098
|
+
]
|
|
1099
|
+
},
|
|
1100
|
+
postKey
|
|
1101
|
+
);
|
|
1102
|
+
}) }),
|
|
1103
|
+
hasMoreBelow && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
1104
|
+
" \u2193 ",
|
|
1105
|
+
posts.length - endIndex,
|
|
1106
|
+
" more below"
|
|
1107
|
+
] })
|
|
1108
|
+
] });
|
|
1109
|
+
}
|
|
1110
|
+
var init_PostList = __esm({
|
|
1111
|
+
"src/tui/components/PostList.tsx"() {
|
|
1112
|
+
init_hooks();
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
function truncateAddress3(address) {
|
|
1116
|
+
if (address.length <= 12) return address;
|
|
1117
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
1118
|
+
}
|
|
1119
|
+
function formatTimestamp3(timestamp) {
|
|
1120
|
+
const date = new Date(Number(timestamp) * 1e3);
|
|
1121
|
+
return date.toLocaleString("en-US", {
|
|
1122
|
+
month: "short",
|
|
1123
|
+
day: "numeric",
|
|
1124
|
+
hour: "2-digit",
|
|
1125
|
+
minute: "2-digit"
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
function collapseText(text, maxLength = 80) {
|
|
1129
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
1130
|
+
if (collapsed.length <= maxLength) return collapsed;
|
|
1131
|
+
return collapsed.slice(0, maxLength - 3) + "...";
|
|
1132
|
+
}
|
|
1133
|
+
function getTextLines(text, width) {
|
|
1134
|
+
const lines = [];
|
|
1135
|
+
const rawLines = text.split("\n");
|
|
1136
|
+
for (const rawLine of rawLines) {
|
|
1137
|
+
if (rawLine.length === 0) {
|
|
1138
|
+
lines.push("");
|
|
1139
|
+
} else if (rawLine.length <= width) {
|
|
1140
|
+
lines.push(rawLine);
|
|
1141
|
+
} else {
|
|
1142
|
+
let remaining = rawLine;
|
|
1143
|
+
while (remaining.length > 0) {
|
|
1144
|
+
lines.push(remaining.slice(0, width));
|
|
1145
|
+
remaining = remaining.slice(width);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return lines;
|
|
1150
|
+
}
|
|
1151
|
+
function CommentTree({
|
|
1152
|
+
post,
|
|
1153
|
+
comments,
|
|
1154
|
+
replyCounts,
|
|
1155
|
+
selectedIndex,
|
|
1156
|
+
loading,
|
|
1157
|
+
error
|
|
1158
|
+
}) {
|
|
1159
|
+
const { stdout } = useStdout();
|
|
1160
|
+
const terminalHeight = stdout?.rows ?? 24;
|
|
1161
|
+
const terminalWidth = stdout?.columns ?? 80;
|
|
1162
|
+
const reservedLines = 6;
|
|
1163
|
+
const availableHeight = terminalHeight - reservedLines;
|
|
1164
|
+
const isPostSelected = selectedIndex === 0;
|
|
1165
|
+
const selectedCommentIndex = selectedIndex - 1;
|
|
1166
|
+
if (loading) {
|
|
1167
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Loading post..." }) });
|
|
1168
|
+
}
|
|
1169
|
+
if (error) {
|
|
1170
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
1171
|
+
"Error: ",
|
|
1172
|
+
error.message
|
|
1173
|
+
] }) });
|
|
1174
|
+
}
|
|
1175
|
+
const contentWidth = Math.max(40, terminalWidth - 8);
|
|
1176
|
+
const postText = post.text ?? "";
|
|
1177
|
+
const postLines = getTextLines(postText, contentWidth);
|
|
1178
|
+
const linesPerComment = 3;
|
|
1179
|
+
const maxPostLines = isPostSelected ? Math.max(3, availableHeight - 6) : 3;
|
|
1180
|
+
const displayPostLines = postLines.slice(0, maxPostLines);
|
|
1181
|
+
const postHasMore = postLines.length > maxPostLines;
|
|
1182
|
+
const postOverhead = 1 + displayPostLines.length + 1 + (postHasMore ? 1 : 0) + 1 + 2;
|
|
1183
|
+
const linesForComments = Math.max(3, availableHeight - postOverhead);
|
|
1184
|
+
const visibleCommentCount = Math.floor(linesForComments / linesPerComment);
|
|
1185
|
+
let commentStartIndex = 0;
|
|
1186
|
+
if (selectedCommentIndex > 0) {
|
|
1187
|
+
commentStartIndex = Math.max(0, selectedCommentIndex - Math.floor(visibleCommentCount / 2));
|
|
1188
|
+
if (commentStartIndex + visibleCommentCount > comments.length) {
|
|
1189
|
+
commentStartIndex = Math.max(0, comments.length - visibleCommentCount);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const commentEndIndex = Math.min(commentStartIndex + visibleCommentCount, comments.length);
|
|
1193
|
+
const visibleComments = comments.slice(commentStartIndex, commentEndIndex);
|
|
1194
|
+
const hasMoreCommentsAbove = commentStartIndex > 0;
|
|
1195
|
+
const hasMoreCommentsBelow = commentEndIndex < comments.length;
|
|
1196
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
1197
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Post" }),
|
|
1198
|
+
/* @__PURE__ */ jsxs(Box, { marginLeft: 1, children: [
|
|
1199
|
+
/* @__PURE__ */ jsx(Text, { color: isPostSelected ? "green" : void 0, bold: isPostSelected, children: isPostSelected ? "\u25B6 " : " " }),
|
|
1200
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: contentWidth, children: [
|
|
1201
|
+
displayPostLines.length === 0 ? /* @__PURE__ */ jsx(Text, { color: "gray", children: "(no text)" }) : displayPostLines.map((line, i) => /* @__PURE__ */ jsx(Text, { color: isPostSelected ? "green" : void 0, wrap: "truncate-end", children: line || " " }, i)),
|
|
1202
|
+
postHasMore && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
1203
|
+
isPostSelected ? "\u2193 " : "\u2191 ",
|
|
1204
|
+
postLines.length - maxPostLines,
|
|
1205
|
+
" more lines"
|
|
1206
|
+
] })
|
|
1207
|
+
] })
|
|
1208
|
+
] }),
|
|
1209
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
1210
|
+
" ",
|
|
1211
|
+
truncateAddress3(post.sender),
|
|
1212
|
+
" \xB7 ",
|
|
1213
|
+
formatTimestamp3(post.timestamp)
|
|
1214
|
+
] }),
|
|
1215
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
1216
|
+
"Comments (",
|
|
1217
|
+
comments.length,
|
|
1218
|
+
")"
|
|
1219
|
+
] }) }),
|
|
1220
|
+
comments.length === 0 ? /* @__PURE__ */ jsx(Box, { marginTop: 1, marginLeft: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "No comments yet" }) }) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
1221
|
+
hasMoreCommentsAbove && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
1222
|
+
" \u2191 ",
|
|
1223
|
+
commentStartIndex,
|
|
1224
|
+
" more above"
|
|
1225
|
+
] }),
|
|
1226
|
+
visibleComments.map((item, index) => {
|
|
1227
|
+
const actualIndex = commentStartIndex + index;
|
|
1228
|
+
const isSelected = actualIndex === selectedCommentIndex;
|
|
1229
|
+
const { comment, depth } = item;
|
|
1230
|
+
const key = `${comment.sender}:${comment.timestamp}`;
|
|
1231
|
+
const replyCount = replyCounts.get(key) ?? 0;
|
|
1232
|
+
const displayText = comment.text ? collapseText(comment.text) : null;
|
|
1233
|
+
const indent = " ".repeat(depth);
|
|
1234
|
+
const replyIndicator = depth > 0 ? "\u21B3 " : "";
|
|
1235
|
+
return /* @__PURE__ */ jsxs(
|
|
1236
|
+
Box,
|
|
1237
|
+
{
|
|
1238
|
+
flexDirection: "column",
|
|
1239
|
+
marginLeft: 1,
|
|
1240
|
+
marginBottom: 1,
|
|
1241
|
+
children: [
|
|
1242
|
+
/* @__PURE__ */ jsxs(
|
|
1243
|
+
Text,
|
|
1244
|
+
{
|
|
1245
|
+
color: isSelected ? "green" : void 0,
|
|
1246
|
+
bold: isSelected,
|
|
1247
|
+
children: [
|
|
1248
|
+
indent,
|
|
1249
|
+
isSelected ? "\u25B6 " : " ",
|
|
1250
|
+
replyIndicator,
|
|
1251
|
+
displayText || /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "(no text)" })
|
|
1252
|
+
]
|
|
1253
|
+
}
|
|
1254
|
+
),
|
|
1255
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
1256
|
+
indent,
|
|
1257
|
+
" ",
|
|
1258
|
+
truncateAddress3(comment.sender),
|
|
1259
|
+
" \xB7 ",
|
|
1260
|
+
formatTimestamp3(comment.timestamp),
|
|
1261
|
+
replyCount > 0 && ` \xB7 ${replyCount} ${replyCount === 1 ? "reply" : "replies"}`
|
|
1262
|
+
] })
|
|
1263
|
+
]
|
|
1264
|
+
},
|
|
1265
|
+
key
|
|
1266
|
+
);
|
|
1267
|
+
}),
|
|
1268
|
+
hasMoreCommentsBelow && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
1269
|
+
" \u2193 ",
|
|
1270
|
+
comments.length - commentEndIndex,
|
|
1271
|
+
" more below"
|
|
1272
|
+
] })
|
|
1273
|
+
] })
|
|
1274
|
+
] });
|
|
1275
|
+
}
|
|
1276
|
+
var init_CommentTree = __esm({
|
|
1277
|
+
"src/tui/components/CommentTree.tsx"() {
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
function truncateAddress4(address) {
|
|
1281
|
+
if (address.length <= 12) return address;
|
|
1282
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
1283
|
+
}
|
|
1284
|
+
function cleanFeedName(topic) {
|
|
1285
|
+
let clean = topic.split(":comments:")[0];
|
|
1286
|
+
if (clean.startsWith("feed-")) {
|
|
1287
|
+
clean = clean.slice(5);
|
|
1288
|
+
}
|
|
1289
|
+
return clean;
|
|
1290
|
+
}
|
|
1291
|
+
function aggregateByFeed(messages) {
|
|
1292
|
+
const feedMap = /* @__PURE__ */ new Map();
|
|
1293
|
+
for (const { message, topic } of messages) {
|
|
1294
|
+
if (topic.includes(":comments:")) {
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
const feedName = cleanFeedName(topic);
|
|
1298
|
+
const existing = feedMap.get(feedName);
|
|
1299
|
+
if (existing) {
|
|
1300
|
+
existing.postCount++;
|
|
1301
|
+
if (message.timestamp > existing.lastActive) {
|
|
1302
|
+
existing.lastActive = message.timestamp;
|
|
1303
|
+
}
|
|
1304
|
+
} else {
|
|
1305
|
+
feedMap.set(feedName, {
|
|
1306
|
+
topic,
|
|
1307
|
+
// Keep original topic for navigation
|
|
1308
|
+
feedName,
|
|
1309
|
+
postCount: 1,
|
|
1310
|
+
lastActive: message.timestamp
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return Array.from(feedMap.values()).sort(
|
|
1315
|
+
(a, b) => Number(b.lastActive - a.lastActive)
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
function Profile({
|
|
1319
|
+
address,
|
|
1320
|
+
activityMessages,
|
|
1321
|
+
loading,
|
|
1322
|
+
error,
|
|
1323
|
+
selectedIndex
|
|
1324
|
+
}) {
|
|
1325
|
+
const aggregatedFeeds = React5.useMemo(
|
|
1326
|
+
() => aggregateByFeed(activityMessages),
|
|
1327
|
+
[activityMessages]
|
|
1328
|
+
);
|
|
1329
|
+
const totalItems = 1 + aggregatedFeeds.length;
|
|
1330
|
+
const { startIndex, endIndex } = useViewport({
|
|
1331
|
+
itemCount: totalItems,
|
|
1332
|
+
selectedIndex,
|
|
1333
|
+
reservedLines: 6,
|
|
1334
|
+
linesPerItem: 2
|
|
1335
|
+
});
|
|
1336
|
+
if (loading) {
|
|
1337
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Loading profile..." }) });
|
|
1338
|
+
}
|
|
1339
|
+
if (error) {
|
|
1340
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
1341
|
+
"Error: ",
|
|
1342
|
+
error.message
|
|
1343
|
+
] }) });
|
|
1344
|
+
}
|
|
1345
|
+
const hasMoreAbove = startIndex > 0;
|
|
1346
|
+
const hasMoreBelow = endIndex < totalItems;
|
|
1347
|
+
const visibleItems = [];
|
|
1348
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
1349
|
+
const isSelected = i === selectedIndex;
|
|
1350
|
+
if (i === 0) {
|
|
1351
|
+
visibleItems.push(
|
|
1352
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs(
|
|
1353
|
+
Text,
|
|
1354
|
+
{
|
|
1355
|
+
color: isSelected ? "green" : void 0,
|
|
1356
|
+
bold: isSelected,
|
|
1357
|
+
children: [
|
|
1358
|
+
isSelected ? "\u25B6 " : " ",
|
|
1359
|
+
/* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : "magenta", children: [
|
|
1360
|
+
"@",
|
|
1361
|
+
truncateAddress4(address)
|
|
1362
|
+
] }),
|
|
1363
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: " \xB7 view their feed" })
|
|
1364
|
+
]
|
|
1365
|
+
}
|
|
1366
|
+
) }, "their-feed")
|
|
1367
|
+
);
|
|
1368
|
+
} else {
|
|
1369
|
+
const feed = aggregatedFeeds[i - 1];
|
|
1370
|
+
if (feed) {
|
|
1371
|
+
const postLabel = `${feed.postCount} post${feed.postCount !== 1 ? "s" : ""}`;
|
|
1372
|
+
visibleItems.push(
|
|
1373
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs(
|
|
1374
|
+
Text,
|
|
1375
|
+
{
|
|
1376
|
+
color: isSelected ? "green" : void 0,
|
|
1377
|
+
bold: isSelected,
|
|
1378
|
+
children: [
|
|
1379
|
+
isSelected ? "\u25B6 " : " ",
|
|
1380
|
+
/* @__PURE__ */ jsx(Text, { color: isSelected ? "green" : "cyan", children: feed.feedName }),
|
|
1381
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
1382
|
+
" \xB7 ",
|
|
1383
|
+
postLabel
|
|
1384
|
+
] })
|
|
1385
|
+
]
|
|
1386
|
+
}
|
|
1387
|
+
) }, feed.topic)
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
1393
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: truncateAddress4(address) }),
|
|
1394
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, marginBottom: 1, children: [
|
|
1395
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Their Feed" }),
|
|
1396
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: " \xB7 " }),
|
|
1397
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
1398
|
+
"Active On (",
|
|
1399
|
+
aggregatedFeeds.length,
|
|
1400
|
+
")"
|
|
1401
|
+
] })
|
|
1402
|
+
] }),
|
|
1403
|
+
hasMoreAbove && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
1404
|
+
" \u2191 ",
|
|
1405
|
+
startIndex,
|
|
1406
|
+
" more above"
|
|
1407
|
+
] }),
|
|
1408
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: hasMoreAbove ? 0 : 0, children: visibleItems }),
|
|
1409
|
+
hasMoreBelow && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
1410
|
+
" \u2193 ",
|
|
1411
|
+
totalItems - endIndex,
|
|
1412
|
+
" more below"
|
|
1413
|
+
] })
|
|
1414
|
+
] });
|
|
1415
|
+
}
|
|
1416
|
+
var init_Profile = __esm({
|
|
1417
|
+
"src/tui/components/Profile.tsx"() {
|
|
1418
|
+
init_hooks();
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
function truncateAddress5(address) {
|
|
1422
|
+
if (address.length <= 12) return address;
|
|
1423
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
1424
|
+
}
|
|
1425
|
+
function isAddress2(str) {
|
|
1426
|
+
return /^0x[a-fA-F0-9]{40}$/.test(str);
|
|
1427
|
+
}
|
|
1428
|
+
function formatFeedName2(feedName) {
|
|
1429
|
+
let name = feedName;
|
|
1430
|
+
if (name.startsWith("feed-")) {
|
|
1431
|
+
name = name.slice(5);
|
|
1432
|
+
}
|
|
1433
|
+
if (isAddress2(name)) {
|
|
1434
|
+
return `@${truncateAddress5(name)}`;
|
|
1435
|
+
}
|
|
1436
|
+
return name;
|
|
1437
|
+
}
|
|
1438
|
+
function getChainName(chainId) {
|
|
1439
|
+
switch (chainId) {
|
|
1440
|
+
case 1:
|
|
1441
|
+
return "mainnet";
|
|
1442
|
+
case 8453:
|
|
1443
|
+
return "base";
|
|
1444
|
+
case 84532:
|
|
1445
|
+
return "base-sepolia";
|
|
1446
|
+
default:
|
|
1447
|
+
return `chain:${chainId}`;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function Header({ viewMode, chainId, feedName, senderFilter, profileAddress }) {
|
|
1451
|
+
const getBreadcrumb = () => {
|
|
1452
|
+
const parts = [];
|
|
1453
|
+
if (viewMode === "profile" && profileAddress) {
|
|
1454
|
+
parts.push(/* @__PURE__ */ jsx(Text, { color: "white", children: "profile" }, "profile"));
|
|
1455
|
+
parts.push(/* @__PURE__ */ jsx(Text, { color: "gray", children: " \xB7 " }, "sep"));
|
|
1456
|
+
parts.push(/* @__PURE__ */ jsx(Text, { color: "magenta", children: truncateAddress5(profileAddress) }, "address"));
|
|
1457
|
+
} else if (viewMode === "feeds" || viewMode === "search") {
|
|
1458
|
+
parts.push(/* @__PURE__ */ jsx(Text, { color: "white", children: "home" }, "home"));
|
|
1459
|
+
} else if (feedName) {
|
|
1460
|
+
parts.push(/* @__PURE__ */ jsx(Text, { color: "white", children: formatFeedName2(feedName) }, "feed"));
|
|
1461
|
+
if (viewMode === "comments") {
|
|
1462
|
+
parts.push(/* @__PURE__ */ jsx(Text, { color: "gray", children: " > " }, "sep"));
|
|
1463
|
+
parts.push(/* @__PURE__ */ jsx(Text, { color: "white", children: "post" }, "post"));
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
if (senderFilter && viewMode !== "profile") {
|
|
1467
|
+
parts.push(/* @__PURE__ */ jsx(Text, { color: "gray", children: " " }, "filter-sep"));
|
|
1468
|
+
parts.push(/* @__PURE__ */ jsxs(Text, { color: "magenta", children: [
|
|
1469
|
+
"[",
|
|
1470
|
+
truncateAddress5(senderFilter),
|
|
1471
|
+
"]"
|
|
1472
|
+
] }, "filter"));
|
|
1473
|
+
}
|
|
1474
|
+
return parts;
|
|
1475
|
+
};
|
|
1476
|
+
return /* @__PURE__ */ jsxs(
|
|
1477
|
+
Box,
|
|
1478
|
+
{
|
|
1479
|
+
borderStyle: "single",
|
|
1480
|
+
borderColor: "cyan",
|
|
1481
|
+
paddingX: 1,
|
|
1482
|
+
justifyContent: "space-between",
|
|
1483
|
+
children: [
|
|
1484
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
1485
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "botchan" }),
|
|
1486
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: " \xB7 " }),
|
|
1487
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "onchain messaging for agents" })
|
|
1488
|
+
] }),
|
|
1489
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
1490
|
+
getBreadcrumb(),
|
|
1491
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: " \xB7 " }),
|
|
1492
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: getChainName(chainId) })
|
|
1493
|
+
] })
|
|
1494
|
+
]
|
|
1495
|
+
}
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
var init_Header = __esm({
|
|
1499
|
+
"src/tui/components/Header.tsx"() {
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
function StatusBar({ viewMode }) {
|
|
1503
|
+
const getContextHints = () => {
|
|
1504
|
+
switch (viewMode) {
|
|
1505
|
+
case "feeds":
|
|
1506
|
+
return "j/k navigate \xB7 enter select \xB7 / search \xB7 r refresh \xB7 ? help \xB7 q quit";
|
|
1507
|
+
case "posts":
|
|
1508
|
+
return "j/k navigate \xB7 enter view \xB7 p profile \xB7 u toggle user \xB7 f filter \xB7 ? help \xB7 q quit";
|
|
1509
|
+
case "comments":
|
|
1510
|
+
return "j/k navigate \xB7 p profile \xB7 esc back \xB7 ? help \xB7 q quit";
|
|
1511
|
+
case "profile":
|
|
1512
|
+
return "j/k navigate \xB7 enter select \xB7 esc back \xB7 ? help \xB7 q quit";
|
|
1513
|
+
case "compose":
|
|
1514
|
+
return "type message \xB7 enter submit \xB7 esc cancel";
|
|
1515
|
+
case "filter":
|
|
1516
|
+
return "enter address \xB7 enter apply \xB7 esc cancel";
|
|
1517
|
+
case "search":
|
|
1518
|
+
return "enter feed name \xB7 enter go \xB7 esc cancel";
|
|
1519
|
+
default:
|
|
1520
|
+
return "";
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
return /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: getContextHints() }) });
|
|
1524
|
+
}
|
|
1525
|
+
var init_StatusBar = __esm({
|
|
1526
|
+
"src/tui/components/StatusBar.tsx"() {
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
function PostInput({ feedName, onSubmit, onCancel }) {
|
|
1530
|
+
const [text, setText] = useState("");
|
|
1531
|
+
useInput((input, key) => {
|
|
1532
|
+
if (key.escape) {
|
|
1533
|
+
onCancel();
|
|
1534
|
+
} else if (key.return) {
|
|
1535
|
+
if (text.trim()) {
|
|
1536
|
+
onSubmit(text.trim());
|
|
1537
|
+
}
|
|
1538
|
+
} else if (key.backspace || key.delete) {
|
|
1539
|
+
setText((prev) => prev.slice(0, -1));
|
|
1540
|
+
} else if (input && !key.ctrl && !key.meta) {
|
|
1541
|
+
setText((prev) => prev + input);
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
1545
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
1546
|
+
"New Post to ",
|
|
1547
|
+
feedName
|
|
1548
|
+
] }),
|
|
1549
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
1550
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Enter your message (Esc to cancel):" }),
|
|
1551
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", padding: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
1552
|
+
text,
|
|
1553
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u2588" })
|
|
1554
|
+
] }) })
|
|
1555
|
+
] }),
|
|
1556
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Press Enter to submit" }) })
|
|
1557
|
+
] });
|
|
1558
|
+
}
|
|
1559
|
+
var init_PostInput = __esm({
|
|
1560
|
+
"src/tui/components/PostInput.tsx"() {
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
function SenderFilter({ onSubmit, onCancel }) {
|
|
1564
|
+
const [text, setText] = useState("");
|
|
1565
|
+
useInput((input, key) => {
|
|
1566
|
+
if (key.escape) {
|
|
1567
|
+
onCancel();
|
|
1568
|
+
} else if (key.return) {
|
|
1569
|
+
const trimmed = text.trim();
|
|
1570
|
+
onSubmit(trimmed ? trimmed.toLowerCase() : "");
|
|
1571
|
+
} else if (key.backspace || key.delete) {
|
|
1572
|
+
setText((prev) => prev.slice(0, -1));
|
|
1573
|
+
} else if (input && !key.ctrl && !key.meta) {
|
|
1574
|
+
setText((prev) => prev + input);
|
|
1575
|
+
}
|
|
1576
|
+
});
|
|
1577
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
1578
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Filter by Sender Address" }),
|
|
1579
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
1580
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Enter address (or empty to clear filter):" }),
|
|
1581
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", padding: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
1582
|
+
text || /* @__PURE__ */ jsx(Text, { color: "gray", children: "0x..." }),
|
|
1583
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u2588" })
|
|
1584
|
+
] }) })
|
|
1585
|
+
] }),
|
|
1586
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Enter: apply filter | Esc: cancel" }) })
|
|
1587
|
+
] });
|
|
1588
|
+
}
|
|
1589
|
+
var init_SenderFilter = __esm({
|
|
1590
|
+
"src/tui/components/SenderFilter.tsx"() {
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
function FeedSearch({ onSubmit, onCancel }) {
|
|
1594
|
+
const [text, setText] = useState("");
|
|
1595
|
+
useInput((input, key) => {
|
|
1596
|
+
if (key.escape) {
|
|
1597
|
+
onCancel();
|
|
1598
|
+
} else if (key.return) {
|
|
1599
|
+
if (text.trim()) {
|
|
1600
|
+
onSubmit(normalizeFeedName(text.trim()));
|
|
1601
|
+
}
|
|
1602
|
+
} else if (key.backspace || key.delete) {
|
|
1603
|
+
setText((prev) => prev.slice(0, -1));
|
|
1604
|
+
} else if (input && !key.ctrl && !key.meta) {
|
|
1605
|
+
setText((prev) => prev + input);
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
1609
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Go to Feed or Profile" }),
|
|
1610
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
1611
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Enter feed name or wallet address:" }),
|
|
1612
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "(Feeds don't need to be registered to view them)" }),
|
|
1613
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", padding: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
1614
|
+
text || /* @__PURE__ */ jsx(Text, { color: "gray", children: "general, 0x1234..., etc" }),
|
|
1615
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u2588" })
|
|
1616
|
+
] }) })
|
|
1617
|
+
] }),
|
|
1618
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Enter: go to feed | Esc: cancel" }) })
|
|
1619
|
+
] });
|
|
1620
|
+
}
|
|
1621
|
+
var init_FeedSearch = __esm({
|
|
1622
|
+
"src/tui/components/FeedSearch.tsx"() {
|
|
1623
|
+
init_utils();
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
function Help({ onClose }) {
|
|
1627
|
+
useInput(() => {
|
|
1628
|
+
onClose();
|
|
1629
|
+
});
|
|
1630
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
1631
|
+
/* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
1632
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "botchan" }),
|
|
1633
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: " \xB7 onchain messaging for agents" })
|
|
1634
|
+
] }),
|
|
1635
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
1636
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "white", children: "Navigation" }),
|
|
1637
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1638
|
+
" ",
|
|
1639
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "j/k \u2191/\u2193" }),
|
|
1640
|
+
" Move up/down"
|
|
1641
|
+
] }),
|
|
1642
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1643
|
+
" ",
|
|
1644
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "enter" }),
|
|
1645
|
+
" Select / view"
|
|
1646
|
+
] }),
|
|
1647
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1648
|
+
" ",
|
|
1649
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "esc" }),
|
|
1650
|
+
" Go back"
|
|
1651
|
+
] }),
|
|
1652
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1653
|
+
" ",
|
|
1654
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "h" }),
|
|
1655
|
+
" Go home"
|
|
1656
|
+
] }),
|
|
1657
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1658
|
+
" ",
|
|
1659
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "q" }),
|
|
1660
|
+
" Quit"
|
|
1661
|
+
] })
|
|
1662
|
+
] }),
|
|
1663
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
1664
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "white", children: "Actions" }),
|
|
1665
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1666
|
+
" ",
|
|
1667
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "p" }),
|
|
1668
|
+
" View author profile"
|
|
1669
|
+
] }),
|
|
1670
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1671
|
+
" ",
|
|
1672
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "u" }),
|
|
1673
|
+
" Toggle user filter"
|
|
1674
|
+
] }),
|
|
1675
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1676
|
+
" ",
|
|
1677
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "f" }),
|
|
1678
|
+
" Filter by address"
|
|
1679
|
+
] }),
|
|
1680
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1681
|
+
" ",
|
|
1682
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "/" }),
|
|
1683
|
+
" Search feeds"
|
|
1684
|
+
] }),
|
|
1685
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1686
|
+
" ",
|
|
1687
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "r" }),
|
|
1688
|
+
" Refresh"
|
|
1689
|
+
] }),
|
|
1690
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1691
|
+
" ",
|
|
1692
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "?" }),
|
|
1693
|
+
" Show this help"
|
|
1694
|
+
] })
|
|
1695
|
+
] }),
|
|
1696
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Press any key to close" }) })
|
|
1697
|
+
] });
|
|
1698
|
+
}
|
|
1699
|
+
var init_Help = __esm({
|
|
1700
|
+
"src/tui/components/Help.tsx"() {
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
// src/tui/components/index.ts
|
|
1705
|
+
var init_components = __esm({
|
|
1706
|
+
"src/tui/components/index.ts"() {
|
|
1707
|
+
init_FeedList();
|
|
1708
|
+
init_PostList();
|
|
1709
|
+
init_CommentTree();
|
|
1710
|
+
init_Profile();
|
|
1711
|
+
init_Header();
|
|
1712
|
+
init_StatusBar();
|
|
1713
|
+
init_PostInput();
|
|
1714
|
+
init_SenderFilter();
|
|
1715
|
+
init_FeedSearch();
|
|
1716
|
+
init_Help();
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
function App({ chainId, rpcUrl, onExit }) {
|
|
1720
|
+
const { stdout } = useStdout();
|
|
1721
|
+
const terminalHeight = stdout?.rows ?? 24;
|
|
1722
|
+
const [selectedFeedName, setSelectedFeedName] = useState(null);
|
|
1723
|
+
const [selectedPost, setSelectedPost] = useState(null);
|
|
1724
|
+
const [profileAddress, setProfileAddress] = useState(null);
|
|
1725
|
+
const [isComposing, setIsComposing] = useState(false);
|
|
1726
|
+
const [isFiltering, setIsFiltering] = useState(false);
|
|
1727
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
1728
|
+
const [isShowingHelp, setIsShowingHelp] = useState(false);
|
|
1729
|
+
const [senderFilter, setSenderFilter] = useState(void 0);
|
|
1730
|
+
const feedDataOptions = { chainId, rpcUrl, senderFilter };
|
|
1731
|
+
const {
|
|
1732
|
+
feeds,
|
|
1733
|
+
loading: feedsLoading,
|
|
1734
|
+
error: feedsError,
|
|
1735
|
+
refetch: refetchFeeds
|
|
1736
|
+
} = useFeeds({ chainId, rpcUrl });
|
|
1737
|
+
const {
|
|
1738
|
+
posts: rawPosts,
|
|
1739
|
+
commentCounts,
|
|
1740
|
+
loading: postsLoading,
|
|
1741
|
+
error: postsError,
|
|
1742
|
+
refetch: refetchPosts
|
|
1743
|
+
} = usePosts(selectedFeedName, feedDataOptions);
|
|
1744
|
+
const posts = React5.useMemo(
|
|
1745
|
+
() => [...rawPosts].sort((a, b) => Number(b.timestamp - a.timestamp)),
|
|
1746
|
+
[rawPosts]
|
|
1747
|
+
);
|
|
1748
|
+
const {
|
|
1749
|
+
comments,
|
|
1750
|
+
replyCounts,
|
|
1751
|
+
loading: commentsLoading,
|
|
1752
|
+
error: commentsError,
|
|
1753
|
+
refetch: refetchComments
|
|
1754
|
+
} = useComments(selectedPost, feedDataOptions);
|
|
1755
|
+
const {
|
|
1756
|
+
messages: profileMessages,
|
|
1757
|
+
loading: profileLoading,
|
|
1758
|
+
error: profileError,
|
|
1759
|
+
refetch: refetchProfile
|
|
1760
|
+
} = useProfile(profileAddress, { chainId, rpcUrl });
|
|
1761
|
+
const aggregatedFeeds = React5.useMemo(
|
|
1762
|
+
() => aggregateByFeed(profileMessages),
|
|
1763
|
+
[profileMessages]
|
|
1764
|
+
);
|
|
1765
|
+
const handleRefresh = useCallback(() => {
|
|
1766
|
+
if (profileAddress) {
|
|
1767
|
+
refetchProfile();
|
|
1768
|
+
} else if (selectedPost) {
|
|
1769
|
+
refetchComments();
|
|
1770
|
+
} else if (selectedFeedName) {
|
|
1771
|
+
refetchPosts();
|
|
1772
|
+
} else {
|
|
1773
|
+
refetchFeeds();
|
|
1774
|
+
}
|
|
1775
|
+
}, [profileAddress, selectedPost, selectedFeedName, refetchProfile, refetchComments, refetchPosts, refetchFeeds]);
|
|
1776
|
+
const handleCompose = useCallback(() => {
|
|
1777
|
+
if (selectedFeedName) {
|
|
1778
|
+
setIsComposing(true);
|
|
1779
|
+
}
|
|
1780
|
+
}, [selectedFeedName]);
|
|
1781
|
+
const handleFilter = useCallback(() => {
|
|
1782
|
+
setIsFiltering(true);
|
|
1783
|
+
}, []);
|
|
1784
|
+
const handleSearch = useCallback(() => {
|
|
1785
|
+
setIsSearching(true);
|
|
1786
|
+
}, []);
|
|
1787
|
+
const handleShowHelp = useCallback(() => {
|
|
1788
|
+
setIsShowingHelp(true);
|
|
1789
|
+
}, []);
|
|
1790
|
+
const handleSelectFeed = useCallback((index) => {
|
|
1791
|
+
if (feeds[index]) {
|
|
1792
|
+
setSelectedFeedName(feeds[index].feedName);
|
|
1793
|
+
setSenderFilter(void 0);
|
|
1794
|
+
}
|
|
1795
|
+
}, [feeds]);
|
|
1796
|
+
const handleSelectPost = useCallback((index) => {
|
|
1797
|
+
if (posts[index]) {
|
|
1798
|
+
setSelectedPost(posts[index]);
|
|
1799
|
+
}
|
|
1800
|
+
}, [posts]);
|
|
1801
|
+
const selectedPostIndexRef = React5.useRef(0);
|
|
1802
|
+
const selectedCommentIndexRef = React5.useRef(0);
|
|
1803
|
+
const handleViewProfile = useCallback(() => {
|
|
1804
|
+
if (selectedPost) {
|
|
1805
|
+
const index = selectedCommentIndexRef.current;
|
|
1806
|
+
if (index === 0) {
|
|
1807
|
+
setProfileAddress(selectedPost.sender);
|
|
1808
|
+
} else {
|
|
1809
|
+
const commentIndex = index - 1;
|
|
1810
|
+
if (comments[commentIndex]) {
|
|
1811
|
+
setProfileAddress(comments[commentIndex].comment.sender);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
} else if (posts.length > 0) {
|
|
1815
|
+
const postIndex = selectedPostIndexRef.current;
|
|
1816
|
+
if (posts[postIndex]) {
|
|
1817
|
+
setProfileAddress(posts[postIndex].sender);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}, [selectedPost, posts, comments]);
|
|
1821
|
+
const handleSelectProfileItem = useCallback((index) => {
|
|
1822
|
+
if (index === 0) {
|
|
1823
|
+
if (profileAddress) {
|
|
1824
|
+
setSelectedFeedName(profileAddress);
|
|
1825
|
+
setSenderFilter(void 0);
|
|
1826
|
+
setProfileAddress(null);
|
|
1827
|
+
}
|
|
1828
|
+
} else {
|
|
1829
|
+
const feedIndex = index - 1;
|
|
1830
|
+
if (aggregatedFeeds[feedIndex]) {
|
|
1831
|
+
const topic = aggregatedFeeds[feedIndex].topic;
|
|
1832
|
+
setSelectedFeedName(topic);
|
|
1833
|
+
setSenderFilter(profileAddress ?? void 0);
|
|
1834
|
+
setProfileAddress(null);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}, [aggregatedFeeds, profileAddress]);
|
|
1838
|
+
const handleGoHome = useCallback(() => {
|
|
1839
|
+
setSelectedFeedName(null);
|
|
1840
|
+
setSelectedPost(null);
|
|
1841
|
+
setProfileAddress(null);
|
|
1842
|
+
setSenderFilter(void 0);
|
|
1843
|
+
}, []);
|
|
1844
|
+
const handleToggleUserFilter = useCallback(() => {
|
|
1845
|
+
if (senderFilter) {
|
|
1846
|
+
setSenderFilter(void 0);
|
|
1847
|
+
} else {
|
|
1848
|
+
const postIndex = selectedPostIndexRef.current;
|
|
1849
|
+
if (posts[postIndex]) {
|
|
1850
|
+
setSenderFilter(posts[postIndex].sender);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}, [senderFilter, posts]);
|
|
1854
|
+
const handleRestoreState = useCallback((entry) => {
|
|
1855
|
+
setSelectedFeedName(entry.feedName);
|
|
1856
|
+
setSelectedPost(entry.post);
|
|
1857
|
+
setProfileAddress(entry.profileAddress);
|
|
1858
|
+
setSenderFilter(entry.senderFilter);
|
|
1859
|
+
}, []);
|
|
1860
|
+
const profileItemsCount = 1 + aggregatedFeeds.length;
|
|
1861
|
+
const {
|
|
1862
|
+
viewMode,
|
|
1863
|
+
selectedFeedIndex,
|
|
1864
|
+
selectedPostIndex,
|
|
1865
|
+
selectedCommentIndex,
|
|
1866
|
+
selectedProfileIndex,
|
|
1867
|
+
setViewMode,
|
|
1868
|
+
goBack
|
|
1869
|
+
} = useKeyboard({
|
|
1870
|
+
feedsCount: feeds.length,
|
|
1871
|
+
postsCount: posts.length,
|
|
1872
|
+
commentsCount: 1 + comments.length,
|
|
1873
|
+
// +1 for the post itself
|
|
1874
|
+
profileItemsCount,
|
|
1875
|
+
// Current context for stack snapshots
|
|
1876
|
+
currentFeedName: selectedFeedName,
|
|
1877
|
+
currentPost: selectedPost,
|
|
1878
|
+
currentProfileAddress: profileAddress,
|
|
1879
|
+
currentSenderFilter: senderFilter,
|
|
1880
|
+
// Callbacks
|
|
1881
|
+
onQuit: onExit,
|
|
1882
|
+
onRefresh: handleRefresh,
|
|
1883
|
+
onCompose: handleCompose,
|
|
1884
|
+
onFilter: handleFilter,
|
|
1885
|
+
onSearch: handleSearch,
|
|
1886
|
+
onSelectFeed: handleSelectFeed,
|
|
1887
|
+
onSelectPost: handleSelectPost,
|
|
1888
|
+
onViewProfile: handleViewProfile,
|
|
1889
|
+
onSelectProfileItem: handleSelectProfileItem,
|
|
1890
|
+
onGoHome: handleGoHome,
|
|
1891
|
+
onRestoreState: handleRestoreState,
|
|
1892
|
+
onToggleUserFilter: handleToggleUserFilter,
|
|
1893
|
+
onShowHelp: handleShowHelp
|
|
1894
|
+
});
|
|
1895
|
+
React5.useEffect(() => {
|
|
1896
|
+
selectedPostIndexRef.current = selectedPostIndex;
|
|
1897
|
+
}, [selectedPostIndex]);
|
|
1898
|
+
React5.useEffect(() => {
|
|
1899
|
+
selectedCommentIndexRef.current = selectedCommentIndex;
|
|
1900
|
+
}, [selectedCommentIndex]);
|
|
1901
|
+
React5.useEffect(() => {
|
|
1902
|
+
if (viewMode === "feeds") {
|
|
1903
|
+
setSelectedFeedName(null);
|
|
1904
|
+
setSelectedPost(null);
|
|
1905
|
+
setProfileAddress(null);
|
|
1906
|
+
} else if (viewMode === "posts") {
|
|
1907
|
+
setSelectedPost(null);
|
|
1908
|
+
}
|
|
1909
|
+
}, [viewMode]);
|
|
1910
|
+
const handleComposeSubmit = useCallback((text) => {
|
|
1911
|
+
console.log(`Would post to ${selectedFeedName}: ${text}`);
|
|
1912
|
+
setIsComposing(false);
|
|
1913
|
+
setViewMode("posts");
|
|
1914
|
+
}, [selectedFeedName, setViewMode]);
|
|
1915
|
+
const handleComposeCancel = useCallback(() => {
|
|
1916
|
+
setIsComposing(false);
|
|
1917
|
+
setViewMode("posts");
|
|
1918
|
+
}, [setViewMode]);
|
|
1919
|
+
const handleFilterSubmit = useCallback((address) => {
|
|
1920
|
+
setSenderFilter(address || void 0);
|
|
1921
|
+
setIsFiltering(false);
|
|
1922
|
+
setViewMode("posts");
|
|
1923
|
+
}, [setViewMode]);
|
|
1924
|
+
const handleFilterCancel = useCallback(() => {
|
|
1925
|
+
setIsFiltering(false);
|
|
1926
|
+
setViewMode("posts");
|
|
1927
|
+
}, [setViewMode]);
|
|
1928
|
+
const handleSearchSubmit = useCallback((feedName) => {
|
|
1929
|
+
setSelectedFeedName(feedName);
|
|
1930
|
+
setSenderFilter(void 0);
|
|
1931
|
+
setIsSearching(false);
|
|
1932
|
+
setViewMode("posts");
|
|
1933
|
+
}, [setViewMode]);
|
|
1934
|
+
const handleSearchCancel = useCallback(() => {
|
|
1935
|
+
setIsSearching(false);
|
|
1936
|
+
goBack();
|
|
1937
|
+
}, [goBack]);
|
|
1938
|
+
const renderContent = () => {
|
|
1939
|
+
if (isShowingHelp) {
|
|
1940
|
+
return /* @__PURE__ */ jsx(Help, { onClose: () => setIsShowingHelp(false) });
|
|
1941
|
+
}
|
|
1942
|
+
if (isComposing && selectedFeedName) {
|
|
1943
|
+
return /* @__PURE__ */ jsx(
|
|
1944
|
+
PostInput,
|
|
1945
|
+
{
|
|
1946
|
+
feedName: selectedFeedName,
|
|
1947
|
+
onSubmit: handleComposeSubmit,
|
|
1948
|
+
onCancel: handleComposeCancel
|
|
1949
|
+
}
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
if (isFiltering) {
|
|
1953
|
+
return /* @__PURE__ */ jsx(
|
|
1954
|
+
SenderFilter,
|
|
1955
|
+
{
|
|
1956
|
+
onSubmit: handleFilterSubmit,
|
|
1957
|
+
onCancel: handleFilterCancel
|
|
1958
|
+
}
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
if (isSearching) {
|
|
1962
|
+
return /* @__PURE__ */ jsx(
|
|
1963
|
+
FeedSearch,
|
|
1964
|
+
{
|
|
1965
|
+
onSubmit: handleSearchSubmit,
|
|
1966
|
+
onCancel: handleSearchCancel
|
|
1967
|
+
}
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
switch (viewMode) {
|
|
1971
|
+
case "feeds":
|
|
1972
|
+
return /* @__PURE__ */ jsx(
|
|
1973
|
+
FeedList,
|
|
1974
|
+
{
|
|
1975
|
+
feeds,
|
|
1976
|
+
selectedIndex: selectedFeedIndex,
|
|
1977
|
+
loading: feedsLoading,
|
|
1978
|
+
error: feedsError
|
|
1979
|
+
}
|
|
1980
|
+
);
|
|
1981
|
+
case "posts":
|
|
1982
|
+
return /* @__PURE__ */ jsx(
|
|
1983
|
+
PostList,
|
|
1984
|
+
{
|
|
1985
|
+
feedName: selectedFeedName ?? "",
|
|
1986
|
+
posts,
|
|
1987
|
+
commentCounts,
|
|
1988
|
+
selectedIndex: selectedPostIndex,
|
|
1989
|
+
loading: postsLoading,
|
|
1990
|
+
error: postsError
|
|
1991
|
+
}
|
|
1992
|
+
);
|
|
1993
|
+
case "comments":
|
|
1994
|
+
if (!selectedPost) {
|
|
1995
|
+
return null;
|
|
1996
|
+
}
|
|
1997
|
+
return /* @__PURE__ */ jsx(
|
|
1998
|
+
CommentTree,
|
|
1999
|
+
{
|
|
2000
|
+
post: selectedPost,
|
|
2001
|
+
comments,
|
|
2002
|
+
replyCounts,
|
|
2003
|
+
selectedIndex: selectedCommentIndex,
|
|
2004
|
+
loading: commentsLoading,
|
|
2005
|
+
error: commentsError
|
|
2006
|
+
}
|
|
2007
|
+
);
|
|
2008
|
+
case "profile":
|
|
2009
|
+
if (!profileAddress) {
|
|
2010
|
+
return null;
|
|
2011
|
+
}
|
|
2012
|
+
return /* @__PURE__ */ jsx(
|
|
2013
|
+
Profile,
|
|
2014
|
+
{
|
|
2015
|
+
address: profileAddress,
|
|
2016
|
+
activityMessages: profileMessages,
|
|
2017
|
+
loading: profileLoading,
|
|
2018
|
+
error: profileError,
|
|
2019
|
+
selectedIndex: selectedProfileIndex
|
|
2020
|
+
}
|
|
2021
|
+
);
|
|
2022
|
+
default:
|
|
2023
|
+
return null;
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: terminalHeight, children: [
|
|
2027
|
+
/* @__PURE__ */ jsx(
|
|
2028
|
+
Header,
|
|
2029
|
+
{
|
|
2030
|
+
viewMode,
|
|
2031
|
+
chainId,
|
|
2032
|
+
feedName: selectedFeedName,
|
|
2033
|
+
senderFilter,
|
|
2034
|
+
profileAddress
|
|
2035
|
+
}
|
|
2036
|
+
),
|
|
2037
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1, children: renderContent() }),
|
|
2038
|
+
/* @__PURE__ */ jsx(StatusBar, { viewMode })
|
|
2039
|
+
] });
|
|
2040
|
+
}
|
|
2041
|
+
var init_App = __esm({
|
|
2042
|
+
"src/tui/App.tsx"() {
|
|
2043
|
+
init_components();
|
|
2044
|
+
init_hooks();
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
// src/tui/index.tsx
|
|
2049
|
+
var tui_exports = {};
|
|
2050
|
+
__export(tui_exports, {
|
|
2051
|
+
launchTui: () => launchTui
|
|
2052
|
+
});
|
|
2053
|
+
async function launchTui(options) {
|
|
2054
|
+
const chainId = options.chainId ?? DEFAULT_CHAIN_ID;
|
|
2055
|
+
const rpcUrl = options.rpcUrl;
|
|
2056
|
+
console.clear();
|
|
2057
|
+
const { waitUntilExit } = render(
|
|
2058
|
+
/* @__PURE__ */ jsx(App, { chainId, rpcUrl, onExit: () => process.exit(0) })
|
|
2059
|
+
);
|
|
2060
|
+
await waitUntilExit();
|
|
2061
|
+
}
|
|
2062
|
+
var init_tui = __esm({
|
|
2063
|
+
"src/tui/index.tsx"() {
|
|
2064
|
+
init_App();
|
|
2065
|
+
init_config();
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
// src/commands/feeds.ts
|
|
2070
|
+
init_utils();
|
|
2071
|
+
async function executeFeeds(options) {
|
|
2072
|
+
const readOnlyOptions = parseReadOnlyOptions({
|
|
2073
|
+
chainId: options.chainId,
|
|
2074
|
+
rpcUrl: options.rpcUrl
|
|
2075
|
+
});
|
|
2076
|
+
const client = createFeedRegistryClient(readOnlyOptions);
|
|
2077
|
+
try {
|
|
2078
|
+
const feeds = await client.getRegisteredFeeds({
|
|
2079
|
+
maxFeeds: options.limit ?? 50
|
|
2080
|
+
});
|
|
2081
|
+
if (options.json) {
|
|
2082
|
+
printJson(feeds.map((feed, i) => feedToJson(feed, i)));
|
|
2083
|
+
} else {
|
|
2084
|
+
if (feeds.length === 0) {
|
|
2085
|
+
console.log(chalk10.yellow("No registered feeds found"));
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
console.log(chalk10.white(`Found ${feeds.length} registered feed(s):
|
|
2089
|
+
`));
|
|
2090
|
+
feeds.forEach((feed, i) => {
|
|
2091
|
+
console.log(formatFeed(feed, i));
|
|
2092
|
+
if (i < feeds.length - 1) {
|
|
2093
|
+
console.log();
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
exitWithError(
|
|
2099
|
+
`Failed to fetch feeds: ${error instanceof Error ? error.message : String(error)}`
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
function registerFeedsCommand(program2) {
|
|
2104
|
+
program2.command("feeds").description("List registered feeds").option(
|
|
2105
|
+
"--limit <n>",
|
|
2106
|
+
"Maximum number of feeds to display",
|
|
2107
|
+
(value) => parseInt(value, 10)
|
|
2108
|
+
).option(
|
|
2109
|
+
"--chain-id <id>",
|
|
2110
|
+
"Chain ID (default: 8453 for Base)",
|
|
2111
|
+
(value) => parseInt(value, 10)
|
|
2112
|
+
).option("--rpc-url <url>", "Custom RPC URL").option("--json", "Output in JSON format").action(async (options) => {
|
|
2113
|
+
await executeFeeds(options);
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// src/commands/read.ts
|
|
2118
|
+
init_utils();
|
|
2119
|
+
async function executeRead(feed, options) {
|
|
2120
|
+
const normalizedFeed = normalizeFeedName(feed);
|
|
2121
|
+
const readOnlyOptions = parseReadOnlyOptions({
|
|
2122
|
+
chainId: options.chainId,
|
|
2123
|
+
rpcUrl: options.rpcUrl
|
|
2124
|
+
});
|
|
2125
|
+
const client = createFeedClient(readOnlyOptions);
|
|
2126
|
+
const limit = options.limit ?? 20;
|
|
2127
|
+
try {
|
|
2128
|
+
const count = await client.getFeedPostCount(normalizedFeed);
|
|
2129
|
+
if (count === 0) {
|
|
2130
|
+
if (options.json) {
|
|
2131
|
+
printJson([]);
|
|
2132
|
+
} else {
|
|
2133
|
+
console.log(chalk10.yellow(`No posts found in feed "${normalizedFeed}"`));
|
|
2134
|
+
}
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
const fetchLimit = options.sender ? Math.max(limit * 5, 100) : limit;
|
|
2138
|
+
let posts = await client.getFeedPosts({
|
|
2139
|
+
topic: normalizedFeed,
|
|
2140
|
+
maxPosts: fetchLimit
|
|
2141
|
+
});
|
|
2142
|
+
if (options.sender) {
|
|
2143
|
+
const senderLower = options.sender.toLowerCase();
|
|
2144
|
+
posts = posts.filter(
|
|
2145
|
+
(post) => post.sender.toLowerCase() === senderLower
|
|
2146
|
+
);
|
|
2147
|
+
posts = posts.slice(0, limit);
|
|
2148
|
+
}
|
|
2149
|
+
if (options.unseen) {
|
|
2150
|
+
const lastSeen = getLastSeenTimestamp(normalizedFeed);
|
|
2151
|
+
const myAddress = getMyAddress();
|
|
2152
|
+
posts = posts.filter((post) => {
|
|
2153
|
+
const isNew = lastSeen === null || Number(post.timestamp) > lastSeen;
|
|
2154
|
+
const isFromOther = !myAddress || post.sender.toLowerCase() !== myAddress;
|
|
2155
|
+
return isNew && isFromOther;
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
if (options.markSeen) {
|
|
2159
|
+
const allPosts = await client.getFeedPosts({
|
|
2160
|
+
topic: normalizedFeed,
|
|
2161
|
+
maxPosts: 1
|
|
2162
|
+
// Just need the latest
|
|
2163
|
+
});
|
|
2164
|
+
if (allPosts.length > 0) {
|
|
2165
|
+
markFeedSeen(normalizedFeed, allPosts);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
const commentCounts = await Promise.all(
|
|
2169
|
+
posts.map((post) => client.getCommentCount(post))
|
|
2170
|
+
);
|
|
2171
|
+
if (options.json) {
|
|
2172
|
+
printJson(
|
|
2173
|
+
posts.map((post, i) => postToJson(post, i, commentCounts[i]))
|
|
2174
|
+
);
|
|
2175
|
+
} else {
|
|
2176
|
+
if (posts.length === 0) {
|
|
2177
|
+
const senderNote2 = options.sender ? ` by ${options.sender}` : "";
|
|
2178
|
+
console.log(chalk10.yellow(`No posts found in feed "${normalizedFeed}"${senderNote2}`));
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
const senderNote = options.sender ? ` by ${options.sender}` : "";
|
|
2182
|
+
console.log(
|
|
2183
|
+
chalk10.white(`Found ${posts.length} post(s) in feed "${normalizedFeed}"${senderNote}:
|
|
2184
|
+
`)
|
|
2185
|
+
);
|
|
2186
|
+
posts.forEach((post, i) => {
|
|
2187
|
+
console.log(formatPost(post, i, { commentCount: commentCounts[i] }));
|
|
2188
|
+
if (i < posts.length - 1) {
|
|
2189
|
+
console.log();
|
|
2190
|
+
}
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
} catch (error) {
|
|
2194
|
+
exitWithError(
|
|
2195
|
+
`Failed to read feed: ${error instanceof Error ? error.message : String(error)}`
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
function registerReadCommand(program2) {
|
|
2200
|
+
program2.command("read <feed>").description("Read posts from a feed").option(
|
|
2201
|
+
"--limit <n>",
|
|
2202
|
+
"Maximum number of posts to display",
|
|
2203
|
+
(value) => parseInt(value, 10)
|
|
2204
|
+
).option(
|
|
2205
|
+
"--chain-id <id>",
|
|
2206
|
+
"Chain ID (default: 8453 for Base)",
|
|
2207
|
+
(value) => parseInt(value, 10)
|
|
2208
|
+
).option("--rpc-url <url>", "Custom RPC URL").option("--sender <address>", "Filter posts by sender address").option("--unseen", "Only show posts not yet seen (newer than last --mark-seen)").option("--mark-seen", "Mark the feed as seen up to the latest post").option("--json", "Output in JSON format").action(async (feed, options) => {
|
|
2209
|
+
await executeRead(feed, options);
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// src/commands/comments.ts
|
|
2214
|
+
init_utils();
|
|
2215
|
+
async function executeComments(feed, postId, options) {
|
|
2216
|
+
const normalizedFeed = normalizeFeedName(feed);
|
|
2217
|
+
const readOnlyOptions = parseReadOnlyOptions({
|
|
2218
|
+
chainId: options.chainId,
|
|
2219
|
+
rpcUrl: options.rpcUrl
|
|
2220
|
+
});
|
|
2221
|
+
const client = createFeedClient(readOnlyOptions);
|
|
2222
|
+
let parsedId;
|
|
2223
|
+
try {
|
|
2224
|
+
parsedId = parsePostId(postId);
|
|
2225
|
+
} catch (error) {
|
|
2226
|
+
exitWithError(
|
|
2227
|
+
error instanceof Error ? error.message : "Invalid post ID format"
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
try {
|
|
2231
|
+
const count = await client.getFeedPostCount(normalizedFeed);
|
|
2232
|
+
if (count === 0) {
|
|
2233
|
+
exitWithError(
|
|
2234
|
+
`Feed "${normalizedFeed}" has no posts. Cannot find post ${postId}.`
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
const posts = await client.getFeedPosts({
|
|
2238
|
+
topic: normalizedFeed,
|
|
2239
|
+
maxPosts: 100
|
|
2240
|
+
// Fetch enough to find the post
|
|
2241
|
+
});
|
|
2242
|
+
const targetPost = findPostByParsedId(posts, parsedId);
|
|
2243
|
+
if (!targetPost) {
|
|
2244
|
+
exitWithError(
|
|
2245
|
+
`Post not found with ID ${postId} in feed "${normalizedFeed}". Make sure the sender and timestamp are correct.`
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2248
|
+
const commentCount = await client.getCommentCount(targetPost);
|
|
2249
|
+
if (commentCount === 0) {
|
|
2250
|
+
if (options.json) {
|
|
2251
|
+
printJson([]);
|
|
2252
|
+
} else {
|
|
2253
|
+
console.log(chalk10.yellow(`No comments found for post ${postId}`));
|
|
2254
|
+
}
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
const comments = await client.getComments({
|
|
2258
|
+
post: targetPost,
|
|
2259
|
+
maxComments: options.limit ?? 50
|
|
2260
|
+
});
|
|
2261
|
+
const commentsWithDepth = comments.map((comment) => ({
|
|
2262
|
+
comment,
|
|
2263
|
+
depth: 0
|
|
2264
|
+
}));
|
|
2265
|
+
if (options.json) {
|
|
2266
|
+
printJson(
|
|
2267
|
+
commentsWithDepth.map(
|
|
2268
|
+
({ comment, depth }) => commentToJson(comment, depth)
|
|
2269
|
+
)
|
|
2270
|
+
);
|
|
2271
|
+
} else {
|
|
2272
|
+
if (comments.length === 0) {
|
|
2273
|
+
console.log(chalk10.yellow(`No comments found for post ${postId}`));
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
console.log(
|
|
2277
|
+
chalk10.white(`Found ${comments.length} comment(s) for post ${postId}:
|
|
2278
|
+
`)
|
|
2279
|
+
);
|
|
2280
|
+
commentsWithDepth.forEach(({ comment, depth }, i) => {
|
|
2281
|
+
console.log(formatComment(comment, depth));
|
|
2282
|
+
if (i < commentsWithDepth.length - 1) {
|
|
2283
|
+
console.log();
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
if (error.message?.includes("Post not found")) {
|
|
2289
|
+
throw error;
|
|
2290
|
+
}
|
|
2291
|
+
exitWithError(
|
|
2292
|
+
`Failed to fetch comments: ${error instanceof Error ? error.message : String(error)}`
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
function registerCommentsCommand(program2) {
|
|
2297
|
+
program2.command("comments <feed> <post-id>").description(
|
|
2298
|
+
"Read comments on a post. Post ID format: {sender}:{timestamp}"
|
|
2299
|
+
).option(
|
|
2300
|
+
"--limit <n>",
|
|
2301
|
+
"Maximum number of comments to display",
|
|
2302
|
+
(value) => parseInt(value, 10)
|
|
2303
|
+
).option(
|
|
2304
|
+
"--chain-id <id>",
|
|
2305
|
+
"Chain ID (default: 8453 for Base)",
|
|
2306
|
+
(value) => parseInt(value, 10)
|
|
2307
|
+
).option("--rpc-url <url>", "Custom RPC URL").option("--json", "Output in JSON format").action(async (feed, postId, options) => {
|
|
2308
|
+
await executeComments(feed, postId, options);
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
// src/commands/profile.ts
|
|
2313
|
+
init_utils();
|
|
2314
|
+
async function executeProfile(address, options) {
|
|
2315
|
+
if (!address.startsWith("0x") || address.length !== 42) {
|
|
2316
|
+
exitWithError("Invalid address format. Must be 0x-prefixed, 42 characters");
|
|
2317
|
+
}
|
|
2318
|
+
const readOnlyOptions = parseReadOnlyOptions({
|
|
2319
|
+
chainId: options.chainId,
|
|
2320
|
+
rpcUrl: options.rpcUrl
|
|
2321
|
+
});
|
|
2322
|
+
const client = createNetClient(readOnlyOptions);
|
|
2323
|
+
const limit = options.limit ?? 20;
|
|
2324
|
+
try {
|
|
2325
|
+
const count = await client.getMessageCount({
|
|
2326
|
+
filter: {
|
|
2327
|
+
appAddress: NULL_ADDRESS,
|
|
2328
|
+
maker: address
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
if (count === 0) {
|
|
2332
|
+
if (options.json) {
|
|
2333
|
+
printJson([]);
|
|
2334
|
+
} else {
|
|
2335
|
+
console.log(chalk10.yellow(`No posts found for address ${address}`));
|
|
2336
|
+
}
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
const startIndex = count > limit ? count - limit : 0;
|
|
2340
|
+
const messages = await client.getMessages({
|
|
2341
|
+
filter: {
|
|
2342
|
+
appAddress: NULL_ADDRESS,
|
|
2343
|
+
maker: address
|
|
2344
|
+
},
|
|
2345
|
+
startIndex,
|
|
2346
|
+
endIndex: count
|
|
2347
|
+
});
|
|
2348
|
+
if (options.json) {
|
|
2349
|
+
printJson(messages.map((msg, i) => postToJson(msg, i)));
|
|
2350
|
+
} else {
|
|
2351
|
+
console.log(
|
|
2352
|
+
chalk10.white(`Found ${messages.length} post(s) by ${address}:
|
|
2353
|
+
`)
|
|
2354
|
+
);
|
|
2355
|
+
messages.forEach((msg, i) => {
|
|
2356
|
+
console.log(formatPost(msg, i, { showTopic: true }));
|
|
2357
|
+
if (i < messages.length - 1) {
|
|
2358
|
+
console.log();
|
|
2359
|
+
}
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
} catch (error) {
|
|
2363
|
+
exitWithError(
|
|
2364
|
+
`Failed to fetch profile: ${error instanceof Error ? error.message : String(error)}`
|
|
2365
|
+
);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
function registerProfileCommand(program2) {
|
|
2369
|
+
program2.command("profile <address>").description("View posts by an address").option(
|
|
2370
|
+
"--limit <n>",
|
|
2371
|
+
"Maximum number of posts to display",
|
|
2372
|
+
(value) => parseInt(value, 10)
|
|
2373
|
+
).option(
|
|
2374
|
+
"--chain-id <id>",
|
|
2375
|
+
"Chain ID (default: 8453 for Base)",
|
|
2376
|
+
(value) => parseInt(value, 10)
|
|
2377
|
+
).option("--rpc-url <url>", "Custom RPC URL").option("--json", "Output in JSON format").action(async (address, options) => {
|
|
2378
|
+
await executeProfile(address, options);
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// src/commands/register.ts
|
|
2383
|
+
init_utils();
|
|
2384
|
+
async function executeRegister(feedName, options) {
|
|
2385
|
+
if (feedName.length > 64) {
|
|
2386
|
+
exitWithError("Feed name cannot exceed 64 characters");
|
|
2387
|
+
}
|
|
2388
|
+
if (feedName.length === 0) {
|
|
2389
|
+
exitWithError("Feed name cannot be empty");
|
|
2390
|
+
}
|
|
2391
|
+
if (options.encodeOnly) {
|
|
2392
|
+
const readOnlyOptions = parseReadOnlyOptions({
|
|
2393
|
+
chainId: options.chainId,
|
|
2394
|
+
rpcUrl: options.rpcUrl
|
|
2395
|
+
});
|
|
2396
|
+
const client2 = createFeedRegistryClient(readOnlyOptions);
|
|
2397
|
+
const txConfig2 = client2.prepareRegisterFeed({ feedName });
|
|
2398
|
+
const encoded = encodeTransaction(txConfig2, readOnlyOptions.chainId);
|
|
2399
|
+
printJson(encoded);
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
const commonOptions = parseCommonOptions(
|
|
2403
|
+
{
|
|
2404
|
+
privateKey: options.privateKey,
|
|
2405
|
+
chainId: options.chainId,
|
|
2406
|
+
rpcUrl: options.rpcUrl
|
|
2407
|
+
},
|
|
2408
|
+
true
|
|
2409
|
+
// supports --encode-only
|
|
2410
|
+
);
|
|
2411
|
+
const client = createFeedRegistryClient(commonOptions);
|
|
2412
|
+
const isRegistered = await client.isFeedRegistered(feedName);
|
|
2413
|
+
if (isRegistered) {
|
|
2414
|
+
exitWithError(`Feed "${feedName}" is already registered`);
|
|
2415
|
+
}
|
|
2416
|
+
const txConfig = client.prepareRegisterFeed({ feedName });
|
|
2417
|
+
const walletClient = createWallet(
|
|
2418
|
+
commonOptions.privateKey,
|
|
2419
|
+
commonOptions.chainId,
|
|
2420
|
+
commonOptions.rpcUrl
|
|
2421
|
+
);
|
|
2422
|
+
console.log(chalk10.blue(`Registering feed "${feedName}"...`));
|
|
2423
|
+
try {
|
|
2424
|
+
const hash = await executeTransaction(walletClient, txConfig);
|
|
2425
|
+
console.log(
|
|
2426
|
+
chalk10.green(
|
|
2427
|
+
`Feed registered successfully!
|
|
2428
|
+
Transaction: ${hash}
|
|
2429
|
+
Feed: ${feedName}`
|
|
2430
|
+
)
|
|
2431
|
+
);
|
|
2432
|
+
} catch (error) {
|
|
2433
|
+
exitWithError(
|
|
2434
|
+
`Failed to register feed: ${error instanceof Error ? error.message : String(error)}`
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
function registerRegisterCommand(program2) {
|
|
2439
|
+
program2.command("register <feed-name>").description("Register a new feed").option(
|
|
2440
|
+
"--chain-id <id>",
|
|
2441
|
+
"Chain ID (default: 8453 for Base)",
|
|
2442
|
+
(value) => parseInt(value, 10)
|
|
2443
|
+
).option("--rpc-url <url>", "Custom RPC URL").option("--private-key <key>", "Private key (0x-prefixed)").option(
|
|
2444
|
+
"--encode-only",
|
|
2445
|
+
"Output transaction data as JSON instead of executing"
|
|
2446
|
+
).action(async (feedName, options) => {
|
|
2447
|
+
await executeRegister(feedName, options);
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// src/commands/post.ts
|
|
2452
|
+
init_utils();
|
|
2453
|
+
var MAX_MESSAGE_LENGTH = 4e3;
|
|
2454
|
+
async function executePost(feed, message, options) {
|
|
2455
|
+
const normalizedFeed = normalizeFeedName(feed);
|
|
2456
|
+
if (message.length === 0) {
|
|
2457
|
+
exitWithError("Message cannot be empty");
|
|
2458
|
+
}
|
|
2459
|
+
const fullMessage = options.body ? `${message}
|
|
2460
|
+
|
|
2461
|
+
${options.body}` : message;
|
|
2462
|
+
if (fullMessage.length > MAX_MESSAGE_LENGTH) {
|
|
2463
|
+
exitWithError(
|
|
2464
|
+
`Message too long (${fullMessage.length} chars). Maximum is ${MAX_MESSAGE_LENGTH} characters.`
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
if (options.encodeOnly) {
|
|
2468
|
+
const readOnlyOptions = parseReadOnlyOptions({
|
|
2469
|
+
chainId: options.chainId,
|
|
2470
|
+
rpcUrl: options.rpcUrl
|
|
2471
|
+
});
|
|
2472
|
+
const client2 = createFeedClient(readOnlyOptions);
|
|
2473
|
+
const txConfig2 = client2.preparePostToFeed({
|
|
2474
|
+
topic: normalizedFeed,
|
|
2475
|
+
text: fullMessage,
|
|
2476
|
+
data: options.data
|
|
2477
|
+
});
|
|
2478
|
+
const encoded = encodeTransaction(txConfig2, readOnlyOptions.chainId);
|
|
2479
|
+
printJson(encoded);
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
const commonOptions = parseCommonOptions(
|
|
2483
|
+
{
|
|
2484
|
+
privateKey: options.privateKey,
|
|
2485
|
+
chainId: options.chainId,
|
|
2486
|
+
rpcUrl: options.rpcUrl
|
|
2487
|
+
},
|
|
2488
|
+
true
|
|
2489
|
+
// supports --encode-only
|
|
2490
|
+
);
|
|
2491
|
+
const client = createFeedClient(commonOptions);
|
|
2492
|
+
const txConfig = client.preparePostToFeed({
|
|
2493
|
+
topic: normalizedFeed,
|
|
2494
|
+
text: fullMessage,
|
|
2495
|
+
data: options.data
|
|
2496
|
+
});
|
|
2497
|
+
const walletClient = createWallet(
|
|
2498
|
+
commonOptions.privateKey,
|
|
2499
|
+
commonOptions.chainId,
|
|
2500
|
+
commonOptions.rpcUrl
|
|
2501
|
+
);
|
|
2502
|
+
console.log(chalk10.blue(`Posting to feed "${normalizedFeed}"...`));
|
|
2503
|
+
try {
|
|
2504
|
+
const hash = await executeTransaction(walletClient, txConfig);
|
|
2505
|
+
const displayText = options.body ? `${message} (+ body)` : message;
|
|
2506
|
+
console.log(
|
|
2507
|
+
chalk10.green(
|
|
2508
|
+
`Message posted successfully!
|
|
2509
|
+
Transaction: ${hash}
|
|
2510
|
+
Feed: ${normalizedFeed}
|
|
2511
|
+
Text: ${displayText}`
|
|
2512
|
+
)
|
|
2513
|
+
);
|
|
2514
|
+
} catch (error) {
|
|
2515
|
+
exitWithError(
|
|
2516
|
+
`Failed to post message: ${error instanceof Error ? error.message : String(error)}`
|
|
2517
|
+
);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
function registerPostCommand(program2) {
|
|
2521
|
+
program2.command("post <feed> <message>").description("Post a message to a feed").option(
|
|
2522
|
+
"--chain-id <id>",
|
|
2523
|
+
"Chain ID (default: 8453 for Base)",
|
|
2524
|
+
(value) => parseInt(value, 10)
|
|
2525
|
+
).option("--rpc-url <url>", "Custom RPC URL").option("--private-key <key>", "Private key (0x-prefixed)").option(
|
|
2526
|
+
"--encode-only",
|
|
2527
|
+
"Output transaction data as JSON instead of executing"
|
|
2528
|
+
).option("--data <data>", "Optional data to attach to the post").option("--body <text>", "Post body (message becomes the title)").action(async (feed, message, options) => {
|
|
2529
|
+
await executePost(feed, message, options);
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// src/commands/comment.ts
|
|
2534
|
+
init_utils();
|
|
2535
|
+
var MAX_MESSAGE_LENGTH2 = 4e3;
|
|
2536
|
+
async function executeComment(feed, postId, message, options) {
|
|
2537
|
+
const normalizedFeed = normalizeFeedName(feed);
|
|
2538
|
+
if (message.length === 0) {
|
|
2539
|
+
exitWithError("Comment cannot be empty");
|
|
2540
|
+
}
|
|
2541
|
+
if (message.length > MAX_MESSAGE_LENGTH2) {
|
|
2542
|
+
exitWithError(
|
|
2543
|
+
`Comment too long (${message.length} chars). Maximum is ${MAX_MESSAGE_LENGTH2} characters.`
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
let parsedId;
|
|
2547
|
+
try {
|
|
2548
|
+
parsedId = parsePostId(postId);
|
|
2549
|
+
} catch (error) {
|
|
2550
|
+
exitWithError(
|
|
2551
|
+
error instanceof Error ? error.message : "Invalid post ID format"
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
const readOnlyOptions = parseReadOnlyOptions({
|
|
2555
|
+
chainId: options.chainId,
|
|
2556
|
+
rpcUrl: options.rpcUrl
|
|
2557
|
+
});
|
|
2558
|
+
const client = createFeedClient(readOnlyOptions);
|
|
2559
|
+
const count = await client.getFeedPostCount(normalizedFeed);
|
|
2560
|
+
if (count === 0) {
|
|
2561
|
+
exitWithError(
|
|
2562
|
+
`Feed "${normalizedFeed}" has no posts. Cannot find post ${postId}.`
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
const posts = await client.getFeedPosts({
|
|
2566
|
+
topic: normalizedFeed,
|
|
2567
|
+
maxPosts: 100
|
|
2568
|
+
// Fetch enough to find the post
|
|
2569
|
+
});
|
|
2570
|
+
const targetPost = findPostByParsedId(posts, parsedId);
|
|
2571
|
+
if (!targetPost) {
|
|
2572
|
+
exitWithError(
|
|
2573
|
+
`Post not found with ID ${postId} in feed "${normalizedFeed}". Make sure the sender and timestamp are correct.`
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
const txConfig = client.prepareComment({
|
|
2577
|
+
post: targetPost,
|
|
2578
|
+
text: message
|
|
2579
|
+
});
|
|
2580
|
+
if (options.encodeOnly) {
|
|
2581
|
+
const encoded = encodeTransaction(txConfig, readOnlyOptions.chainId);
|
|
2582
|
+
printJson(encoded);
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
const commonOptions = parseCommonOptions(
|
|
2586
|
+
{
|
|
2587
|
+
privateKey: options.privateKey,
|
|
2588
|
+
chainId: options.chainId,
|
|
2589
|
+
rpcUrl: options.rpcUrl
|
|
2590
|
+
},
|
|
2591
|
+
true
|
|
2592
|
+
// supports --encode-only
|
|
2593
|
+
);
|
|
2594
|
+
const walletClient = createWallet(
|
|
2595
|
+
commonOptions.privateKey,
|
|
2596
|
+
commonOptions.chainId,
|
|
2597
|
+
commonOptions.rpcUrl
|
|
2598
|
+
);
|
|
2599
|
+
console.log(chalk10.blue(`Commenting on post ${postId}...`));
|
|
2600
|
+
try {
|
|
2601
|
+
const hash = await executeTransaction(walletClient, txConfig);
|
|
2602
|
+
console.log(
|
|
2603
|
+
chalk10.green(
|
|
2604
|
+
`Comment posted successfully!
|
|
2605
|
+
Transaction: ${hash}
|
|
2606
|
+
Post: ${postId}
|
|
2607
|
+
Comment: ${message}`
|
|
2608
|
+
)
|
|
2609
|
+
);
|
|
2610
|
+
} catch (error) {
|
|
2611
|
+
exitWithError(
|
|
2612
|
+
`Failed to post comment: ${error instanceof Error ? error.message : String(error)}`
|
|
2613
|
+
);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
function registerCommentCommand(program2) {
|
|
2617
|
+
program2.command("comment <feed> <post-id> <message>").description(
|
|
2618
|
+
"Comment on a post. Post ID format: {sender}:{timestamp}"
|
|
2619
|
+
).option(
|
|
2620
|
+
"--chain-id <id>",
|
|
2621
|
+
"Chain ID (default: 8453 for Base)",
|
|
2622
|
+
(value) => parseInt(value, 10)
|
|
2623
|
+
).option("--rpc-url <url>", "Custom RPC URL").option("--private-key <key>", "Private key (0x-prefixed)").option(
|
|
2624
|
+
"--encode-only",
|
|
2625
|
+
"Output transaction data as JSON instead of executing"
|
|
2626
|
+
).action(async (feed, postId, message, options) => {
|
|
2627
|
+
await executeComment(feed, postId, message, options);
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
// src/commands/config.ts
|
|
2632
|
+
init_utils();
|
|
2633
|
+
async function confirm(message) {
|
|
2634
|
+
const rl = readline.createInterface({
|
|
2635
|
+
input: process.stdin,
|
|
2636
|
+
output: process.stdout
|
|
2637
|
+
});
|
|
2638
|
+
return new Promise((resolve) => {
|
|
2639
|
+
rl.question(`${message} (y/N): `, (answer) => {
|
|
2640
|
+
rl.close();
|
|
2641
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
2642
|
+
});
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
async function executeConfig(options) {
|
|
2646
|
+
if (options.reset) {
|
|
2647
|
+
const statePath = getStateFilePath();
|
|
2648
|
+
console.log(chalk10.yellow(`This will delete all stored state at:`));
|
|
2649
|
+
console.log(chalk10.white(` ${statePath}`));
|
|
2650
|
+
console.log(chalk10.yellow(`
|
|
2651
|
+
This includes:`));
|
|
2652
|
+
console.log(chalk10.white(` - All "last seen" timestamps for feeds`));
|
|
2653
|
+
console.log(chalk10.white(` - Your configured address`));
|
|
2654
|
+
if (!options.force) {
|
|
2655
|
+
const confirmed = await confirm(chalk10.red("\nAre you sure you want to reset?"));
|
|
2656
|
+
if (!confirmed) {
|
|
2657
|
+
console.log(chalk10.gray("Cancelled."));
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
resetState();
|
|
2662
|
+
console.log(chalk10.green("State reset successfully."));
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
if (options.myAddress) {
|
|
2666
|
+
if (!options.myAddress.match(/^0x[a-fA-F0-9]{40}$/)) {
|
|
2667
|
+
console.error(chalk10.red("Invalid address format. Expected 0x followed by 40 hex characters."));
|
|
2668
|
+
process.exit(1);
|
|
2669
|
+
}
|
|
2670
|
+
setMyAddress(options.myAddress);
|
|
2671
|
+
console.log(chalk10.green(`Set my address to: ${options.myAddress}`));
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
if (options.clearAddress) {
|
|
2675
|
+
clearMyAddress();
|
|
2676
|
+
console.log(chalk10.green("Cleared my address."));
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
const state = getFullState();
|
|
2680
|
+
const myAddress = getMyAddress();
|
|
2681
|
+
console.log(chalk10.cyan("Botchan Configuration\n"));
|
|
2682
|
+
console.log(chalk10.white(`State file: ${getStateFilePath()}`));
|
|
2683
|
+
console.log(chalk10.white(`My address: ${myAddress ?? chalk10.gray("(not set)")}`));
|
|
2684
|
+
const feedCount = Object.keys(state.feeds).length;
|
|
2685
|
+
console.log(chalk10.white(`Tracked feeds: ${feedCount}`));
|
|
2686
|
+
if (feedCount > 0 && feedCount <= 20) {
|
|
2687
|
+
console.log(chalk10.gray("\nLast seen timestamps:"));
|
|
2688
|
+
for (const [feed, data] of Object.entries(state.feeds)) {
|
|
2689
|
+
const date = new Date(data.lastSeenTimestamp * 1e3);
|
|
2690
|
+
console.log(chalk10.gray(` ${feed}: ${date.toLocaleString()}`));
|
|
2691
|
+
}
|
|
2692
|
+
} else if (feedCount > 20) {
|
|
2693
|
+
console.log(chalk10.gray(`
|
|
2694
|
+
(${feedCount} feeds tracked, use --json for full list)`));
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
function registerConfigCommand(program2) {
|
|
2698
|
+
program2.command("config").description("View or modify botchan configuration").option("--my-address <address>", "Set your address (to filter out own posts with --unseen)").option("--clear-address", "Clear your configured address").option("--show", "Show current configuration (default)").option("--reset", "Reset all state (clears last-seen timestamps and address)").option("--force", "Skip confirmation prompt for --reset").action(async (options) => {
|
|
2699
|
+
await executeConfig(options);
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// src/cli/index.ts
|
|
2704
|
+
var require2 = createRequire(import.meta.url);
|
|
2705
|
+
var { version } = require2("../../package.json");
|
|
2706
|
+
var program = new Command();
|
|
2707
|
+
program.name("botchan").description(
|
|
2708
|
+
"CLI tool for AI agents and humans to interact with topic-based message feeds on Net Protocol"
|
|
2709
|
+
).version(version);
|
|
2710
|
+
registerFeedsCommand(program);
|
|
2711
|
+
registerReadCommand(program);
|
|
2712
|
+
registerCommentsCommand(program);
|
|
2713
|
+
registerProfileCommand(program);
|
|
2714
|
+
registerRegisterCommand(program);
|
|
2715
|
+
registerPostCommand(program);
|
|
2716
|
+
registerCommentCommand(program);
|
|
2717
|
+
registerConfigCommand(program);
|
|
2718
|
+
program.command("explore", { isDefault: true }).description("Launch interactive feed explorer (TUI)").option(
|
|
2719
|
+
"--chain-id <id>",
|
|
2720
|
+
"Chain ID (default: 8453 for Base)",
|
|
2721
|
+
(value) => parseInt(value, 10)
|
|
2722
|
+
).option("--rpc-url <url>", "Custom RPC URL").action(async (options) => {
|
|
2723
|
+
const { launchTui: launchTui2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
|
|
2724
|
+
await launchTui2(options);
|
|
2725
|
+
});
|
|
2726
|
+
program.parse();
|
|
2727
|
+
//# sourceMappingURL=index.mjs.map
|
|
2728
|
+
//# sourceMappingURL=index.mjs.map
|