···1515import {
1616 graphemeLength,
1717 validateReplyText,
1818- bskyPostWebUrl,
1918 MAX_REPLY_GRAPHEMES,
2019 type StrongRef,
2120} from '../lib/social/records';
2121+import { atmospherePostWebUrl } from '../lib/social/atmosphere-url';
22222323export interface PostActionsProps {
2424 /** The companion `app.bsky.feed.post` AT-URI (the article's Bluesky post). */
···169169 setFlash( {
170170 kind: 'success',
171171 message: composer === 'reply' ? 'Reply posted to Bluesky.' : 'Quote posted to Bluesky.',
172172- href: bskyPostWebUrl( uri ),
172172+ href: atmospherePostWebUrl( uri ),
173173 } );
174174 refresh();
175175 } catch ( err ) {
···189189 return null;
190190 }
191191192192- const threadUrl = bskyPostWebUrl( postUri );
192192+ const threadUrl = atmospherePostWebUrl( postUri );
193193194194 // Signed-out: prompt sign-in (same OAuth flow as the editor) before any action.
195195 if ( status !== 'signed-in' || ! agent || ! did ) {
···227227 </p>
228228 ) }
229229 <a className="post-actions__thread" href={ threadUrl } target="_blank" rel="noopener noreferrer">
230230- View this post on Bluesky
230230+ View this post on the ATmosphere
231231 </a>
232232 </div>
233233 );
···323323 { flash.message }{ ' ' }
324324 { flash.href && (
325325 <a href={ flash.href } target="_blank" rel="noopener noreferrer">
326326- View on Bluesky
326326+ View on the ATmosphere
327327 </a>
328328 ) }
329329 </p>
-13
src/lib/social/records.test.ts
···66 buildQuote,
77 graphemeLength,
88 validateReplyText,
99- bskyPostWebUrl,
109 MAX_REPLY_GRAPHEMES,
1110 type StrongRef,
1211} from './records';
···9594 expect( graphemeLength( '๐จโ๐ฉโ๐งโ๐ฆ' ) ).toBe( 1 );
9695 // Flag = two regional-indicator codepoints, one grapheme.
9796 expect( graphemeLength( '๐ซ๐ท' ) ).toBe( 1 );
9898- } );
9999-} );
100100-101101-describe( 'bskyPostWebUrl', () => {
102102- it( 'maps an at:// post uri to its bsky.app profile/post URL', () => {
103103- expect( bskyPostWebUrl( SUBJECT.uri ) ).toBe(
104104- 'https://bsky.app/profile/did:plc:writer/post/3kpost'
105105- );
106106- } );
107107-108108- it( 'falls back to bsky.app for an unparseable uri', () => {
109109- expect( bskyPostWebUrl( 'not-an-at-uri' ) ).toBe( 'https://bsky.app' );
11097 } );
11198} );
11299
-10
src/lib/social/records.ts
···9696}
97979898/**
9999- * The bsky.app web URL for a post AT-URI, for "view thread on Bluesky" links.
100100- * `at://<did>/app.bsky.feed.post/<rkey>` โ `https://bsky.app/profile/<did>/post/<rkey>`.
101101- * Falls back to the bsky.app home for an unparseable URI.
102102- */
103103-export function bskyPostWebUrl( postUri: string ): string {
104104- const match = postUri.match( /^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/(.+)$/ );
105105- return match ? `https://bsky.app/profile/${ match[ 1 ] }/post/${ match[ 2 ] }` : 'https://bsky.app';
106106-}
107107-108108-/**
10999 * Count user-perceived characters (grapheme clusters), matching Bluesky's 300-grapheme
110100 * rule. `Intl.Segmenter` is browser-native and counts ZWJ emoji / flags as one each, so
111101 * a family emoji costs one toward the cap โ not the number of underlying codepoints.