profile: add gated public profile controls and post expose actions#5501
profile: add gated public profile controls and post expose actions#5501jamesacklin wants to merge 26 commits intodevelopfrom
Conversation
|
prob want to reveal the public link when they turn on the public profile - or just keep it persistent below so they always see it - i like the latter |
Done and updated the screenshot in the description. The IP address will be replaced by the user's real ship URL. |
|
Functionality looks good in testing (iOS and web). Still reviewing the code... |
latter-bolden
left a comment
There was a problem hiding this comment.
This looks awesome! I don't have any feedback on the UI code, but we should clean things up at the api, db, and sync layers before merging.
Shows a tappable "Publicly viewable" indicator (eye icon) in the ChatMessageReplySummary addendum for any top-level channel post that has been exposed on the user's public profile. Tapping opens the public expose URL in the browser. - Add getCurrentShipUrl() to urbit API config - Add getExposedPostCitesNormalized() to profileApi (single self-contact scry returns all exposed cite paths at once) - Add useExposedPostCites() React Query hook (5min stale time, shared across all message instances) - ChatMessage derives isExposed and publicPostUrl, passes to ChatMessageReplySummary - ChatMessageReplySummary renders EyeOpen icon + "Publicly viewable" pressable when showPubliclyViewable=true Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes: 1. Invalidate ['exposedPostCites'] React Query cache immediately after setPublicProfilePostShown succeeds, so the byline appears in the chat without waiting for the 5min stale time to expire. 2. Expand each expose cite to all path format variants (UD, numeric, normalized) when building the Set in getExposedPostCitesNormalized, matching the same variant logic used by getPublicProfilePostShown. This prevents mismatches when %expose stores IDs in UD dot-separated format while the lookup uses dots-removed numeric format. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show the eye icon + "Publicly viewable" pressable in both the notebook index (list) view and the notebook detail view for posts that have been exposed on the user's public profile. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show the eye icon + "Publicly viewable" pressable in both the gallery index (card footer) view and the gallery detail view for posts that have been exposed on the user's public profile. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a post is exposed or hidden, show a toast message outside the modal context and copy the public URL to clipboard on expose. Fixes ship URL being empty on web by falling back to window.location.origin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the public profile toggle is enabled, shows a read-only URL field with a Copy button inside the same bordered box, followed by the widget toggles. All controls are now consolidated in a single bordered section. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consolidates the expose reference path, isExposed, and publicPostUrl computations from four separate components into a single reusable hook. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Relocate usePostExposeState hook from ui/hooks/ to hooks/ (correct location) - Move hook call from ChatMessage to BaseScrollerItem in Scroller so expose state is computed once per item and passed down as props - Add isExposed/publicPostUrl to RenderItemProps in componentsKits - Accept isExposed/publicPostUrl as props in ChatMessage (with safe defaults) - Remove usePostExposeState hook call from ChatMessage internals - Add isExposed to ChatMessage memo comparison so memoization works correctly for messages whose expose state didn't change This aligns with the reviewer's suggestion to pull the hook up higher in the tree to gel with the input memoization, and ensures that when expose cites change only the specific ChatMessage whose isExposed prop changed triggers a full re-render of the component body. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add exposedCites column to contacts table (JSON-encoded string[]) - Add getContactExposedCites read query backed by contacts table - Add syncExposedCites() to sync.ts; called from syncContacts - Update useExposedPostCites in dbHooks to read from DB via React Query - Replace queryClient.invalidateQueries with store.syncExposedCites() in MessageActions after expose/unexpose to trigger DB sync This addresses the reviewer's concern that expose cites data should be read from the DB rather than fetched live, and ensures React Query can cache and deduplicate the subscription across all subscribers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ents - In syncContacts, fetch expose cites via getExposedPostCitesNormalized in parallel with getContacts() rather than sequentially inside the writer. Both ultimately read from %contacts so there is no extra agent round-trip, and the parallel fetch removes the sequential waterfall latency. - In handleContactUpdate, re-sync expose cites when a self-contact update event arrives. %expose writes expose-cites back to %contacts on each show/hide poke, which triggers a /v1/news subscription event, so this keeps the DB current reactively without any additional polling. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
f1fa0ba to
d8c320b
Compare
* Fix expose toggle payload and toast behavior * Switch app imports to @tloncorp/api * Add forward migration for exposed posts table * Fix shared tests for parallel contact scries --------- Co-authored-by: James Acklin <jamesacklin@Jamess-Mac-mini.local>
|
@latter-bolden should be g2g for a re-review |
packages/app/ui/components/DetailPostUsingContentConfiguration/contextmenu.tsx
Outdated
Show resolved
Hide resolved
packages/app/ui/components/ChatMessage/ChatMessageActions/MessageActions.tsx
Outdated
Show resolved
Hide resolved
- Extract getExposeReferencePath and useHandleExposeSuccess to shared postUtils - Replace imperative useEffect in MessageActions with usePostExposeState hook + React Query caching - Simplify try/catch towers in profileApi with tryScryPaths helper - Revert Hoon v3 downgrade back to v4 in channel-utils Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Unify duplicate helpers and optimize expose caching
The Clipboard import in postUtils.tsx broke postUtils.test.ts in CI because react-native modules can't load in the Vitest environment. Moved getExposeReferencePath and useHandleExposeSuccess to exposeUtils.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
patosullivan
left a comment
There was a problem hiding this comment.
Looks good overall, one question though.
| export function getExposeReferencePath(post: db.Post) { | ||
| const [kind, host, channelName] = post.channelId.split('/'); | ||
| const postId = post.id.replaceAll('.', ''); | ||
| return `/1/chan/${kind}/${host}/${channelName}/msg/${postId}`; |
There was a problem hiding this comment.
This only works for chat posts (msg, no curio or note), right?
There was a problem hiding this comment.
Ah, good catch - will address
Summary
This PR adds public profile controls to the profile editor and wires them to
%profilevia pokes/scries. It also adds a message action for exposing individual posts to the public profile via the%exposeagent, shows a "Publicly viewable" indicator on exposed posts in chat, notebook, and gallery views, and shows a toast notification with clipboard copy of the public URL on expose/hide. All controls are gated behind a Tlon-only experimental feature flag.Fixes TLON-5380
Changes
Added
packages/api/src/client/profileApi.ts(exported from@tloncorp/api):getPublicProfileEnabled/setPublicProfileEnabled— bind/unbind the public route via the%profileapp; on disable, clears all%exposecites viaclearPublicExposeCitesgetPublicProfileLayout/setPublicProfileWidgetEnabled— scry and poke layout widgetsgetPublicProfilePostShown/setPublicProfilePostShown— check and toggle per-post expose state via%exposegetExposedPostCitesNormalized— fetches all exposed cite paths from the self-contact in one scry, expanding each to all path variants (UD, numeric, normalized)Added
contact_exposed_poststable to the DB schema with relations tocontactsandposts:getContactExposedCites/getContactExposedPostIds— read queries returning a set of exposed reference paths / post IDs for a given contactsetContactExposedCites— write query that replaces all exposed cites for a contact in a single transactionExpose cites are now DB-backed, not live-scried per post render:
syncContactsfetches expose cites in parallel withgetContacts()viaPromise.all(no sequential waterfall)handleContactUpdatere-syncs expose cites whenever the current user's self-contact changes via the/v1/newssubscription (these events fire because%exposewritesexpose-citesback to the%contactsprofile on every show/hide poke)useExposedPostCitesanduseExposedPostIdsindbHooks.tsnow read from the DB instead of scrying on every renderExtracted
usePostExposeStatehook (packages/app/hooks/usePostExposeState.ts) that derives expose state and public URL from the DB-backed query results — used byBaseScrollerItem,NotebookPost, andGalleryPostMoved expose state computation out of
ChatMessageand up toBaseScrollerIteminScroller.tsx:isExposedandpublicPostUrlare now passed as props to the generic render componentisExposedto thememo()comparator so only messages whose expose state changed re-renderImplemented public profile controls in
packages/app/ui/components/EditProfileScreenView.tsx:/profileon/off with a persistent public URL field belowAdded
PublicProfileExposeActiontoChatMessageActions/MessageActions.tsx:onExposeSuccess(message, url)which copies the public URL to clipboard and shows a toast: "Post published; URL copied"onExposeSuccess(message)which shows a toast: "Post hidden from public profile"Toast + clipboard handled outside the React Native
<Modal>context:onExposeSuccesscallback is threaded fromScroller.tsx,ChatMessage.tsx, andcontextmenu.tsxdown throughChatMessageActionsProps→MessageActions→PublicProfileExposeActionuseToastandClipboard.setStringare called at the parent level (outside the modal) where React context is accessibleAdded "Publicly viewable" byline to exposed posts across all post types:
{shipUrl}/expose/1/chan/...)ChatMessageReplySummary): appears below any top-level exposed post in the message addendum sectionNotebookPost): appears in the reply summary footer on the index view and below the header divider in the detail viewGalleryPost): appears in the card footer on the index view and below the author row in the detail viewgetCurrentShipUrl()inpackages/api/src/client/urbit.tsfalls back towindow.location.originon web (whereconfig.shipUrlis intentionally empty)Added
publicProfileControlsfeature flag inpackages/app/lib/featureFlags.ts(onlyTlon: true, default off)Fixed expose toggle poke payload and toast behavior; switched
packages/appimports from@tloncorp/shared/apito@tloncorp/apiAdded forward DB migration for the
contact_exposed_poststableFixed shared tests for parallel contact scries
How did I test?
Public profile page:
Post expose action:
Toast and clipboard:
https://yourship.tlon.network/expose/1/chan/...)Publicly viewable byline:
{shipUrl}/expose/1/chan/...URLRisks and impact
%profile/%exposepoke/scry integrationRollback plan
EditProfileScreenViewno longer renders public profile controls.MessageActionsno longer renders the expose action or callsonExposeSuccess.ChatMessageReplySummary,NotebookPost, andGalleryPostno longer render the "Publicly viewable" byline.packages/api/src/client/profileApi.tsexports/usages are removed by revert.contact_exposed_postsDB table and related queries are removed by revert.Screenshots / videos
Exposé experience:
Screen.Recording.2026-02-25.at.3.35.36.PM.mov
Profile controls: