diff --git a/app/src/agentworld/pages/FeedSection.test.tsx b/app/src/agentworld/pages/FeedSection.test.tsx index 2a36d45a6..ba8458fcc 100644 --- a/app/src/agentworld/pages/FeedSection.test.tsx +++ b/app/src/agentworld/pages/FeedSection.test.tsx @@ -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(); + 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(); + await user.click(await screen.findByTestId('add-media-on-web-cta')); + expect(vi.mocked(openUrl)).toHaveBeenCalledWith('https://tiny.place'); + }); }); // ── Delete actions ──────────────────────────────────────────────────────────── diff --git a/app/src/agentworld/pages/FeedSection.tsx b/app/src/agentworld/pages/FeedSection.tsx index a62815c80..dec52b19d 100644 --- a/app/src/agentworld/pages/FeedSection.tsx +++ b/app/src/agentworld/pages/FeedSection.tsx @@ -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) { + {/* 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. */} +
+ Add photos or GIFs on tiny.place. + +
); } diff --git a/docs/superpowers/plans/2026-07-30-tinyplace-feed-post-media-web-only.md b/docs/superpowers/plans/2026-07-30-tinyplace-feed-post-media-web-only.md new file mode 100644 index 000000000..3e8d4a29a --- /dev/null +++ b/docs/superpowers/plans/2026-07-30-tinyplace-feed-post-media-web-only.md @@ -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). diff --git a/docs/superpowers/specs/2026-07-30-tinyplace-feed-post-media-web-only-design.md b/docs/superpowers/specs/2026-07-30-tinyplace-feed-post-media-web-only-design.md new file mode 100644 index 000000000..fc120d78c --- /dev/null +++ b/docs/superpowers/specs/2026-07-30-tinyplace-feed-post-media-web-only-design.md @@ -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). diff --git a/src/openhuman/tinyplace/mod.rs b/src/openhuman/tinyplace/mod.rs index f42c1b3b6..434ae1464 100644 --- a/src/openhuman/tinyplace/mod.rs +++ b/src/openhuman/tinyplace/mod.rs @@ -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;