feat(tinyplace): resolve feed post media as web-only (#4924) (#5295)

This commit is contained in:
CodeGhost21
2026-07-31 02:03:44 +05:30
committed by GitHub
parent bb8383682e
commit 4ecc21290d
5 changed files with 188 additions and 0 deletions
@@ -16,6 +16,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
import { PaymentRequiredError } from '../../lib/agentworld/invokeApiClient';
import { fetchWalletStatus } from '../../services/walletApi';
import { openUrl } from '../../utils/openUrl';
import { apiClient } from '../AgentWorldShell';
import FeedSection, { FEED_PAGE_SIZE } from './FeedSection';
@@ -63,6 +64,10 @@ vi.mock('../hooks/useTinyplaceStream', () => ({
vi.mock('../../services/walletApi', () => ({ fetchWalletStatus: vi.fn() }));
// External-link opener — assert the composer's media CTA hands off to the OS
// browser without actually invoking Tauri. Returns a Promise (the CTA voids it).
vi.mock('../../utils/openUrl', () => ({ openUrl: vi.fn(() => Promise.resolve()) }));
// ── Sample data (generic placeholders) ───────────────────────────────────────
const MY_AGENT_ID = 'my-addr';
@@ -665,6 +670,29 @@ describe('post composer', () => {
});
expect(screen.queryByPlaceholderText(/what's on your mind/i)).not.toBeInTheDocument();
});
// Post media is web-only (#4924): the tiny.place backend serves no post-media
// field, so instead of an in-app upload the composer points users to the web
// app. Unit coverage only by design — the one real cross-process effect (the OS
// browser opening) rides on the mocked `openUrl` and isn't worth a WDIO E2E.
test('renders the web-only "add media" note in the composer', async () => {
vi.mocked(apiClient.graphql.homeFeed).mockResolvedValue({ items: [sampleFeedItem], count: 1 });
render(<FeedSection />);
const note = await screen.findByTestId('add-media-on-web');
expect(note).toHaveTextContent(/add photos or gifs on tiny\.place/i);
expect(screen.getByTestId('add-media-on-web-cta')).toHaveAttribute(
'data-analytics-id',
'feed.addMediaOnWeb'
);
});
test('media CTA opens the tiny.place feed via openUrl', async () => {
const user = userEvent.setup();
vi.mocked(apiClient.graphql.homeFeed).mockResolvedValue({ items: [sampleFeedItem], count: 1 });
render(<FeedSection />);
await user.click(await screen.findByTestId('add-media-on-web-cta'));
expect(vi.mocked(openUrl)).toHaveBeenCalledWith('https://tiny.place');
});
});
// ── Delete actions ────────────────────────────────────────────────────────────
+42
View File
@@ -14,6 +14,13 @@
* - Inline post composer at the top of the feed (refetches feed on success)
* - Delete post / delete comment (own content only, via an in-app ConfirmDialog)
*
* Post *media* (images / GIFs) is intentionally NOT composable in-app: the
* tiny.place backend serves no media field on posts (neither `PostCreate` nor
* the read-side `Post` / `GqlPost` in the vendored SDK carry one), so an
* in-app upload would round-trip to nothing and render nowhere. Attaching media
* is therefore web-only on tiny.place; the composer links users there for it.
* See #4924 (and the same web-only resolution as marketplace selling, #4920).
*
* Pattern mirrors ExploreSection / MarketplaceSection: useState + useEffect
* fetch, PanelScaffold wrapper, StatusBlock for loading/error/empty states.
*/
@@ -32,6 +39,7 @@ import {
} from '../../lib/agentworld/invokeApiClient';
import { useT } from '../../lib/i18n/I18nContext';
import { fetchWalletStatus } from '../../services/walletApi';
import { openUrl } from '../../utils/openUrl';
import { apiClient } from '../AgentWorldShell';
import ConfirmDialog from '../components/ConfirmDialog';
import StatusBlock from '../components/StatusBlock';
@@ -40,6 +48,18 @@ import { relativeTime } from './relativeTime';
const log = debug('agentworld:feed');
// Attaching images / GIFs to a post is web-only — the tiny.place backend serves
// no post-media field, so we point users at the web app for it (#4924). tiny.place's
// home page is the feed, where the media composer lives.
//
// Single hardcoded origin by design: the *API* host varies by env (api.tiny.place
// vs staging-api.tiny.place), but there is exactly one human-facing website —
// `tiny.place` — used unconditionally across the codebase for the same reason
// (post permalinks: `TINYPLACE_WEB_ORIGIN` in tinyplace/agent_tools/flows_write.rs;
// `FUND_PAGE_URL` in X402ConfirmDialog.tsx; `SELL_ON_WEB_URL` in IdentitiesSection.tsx).
// There is no per-network web frontend to derive this from.
const ADD_MEDIA_ON_WEB_URL = 'https://tiny.place';
/**
* Home-feed items fetched per page (also the initial page size). The
* `tinyplace_graphql_home_feed` RPC accepts `limit`/`offset`
@@ -354,6 +374,28 @@ function FeedComposer({ myAgentId, onPostCreated }: FeedComposerProps) {
</Button>
</div>
</div>
{/* Adding images / GIFs to a post is web-only (#4924): the tiny.place
backend serves no post-media field, so an in-app upload would render
nowhere. Point users at the web app instead of dead-ending. */}
<div
className="mt-2 flex items-center gap-1.5 pl-[2.625rem] text-[11px] text-content-faint"
data-testid="add-media-on-web">
<span>Add photos or GIFs on tiny.place.</span>
<button
type="button"
data-testid="add-media-on-web-cta"
data-analytics-id="feed.addMediaOnWeb"
className="font-medium text-primary-400 underline-offset-2 hover:underline focus:underline focus:outline-none"
onClick={() => {
// Static destination URL only — no post body / PII. openUrl owns the
// outcome diagnostics (low-PII breadcrumb + https fallback), so we
// don't re-observe success/error here.
log('[tinyplace][ui] add-media-on-web: opening %s', ADD_MEDIA_ON_WEB_URL);
void openUrl(ADD_MEDIA_ON_WEB_URL);
}}>
Open tiny.place
</button>
</div>
</div>
);
}
@@ -0,0 +1,46 @@
# Feed Post Media Web-Only Implementation Plan (#4924)
**Goal:** Replace the text-only feed composer's missing-media gap with a note
pointing users to tiny.place, and record that post media is web-only by design.
Mirrors the identity-marketplace seller-web-only resolution (#4920 / PR #5193).
**Architecture:** Additive UI only. A persistent note at the bottom of
`FeedComposer` opens `https://tiny.place` via the existing `openUrl()` helper. No
SDK, core, `manifest.rs`, or submodule changes — the tiny.place backend serves no
post-media field. Scope recorded in two doc comments.
**Tech stack:** React 19 + TypeScript, Vitest + Testing Library,
`app/src/utils/openUrl.ts`, Rust module doc comment.
## Global constraints
- **No i18n:** `FeedComposer` is hardcoded English (`"What's on your mind?"`,
`"Post"`, `"Posting…"`). New strings match — no `useT()`, no locale-file edits.
- **Web URL is a hardcoded prod constant:** `https://tiny.place` (home = feed).
Mirrors `SELL_ON_WEB_URL` / `FUND_PAGE_URL`.
- **External links go through `openUrl`** — never a raw `window.open`.
- **Out of scope (do not touch):** `vendor/tinyplace/**` (incl. the submodule
pointer), `manifest.rs`, `invokeApiClient.ts`, the compose/like/comment flows.
## Tasks
- [x] **Task 1 — Composer note + CTA.** `FeedSection.tsx`: import `openUrl`; add
`ADD_MEDIA_ON_WEB_URL`; render a note (`data-testid="add-media-on-web"`) + text
CTA (`data-testid="add-media-on-web-cta"`, `data-analytics-id="feed.addMediaOnWeb"`)
at the bottom of `FeedComposer` that `void openUrl(ADD_MEDIA_ON_WEB_URL)`. Header
docstring gains a "post media is web-only" note.
- [x] **Task 2 — Rust scope note.** `src/openhuman/tinyplace/mod.rs`: add a
"Feed scope (post media is web-only)" doc-comment section after the marketplace
one. Docs-only; `cargo fmt --check` verifies.
- [x] **Task 3 — Tests.** `FeedSection.test.tsx`: mock `openUrl`; assert the note
renders and the CTA calls `openUrl('https://tiny.place')`.
- [x] **Task 4 — GitHub bookkeeping (outward-facing).** Done: commented the
web-only resolution on #4924, and marked the #4776 §2 "Images / media in posts"
item N/A (web-only) via an epic status comment (the epic body is owner-gated).
## Verification
- `pnpm --filter openhuman-app test` — FeedSection green (incl. 2 new cases).
- `tsc --noEmit` clean; `eslint` no new errors; `prettier --check` clean.
- `cargo fmt --check` clean (doc-comment-only Rust change).
- `git diff vendor/tinyplace` empty — submodule pointer untouched (mergeable).
@@ -0,0 +1,63 @@
# Design: Feed post media is web-only (#4924)
Part of #4776 (Tiny Place audit) · §2 Feed · sibling of the #4920 web-only resolution (PR #5193).
## Problem
The audit item "Images / media in posts" fails at the **contract** level, not just
the UI. Posts are text-only end-to-end:
- The vendored `tinyplace` SDK carries no media field on the **write** side
(`PostCreate` = `{ body, content_type, post_id }`) or the **read** side
(`Post` / `GqlPost` = `body`, counts, author, timestamps — no media).
- The tiny.place **backend** serves no post-media field. Verified against
tiny.place `main` (`d2545054`, the commit openhuman already pins): the SDK
structs there still have no `image` / `media` / `gif` field.
- The desktop composer sends `{ body }` only (`FeedSection.tsx`
`feeds.createPost``manifest.rs` → SDK `feeds::create_post`).
Because `vendor/tinyplace` is a **pinned git submodule** (openhuman can only bump
the pointer to a commit that exists on the tiny.place remote), the media contract
cannot be added from this repo. A prior WIP branch pinned the submodule to a
**local-only** SDK commit to make it build — that is unmergeable (a fresh clone/CI
cannot fetch the commit) and, being SDK-only, still would not render media because
the backend serves none.
## Decision
Resolve **web-only**, mirroring the identity-marketplace seller resolution (#4920 /
PR #5193). Rather than ship an in-app upload against a non-existent contract — which
would round-trip to nothing and render nowhere — the desktop feed composer shows a
small note that attaching images/GIFs happens on tiny.place, with a CTA that opens
the web app via the existing `openUrl()` helper.
## Changes
1. **UI** (`app/src/agentworld/pages/FeedSection.tsx`) — additive only. A subtle
note + text CTA at the bottom of `FeedComposer` opens `https://tiny.place` (the
home page is the feed) through `openUrl()`. Hardcoded prod URL, matching the
`SELL_ON_WEB_URL` (IdentitiesSection) and `FUND_PAGE_URL` (X402ConfirmDialog)
precedents. Strings are hardcoded English to match the surrounding
`FeedComposer`, which uses no `useT()` (same rationale as #5193).
2. **Scope docstrings** — a "Feed scope (post media is web-only)" note in the
`FeedSection.tsx` header and in `src/openhuman/tinyplace/mod.rs`, next to the
existing marketplace web-only note.
3. **Test** (`FeedSection.test.tsx`) — two co-located Vitest cases: the note
renders in the composer, and the CTA calls `openUrl('https://tiny.place')`
(mock `openUrl`). Covers the new diff lines for the ≥80% changed-line gate.
## Out of scope (YAGNI)
- Vendored SDK media fields (`PostCreate.image` / `PostMedia` / `Post.media`).
- Core `manifest.rs` post-media handling; `invokeApiClient.ts` media params.
- Any `vendor/tinyplace` submodule pointer bump.
- Full i18n conversion of the composer (new strings are hardcoded English to match).
## Follow-up to genuinely close in-app (tiny.place-first, not this repo)
1. tiny.place backend: persist + serve a post-media field.
2. tiny.place SDK: add media to `PostCreate` + read-side `Post` / `GqlPost`; release.
3. openhuman: bump the submodule pointer, then wire handler + composer upload +
renderer. Tracked under the Tiny Place epic (#4190 / #4776).
+9
View File
@@ -34,6 +34,15 @@
//! `marketplace_list_offers` takes a `name` filter for offers targeting a handle
//! you own. The desktop Trading tab links sellers to the web app for the actions
//! it cannot perform. See #4920.
//!
//! ## Feed scope (post media is web-only)
//!
//! Feed posts here are **text-only**. Attaching images / GIFs to a post is
//! **web-only on tiny.place**: the backend serves no media field — neither the
//! write-side `PostCreate` nor the read-side `Post` / `GqlPost` in the vendored
//! SDK carries one — so an in-app upload would round-trip to nothing and render
//! nowhere. The desktop feed composer links users to the web app to attach
//! media instead of dead-ending. See #4924.
pub(crate) mod agent;
mod agent_tools;