Desktop client reaches full feature parity with mobile for the two
heaviest sections. Contacts + Devices screen + polish pass remain for
rc1.
Feed section (src/sections/feed/ + src/lib/feed.ts):
* Left pane — FeedTabs: For You / Following / Trending 24h + a
hashtag input that promotes to a tab on Enter; breadcrumb back-
navigation when you drill into an author wall or hashtag.
* Right pane — FeedPane: two sub-columns. Scrollable post list
(truncated body, likes/views/hashtags footer, active highlight)
+ PostDetail with full body, hashtag links (click → hashtag tab),
inline attachment image, like/unlike button, Delete (if mine).
On-mount side-effects: bumpView + fetchStats for liked-by-me.
* ComposeModal — new-post dialog. Ctrl/Cmd+N opens it; Ctrl+Enter
submits. Byte counter against 4000 limit, live hashtag preview.
Uses publishAndCommit (server-side image scrub happens when
attachments land in rc1).
* lib/feed.ts — full mirror of mobile's feed.ts:
fetchForYou/Timeline/Trending/Author/Hashtag/Post/Stats,
bumpView, like/unlike/delete/follow/unfollow, publishPost +
publishAndCommit + buildCreatePostTx. Uses window.crypto.subtle
for SHA-256 (no expo-crypto dep). Same canonical-bytes as mobile.
Wallet section (src/sections/wallet/ + new bits in src/lib/api.ts):
* WalletOverview (left): account card (balance + shortened pub +
Send/Receive/Refresh) and transaction history grouped by row.
Amount colour-codes by direction; pretty tx-type labels.
* WalletDetailPane (right): selected tx — big signed amount,
2-column key/value grid (id, from, to, amount, fee, time, block,
gas), collapsible JSON payload + payload_hex fallback. Mirror of
mobile /tx/[id] layout.
* SendModal — transfer tx with @username / DC-address / hex pub
resolution via resolveAccount. Balance + fee preview; refuses
self-transfer (would roundtrip through mempool for no reason).
* ReceiveModal — pub + Copy button. QR in rc1 once we pull in a
qrcode lib.
* lib/api.ts: TxRow + TxDetail types, getTxHistory, getTxDetail,
resolveAccount (handles hex/@username/DC-address).
Store adds feedTab + feedSelectedPost + walletSel so selection state
survives section-switches. FeedTab discriminated union covers the
hashtag + author sub-states so breadcrumbs know what to render.
Typecheck + renderer build both pass. Node API used as-is — no
server changes in this release.
94 lines
3.0 KiB
TypeScript
94 lines
3.0 KiB
TypeScript
// PostList — rows within the Feed middle column. Clicking a row sets
|
|
// the selected post in the store; the detail pane reacts.
|
|
|
|
import React from 'react';
|
|
import { useStore } from '@/lib/store';
|
|
import type { FeedPostItem } from '@/lib/feed';
|
|
import { shortAddr } from '@/lib/crypto';
|
|
|
|
interface Props {
|
|
posts: FeedPostItem[];
|
|
activeID: string | null;
|
|
}
|
|
|
|
export function PostList({ posts, activeID }: Props): React.ReactElement {
|
|
const select = useStore(s => s.setFeedSelectedPost);
|
|
return (
|
|
<div>
|
|
{posts.map(p => (
|
|
<PostRow
|
|
key={p.post_id}
|
|
post={p}
|
|
active={p.post_id === activeID}
|
|
onClick={() => select(p.post_id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PostRow({ post, active, onClick }: {
|
|
post: FeedPostItem; active: boolean; onClick: () => void;
|
|
}) {
|
|
const author = shortAddr(post.author, 6);
|
|
return (
|
|
<div
|
|
onClick={onClick}
|
|
style={{
|
|
padding: '12px 14px', borderBottom: '1px solid #1f1f1f',
|
|
cursor: 'pointer',
|
|
background: active ? '#0a1a29' : 'transparent',
|
|
}}
|
|
onMouseEnter={e => { if (!active) (e.currentTarget as HTMLDivElement).style.background = '#0a0a0a'; }}
|
|
onMouseLeave={e => { if (!active) (e.currentTarget as HTMLDivElement).style.background = 'transparent'; }}
|
|
>
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', gap: 8,
|
|
color: '#8b8b8b', fontSize: 11, marginBottom: 4,
|
|
}}>
|
|
<span style={{ fontFamily: 'monospace', color: '#d0d0d0' }}>{author}</span>
|
|
<span>·</span>
|
|
<span>{formatRelative(post.created_at)}</span>
|
|
</div>
|
|
<div className="selectable" style={{
|
|
color: '#fff', fontSize: 13, lineHeight: 1.45,
|
|
whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
|
// Visual truncate; the detail pane shows the full thing.
|
|
display: '-webkit-box',
|
|
WebkitLineClamp: 4,
|
|
WebkitBoxOrient: 'vertical',
|
|
overflow: 'hidden',
|
|
} as React.CSSProperties}>
|
|
{post.content}
|
|
</div>
|
|
{post.has_attachment && (
|
|
<div style={{ color: '#6a6a6a', fontSize: 11, marginTop: 4 }}>
|
|
🖼 attachment
|
|
</div>
|
|
)}
|
|
<div style={{
|
|
color: '#6a6a6a', fontSize: 11, marginTop: 6,
|
|
display: 'flex', gap: 12,
|
|
}}>
|
|
<span>❤ {post.likes}</span>
|
|
<span>👁 {post.views}</span>
|
|
{post.hashtags && post.hashtags.length > 0 && (
|
|
<span style={{ color: '#1d9bf0' }}>
|
|
{post.hashtags.slice(0, 3).map(t => `#${t}`).join(' ')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function formatRelative(unixSec: number): string {
|
|
const diff = Math.floor(Date.now() / 1000) - unixSec;
|
|
if (diff < 60) return `${diff}s`;
|
|
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
|
|
if (diff < 604800) return `${Math.floor(diff / 86400)}d`;
|
|
const d = new Date(unixSec * 1000);
|
|
return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
}
|