feat(desktop): Feed + Wallet sections (v2.2.0-alpha6)
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.
This commit is contained in:
93
desktop/src/sections/feed/PostList.tsx
Normal file
93
desktop/src/sections/feed/PostList.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
// 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' });
|
||||
}
|
||||
Reference in New Issue
Block a user