Skip to content

feat: admin-customizable social share messages and share image#340

Open
tnrdd wants to merge 2 commits into
mainfrom
feat/social-share-tab
Open

feat: admin-customizable social share messages and share image#340
tnrdd wants to merge 2 commits into
mainfrom
feat/social-share-tab

Conversation

@tnrdd

@tnrdd tnrdd commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What this does

Adds a Social tab to the Flow Council admin panel (between Communications and UI) so round admins can put their own voice and @Handles into the share posts voters and donors see, replace the default link-preview image per round, and drop the Post-on-Lens option.

Config lives under Round.details JSON, so existing rounds need zero migration and unconfigured rounds keep today's behavior (minus Lens). Saving uses the same round-admin authorization as the other tabs.

Admin capabilities

  • Social accounts — named entries with per-platform X and Farcaster handles; paste a profile URL or bare handle (auto-normalized via the existing handle logic); up to 10 per round.
  • Two message templates (vote + donation) — pre-filled with today's defaults; plain-text tokens @[Account], {round name}, {round link} resolved per platform at draft time. Tokens are references, so editing a handle later updates future posts. Clearing a message reverts to the default.
  • Live per-platform counters — X 280 counting links as 23 chars; Farcaster 320 with the link travelling as an embed card. Save is blocked (client and server) while either platform is over, naming the offending one.
  • Preview on X / Preview on Farcaster — open the real compose screens with current (even unsaved) content; nothing is posted on the user's behalf.
  • Share image — overrides the OpenGraph card on the round page everywhere links unfurl (X, Farcaster, Discord, Telegram, Slack). PNG/JPEG/WebP, max 1MB, suggested 1200×630. Each upload gets a versioned URL and the previous object is deleted best-effort (never blocks saving).

Voter/donor surface

Post to X and Cast to Farcaster after voting or opening a donation stream, with the admin's message and per-platform handles. Lens is removed from both surfaces for all rounds.

Legal

  • /terms: Copyright Complaints section (report → review → remove, repeat-infringer clause) plus an explicit content-removal right.
  • /privacy: "Content you publish through the Services" disclosure. Both re-dated. (Counsel review is a separate ops step per the spec.)

Testing

  • Unit: 372 passed, 1 skipped (new socialShare, validation, images/route suites cover token resolution per platform, char counting, handle normalization, the share-image upload cap).
  • Integration: 158/158 (round-trip persistence, social-only PATCH preserving other fields, non-admin rejection, over-limit rejection naming the platform, and S3 delete call-discipline including the cross-tenant-refusal and preserve-on-omit cases).
  • typecheck / lint / prettier clean. Zero new npm dependencies.

Review + hardening

Full multi-dimension review (correctness, perf, security, design, spec-compliance). Findings fixed and re-verified by the raising reviewer:

  • Security (Critical): the share-image delete key was derived from the stored URL, allowing cross-tenant object deletion. Now scoped so deletes can only ever target share-images/<caller-address>/ keys; re-verified closed against both exploit variants.
  • Correctness (High): whitespace in account names silently broke saved mentions — tokens are now trimmed on both sides.
  • Plus 8 Medium fixes (rename-rewrite collisions, mention-insert range, preserve-if-omitted shareImageUrl, scheme-less URL pastes, upload/PATCH sequencing, zod kept out of the client bundle, object-URL memo/revoke, the Next button and disabled-state consistency).

Ops

  • S3 delete permission: done. The s3-uploads IAM user was missing s3:DeleteObject; the grant was added (scoped to flow-councils/share-images/*) and verified with a real put/delete round-trip, so replaced images are actually cleaned up.
  • Still open (non-code): counsel review of the new /terms and /privacy language, and one live end-to-end QA pass producing real X/Farcaster drafts with the image card (the compose handoff can't be exercised in the test harness).

Add a Social tab to the Flow Council admin panel (between Communications
and UI) where round admins configure share posts for voters and donors:

- Named social accounts with per-platform X/Farcaster handles (URL or bare
  handle, normalized; up to 10 per round)
- Vote and donation message templates with @[Account]/{round name}/{round
  link} tokens, resolved per platform at draft time (tokens are references)
- Live per-platform counters (X 280 with links as 23 chars; Farcaster 320
  with the link as an embed) and save blocking when over limit
- Preview on X / Cast to Farcaster buttons opening the real composers
- Per-round share image overriding the OpenGraph card everywhere links
  unfurl, with best-effort replacement of the prior object in S3
- Remove the Post on Lens share option from both surfaces
- Copyright Complaints section on /terms and a public-content disclosure
  on /privacy

Config lives under Round.details JSON (no migration); saving uses the
existing round-admin authorization.
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
platform Ready Ready Preview, Comment Jul 3, 2026 10:59am

Request Review

@gaston-review gaston-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gaston Review

Verdict: Approved

Score: ████████░░ 8/10

Pull Request Summary

This PR adds an admin-facing "Social" tab to the Flow Council launchpad, letting round operators customize post-vote and post-donation share messages (with @mention tokens, {round name}, {round link}), configure per-platform handles (X + Farcaster), and upload a custom OG share image. It removes the Lens share option, adds server-side character-limit enforcement per platform, wires up OG metadata on the round page, updates privacy/terms for the new UGC, and includes thorough integration and unit tests.

Review Summary

🟡 Warning — Double error swallowing on S3 delete hides failures
deleteObjectByPublicUrl (s3.ts:26-29) catches and logs S3 errors internally without re-throwing. The outer try/catch in route.ts:273-276 is therefore dead code — it can never fire. This means any non-S3 errors thrown between retrieving previousShareImageUrl and calling deleteObjectByPublicUrl would also be silently swallowed. Either remove the inner catch and let the outer one handle it, or remove the outer catch since the inner one already handles it. The test 'succeeds when the old-image S3 delete fails' passes only because it mocks the entire function to reject, bypassing the inner try/catch.

🔵 Suggestion — generateMetadata imports db directly from API layer
The round page (page.tsx) imports db from @/app/api/flow-council/db to power generateMetadata. This works in Next.js Server Components, but it couples the page layer directly to the API's internal database module. If the round metadata fetching logic (already implemented in fetchRoundMetadata.ts) were extended with a server-side variant, you'd avoid this cross-layer dependency. Not blocking since it works, but worth noting for future maintainability.

Solid, well-structured feature. Security considerations are handled properly — S3 deletes scoped to the caller's own prefix, admin auth reused from existing infra, server-side character-limit enforcement, Zod schema validation. Test coverage is excellent, especially the edge cases around handle normalization, image lifecycle, and cross-platform mention resolution. A few minor items below but nothing blocking.

📌 5 inline comments
🟡 Warning: 2 · 🔵 Suggestion: 2 · ⚪ Nitpick: 1


🔍 Reviewed by Gaston

Comment thread src/app/api/flow-council/s3.ts Outdated
Comment thread src/app/api/flow-council/rounds/route.ts
Comment thread src/app/flow-councils/lib/socialShare.ts
Comment thread src/app/flow-councils/social/social.tsx
Comment thread src/app/flow-councils/[chainId]/[councilId]/page.tsx
…thorized round PATCH

- s3.ts deleteObjectByPublicUrl no longer swallows errors internally; the
  rounds PATCH is the single best-effort catch site (removes the dead
  double-catch flagged in review)
- return HTTP 403 instead of 200 for the not-found/not-authorized round
  PATCH response so res.ok reflects the failure

@gaston-review gaston-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gaston Review

Verdict: Approved

Score: ████████░░ 8/10

Pull Request Summary

This PR adds an admin-facing "Social" tab for customizing post-vote and post-donation share messages with @mention tokens, per-platform handles (X + Farcaster), and a custom OG share image. It removes the Lens share option, adds server-side character-limit enforcement, wires up OG metadata on the round page, updates privacy/terms for the new UGC, and includes thorough tests. The latest commit addresses my two prior review findings: the double error-swallowing on S3 deletes (now a single try/catch in the route) and the missing 403 status on unauthorized PATCH responses.

Both fixes from my prior review are clean and correct. deleteObjectByPublicUrl now throws naturally and the route's single try/catch handles it as best-effort — exactly what I asked for. The 403 on the "not found or not authorized" response is in place. The remaining prior items (cross-layer db import, orphan-on-PATCH-failure, module-level regex) were suggestions/nitpicks and don't block. This is production-ready.


🔍 Reviewed by Gaston

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant