From 5a8e4694f2546ca1065a4c7d8ff05a06708c4486 Mon Sep 17 00:00:00 2001 From: astitva Date: Fri, 3 Jul 2026 01:17:41 +0530 Subject: [PATCH 1/3] refactor: extract relativeTime into shared agentworld util --- app/src/agentworld/pages/FeedSection.tsx | 354 +++++++----- app/src/agentworld/pages/JobsSection.tsx | 546 +++++++++++------- app/src/agentworld/pages/LedgerSection.tsx | 186 +++--- app/src/agentworld/utils/relativeTime.test.ts | 32 + app/src/agentworld/utils/relativeTime.ts | 10 + 5 files changed, 712 insertions(+), 416 deletions(-) create mode 100644 app/src/agentworld/utils/relativeTime.test.ts create mode 100644 app/src/agentworld/utils/relativeTime.ts diff --git a/app/src/agentworld/pages/FeedSection.tsx b/app/src/agentworld/pages/FeedSection.tsx index 63bd633793..db8aaff276 100644 --- a/app/src/agentworld/pages/FeedSection.tsx +++ b/app/src/agentworld/pages/FeedSection.tsx @@ -17,32 +17,32 @@ * Pattern mirrors ExploreSection / MarketplaceSection: useState + useEffect * fetch, PanelScaffold wrapper, StatusBlock for loading/error/empty states. */ -import debug from 'debug'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import debug from "debug"; +import { useCallback, useEffect, useRef, useState } from "react"; -import PanelScaffold from '../../components/layout/PanelScaffold'; -import Button from '../../components/ui/Button'; +import PanelScaffold from "../../components/layout/PanelScaffold"; +import Button from "../../components/ui/Button"; import { type GqlComment, type GqlHomeFeedItem, type GqlPost, type LikeResult, PaymentRequiredError, -} from '../../lib/agentworld/invokeApiClient'; -import { fetchWalletStatus } from '../../services/walletApi'; -import { apiClient } from '../AgentWorldShell'; -import ConfirmDialog from '../components/ConfirmDialog'; - -const log = debug('agentworld:feed'); +} from "../../lib/agentworld/invokeApiClient"; +import { fetchWalletStatus } from "../../services/walletApi"; +import { apiClient } from "../AgentWorldShell"; +import ConfirmDialog from "../components/ConfirmDialog"; +import { relativeTime } from "../utils/relativeTime"; +const log = debug("agentworld:feed"); // ── State types ─────────────────────────────────────────────────────────────── type FeedState = - | { status: 'loading' } - | { status: 'wallet_unconfigured' } - | { status: 'payment_required'; challenge: unknown } - | { status: 'error'; message: string } - | { status: 'ok'; items: GqlHomeFeedItem[] }; + | { status: "loading" } + | { status: "wallet_unconfigured" } + | { status: "payment_required"; challenge: unknown } + | { status: "error"; message: string } + | { status: "ok"; items: GqlHomeFeedItem[] }; /** * Result of resolving the local wallet on mount. @@ -60,27 +60,19 @@ type FeedState = * * `agentId` is the resolved Solana address when one exists, else `null`. */ -type WalletConfigured = 'resolving' | 'no' | 'yes' | 'unknown'; -type WalletResolution = { agentId: string | null; configured: WalletConfigured }; +type WalletConfigured = "resolving" | "no" | "yes" | "unknown"; +type WalletResolution = { + agentId: string | null; + configured: WalletConfigured; +}; // ── Helpers ─────────────────────────────────────────────────────────────────── -function relativeTime(iso: string): string { - const ms = Date.now() - new Date(iso).getTime(); - const mins = Math.floor(ms / 60000); - if (mins < 1) return 'just now'; - if (mins < 60) return `${mins}m ago`; - const hrs = Math.floor(mins / 60); - if (hrs < 24) return `${hrs}h ago`; - const days = Math.floor(hrs / 24); - return `${days}d ago`; -} - function isWalletLocked(message: string): boolean { return ( - message.includes('wallet is not configured') || - message.includes('wallet secret material is missing') || - message.includes('no signer configured') + message.includes("wallet is not configured") || + message.includes("wallet secret material is missing") || + message.includes("no signer configured") ); } @@ -89,14 +81,21 @@ function postCreatedAtMillis(item: GqlHomeFeedItem): number { return Number.isFinite(millis) ? millis : 0; } -function sortedHomeFeedItems(result: { items?: GqlHomeFeedItem[] } | null | undefined) { +function sortedHomeFeedItems( + result: { items?: GqlHomeFeedItem[] } | null | undefined, +) { const items = Array.isArray(result?.items) ? [...result.items] : []; - const originalOrder = items.map(item => item.post.postId).join('\0'); + const originalOrder = items.map((item) => item.post.postId).join("\0"); - items.sort((left, right) => postCreatedAtMillis(right) - postCreatedAtMillis(left)); + items.sort( + (left, right) => postCreatedAtMillis(right) - postCreatedAtMillis(left), + ); - if (items.length > 1 && originalOrder !== items.map(item => item.post.postId).join('\0')) { - log('sorted home feed newest-first', { + if ( + items.length > 1 && + originalOrder !== items.map((item) => item.post.postId).join("\0") + ) { + log("sorted home feed newest-first", { count: items.length, newestCreatedAt: items[0]?.post.createdAt, oldestCreatedAt: items.at(-1)?.post.createdAt, @@ -107,7 +106,15 @@ function sortedHomeFeedItems(result: { items?: GqlHomeFeedItem[] } | null | unde } /** Centered status message for loading / error / info states. */ -function StatusBlock({ tone, title, body }: { tone: string; title: string; body?: string }) { +function StatusBlock({ + tone, + title, + body, +}: { + tone: string; + title: string; + body?: string; +}) { return (

{title}

@@ -118,7 +125,7 @@ function StatusBlock({ tone, title, body }: { tone: string; title: string; body? /** Initial letter avatar circle for when no avatarUrl is available. */ function InitialAvatar({ name }: { name: string }) { - const initial = (name[0] ?? '?').toUpperCase(); + const initial = (name[0] ?? "?").toUpperCase(); return (
{initial} @@ -145,16 +152,21 @@ function InitialAvatar({ name }: { name: string }) { function useWalletResolution(): WalletResolution { const [resolution, setResolution] = useState({ agentId: null, - configured: 'resolving', + configured: "resolving", }); useEffect(() => { let cancelled = false; void fetchWalletStatus() - .then(status => { + .then((status) => { if (cancelled) return; - const solana = (status.accounts ?? []).find(a => a.chain === 'solana'); + const solana = (status.accounts ?? []).find( + (a) => a.chain === "solana", + ); const address = solana?.address ?? null; - setResolution({ agentId: address, configured: address !== null ? 'yes' : 'no' }); + setResolution({ + agentId: address, + configured: address !== null ? "yes" : "no", + }); }) .catch(() => { // Transport/RPC failure: we can't prove the wallet is absent, so mark @@ -162,7 +174,7 @@ function useWalletResolution(): WalletResolution { // handles any wallet-locked error rather than us showing a false // "not configured" state for a wallet that may well exist. if (cancelled) return; - setResolution({ agentId: null, configured: 'unknown' }); + setResolution({ agentId: null, configured: "unknown" }); }); return () => { cancelled = true; @@ -182,7 +194,7 @@ function CommentComposer({ postId: string; onCommentAdded: () => void; }) { - const [body, setBody] = useState(''); + const [body, setBody] = useState(""); const [submitting, setSubmitting] = useState(false); const handleSubmit = async () => { @@ -190,10 +202,10 @@ function CommentComposer({ setSubmitting(true); try { await apiClient.feeds.addComment(handle, postId, body.trim()); - setBody(''); + setBody(""); onCommentAdded(); } catch (err) { - console.error('[FeedSection] add comment failed:', err); + console.error("[FeedSection] add comment failed:", err); } finally { setSubmitting(false); } @@ -204,9 +216,9 @@ function CommentComposer({ setBody(e.target.value)} - onKeyDown={e => { - if (e.key === 'Enter') void handleSubmit(); + onChange={(e) => setBody(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") void handleSubmit(); }} placeholder="Write a comment..." disabled={submitting} @@ -219,8 +231,9 @@ function CommentComposer({ variant="primary" size="md" onClick={() => void handleSubmit()} - disabled={!body.trim() || submitting}> - {submitting ? 'Posting...' : 'Comment'} + disabled={!body.trim() || submitting} + > + {submitting ? "Posting..." : "Comment"}
); @@ -242,7 +255,7 @@ interface FeedComposerProps { } function FeedComposer({ myAgentId, onPostCreated }: FeedComposerProps) { - const [draft, setDraft] = useState(''); + const [draft, setDraft] = useState(""); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const textareaRef = useRef(null); @@ -253,7 +266,7 @@ function FeedComposer({ myAgentId, onPostCreated }: FeedComposerProps) { // Auto-grow the textarea with its content (capped), so the composer expands // naturally instead of scrolling inside two fixed rows. const autoSize = (el: HTMLTextAreaElement) => { - el.style.height = 'auto'; + el.style.height = "auto"; el.style.height = `${Math.min(el.scrollHeight, 200)}px`; }; @@ -264,9 +277,9 @@ function FeedComposer({ myAgentId, onPostCreated }: FeedComposerProps) { setError(null); try { await apiClient.feeds.createPost(body); - setDraft(''); + setDraft(""); if (textareaRef.current) { - textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = "auto"; } onPostCreated(); } catch (err) { @@ -283,13 +296,13 @@ function FeedComposer({ myAgentId, onPostCreated }: FeedComposerProps) {