Compare commits

...

510 Commits

Author SHA1 Message Date
vineyardbovines 419eaa9c83 Bump mediabunny to 1.49.0 in lockfile
Mediabunny shipped 1.49.0 since this PR was first committed; the
lockfile check ("No manual pnpm-lock.yaml edits") was failing because
a fresh `pnpm install --no-frozen-lockfile` would resolve to 1.49.0
within our `^1.25.3` range. Matches what CI expects from a fresh
install against main's lockfile.
2026-06-23 11:23:00 -04:00
vineyardbovines a9179fa296 Address review: extract shared compression constants, log empty output
- Move TARGET_BITRATE, MAX_DIMENSION, and the 25 MB skip threshold to a
  new src/lib/media/video/constants.ts so both compress.web.ts and the
  upcoming native engine use the same values. Codec lists stay local to
  compress.web.ts since they are mediabunny-specific.
- When mediabunny's BufferTarget returns a null buffer after a clean
  execute(), log via logger.error so Sentry captures the unexpected
  state. The outer catch still demotes the throw to a warn and falls
  back to the original blob, so user-visible behavior is unchanged.
2026-06-23 11:09:10 -04:00
vineyardbovines 36785ffce0 fix: rebase integration with main
- restore native getVideoMetadata via react-native-compressor so
  draft-restore on native still works after pickVideo.ts removal
- widen metadata signatures to File | string to match call sites
- mark conversion.cancel() with void to satisfy no-floating-promises
- pnpm install for mediabunny
2026-06-18 17:59:36 -04:00
Samuel Newman 50fd127373 dispose of input once finished 2026-06-18 17:51:19 -04:00
Samuel Newman 1e8eccffd2 process videos on web with webcodecs 2026-06-18 17:51:19 -04:00
Eric Bailey dd452a4336 Fix nightly build workflow (#10943)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:13:41 -05:00
Eric Bailey 1a8ca710ee Bump version to 1.126.0 (#10942) 2026-06-17 17:56:51 -05:00
Eric Bailey 6f916902e7 Add nightly iOS/Android build workflow (#10940)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 17:53:08 -05:00
Samuel Newman d10137fd78 Simplify Android build: bundletool + eas submit (#9855)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-17 16:42:55 -05:00
Samuel Newman 2347d4f074 v1.125 release prep (#10936) 2026-06-17 05:28:47 -07:00
Alex Benzer b368cea557 Style follow buttons in expanded bundles, add client event on expand (#10915) 2026-06-17 03:13:57 -07:00
DS Boyce 728ee15e27 Flash message bubble when tapping a reply (#10929) 2026-06-17 03:11:56 -07:00
Samuel Newman 6af477bcc2 Replace setup-cocoapods action with inline version assert (#10932)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 02:51:59 -07:00
Samuel Newman 1b5242c1d6 Rip out Modal system completely (#9903) 2026-06-17 02:45:01 -07:00
DS Boyce 6bf1572ffc Increase chat bubble max width to 85% (was 80%) (#10930)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-06-17 02:29:05 -07:00
pfrazee c3a110a939 Nightly source-language update 2026-06-17 03:20:23 +00:00
Spence Pope f92712f34d Persist selected QR code color theme in invite friends dialog (#10892) 2026-06-16 20:07:50 -04:00
Eric Bailey 9825057bef Fix for AutosizedTextarea not resetting height after clear on Android (#10927) 2026-06-16 16:16:27 -05:00
DS Boyce e057d3bd54 Get unread counts vs counting convos (#10878) 2026-06-16 13:45:07 -07:00
Samuel Newman b90d3108ed Fix quote causing chat message to overflow on web (#10926) 2026-06-16 13:39:08 -07:00
Eric Bailey 255af85428 Ensure text composed via IME is visible (#10925) 2026-06-16 14:44:28 -05:00
Samuel Newman b8a6a8c8ce Pin every action to latest SHA (#10779)
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 14:35:19 -05:00
Samuel Newman 4c34149106 Delete gallery fallback (#10922) 2026-06-16 11:44:27 -07:00
Samuel Newman 2257e834dc Run eslint --fix (#10923) 2026-06-16 11:44:00 -07:00
Samuel Newman 8d4f59a146 Fix DM embeds being dropped when sending (#10924)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:43:49 -07:00
Samuel Newman 721965e67c Swipe to reply in chat (#10920)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:38:36 -07:00
Samuel Newman bb6847f396 Replace Add User to Lists Modal with modern Dialog (#9677)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-16 12:41:05 -05:00
Samuel Newman b8fdce6478 Message replies in chat (#10903)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:26:48 -07:00
DS Boyce f075187e57 Reset textarea height after clear (#10911) 2026-06-16 09:25:34 -07:00
dependabot[bot] 9dc73d2ec7 Bump vite from 8.0.12 to 8.0.16 in /bskyembed (#10912)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 07:36:59 -07:00
pfrazee 1c2fd5b280 Nightly source-language update 2026-06-16 03:20:12 +00:00
Eric Bailey d7e285aa24 Fix over-eager URL faceting in DMs composer (#10877) 2026-06-15 13:38:59 -05:00
Samuel Newman fdd08330f7 [Chat] Hide unread state for selected chat list item (#10910)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:16:20 -07:00
Samuel Newman 042fdf659d Use pnpm 11.7.0 (#10900) 2026-06-15 10:16:07 -07:00
Benjamin Wingerter 6dcb4c9a1f fix: truncate long display names in group chat member picker (#10899) (#10901) 2026-06-15 04:46:43 -07:00
Samuel Newman d20d021173 [Chat] Fix member count in SearchablePeopleList (#10909) 2026-06-15 02:47:43 -07:00
Samuel Newman 0204848cc4 [Chat] fix: enforce convo-present invariant for active convo states (#10906)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-15 02:47:14 -07:00
Samuel Newman 02a47fb4a6 Fix scroll edge effects on iOS 27 (#10907) 2026-06-14 07:07:33 -07:00
pfrazee 575b76a71e Nightly source-language update 2026-06-14 03:20:57 +00:00
Samuel Newman feff61a031 Add long-press menu to Chat tab for marking all as read (#10890) 2026-06-13 05:23:28 -07:00
pfrazee e1f86e49e7 Nightly source-language update 2026-06-13 03:18:35 +00:00
DS Boyce 6028532981 Update OTA docs (#10895) 2026-06-12 16:03:12 -07:00
DS Boyce 825d25847c Fix pluralization for group chat member counts (#10896) 2026-06-13 01:01:13 +03:00
Samuel Newman cd404023fb Fix chat firehose event handling and error recovery (#10884) 2026-06-13 01:01:00 +03:00
Samuel Newman 52b945b7be [Chat] Restructure MessageItem to prevent alignment issues (#10888) 2026-06-13 00:59:50 +03:00
Samuel Newman 9c346e5494 [Chat] Fix block dialog overflow issues (#10887) 2026-06-13 00:59:37 +03:00
Samuel Newman 714b1e205d Fix invite card avatar pile not reflecting member count (#10891) 2026-06-13 00:57:31 +03:00
Samuel Newman 1f90a4dfd9 [Chat] Change chat invite border color to low (#10886) 2026-06-13 00:57:16 +03:00
Samuel Newman 7310426ea7 [Chat] Hide blocked accounts' reactions in chats (#10889)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:19:22 -07:00
Alex Benzer 581d2b296d Fixes follow buttons inside notification bundles not changing state on follow (#10882) 2026-06-12 11:18:53 -07:00
DS Boyce ec695a42c5 Upgrade to pnpm 11.5.3 (#10879) 2026-06-12 00:01:41 -07:00
Austin McKinley 6c04170734 Replace broken @claude workflow with Bedrock-based claude-review/mention (#10880)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:36:10 -07:00
pfrazee bce0b51c2e Nightly source-language update 2026-06-12 03:20:29 +00:00
Spence Pope 0a9f7423e2 Keep lightbox image index on device rotation (#10867) 2026-06-11 15:58:11 -05:00
Samuel Newman f0e20d4c56 Make StandardSiteEmbed peekable (#10866) 2026-06-11 15:49:09 -05:00
Samuel Newman b281e23f11 Bump version to v1.125.0 (#10876) 2026-06-11 22:29:18 +03:00
Samuel Newman bd775070e8 [Chat] Use relatedprofiles for reactions/new message logs (#10875) 2026-06-11 11:12:11 -07:00
DS Boyce 3d3d7ba675 Fix cursor position when editing a group chat name (#10872) 2026-06-11 11:06:39 -07:00
DS Boyce f96e6ab985 Bump font size for disabled invite link dialog text (#10874) 2026-06-11 11:06:23 -07:00
DS Boyce d57eeb986b Fix followed by chat invites (#10873) 2026-06-11 11:05:07 -07:00
DS Boyce 91053e3f70 Optimistically update chat invite button status (#10870) 2026-06-11 09:48:23 -07:00
DS Boyce b0eaecb01b Fix Rescind request button (#10869) 2026-06-11 09:45:08 -07:00
Samuel Newman c96d63fa5f [Chat] Update bskyogcard lexicon version (#10871) 2026-06-11 09:34:18 -07:00
Samuel Newman bd034ac1a8 [Chat] Update single-convo cache on firehose mute/unmute events (#10865)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 09:08:28 -07:00
Spence Pope ba72110a12 Fix overlapping touch targets for profile header buttons (#10868) 2026-06-11 12:04:46 -04:00
Spence Pope 302026d684 [bskyembed] Add gallery embed support (#10818) 2026-06-11 12:02:20 -04:00
DS Boyce c8634105c1 Run codemod for replacing BskyAgent with AtpAgent (#10862) 2026-06-11 08:46:01 -07:00
DS Boyce e3e7db2350 Redirect to messages after leaving a chat (#10861) 2026-06-11 08:45:37 -07:00
DS Boyce fbd81ad025 Change Users I follow to People I follow (#10860) 2026-06-11 08:45:16 -07:00
Jim Calabro 5c04a41a11 Add a Metrics Server to bskyweb (#10863) 2026-06-11 08:52:12 -04:00
Eric Bailey 0187a49534 Release Prep 1.124.0 (#10859) 2026-06-10 17:00:41 -05:00
DS Boyce d3f8e740de Log when a chat invite is posted or messaged (#10858) 2026-06-10 14:44:36 -07:00
Eric Bailey b4dff0f4b7 Let text wrap in invite dialog (#10855) 2026-06-10 16:06:09 -05:00
DS Boyce 871e9506df Add new analytics events for group chats (#10830) 2026-06-10 13:56:39 -07:00
Spence Pope 974d2b15d5 Fix stale push notification replay after account switch (#10792) 2026-06-10 15:46:35 -05:00
Spence Pope b667cafca1 Use add-only photo permission when saving QR codes (#10854) 2026-06-10 16:39:40 -04:00
Samuel Newman 156903070b [Chat] Power conversation settings screen via useConvoQuery (#10853)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:23:47 -07:00
estrattonbailey 3feea02548 Nightly source-language update 2026-06-10 19:37:34 +00:00
Samuel Newman 2efdc3fe35 [Chat] NUX (#10848)
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-10 14:35:35 -05:00
Samuel Newman 2d896983ab Peek preview for external link embeds (#10704)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:35:16 -05:00
DS Boyce b47948dca7 Add image to invite link NUX (#10852) 2026-06-10 11:57:24 -07:00
Samuel Newman 8db30c7d31 [Chat] Handle moderation lock override in group chats (#10835)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:57:12 -07:00
Samuel Newman 3b5ede37f7 [Chat] Reusable loading/unavailable states for chat invites (#10819)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:54:37 -07:00
Michael Black 3095bbfbec bskyweb: add video objects to jsonld SEO (#10814) 2026-06-10 13:54:24 -05:00
Samuel Newman b3985bfad5 Add indication of total number of images in ComposerReplyTo (#10836)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-10 11:49:52 -07:00
Spence Pope c6f9803676 Add profile QR code for sharing and inviting friends (#10659) 2026-06-10 14:14:45 -04:00
DS Boyce adb39b4442 Invert group chat feature gate (#10851) 2026-06-10 10:41:34 -07:00
DS Boyce 01054624ef Restore on-device translation from overflow menu (#10850) 2026-06-10 12:31:48 -05:00
Alex Benzer 8c8617d5f2 New layout for expanded notification bundles (#10664)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-10 12:23:07 -05:00
DS Boyce 50dabf3f65 Enable Google Translate in post overflow menu (#10849) 2026-06-10 10:14:48 -07:00
Samuel Newman e5335fa52b [Chat] Fix duplicate messages when history fetch races firehose (#10839)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 09:44:44 -07:00
Samuel Newman 9e9a4024c3 [Chat] Delete GroupChatGate (#10846) 2026-06-10 09:44:28 -07:00
Eric Bailey 209ecafdc1 Fix video preview collapse (#10847) 2026-06-10 11:30:48 -05:00
DS Boyce 90f8fe59ba Put display name and handle on one line in invite dialog (#10824) 2026-06-10 09:17:09 -07:00
DS Boyce a69836216c Tweak spacing in invite link dialog (#10829) 2026-06-10 09:09:19 -07:00
Samuel Newman 8fcf25b290 [Chat] Fix source of truth for group size limit (#10845) 2026-06-10 08:17:18 -07:00
Eric Bailey 8b4a5238be [Chat] Sync convo agent with shadow cache (#10827)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-06-10 08:16:58 -07:00
Samuel Newman 17ff9af200 [Chat] Some tweaks (#10840) 2026-06-10 06:38:31 -07:00
Samuel Newman 7379ad3c3b [Chat] Fix optimistic updates by using partial keys (#10838)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 03:56:54 -07:00
Samuel Newman 9a2e636c17 Remove leave-convo suppression, use real optimistic update (#10834)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 01:58:16 -07:00
Samuel Newman f321122656 use opus 4.8 in github claude (#10837) 2026-06-10 01:55:03 -07:00
Samuel Newman d6a14ade54 [Chat] revalidate join links on leave (#10833) 2026-06-10 01:33:21 -07:00
DS Boyce 8720d05242 Clear search term after adding chat member (#10831) 2026-06-09 23:47:13 -07:00
DS Boyce fc14427134 Support dim theme on join request landing page (#10832) 2026-06-09 23:31:45 -07:00
pfrazee 76694a2ba8 Nightly source-language update 2026-06-10 03:18:50 +00:00
Michael Black 38fb9c2165 bskyweb: meta noindex tag for non-indexable pages (#10803) 2026-06-09 15:54:54 -05:00
Eric Bailey db2e190f4c Hide domain if matches handle, new Standard Site icon treatment (#10767) 2026-06-09 15:54:16 -05:00
Eric Bailey 7f6cfaee71 Fix image collapse in FF (#10826) 2026-06-09 15:15:37 -05:00
DS Boyce 0558e8b79a Change Ignore to Reject for chat invites (#10825) 2026-06-09 12:40:35 -07:00
Samuel Newman 852cb53cab [Chat] add lil gap between chats on web (#10821) 2026-06-09 22:24:28 +03:00
DS Boyce c27f50e038 Add list of mutual chats to block dialog (#10727)
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:18:58 -05:00
DS Boyce 189dad8cb7 Address chat design nits (#10823) 2026-06-09 11:36:08 -07:00
DS Boyce de5ae04809 Fix link to profile from chat invite embed (#10822) 2026-06-09 11:35:45 -07:00
Samuel Newman 6ba5154bcc [Chat] Show invite link label in chat list preview (#10811)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:07:45 -07:00
DS Boyce 994e27ded1 Don't suggest users who can't be messaged/added (#10820) 2026-06-09 10:45:50 -07:00
DS Boyce df8c731f1d Fix back button behavior on conversation settings screen (#10801)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-06-09 10:16:00 -07:00
Samuel Newman 3fdfebfe29 [Chat] Surface blocked members to top of group member list for owner (#10726)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 10:15:03 -07:00
DS Boyce 750c55eff8 Display toast after reacting emoji reaction limit (#10799) 2026-06-09 09:36:35 -07:00
DS Boyce 986cf62da1 Fix text truncating in image menu on iOS (#10802) 2026-06-09 09:35:49 -07:00
Michael Black be30947089 bskyweb: extract gallery media for SEO (#10813) 2026-06-09 11:10:33 -05:00
Samuel Newman fb4133cdc7 [Chat] Filter out groups from badge counts when AA'd (#10816) 2026-06-09 18:33:04 +03:00
Spence Pope e339a2a18c version number (#10815) 2026-06-09 08:24:34 -07:00
DS Boyce c52231a449 Update handling of join link previews (#10798)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-06-09 07:23:33 -07:00
Samuel Newman 4c4ebd0510 Catch NotAllowedError in web GIF player + lint modules/ (#10806) 2026-06-09 07:20:34 -07:00
Spence Pope bc94c8c07b Fix composer horizontal swipe triggering browser back/forward (#10812) 2026-06-09 10:11:21 -04:00
Samuel Newman 22643c76d0 Fix fullscreen infinite render loop in video embeds (#10807) 2026-06-09 06:51:36 -07:00
Samuel Newman 84c4223ff9 [Chat] Gate group chats for under-18 users (#10548)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-09 06:31:22 -07:00
surfdude29 2cb225dddd Mark string for translation and add context (#10805) 2026-06-09 06:03:52 -07:00
DS Boyce 8015c26955 Hide last message in chat list if it's blocked (#10800) 2026-06-09 08:13:15 +03:00
pfrazee c6e6b83d50 Nightly source-language update 2026-06-09 03:18:45 +00:00
DS Boyce bfbd1cef32 Upgrade to pnpm 11.5.2 (#10793) 2026-06-08 13:53:09 -07:00
Eric Bailey 690b8184a1 Refactor age assurance flags and contexts (#10794) 2026-06-08 15:34:51 -05:00
Samuel Newman 46e54d2375 [Chat] Try and resolve loading bug once and for all (#10791) 2026-06-08 23:14:43 +03:00
Samuel Newman 38ee14d517 Remove social proof from group invites (#10787) 2026-06-08 23:14:29 +03:00
DS Boyce 1c5b3dd3a1 Only show Message option if chat member can be messaged (#10788) 2026-06-08 12:15:13 -07:00
DS Boyce 0c89198ded Remove TODO for uninviting group chat members (#10789) 2026-06-08 12:14:51 -07:00
DS Boyce fe9dabbc4a Open edit chat name dialog via tap (#10790) 2026-06-08 12:14:24 -07:00
DS Boyce 4142eb4834 Add fade-in animation to chat footer/composer (#10786) 2026-06-08 12:13:43 -07:00
Samuel Newman 9b5fc19613 [Chat] Centralize conversation report-subject resolution (#10754)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 10:07:35 -07:00
Spence Pope 0090285fc0 Refactor photo embed analytics to post:photoEmbed:* namespace (#10784)
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:44:59 -04:00
Samuel Newman c4f3a2cb89 [Chat] Rename invite route to /chat/ (#10782) 2026-06-08 09:26:23 -07:00
Samuel Newman d9345cb1cf [Chat] Fix footer logic (#10785) 2026-06-08 09:20:37 -07:00
DS Boyce 5c0429a3f7 Block posting of invalid chat invites (#10757)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-06-08 05:28:24 -07:00
Samuel Newman 16046cd85e [Chat] Invite link dialog tweaks (#10750) 2026-06-08 05:27:58 -07:00
Samuel Newman eb79c4d53c Show accept/reject footer for empty request convos (#10781)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 05:10:41 -07:00
DS Boyce 6997d220a0 Add Leave chat button to settings for chat owners (#10764)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-06-08 04:57:08 -07:00
DS Boyce e60e31733a Use new listConvoRequests endpoint (#10755)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-06-08 04:36:34 -07:00
DS Boyce c02f2a1eda Fix alignment of error screen (#10763) 2026-06-08 03:51:11 -07:00
Samuel Newman 35258f1a16 [Chat] Fix overflowing chat names (#10780) 2026-06-08 03:50:44 -07:00
surfdude29 753015b59f Add plural formatting to "Group name is too long" strings (#10769) 2026-06-08 02:42:50 -07:00
Eric Bailey 0ce2ada9b0 Release 1.123.0 (#10766) 2026-06-06 10:26:44 -05:00
Eric Bailey 7577933378 Fix draft image pres with 4+ images (#10765) 2026-06-06 09:55:39 -05:00
estrattonbailey b9cd7e992a Nightly source-language update 2026-06-05 22:59:53 +00:00
surfdude29 a253e877fc Add plural formatting for error toast (#10756) 2026-06-05 17:57:57 -05:00
Eric Bailey 11dd259716 [Drafts] Always write embedGallery for image embeds (#10761)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:56:08 -05:00
Eric Bailey 0cc6d49388 Android share intent: logging + temp file cleanup (#10762)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:35:21 -05:00
Samuel Newman 5eeb80fe96 Fix Android crash when sharing unreadable media into the app (#10674) 2026-06-06 01:03:29 +03:00
DS Boyce a675cbacb6 Link to owner's profile from link embed (#10759) 2026-06-05 14:26:30 -07:00
DS Boyce 1d5fb89c80 Improve error messages for LeaveConvoPrompt (#10760) 2026-06-05 14:19:48 -07:00
Samuel Newman ac63ee7739 Remove useWindowDimensions from ProfileBadges (#10738) 2026-06-05 16:17:05 -05:00
Samuel Newman a5338090cb Let specified facets take priority in feed descriptions (#10740) 2026-06-05 16:13:26 -05:00
Samuel Newman fb8316fd85 Fix external card long press on web (#10739) 2026-06-05 16:04:21 -05:00
Samuel Newman b236f274c3 [Chat] Enforce limits for group size (#10753) 2026-06-05 23:17:29 +03:00
Samuel Newman 2e9bfd363c [Chat] Put pointer_events_none on chat bubbles in ChatListItem (#10752) 2026-06-05 13:09:55 -07:00
Samuel Newman 32c0c0ec83 "Follows You" string dedupe (#10751) 2026-06-05 13:08:39 -07:00
Samuel Newman 6d468c8ca4 [CI] Remove Wandalen/wretry.action, call pnpm install directly (#10745) 2026-06-05 22:49:02 +03:00
Samuel Newman a5aa985b33 Fix feed scroll reset on device rotation (#10737) 2026-06-05 22:48:34 +03:00
Eric Bailey fd8df7e201 Fire embed:standardSite:view from feed viewability, not embed mount (#10736)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:50:16 -05:00
Samuel Newman f8aae4a192 [Chat] Disable font scaling on fixed-height chat invite cards (#10744)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 18:18:27 +03:00
Samuel Newman 67ed59fbcd Align group invite settings with eligibility logic (#10748)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 18:10:14 +03:00
Samuel Newman 7d4d7642fd [Chat] Sync single-convo cache on chat lock firehose events (#10746)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 06:39:23 -07:00
Samuel Newman f81bb6f494 [Chat] Fix navigation stacking on messages links in split view (#10741)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 06:38:48 -07:00
Samuel Newman 3cdc0604ca [Chat] Fix stale reactions dialog after removing a reaction (#10743)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 06:38:23 -07:00
Samuel Newman d3f073b3d2 [Chat] Request screen tweaks (#10747) 2026-06-05 06:35:06 -07:00
estrattonbailey 96e321dfd9 Nightly source-language update 2026-06-05 02:21:15 +00:00
Spence Pope 68da797180 Add OTA fallback for gallery embed (#10734)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-04 21:19:35 -05:00
Eric Bailey 2f61c0a7b4 Gate StandardSite subscribe button custom theme on AAA contrast (#10735)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:51:34 -05:00
Samuel Newman f84c85aac6 Use Reanimated layout animations for Android composer (#8546) 2026-06-05 00:37:42 +03:00
Spence Pope 144ba30ea6 Add gallery embed type support (display + compose) (#10707) 2026-06-04 17:22:40 -04:00
DS Boyce de926d1838 Show a banner when there are incoming chat requests (#10498)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-06-04 14:22:29 -07:00
DS Boyce 45bd5b45e8 Reduce stale time for join link preview queries (#10733) 2026-06-04 14:11:50 -07:00
Eric Bailey 923e1635d8 V smol log cleanups (#10731) 2026-06-04 15:56:37 -05:00
Samuel Newman ded2ca3744 [Chat] Add chat invite as message embed type (#10728) 2026-06-04 13:11:03 -07:00
DS Boyce 09fa93553b Fix useJoinRequestMutation (#10730) 2026-06-04 13:08:51 -07:00
DS Boyce b9637c0305 Improve error message for group chat join link (#10729) 2026-06-04 12:57:22 -07:00
Spence Pope 6677ae977c Show image count badge on in-feed carousel (#10719) 2026-06-04 14:25:35 -04:00
Spence Pope 58783ca5ae ensure alt text is scrollable (#10671)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-04 11:24:50 -05:00
Spence Pope d3c9ed027c Fix web carousel horizontal swipe triggering browser back/forward (#10718)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-04 11:23:16 -05:00
Eric Bailey 795d493dea Apply per-use-case image size configs (#10690)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:23:04 -05:00
Samuel Newman 66db349ef2 Switch dynamic app icon library to @bsky.app, fix build flakiness (#10724) 2026-06-04 18:46:49 +03:00
DS Boyce 08cd4e58b0 Add group chat join links to supported embed types (#10454) 2026-06-04 08:11:50 -07:00
Eric Bailey c79b5bfa08 Disable overscroll on Android to fix settle discrepancy (#10717) 2026-06-04 09:29:36 -05:00
heikadog 49f8e5031f Fix labeller highlight when selected on bottom bar (#10716) 2026-06-04 03:28:40 -07:00
surfdude29 9f200aab05 Use correct Plural macro in InviteLinkDialog (#10723) 2026-06-04 00:51:51 -07:00
surfdude29 a19d652a39 Fix invalid zero plural category and add plural formatting (#10722) 2026-06-03 23:50:56 -07:00
pfrazee ad4591eb29 Nightly source-language update 2026-06-04 03:21:17 +00:00
Samuel Newman 153772b760 expo-image tweaks (#10710) 2026-06-04 01:03:11 +03:00
Samuel Newman 57ae26fcaa Make extra sure the drawer gesture doesn't accidentally trigger on Android (#10713) 2026-06-03 13:57:45 -07:00
Spence Pope 0a5a994170 Fix image peek in carousels for quote posts (#10688)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-03 15:38:36 -05:00
DS Boyce f99837040e Compile i18n before running lint (#10711) 2026-06-03 13:21:13 -07:00
Samuel Newman 4367b9d047 [Android] Fix horizontal swipes being cancelled when leaving bounds (#10712) 2026-06-03 15:01:54 -05:00
DS Boyce 84d1785e12 Enable ESLint errors and suppress existing violations (#10490) 2026-06-03 11:27:44 -07:00
DS Boyce cdad723ea1 Hide accept button for locked chats (#10696) 2026-06-03 10:23:13 -07:00
DS Boyce 1b4c72f516 Mount message dialogs once at the list level (#10435)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-06-03 09:28:11 -07:00
Spence Pope b6650d8d5c Fix alt text selection in lightbox on Android (and iOS) (#10698)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-03 09:58:43 -05:00
Samuel Newman 21679b66dd Mention default <Text> style in CLAUDE.md (#10705) 2026-06-03 09:39:31 -05:00
RetroSunstar a046d8d96a Use square icons for labellers in Automation Label preview card (#10701) 2026-06-03 03:47:35 -07:00
Samuel Newman 00497964c1 [ALF] Set text to leading_snug by default (#10627) 2026-06-03 09:05:46 +03:00
pfrazee f4f66af31c Nightly source-language update 2026-06-03 03:21:33 +00:00
DS Boyce 3c06ecfa1a Transpile @atproto/api package for older browser support (#10700) 2026-06-02 21:13:34 -05:00
Samuel Newman 711334b34e Add iOS peek long-press context menu for image embeds (#10300)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-02 21:03:55 -05:00
Eric Bailey c88218ab87 Revert "[ALG-96] Don't fall back to search for onboarding user suggestions (#10680)" (#10699) 2026-06-02 16:28:48 -05:00
Spence Pope 0a7c087078 fix blur on image alt text background (#10658) 2026-06-02 16:01:16 -05:00
DS Boyce 73dae02df3 Create screen for managing chat requests (#10635) 2026-06-02 13:28:06 -07:00
DS Boyce 75490c3cdf Don't show Suggested heading if there are no suggestions (#10695) 2026-06-02 13:27:43 -07:00
Eric Bailey b87a51b485 Prefer opengraph image, fall back to Standard Site coverImage (#10694) 2026-06-02 15:07:14 -05:00
DS Boyce 56496520d6 Create logged-out view for group chat invites (#10598)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-06-02 12:02:49 -07:00
dependabot[bot] d08ab487d8 Bump actions/create-github-app-token from 1 to 3 (#10691)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 11:57:43 -07:00
dependabot[bot] a2c8b47b87 Bump marocchino/sticky-pull-request-comment from 2 to 3 (#10692)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 11:56:46 -07:00
dependabot[bot] 8e5ac6790f Bump slackapi/slack-github-action from 2.1.1 to 3.0.3 (#10693)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 11:55:39 -07:00
Austin McKinley 53fa6b2493 ci: add dependabot config with 7-day cooldown (#10475)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 11:04:28 -07:00
Eric Bailey 61e10f7e0b Remove image upload flags (#10687) 2026-06-02 12:42:10 -05:00
Eric Bailey 59765138ce [ALG-96] Don't fall back to search for onboarding user suggestions (#10680) 2026-06-02 12:29:24 -05:00
DS Boyce dfa82904ec Run GitHub actions in parallel (#10509)
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:24:55 -05:00
DS Boyce bdbf31a254 Use expo/expo-github-action v9 (#10689) 2026-06-02 09:47:22 -07:00
Samuel Newman 2b31047b0d Fix scrollbar behaviour in splitview (#10685) 2026-06-02 19:41:48 +03:00
Samuel Newman fd9f988b90 Ungate video upload size change (#10683) 2026-06-02 07:06:47 -07:00
Naresh Rawat cbf8e95bdb Message sent time is not updated immediately in ChatList screen (#7768) 2026-06-02 04:25:37 -07:00
Samuel Newman 771fd5ddf6 [Chat] Tweak styles in invite link dialog (#10638) 2026-06-02 03:23:51 -07:00
pfrazee 26e48e3876 Nightly source-language update 2026-06-02 03:22:13 +00:00
Samuel Newman 2abfba9771 Remove unused slider dep (#10618) 2026-06-01 15:36:34 -07:00
Eric Bailey 0dfd7cfa12 Update mod SDK (#10682) 2026-06-01 17:26:50 -05:00
Samuel Newman 237232f96c Update date-fns to latest (#10545) 2026-06-01 15:21:50 -07:00
Samuel Newman db1c4dc6e3 Update Intl polyfills (#10587)
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2026-06-01 15:03:53 -07:00
Iñigo Cilveti 751eda59fd Fix duplicate haptic feedback when liking posts (#10678) 2026-06-01 14:40:48 -07:00
Samuel Newman 62d1e88ce1 Fix Android crash in chat keyboard scroll (#10673)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 14:32:41 -07:00
DS Boyce ba24c7c357 Display join requests on chat list (#10679) 2026-06-01 14:32:36 -07:00
DS Boyce 0d38e2ab3f Show chat info panel for empty chats (#10677) 2026-06-01 10:15:21 -07:00
Samuel Newman bccca283b9 [Chat] Add /c/:code handler to bskyweb (#10630) 2026-06-01 08:28:38 -07:00
Samuel Newman 601dae0cda fix(ios): fix Hermes startup race in ExpoBridgeModule (patch) (#10672)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 07:56:34 -07:00
Samuel Newman 9e275d3898 [pnpm] Use latest pnpm (#10585) 2026-06-01 05:35:37 -07:00
Samuel Newman 855d785bf8 Gate new group chat creation on canCreateGroups (#10656)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 05:18:15 -07:00
Samuel Newman bac06bdf23 Switch sonner-native patch to target ESM source (#10668) 2026-05-31 13:45:28 -07:00
pfrazee 3205fcd7b3 Nightly source-language update 2026-05-30 03:17:17 +00:00
Samuel Newman d0ad7819bf [Chat] Add chat disabled banner (#10573) 2026-05-29 22:46:15 +03:00
Samuel Newman b0708dcefb Fix email verification dialog (#10657) 2026-05-29 11:03:00 -07:00
DS Boyce ce4efc511b Refactor notification settings as dialogs (#10646)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-05-29 09:35:14 -07:00
DS Boyce e52adeac12 Fix layout of NotFound screen and convert to ALF (#10649) 2026-05-29 06:59:08 -07:00
Samuel Newman a5e29442b5 [Chat] Add other chat route handlers to bskyweb (#10639) 2026-05-29 06:35:32 -07:00
Samuel Newman 806f31dc93 Use pnpm to handle node/pnpm versions in Dockerfile (#10642) 2026-05-29 05:40:24 -07:00
dependabot[bot] 98f14bab39 Bump golang.org/x/crypto from 0.40.0 to 0.45.0 in /bskyweb (#10640)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 04:31:52 -07:00
Samuel Newman 9f69e7f8ff Bump version to v1.123.0 (#10647) 2026-05-29 04:13:03 -07:00
pfrazee 28106639f4 Nightly source-language update 2026-05-29 03:20:13 +00:00
Eric Bailey 5123680a11 Fix spacing when no description in SS pub card (#10648) 2026-05-28 17:55:49 -05:00
Eric Bailey bcdf707a8a Prevent silent Denis failures in the future (#10645) 2026-05-28 16:39:00 -05:00
DS Boyce 3e46ca391f Colocate all chat log event handlers (#10632) 2026-05-28 12:51:40 -07:00
DS Boyce 672cebef62 Disable other chat settings when a chat is locked (#10623) 2026-05-28 11:14:08 -07:00
Eric Bailey af04e8b496 Moar Standard Site Tweaks (#10641) 2026-05-28 11:32:03 -05:00
DS Boyce 5e27d8c85d Update presentation of DM header (#10619)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-05-28 09:31:52 -07:00
Michael Black 60917d1125 Refactor and improve seo schema.org data (#10613) 2026-05-28 11:26:25 -05:00
Samuel Newman 6432c925c2 [Chat] Add OpenGraph card for chat invite links (#10629) 2026-05-28 10:22:49 -03:00
Eric Bailey 7f5dd40069 Release prep 1.122.0 (#10634) 2026-05-27 15:29:06 -05:00
Eric Bailey 13f494502d Expand allowed "live now" platforms (#10633) 2026-05-27 14:42:01 -05:00
Samuel Newman e8f61ac3da [pnpm] Migrate devenv (#10480) 2026-05-27 14:39:36 -05:00
Samuel Newman addfdd192a [Chat] Tweak messageitem blocked state (#10631) 2026-05-27 10:00:45 -07:00
Eric Bailey 3d8875374e Standard Site cards tweaks (#10614) 2026-05-27 11:53:26 -05:00
DS Boyce 721a08ffe1 Handle new group chat log events (#10624) 2026-05-27 09:32:06 -07:00
DS Boyce 4e4e1e1190 Add ability to report a group chat (#10485) 2026-05-27 09:30:33 -07:00
DS Boyce 731f32abda Add XRPC error handling for creating new chats (#10622) 2026-05-27 08:05:51 -07:00
DS Boyce a892b3dcc9 Remove hard-coded chat member limit (#10625) 2026-05-27 10:50:19 +03:00
pfrazee 7ac288de12 Nightly source-language update 2026-05-27 03:20:32 +00:00
Samuel Newman 5bad7d3ceb [Chat] Add message item blocked state (#10616) 2026-05-26 10:22:18 -07:00
Samuel Newman 8720ef72ce Bump RNKC (#10617) 2026-05-26 10:15:51 -07:00
Samuel Newman afa6a98349 [Chat] Empty state for native (#10603) 2026-05-26 10:01:10 -07:00
Samuel Newman 1113686e30 [Chat] Prevent reactions when unavailable (#10615) 2026-05-26 09:55:30 -07:00
Samuel Newman 308b2e05f1 [Chat] Moderation stuff (#10610) 2026-05-26 09:55:22 -07:00
Spence Pope 3f4cd1c683 Don't cache trending GIFs (#10612) 2026-05-26 10:55:42 -05:00
surfdude29 a5a6293467 Tweak location strings (#10518) 2026-05-26 10:51:58 -05:00
surfdude29 dc62ce2fb0 Fix "under 18" admonition being incorrectly shown on Moderation screen (#10521)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-05-26 10:51:48 -05:00
surfdude29 68431182ef Improve logic to show most relevant error message in AA restricted regions (#10522)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-05-26 10:45:07 -05:00
Alex Benzer 4e2949c93a Add nav:click event to all main nav items (#10540)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-05-26 09:28:40 -05:00
Samuel Newman 1790fddc78 Enable 300mb video uploads (#10497) 2026-05-26 09:23:43 -05:00
pfrazee 4eb692cf87 Nightly source-language update 2026-05-26 03:19:30 +00:00
Samuel Newman 3552c26449 [pnpm] Set trustPolicy to no-downgrade (#10570) 2026-05-25 16:31:30 -05:00
Samuel Newman 98cdeef5b2 Remove FontAwesome (#10528)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-05-25 16:29:46 -05:00
Eric Bailey f5dcce1718 Standard Site link card improvements (#10608) 2026-05-25 15:49:30 -05:00
Samuel Newman 709589fa36 Reduce lightbox button size on native (#10500) 2026-05-25 04:34:35 -07:00
Samuel Newman ebed23657f Detect missing H.264/AAC codec support on web video (#10499)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 04:23:21 -07:00
Samuel Newman c1f42ef877 Fix image cropping not saving on iOS (#10404)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 04:22:39 -07:00
Samuel Newman 5281c48de3 [Chat] Restore back button to settings screens (#10602) 2026-05-25 04:12:40 -07:00
Samuel Newman bf7633b794 Fix status bars not showing (#10605) 2026-05-25 04:09:28 -07:00
Samuel Newman d404363093 Fix left nav badge overlap (#10604) 2026-05-25 03:47:22 -07:00
Samuel Newman c4861cd073 [Chat] Fix unusably small emoji picker (#10591) 2026-05-25 03:46:11 -07:00
DS Boyce 537a1f7120 Update loading state for chats (#10596) 2026-05-25 01:58:46 -07:00
Samuel Newman bf83f165ee [Chat] Set the sender avatar as the group image in NSE (#10600) 2026-05-23 01:48:11 -07:00
pfrazee 1b14ea2413 Nightly source-language update 2026-05-23 03:16:14 +00:00
Spence Pope 5db37729bb [APP-2160] Build new standard.site link card UI (#10468)
Co-authored-by: vineyardbovines <spencer@thepope.dev>
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:36:06 -05:00
DS Boyce 6159cd7777 Add loading state to message composer and input (#10595) 2026-05-22 13:57:19 -07:00
DS Boyce 0d14513939 Update appearance of blocked chat footer (#10597) 2026-05-22 13:56:51 -07:00
Samuel Newman 66d37f55b4 Fix autofocusing Emoji reaction picker search (#10599) 2026-05-22 23:07:47 +03:00
DS Boyce 96ae32b78e Fix chat message spacing and alignment (#10579) 2026-05-22 11:08:03 -07:00
DS Boyce 4bdedc55be Add Info Panel to direct chats (#10576) 2026-05-22 10:34:21 -07:00
DS Boyce f2e7028709 Update appearance of chat when blocked (#10582) 2026-05-22 09:46:57 -07:00
Samuel Newman 2a546bfd52 [Chat] Disable convo avatar in NSE (#10594) 2026-05-22 09:30:20 -07:00
Samuel Newman c21c031e74 [pnpm] Attempt to remove eas-cli from devDeps (#10589) 2026-05-22 09:17:12 -07:00
Samuel Newman 42d993a78f [Chat] Fix iOS group chat notifications rendering as direct messages (#10590)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 07:51:43 -07:00
DS Boyce 7c5a9ab40d Fix issue where suggested language prompt could not be dismissed (#10581) 2026-05-22 01:54:54 -07:00
Samuel Newman 2f4049e6e0 Bump atproto to latest (#10586) 2026-05-22 00:25:25 -07:00
DS Boyce 53df5e62bb Wrap group chat names when appropriate (#10583) 2026-05-21 23:47:41 -07:00
DS Boyce 4c73b171d6 Reduce the number of join requests per page to 20 (#10578) 2026-05-21 23:39:11 -07:00
pfrazee 6f0912346c Nightly source-language update 2026-05-22 03:19:03 +00:00
Samuel Newman 6fd81fe5dc ESM atproto packages (#10543) 2026-05-21 16:41:04 -05:00
DS Boyce e1e5f28e98 Display badges next to chat inviter's display name (#10451) 2026-05-21 12:54:37 -07:00
DS Boyce f653faab94 Fix word wrapping in image menu on Android (#10577) 2026-05-21 10:32:35 -07:00
Samuel Newman 7da3f385a8 Fix stuck tooltip bubble on Safari (#10560)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:49:58 -07:00
RetroSunstar 0858010a5d Fix Image Options not opening on web (#10544) 2026-05-21 09:32:29 -07:00
Samuel Newman 901b1a3a9e [Chat] Remove ChatEmptyPill (#10575) 2026-05-21 09:25:05 -07:00
Samuel Newman c0f7ffa728 [Chat] Make group header a link, fix behaviour with long text (#10572) 2026-05-21 09:21:06 -07:00
pfrazee 1b546c54db Nightly source-language update 2026-05-21 03:19:49 +00:00
Samuel Newman 7fe2f892f6 Revert "Skip installing expo cli in CI (#10566)" (#10568) 2026-05-20 21:30:53 +03:00
Samuel Newman b73f9110e5 Skip installing expo cli in CI (#10566) 2026-05-20 09:50:44 -07:00
Samuel Newman c4ecb007f3 [Chat] New post attachment style in composer (#10563) 2026-05-20 09:50:34 -07:00
Samuel Newman 3d4ba946b3 [pnpm] Fix CI attempt 5 (#10567) 2026-05-20 09:33:02 -07:00
Samuel Newman a2417e043e [Chat] Add embeds to the optimistic message (#10564) 2026-05-20 09:30:04 -07:00
Samuel Newman 21446b43af [Chat] MessageItem tweaks (#10562) 2026-05-20 09:26:24 -07:00
Samuel Newman 4908816253 [pnpm] Fix CI attempt 4 (#10565) 2026-05-20 08:58:55 -07:00
Samuel Newman c1817d3425 [Chat] Handle new chat notification reasons (#10530)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:20:35 -07:00
DS Boyce 9cad883ab8 Update appearance of chat list item states (#10556)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-05-20 07:17:25 -07:00
Samuel Newman 46a4f6cbee Run prettier (#10559) 2026-05-20 04:47:59 -07:00
Samuel Newman 5088f00c35 Fix up prettier config, apply to more files (#10542) 2026-05-20 03:53:19 -07:00
DS Boyce 70a1a665ed Fix eas invocation for GitHub Actions (#10555) 2026-05-20 03:52:35 -07:00
DS Boyce 2f80d983d3 Update Dockerfile.embedr to work with pnpm (#10554) 2026-05-20 12:03:37 +03:00
pfrazee 20f1c60782 Nightly source-language update 2026-05-20 03:19:04 +00:00
DS Boyce 34ae819aab Fix some issues with message spacing (#10553) 2026-05-19 13:06:21 -07:00
Samuel Newman 7b6a81ac23 Remove unread state from locked chats (#10552) 2026-05-19 22:22:27 +03:00
Samuel Newman 8cea36f3c9 [pnpm] Migrate bskyembed (#10482) 2026-05-19 11:51:10 -07:00
Samuel Newman 09253457f9 Gate group chat invites on allowGroupInvites (#10546)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:47:54 -07:00
DS Boyce 12ab1dd2fd Fix CI by restoring changes from #10538 (#10550) 2026-05-19 11:46:07 -07:00
DS Boyce b4054bf2a7 Update GitHub Actions to Node.js v24-compatible versions (#10539) 2026-05-19 11:06:46 -07:00
Eric Bailey 2cee865f4a Update husky (#10549) 2026-05-19 09:57:11 -05:00
Samuel Newman 683ae7610f [pnpm] Install node version via devEngines.runtime (#10503) 2026-05-19 07:37:55 -07:00
Samuel Newman 366cf7e040 Rebalance splitview columns (#10512) 2026-05-19 07:33:10 -07:00
dependabot[bot] 8e8dc7561f Bump ws from 8.20.0 to 8.20.1 (#10537)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 03:27:52 -07:00
DS Boyce af6041d7bc Fix CI attempt 3 (#10538) 2026-05-19 08:23:18 +03:00
pfrazee b795757a86 Nightly source-language update 2026-05-19 03:18:28 +00:00
DS Boyce bc148ede43 Tweak reaction size and padding (#10534) 2026-05-18 14:37:29 -07:00
DS Boyce f9f11c802b Exclude @atproto and @bsky.app packages from minimum age exclusion (#10536) 2026-05-18 14:05:13 -07:00
Samuel Newman 632ce5cc37 [Chat] set reaction count font weight to medium (#10532) 2026-05-18 23:54:48 +03:00
DS Boyce 52342f1cda Fix pnpm lockfile integrity check (#10529) 2026-05-18 13:52:43 -07:00
Samuel Newman 79872b8be5 [pnpm] Fix CI attempt 2 (#10533) 2026-05-18 23:33:54 +03:00
Samuel Newman 5b622079b6 [pnpm] Fix CI (#10531) 2026-05-18 13:22:26 -07:00
Samuel Newman 0dc062294f Move headerMode out of global shell state (#10527) 2026-05-18 10:53:12 -07:00
Samuel Newman e40c9bcf98 [Chat] Persist left column scroll in splitview (#10513) 2026-05-18 10:47:41 -07:00
Samuel Newman b1ce519361 [Chat] adjust list insets on web (#10524) 2026-05-18 10:00:20 -07:00
Samuel Newman ff731849b0 Migrate from Yarn 1 to pnpm (#10465)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-05-18 09:51:04 -07:00
surfdude29 b7c2c2f183 Add missing space to string (#10511) 2026-05-18 09:35:11 -07:00
Samuel Newman e58a09e0ca Fix automatic footer compensation in Layout.Content (#10526) 2026-05-18 04:57:06 -07:00
pfrazee 45597de6e0 Nightly source-language update 2026-05-16 03:16:55 +00:00
DS Boyce 5da73c2d67 Add per-file codemod for updating useLingui to v5 (#10505) 2026-05-15 13:28:40 -07:00
DS Boyce 2bf6bdb6bc Update chat presentation (#10444) 2026-05-15 13:25:47 -07:00
Samuel Newman 0eb382da03 [Chat] Split view layout for web (#10258) 2026-05-15 12:46:53 -07:00
DS Boyce 5b7bc56a12 Address any types in List component (#10504) 2026-05-15 12:42:32 -07:00
DS Boyce cc7d8296f0 Fix unnecessary scrollbars on web (#10502) 2026-05-15 11:56:19 -07:00
Samuel Newman 9d98890fd2 Fix reply failures caused by fetching posts from PDS (#9925) 2026-05-15 11:43:07 -07:00
DS Boyce e0cb1c3413 Remove unused code for unsupported 'invited' status (#10492) 2026-05-15 09:29:09 -07:00
DS Boyce f963c90eb5 Update appearance of chat disabled message (#10488) 2026-05-15 09:28:48 -07:00
DS Boyce 8756c6c0ac Update message context menu styles (#10487) 2026-05-15 09:28:26 -07:00
Eric Bailey b41dbec48f Remove KLIPY from external media settings (#10496) 2026-05-15 10:54:42 -05:00
pfrazee e52b7df39b Nightly source-language update 2026-05-15 03:19:40 +00:00
DS Boyce 0b63ee40c7 Don't show Follow link if user is blocked (#10491) 2026-05-14 10:36:59 -07:00
DS Boyce e18b76d897 Display icon on chat list for system messages (#10493) 2026-05-14 10:28:53 -07:00
DS Boyce c81174e4f0 Fix labels in invite link dialog (#10472) 2026-05-14 05:30:07 -07:00
pfrazee da44df9124 Nightly source-language update 2026-05-14 03:17:55 +00:00
DS Boyce a07e364a87 Use i18n.date for date dividers (#10486) 2026-05-13 15:09:10 -07:00
DS Boyce b41d0ad97d Improve birthdate handling (#10477) 2026-05-13 13:52:59 -07:00
Eric Bailey d20df78f7e Stop writing associatedRecord to embeds, may need to adjust schema (#10489) 2026-05-13 15:52:43 -05:00
DS Boyce 9e55bb71ef Add export data dialog to chat settings (#10478) 2026-05-13 12:30:00 -07:00
Samuel Newman 34b3c52729 Keep screen awake when playing youtube video (#10479) 2026-05-13 08:51:53 -07:00
rafael e9ad74bb01 fix did query string arg in ogcard (#10484) 2026-05-13 12:28:20 -03:00
Fahad Ahmed a7439eb459 Fix stale display name for renamed pinned lists (#10466) 2026-05-13 08:19:29 -07:00
pfrazee e776c37d17 Nightly source-language update 2026-05-13 03:18:59 +00:00
Austin McKinley ddcf7d74e9 ci: build link image on push to main (#10471)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 13:07:22 -07:00
DS Boyce a8debddd08 Update chat menu styles and options (#10455) 2026-05-12 12:53:47 -07:00
Austin McKinley f0fec7a9da ci: build ogcard image on push to main (#10469)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 12:45:34 -07:00
Samuel Newman f527b35244 Add avatar-bubbles endpoint to bskyogcard (#10327)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: rafaeleyng <rafaeleyng@gmail.com>
2026-05-12 13:54:00 -03:00
Spence Pope a03cbd8b1a [APP-2170] Render GIFs through ConstrainedImage (#10453)
Co-authored-by: vineyardbovines <spencer@thepope.dev>
2026-05-12 11:17:46 -04:00
dependabot[bot] 859e172651 Bump axios from 1.13.6 to 1.16.0 in /dev-env (#10461)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 23:53:37 -07:00
pfrazee c5600c5a17 Nightly source-language update 2026-05-12 03:19:10 +00:00
DS Boyce bfd24f6d9e Update appearance of new messages pill (#10442) 2026-05-11 09:40:56 -07:00
Samuel Newman b2335dcdb1 Update react-native-keyboard-controller (#10329) 2026-05-11 08:54:01 -07:00
Spence Pope 9b5be84c5a Add region notice to age assurance card (#10439) 2026-05-11 08:27:24 -04:00
Jim Calabro 28428a5750 Add Prometheus Metrics to Bskyweb (#10370) 2026-05-11 05:41:44 -04:00
pfrazee 10b8f694ef Nightly source-language update 2026-05-09 03:17:25 +00:00
DS Boyce 1609778ff6 Update chat settings (#10449) 2026-05-08 15:49:44 -07:00
DS Boyce 75d8fb3dbd Group runs of system messages under a single toggle (#10388) 2026-05-08 15:24:34 -07:00
Spence Pope 8b9361016a [APP-2146] Update GIF content filtering and [APP-2152] harden web GIFs (#10443)
Co-authored-by: vineyardbovines <spencer@thepope.dev>
2026-05-08 15:34:07 -04:00
DS Boyce a889a1b081 Update presentation of chat requests (#10417) 2026-05-08 12:05:05 -07:00
DS Boyce c8e4815837 Update chat requests header (#10446) 2026-05-08 11:53:02 -07:00
DS Boyce 5640aed7e3 Preserve memoization of message list (#10434) 2026-05-08 10:54:08 -07:00
Samuel Newman 92fdb6630d Align composer video preview with in-feed embed (#10447)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:37:07 -07:00
Samuel Newman 92199774d5 [Chat] Set the default listConvos limit to 10 (#10448) 2026-05-08 10:29:56 -07:00
pfrazee e4f35fae08 Nightly source-language update 2026-05-08 03:15:18 +00:00
Eric Bailey fa8c2224b7 Add associatedRecord to external embeds, if available (#10445) 2026-05-07 19:08:29 -05:00
DS Boyce 9a2cabf64c Update context menu presentation (#10426) 2026-05-07 13:24:55 -07:00
DS Boyce 10cb0dbf21 Improve initial loading state for chat settings (#10424) 2026-05-07 12:00:42 -07:00
DS Boyce eddbf6ba9f Update presentation of settings menus (#10425) 2026-05-07 12:00:21 -07:00
DS Boyce 2907ff0cd8 Fix issue where last RecentProfileItem can't be removed (#10441) 2026-05-07 10:07:08 -07:00
pfrazee 1aeda24975 Nightly source-language update 2026-05-07 03:18:24 +00:00
Spence Pope 445aef8fec update locale in klipy api (#10432) 2026-05-06 18:16:06 -04:00
DS Boyce c91eae3720 Show message when chat is locked or ended (#10413) 2026-05-06 10:42:54 -07:00
DS Boyce 945d8b229d Reduce intensity of drop shadow on chat reactions (#10414) 2026-05-06 10:06:42 -07:00
Samuel Newman 7c22abcb66 [Chat] Add emoji prop to message displayname (#10431) 2026-05-06 09:39:11 -07:00
DS Boyce 26ca0a671b Improve localization and accessibility for the reactions dialog (#10409) 2026-05-06 09:36:57 -07:00
DS Boyce 276143c92b Display chat message underneath embed, not above (#10423) 2026-05-06 09:35:48 -07:00
Samuel Newman 0b6ae1cc21 Fix picking offloaded videos for IOS 26 (#10428) 2026-05-06 06:40:22 -07:00
Samuel Newman a7ed3ee3cc [Chat] Fix assumption that every chat has an owner (#10373) 2026-05-06 06:11:07 -07:00
pfrazee 2aa544fade Nightly source-language update 2026-05-06 03:17:44 +00:00
Eric Bailey 8e06c9fbb7 Add ability for users to send error reports (#10427) 2026-05-05 17:21:42 -05:00
Eric Bailey 9bf06a80a5 Add push noty script and docs (#10380) 2026-05-05 14:34:55 -05:00
DS Boyce 6f4ae14f36 Update to Node.js v24 (#10419) 2026-05-05 11:51:42 -07:00
Eric Bailey cc5b215863 Bump mod SDK and add more details to mod info dialog (#10420) 2026-05-05 13:28:06 -05:00
DS Boyce a24d30ab47 Add ability to follow from chat settings (#10415) 2026-05-05 09:51:40 -07:00
DS Boyce c21138ba63 Differentiate reactions that include the viewer (#10416) 2026-05-05 09:29:52 -07:00
Spence Pope a7fabd9e0d Refresh lightbox designs (#10330)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:25:29 -04:00
Spence Pope 48ea9e0b96 [APP-2067] Rebuild GIF Dialog (#10261)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 11:06:18 -04:00
pfrazee 3a997031e0 Nightly source-language update 2026-05-05 03:17:16 +00:00
DS Boyce 482fb77e36 Re-enable group invite link management (#10407) 2026-05-04 15:44:27 -07:00
DS Boyce e34080c4a0 Fix some minor issues with the invite link dialog (#10408) 2026-05-04 14:44:31 -07:00
DS Boyce 923d06229c Don't sqaure corners around emoji-only messages (#10411) 2026-05-04 14:44:08 -07:00
Eric Bailey 098ffcad3f Bump tapper/sift (#10412) 2026-05-04 16:10:41 -05:00
Samuel Newman 1b3b8e493a [Chat] Defer message send until after textinput resizes (#10405) 2026-05-04 14:05:15 -07:00
Samuel Newman dc9b9a887b [Chat] Reenable the gc gate (#10406) 2026-05-04 10:24:25 -07:00
ave 2c5d7f20b0 Remove unnecessary 90ms latency from tap events on android (#10392) 2026-05-04 05:39:26 -07:00
pfrazee 85c7cefd7c Nightly source-language update 2026-04-30 03:17:28 +00:00
DS Boyce f1764f1d61 Move useListConvoMembersQuery inside AddMembersFlow (#10386) 2026-04-29 16:49:47 +01:00
Samuel Newman 736c9625ed Fix end of feed border color (#10393) 2026-04-29 07:19:23 -07:00
DS Boyce b97cd413e3 Update CLAUDE.md with additional context and instructions (#10385) 2026-04-29 03:53:07 -07:00
Evan 8bfce09628 Fix search query autocomplete lagging behind (#10381) 2026-04-28 06:22:48 -07:00
DS Boyce 28327459e4 Enable @typescript-eslint/no-explicit-any (#10383) 2026-04-28 10:59:04 +01:00
Samuel Newman 179f3c82e7 Use tsgo for typechecking (#9474) 2026-04-28 01:44:15 -07:00
Samuel Newman 46b8a5819b Harden iOS build extraction in bundle-deploy workflow (#10372)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-25 10:45:15 +03:00
pfrazee 8dacc91908 Nightly source-language update 2026-04-25 03:12:59 +00:00
DS Boyce 5bf38e3721 Fix avatar bubble sizes when there's only one chat member (#10369) 2026-04-24 13:59:07 -07:00
Samuel Newman 7eec65ac78 [Chat] Temporarily turn off gate (#10368) 2026-04-24 22:45:22 +03:00
Eric Bailey cdb8d4bfb8 Group Clops Feature Branch (#10360)
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 11:38:50 -07:00
Samuel Newman e832791367 [Chat] Improve gate (#10359) 2026-04-24 08:48:57 -05:00
Samuel Newman deb3f26d3e Fix send button disabled when sharing post via DM without text (#10355)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 05:50:08 -05:00
pfrazee a90bb66d67 Nightly source-language update 2026-04-24 03:15:36 +00:00
Eric Bailey 9762a6eef4 See if we can harden the build extraction steps (#10348) 2026-04-23 17:02:17 -05:00
Samuel Newman 7b490d8bcd Only lock viewport zoom while text inputs are focused (#10340)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 10:42:05 -07:00
DS Boyce 6fc31eeb48 Clean up minor issues in MessgeItem (#10338) 2026-04-23 09:30:50 -07:00
pfrazee bf371d7ecc Nightly source-language update 2026-04-23 03:15:31 +00:00
DS Boyce 9ba11baab7 Display actual number of group clip clop requests (#10333) 2026-04-22 12:31:38 -07:00
DS Boyce 8d6d4f9850 Simplify chat margins (#10334) 2026-04-22 12:31:08 -07:00
Samuel Newman 18143d051b Bump version from 1.121.0 to 1.122.0 (#10336) 2026-04-22 22:04:51 +03:00
DS Boyce 38c8adcc27 Fix some minor chat issues (#10332) 2026-04-22 10:18:01 -07:00
DS Boyce dcc06a90a0 Display sender name in last message for group clip clops (#10320)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-04-22 10:03:53 -07:00
Eric Bailey c7e9efbf99 Prep release 1.121.0 (#10331) 2026-04-22 11:04:03 -05:00
Samuel Newman 1c38665d4c Replace lande with native language detection (#9974)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-04-22 10:26:14 -05:00
pfrazee 5d40532aa9 Nightly source-language update 2026-04-22 03:14:44 +00:00
Samuel Newman abe0ca521d Fix slow re-renders when toggling content languages (#10128)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 17:16:26 -05:00
DS Boyce 07344f70fc Fix missing recId values for suggested user events (#10291)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-04-21 15:59:13 -05:00
Samuel Newman bfdaab0a14 [Chat] Workaround for autocomplete suggestion bug on iOS (#10321) 2026-04-21 15:56:07 -05:00
Samuel Newman 3153ea4302 Refactor minimal shell mode to refcounting (#10319) 2026-04-21 13:04:02 -07:00
Samuel Newman 6d53459e92 [Chat] Gate groupchat screen (#10316) 2026-04-21 11:21:19 -07:00
DS Boyce 2ab1e2c9e9 Display system messages in chats (#10312)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-04-21 10:43:22 -07:00
Samuel Newman 0df1d6f53e [Chat] Message context menu fixes (#10314) 2026-04-21 09:24:46 -07:00
Samuel Newman 18052f08e0 Update claude workflow to 4.7 (#10315) 2026-04-21 06:19:52 -07:00
Spence Pope 3931b90818 don't screenshot dictation placeholders as pasted images (#10313) 2026-04-20 23:35:22 -07:00
pfrazee 985129dd34 Nightly source-language update 2026-04-21 03:14:34 +00:00
DS Boyce 8c2e4c6fad Write chat declaration record in response to different events (#10216)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-04-20 15:39:12 -05:00
DS Boyce d58ff89441 Require age assurance to access chat settings (#10053) 2026-04-20 15:34:32 -05:00
Samuel Newman 014ffac903 Delete "non-standard" styles from pal (#10279) 2026-04-20 11:34:25 -07:00
Samuel Newman ae0c2e8697 [Web] Scale animation when clicking on images (#10305) 2026-04-20 13:09:35 -05:00
Samuel Newman b8cabfaae6 Skip empty posts when publishing threads (#10307)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 09:33:38 -07:00
Spence Pope 444c5787c5 [APP-2037] add bsky video (#10281)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-04-20 07:21:25 -07:00
surfdude29 2798e98c9c Add context for translators and tweak strings (#10280) 2026-04-20 07:06:18 -07:00
Samuel Newman 9778a3da16 Remove decelerationRate from embed gallery FlatList (#10298)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-20 06:54:25 -07:00
Samuel Newman 52b8201d2f Add READMEs to modules (#10306) 2026-04-20 06:53:04 -07:00
Thomas May dddc022747 nit: fix perf regression in hotkeys (#10302)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-04-20 06:15:05 -07:00
pfrazee 5df51bdea7 Nightly source-language update 2026-04-19 03:14:38 +00:00
Samuel Newman bc3672ceeb [Chat] Remove convoState dependency from header (#10293) 2026-04-18 12:58:16 -07:00
pfrazee 35411e88c9 Nightly source-language update 2026-04-18 03:13:09 +00:00
DS Boyce 935347c73d Use convo view instead of convo state for driving the UI (#10290) 2026-04-17 15:23:44 -07:00
DS Boyce feebc6a98b Fix layout issue with avatar bubbles (#10289) 2026-04-17 13:50:41 -07:00
Eric Bailey 587dc8dfe8 Squash carousel in chat messages, fix rounding (#10286) 2026-04-17 14:24:42 -05:00
Samuel Newman 43108533eb [Chat] Fix embed offset (#10285) 2026-04-17 11:44:48 -07:00
DS Boyce adca192f3a Add ability to edit group clip clop name (#10275) 2026-04-17 11:44:34 -07:00
Samuel Newman 591504307d [Chat] Minor bugfixes (#10284) 2026-04-17 10:50:52 -07:00
DS Boyce bc9ad2c2d9 Fix crash when deleting account from NoAccessScreen (#10283) 2026-04-17 10:48:08 -07:00
DS Boyce 9e9ff70682 Enable access for group clip clops with no messages (#10276) 2026-04-17 10:40:37 -07:00
Samuel Newman 226a321a27 Fix GrowthBook cache breaking on cold start (#10282)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 10:12:12 -07:00
Samuel Newman e58feaeb0f Animate border radius in lightbox transition (#10272) 2026-04-17 05:45:56 -07:00
Spence Pope a77b6e3525 [APP-2066] Migrate from Tenor to KLIPY (#10240)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 08:43:13 -04:00
Samuel Newman a97b15b204 EmojiPicker component (#10249) 2026-04-17 02:42:10 -07:00
pfrazee 8f56fca82c Nightly source-language update 2026-04-17 03:14:49 +00:00
DS Boyce 524cbc514d Don't toggle the date divider when tapping a reaction (#10274) 2026-04-16 13:06:09 -07:00
Samuel Newman 3358e1947b Lazily measure lightbox thumbnails (#10270) 2026-04-16 12:24:11 -07:00
DS Boyce 6e3c9c3a9f Add flow for adding people to a group clip clop (#10255) 2026-04-16 11:55:19 -07:00
DS Boyce ac68cfe98c Remove own reaction from reactions dialog on tap (#10231)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-04-16 11:16:43 -07:00
DS Boyce 36c95d7dc6 Remove console log (#10273) 2026-04-16 10:57:48 -07:00
DS Boyce 9f3c21e298 Add header to group clip clops (#10254)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-04-16 10:48:30 -07:00
DS Boyce e10c05d735 Reveal chat timestamp on tap (#10262) 2026-04-16 10:17:01 -07:00
DS Boyce a9e170b6d0 Integrate more group chat settings (#10253) 2026-04-16 10:15:59 -07:00
Samuel Newman f51602b3fe Support group chats across other surfaces (#10266) 2026-04-16 09:55:30 -07:00
Eric Bailey cc8f22887f [APP-2031] DM composer fast-follows (#10269) 2026-04-16 11:37:03 -05:00
Samuel Newman cc861093c2 Make padding equal in the bottom bar (#10265) 2026-04-16 08:31:38 -07:00
Eric Bailey 8c5899fc93 Add warning log to gates init (#10268) 2026-04-16 10:06:47 -05:00
pfrazee 6d8b4a2070 Nightly source-language update 2026-04-16 03:15:06 +00:00
Eric Bailey 5fb3af71c6 Add gate debug code (#10264) 2026-04-15 20:43:01 -05:00
903 changed files with 318456 additions and 143471 deletions
+1
View File
@@ -0,0 +1 @@
alt-blur@3076a2b5a593b0d7b37198b5aebfe3a222afd52d
+9
View File
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(pnpm typecheck *)",
"Bash(pnpm lint *)",
"Bash(pnpm test *)"
]
}
}
+63
View File
@@ -0,0 +1,63 @@
You are an experienced senior React Native engineer reviewing a pull
request in the Bluesky Social app — a cross-platform (iOS, Android, Web)
React Native + Expo application. Read the repo's CLAUDE.md before forming
an opinion; it describes the architecture, the ALF design system, and the
codebase conventions.
Your audience is other senior engineers. Write peer-to-peer, not
teacher-to-junior. Most PRs in this repo are fine; a review that says so
is a valid and common outcome.
Report a finding only if you can name a concrete scenario — specific
input, platform, navigation path, or operating condition — in which the
change causes incorrect behavior, a crash, a visual regression, a test
failure, a security issue, or a real regression visible to users. Style,
naming, and micro-optimizations are out of scope unless they introduce a
defect. Do not speculate that a change "might" break unrelated code
without pointing to the specific caller or code path. Do not repeat what
the diff does.
Where this codebase differs from a typical web app:
- Three platforms from one codebase. Web-only APIs (DOM, window),
native-only modules, and platform-specific files (.web.tsx, .ios.tsx,
.android.tsx) are common sources of single-platform breakage. When a
change touches shared code, consider all three targets.
- User-facing strings must go through Lingui (the `Trans` macro /
`useLingui`). Hardcoded English strings in UI are a finding. Do not
flag missing translations in catalog files — extraction and
compilation run in CI.
- New UI should use ALF (`#/alf`, `#/components`) rather than legacy
patterns (`#/view/com`, StyleSheet.create); flag newly written code
that adopts deprecated patterns, but don't flag pre-existing code the
PR merely touches.
- Server state lives in TanStack Query under src/state/queries. Watch
for cache-shape changes without corresponding invalidation updates,
and optimistic updates that can leave stale cache on failure.
- List rendering is performance-critical (the main feed). Changes to
feed items, FlatList usage, or anything in a hot render path deserve
scrutiny for re-render storms — unstable callback/object identities
passed to memoized children, missing memoization on expensive
computation.
- Moderation and content-filtering logic (labels, mutes, blocks,
hidden posts) is trust-and-safety-critical: a regression that shows
content that should be filtered is a blocking finding.
- Deep links, push-notification routing, and the navigation state
machine have platform-specific edge cases; changes there should name
the platforms they were verified on.
- The embed (bskyembed) and web deployment surfaces (bskyweb, link,
ogcard services in Go) ship separately from the app; changes there
have their own blast radius.
For each finding, state the scenario in one or two sentences, cite
file:line, and mark severity (blocking / non-blocking). If you are
uncertain but the potential impact is high (crash on startup, moderation
bypass, broken auth), include it and say what you are uncertain about.
Otherwise, prefer silence over guessing.
If there are no findings that meet this bar, say briefly that the PR
looks fine and note what you checked.
Post your review as a single top-level PR comment. Per-finding inline
comments are also welcome where they'd anchor a reader to the specific
lines involved.
+44
View File
@@ -0,0 +1,44 @@
version: 2
# Dependabot auto-update config.
#
# Cooldown (7 days) is the point of this config: it delays version-update
# PRs until a newly-published version has aged. Supply-chain attacks like
# the tanstack Shai-Hulud compromise (2026-05-11) live minutes-to-hours
# before the registry yanks them; a 7-day cooldown keeps poisoned
# versions out of our lockfiles.
#
# Security updates bypass cooldown and continue to flow immediately. See:
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#cooldown
#
# Auto-merge is deliberately NOT enabled. Every dependabot PR gets human
# review.
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: monday
cooldown:
default-days: 7
open-pull-requests-limit: 5
groups:
production:
dependency-type: production
update-types: [minor, patch]
development:
dependency-type: development
update-types: [minor, patch]
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
cooldown:
default-days: 7
open-pull-requests-limit: 3
groups:
actions:
patterns: ["*"]
update-types: [minor, patch]
@@ -22,13 +22,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
@@ -36,7 +36,7 @@ jobs:
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
@@ -51,7 +51,7 @@ jobs:
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
@@ -23,13 +23,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME }}
@@ -37,7 +37,7 @@ jobs:
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
@@ -46,7 +46,7 @@ jobs:
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
@@ -22,13 +22,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
@@ -36,7 +36,7 @@ jobs:
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
@@ -45,7 +45,7 @@ jobs:
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
+8 -10
View File
@@ -1,11 +1,9 @@
name: build-and-push-link-aws
on:
workflow_dispatch:
pull_request:
paths:
- "bskylink/**"
- "Dockerfile.bskylink"
- ".github/workflows/build-and-push-link-aws.yaml"
push:
branches:
- main
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
@@ -24,13 +22,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
@@ -38,7 +36,7 @@ jobs:
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
@@ -47,7 +45,7 @@ jobs:
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
push: true
@@ -1,11 +1,9 @@
name: build-and-push-ogcard-aws
on:
workflow_dispatch:
pull_request:
paths:
- "bskyogcard/**"
- "Dockerfile.bskyogcard"
- ".github/workflows/build-and-push-ogcard-aws.yaml"
push:
branches:
- main
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
@@ -24,13 +22,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
@@ -38,7 +36,7 @@ jobs:
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
@@ -47,7 +45,7 @@ jobs:
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
push: true
+148 -97
View File
@@ -10,6 +10,23 @@ on:
options:
- testflight-android
- production
workflow_call:
inputs:
profile:
type: string
description: Build profile to use
required: true
outputs:
package-version:
description: Version from package.json
value: ${{ jobs.build.outputs.package-version }}
version-code:
description: Android version code
value: ${{ jobs.build.outputs.version-code }}
# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code
permissions:
contents: read
jobs:
build:
@@ -19,6 +36,10 @@ jobs:
concurrency:
group: android-build
cancel-in-progress: false
outputs:
package-version: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}
version-code: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}
apk-artifact-name: build-${{ steps.timestamp.outputs.time }}.apk
steps:
- name: Check for EXPO_TOKEN
run: >
@@ -28,42 +49,42 @@ jobs:
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 5
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: yarn
node-version-file: package.json
cache: pnpm
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
uses: dcarbone/install-jq-action@b7ef57d46ece78760b4019dbc4080a1ba2a40b45 # v3.2.0
- name: 🔨 Setup EAS
uses: expo/expo-github-action@main
- name: ⚙️ Install dependencies
run: pnpm install --frozen-lockfile
- name: 🔨 Setup Expo CLI
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
with:
expo-version: latest
eas-version: latest
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup EAS local builds
run: yarn global add eas-cli-local-build-plugin
- uses: actions/setup-java@v4
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: "temurin"
java-version: "17"
- name: ⚙️ Install dependencies
run: yarn install --frozen-lockfile
- name: 🔤 Compile translations
run: yarn intl:build 2>&1 | tee i18n.log
run: pnpm intl:build 2>&1 | tee i18n.log
- name: Check for i18n compilation errors
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation
errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
@@ -88,110 +109,81 @@ jobs:
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p android
pnpm use-build-number-with-bump
pnpm eas build -p android
--profile $PROFILE
--local --output build.aab --non-interactive
- name: ✍️ Rename Testflight bundle
if: ${{ inputs.profile != 'production' }}
run: mv build.aab build.apk
- name: ⏰ Get a timestamp
id: timestamp
uses: nanzm/get-time-action@master
with:
format: "MM-DD-HH-mm-ss"
- name: 🚀 Upload Production Artifact
id: upload-artifact-production
if: ${{ inputs.profile == 'production' }}
uses: actions/upload-artifact@v4
with:
retention-days: 30
compression-level: 6
name: build-${{ steps.timestamp.outputs.time }}.aab
path: build.aab
- name: 🚀 Upload Testflight Artifact
id: upload-artifact-testflight
if: ${{ inputs.profile != 'production' }}
uses: actions/upload-artifact@v4
with:
retention-days: 30
compression-level: 6
name: build-${{ steps.timestamp.outputs.time }}.apk
path: build.apk
- name: 📚 Get version from package.json
id: get-build-info
run: bash scripts/setGitHubOutput.sh
- name: 🔔 Notify Slack of Production Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
"text": "Android production build for Google Play Store submission is ready!\n```Artifact: ${{ steps.upload-artifact-production.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
}
- name: 🚀 Submit to Google Play
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
PROFILE: ${{ inputs.profile || 'testflight-android' }}
run: pnpm eas submit -p android --profile $PROFILE --non-interactive --path build.aab
- name: 🔔 Notify Slack of Testflight Build
if: ${{ inputs.profile != 'production' }}
uses: slackapi/slack-github-action@v1.25.0
- name: 🔔 Notify Slack of Play Store Submission
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact-testflight.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
{"text": "Android ${{ inputs.profile || 'testflight-android' }} build submitted to Google Play!\n```Version Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
- name: 🧹 Clear Metro cache
if: ${{ inputs.profile == 'production' }}
# https://github.com/expo/eas-cli/issues/2959#issuecomment-2749791326
run: rm -rf ${TMPDIR:-/tmp}/metro-cache
- name: 🔧 Setup bundletool
uses: amyu/setup-bundletool@cc2e1857284660bd625e43f2c8a45626f034302f # v1.1
with:
bundletool-version: "1.17.2"
- name: 🏗️ Build Production APK
if: ${{ inputs.profile == 'production' }}
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p android
--profile production-apk
--local --output build.apk --non-interactive
- name: 🔑 Decode keystore
run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode >
keystore.jks
- name: 🚀 Upload Production APK Artifact
id: upload-artifact-production-apk
if: ${{ inputs.profile == 'production' }}
uses: actions/upload-artifact@v4
- name: 📦 Build signed universal APK
run: |
bundletool build-apks \
--bundle=build.aab \
--output=universal.apks \
--mode=universal \
--ks=keystore.jks \
--ks-pass=pass:${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \
--ks-key-alias=${{ secrets.ANDROID_KEY_ALIAS }} \
--key-pass=pass:${{ secrets.ANDROID_KEY_PASSWORD }}
- name: 📋 Rename to .zip for extraction
run: mv universal.apks universal.zip
- name: 📦 Extract universal APK
run: unzip -p universal.zip universal.apk > build.apk
- name: ⏰ Get a timestamp
id: timestamp
run: echo "time=$(date -u +'%m-%d-%H-%M-%S')" >> "$GITHUB_OUTPUT"
- name: 🚀 Upload APK Artifact
id: upload-artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
retention-days: 30
compression-level: 6
name: build-${{ steps.timestamp.outputs.time }}.apk
path: build.apk
- name: 🔔 Notify Slack of Production APK Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0
- name: 🔔 Notify Slack of APK Artifact
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{
"text": "Android production build for GitHub/Obtanium is ready!\n```Artifact: ${{ steps.upload-artifact-production-apk.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
{"text": "Android ${{ inputs.profile || 'testflight-android' }} APK is ready for testing!\n```Artifact: ${{ steps.upload-artifact.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: ${{ inputs.profile == 'testflight-android' }}
with:
path: most-recent-testflight-commit.txt
@@ -202,3 +194,62 @@ jobs:
env:
GITHUB_SHA: ${{ github.sha }}
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
# Releases are cut from tags named after the version (e.g. "1.124.0"), so when a production
# build is dispatched against such a tag we attach the APK to the matching release. This runs
# as a separate job so that `contents: write` is isolated here and the build job stays read-only.
attachToRelease:
name: Attach APK to GitHub Release
runs-on: ubuntu-latest
needs: [build]
if: ${{ inputs.profile == 'production' && github.ref_type == 'tag' && github.repository == 'bluesky-social/social-app' }}
permissions:
contents: write
steps:
# We only attach to a release that already exists — never create one.
- name: 🔎 Check for matching GitHub Release
id: release-check
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
run: |
status=$(curl -sS -o /dev/null -w '%{http_code}' \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}")
if [ "$status" = "200" ]; then
echo "Found GitHub Release for tag $TAG"
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "No GitHub Release found for tag $TAG (HTTP $status); skipping APK attachment."
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: ⬇️ Download APK artifact
if: ${{ steps.release-check.outputs.exists == 'true' }}
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: ${{ needs.build.outputs.apk-artifact-name }}
- name: 🏷️ Rename APK for release
if: ${{ steps.release-check.outputs.exists == 'true' }}
run: cp build.apk "Bluesky-${{ needs.build.outputs.package-version }}.apk"
- name: 📎 Attach APK to GitHub Release
id: attach
if: ${{ steps.release-check.outputs.exists == 'true' }}
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ github.ref_name }}
files: Bluesky-${{ needs.build.outputs.package-version }}.apk
fail_on_unmatched_files: true
- name: 🔔 Notify Slack of Release Attachment
if: ${{ steps.release-check.outputs.exists == 'true' }}
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{"text": "Android APK attached to GitHub Release ${{ github.ref_name }}!\n```Asset: Bluesky-${{ needs.build.outputs.package-version }}.apk\nRelease: ${{ steps.attach.outputs.url }}```"}
+151 -40
View File
@@ -10,6 +10,31 @@ on:
options:
- testflight
- production
assignTestFlightGroup:
type: boolean
description: Assign the build to the "QA Team" TestFlight group after submitting
default: false
workflow_call:
inputs:
profile:
type: string
description: Build profile to use
required: true
assignTestFlightGroup:
type: boolean
description: Assign the build to the "QA Team" TestFlight group after submitting
default: false
outputs:
package-version:
description: Version from package.json
value: ${{ jobs.build.outputs.package-version }}
build-number:
description: iOS build number
value: ${{ jobs.build.outputs.build-number }}
# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code
permissions:
contents: read
jobs:
build:
@@ -19,6 +44,9 @@ jobs:
concurrency:
group: ios-build
cancel-in-progress: false
outputs:
package-version: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}
build-number: ${{ steps.ipa-build-number.outputs.build-number }}
steps:
- name: Check for EXPO_TOKEN
run: >
@@ -28,55 +56,61 @@ jobs:
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 5
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: yarn
node-version-file: package.json
cache: pnpm
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
- name: 🔨 Setup EAS
uses: expo/expo-github-action@main
with:
expo-version: latest
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup EAS local builds
run: yarn global add eas-cli-local-build-plugin
uses: dcarbone/install-jq-action@b7ef57d46ece78760b4019dbc4080a1ba2a40b45 # v3.2.0
- name: ⚙️ Install dependencies
run: yarn install --frozen-lockfile
run: pnpm install --frozen-lockfile
- uses: maxim-lobanov/setup-xcode@v1
- name: 🔨 Setup Expo CLI
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
with:
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: "26.4"
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
with:
version: 1.16.2
- name: ☕️ Assert Cocoapods version
run: |
EXPECTED=1.16.2
ACTUAL=$(pod --version)
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "Expected Cocoapods $EXPECTED but runner has $ACTUAL."
echo "The version ships preinstalled with the macOS runner image: https://github.com/actions/runner-images/blob/main/images/macos/macos-26-Readme.md"
echo "If the runner image changed, update EXPECTED here or reinstall the pinned version."
exit 1
fi
- name: 💾 Cache Pods
uses: actions/cache@v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: pods-cache
with:
path: ./ios/Pods
# We'll use the yarn.lock for our hash since we don't yet have a Podfile.lock. Pod versions will not
# change unless the yarn version changes as well.
key: ${{ runner.os }}-pods-${{ hashFiles('yarn.lock') }}
# We'll use the pnpm-lock.yaml for our hash since we don't yet have a Podfile.lock. Pod versions will not
# change unless the pnpm version changes as well.
key: ${{ runner.os }}-pods-${{ hashFiles('pnpm-lock.yaml') }}
- name: 🔤 Compile translations
run: yarn intl:build 2>&1 | tee i18n.log
run: pnpm intl:build 2>&1 | tee i18n.log
- name: Check for i18n compilation errors
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation
errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
# EXPO_PUBLIC_ENV is handled in eas.json
- name: ✏️ Write environment variables
@@ -100,8 +134,8 @@ jobs:
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p ios
pnpm use-build-number-with-bump
pnpm eas build -p ios
--profile $PROFILE
--local --output build.tar.gz --non-interactive
@@ -109,43 +143,120 @@ jobs:
run: |
if [ -f "build.tar.gz" ]; then
echo "Extracting build.tar.gz..."
mkdir ios-build
rm -rf ios-build
mkdir -p ios-build
tar -xzf build.tar.gz -C ios-build
echo "Extraction completed successfully"
echo ""
echo "Top-level extracted files:"
find ios-build -maxdepth 3 -print
echo ""
echo "Searching for IPA..."
IPA_PATH="$(find ios-build -type f -name '*.ipa' -print -quit)"
if [ -z "$IPA_PATH" ]; then
echo "ERROR: No .ipa found anywhere under ios-build."
echo "Archive contents:"
tar -tzf build.tar.gz | sed -n '1,200p'
exit 1
fi
BUILD_DIR="$(dirname "$IPA_PATH")"
echo "Found IPA at: $IPA_PATH"
echo "Build dir: $BUILD_DIR"
echo ""
echo "Build dir contents:"
ls -la "$BUILD_DIR"
echo "BUILD_DIR=$BUILD_DIR" >> $GITHUB_ENV
else
echo "Archive file not found!"
exit 1
fi
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path ios-build/ios/build/Bluesky.ipa
run: pnpm eas submit -p ios --non-interactive --path "$BUILD_DIR/Bluesky.ipa"
- name: 🪲 Upload dSYM to Sentry
run: >
SENTRY_ORG=blueskyweb
SENTRY_PROJECT=app
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
yarn sentry-cli debug-files upload ios-build/ios/build/Bluesky.app.dSYM.zip --include-sources
pnpm sentry-cli debug-files upload "$BUILD_DIR/Bluesky.app.dSYM.zip" --include-sources
- name: 📚 Get version from package.json
id: get-build-info
run: bash scripts/setGitHubOutput.sh
# Read the build number straight from the IPA's CFBundleVersion. This is the value
# baked in at build time by use-build-number-with-bump (remote counter + 1) and the
# number that actually lands in App Store Connect. `eas build:version:get` reads the
# remote counter, which a --local build does not advance, so it can be off by one —
# using it here would make distribute_only poll for a nonexistent build.
- name: 🔢 Read build number from IPA
id: ipa-build-number
run: |
plist_dir="$(mktemp -d)"
unzip -o -q "$BUILD_DIR/Bluesky.ipa" 'Payload/*.app/Info.plist' -d "$plist_dir"
plist="$(find "$plist_dir" -name Info.plist -print -quit)"
build_number="$(/usr/libexec/PlistBuddy -c 'Print CFBundleVersion' "$plist")"
rm -rf "$plist_dir"
if [ -z "$build_number" ]; then
echo "ERROR: could not read CFBundleVersion from IPA"
exit 1
fi
echo "IPA build number: $build_number"
echo "build-number=$build_number" >> "$GITHUB_OUTPUT"
# eas submit only uploads to App Store Connect; it can't assign a build to a
# TestFlight group. fastlane's distribute_only mode skips the upload and assigns the
# already-submitted build to the group, polling until Apple finishes processing it.
- name: 🧪 Assign build to TestFlight group
if: ${{ inputs.assignTestFlightGroup }}
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }}
APP_VERSION: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}
BUILD_NUMBER: ${{ steps.ipa-build-number.outputs.build-number }}
run: |
# Ensure the API key material is removed even if fastlane exits non-zero
# (the step runs under `bash -e`, which would otherwise abort before cleanup).
trap 'rm -f asc_api_key.json' EXIT
# fastlane's Token.from_json_file expects the .p8 contents inline under "key"
# (PEM with embedded newlines), not a path. jq handles the newline escaping.
key_content="$(echo "$ASC_KEY_P8_BASE64" | base64 --decode)"
jq -n \
--arg key_id "$ASC_KEY_ID" \
--arg issuer_id "$ASC_ISSUER_ID" \
--arg key "$key_content" \
'{key_id: $key_id, issuer_id: $issuer_id, key: $key, in_house: false}' \
> asc_api_key.json
# app_platform is required in non-interactive mode: distribute_only otherwise
# calls fetch_app_platform, which prompts for input and crashes without a TTY.
fastlane run upload_to_testflight \
api_key_path:"$PWD/asc_api_key.json" \
distribute_only:true \
app_platform:"ios" \
app_identifier:"xyz.blueskyweb.app" \
app_version:"$APP_VERSION" \
build_number:"$BUILD_NUMBER" \
groups:"QA Team" \
notify_external_testers:true
- name: 🔔 Notify Slack of Production Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{
"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_IOS_BUILD_NUMBER }}```"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
{"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.ipa-build-number.outputs.build-number }}```"}
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: ${{ inputs.profile == 'testflight' }}
with:
path: most-recent-testflight-commit.txt
+164 -84
View File
@@ -18,6 +18,10 @@ on:
description: Runtime version (in x.x.x format) that this update is for
required: true
# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code
permissions:
contents: read
jobs:
bundleDeploy:
if: github.repository == 'bluesky-social/social-app'
@@ -48,7 +52,7 @@ jobs:
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
@@ -56,52 +60,48 @@ jobs:
if: ${{ github.ref != 'refs/heads/main' }}
run: git fetch origin main:main --depth 100
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: yarn
node-version-file: package.json
cache: pnpm
- name: 📷 Check fingerprint and install dependencies
id: fingerprint
uses: bluesky-social/github-actions/fingerprint-native@main
uses: bluesky-social/github-actions/fingerprint-native@ebc6aa6d7466dc1e78b1e832041b7b81f6f95030 # v0.1.0
with:
profile: ${{ inputs.channel || 'testflight' }}
previous-commit-tag: ${{ inputs.runtimeVersion }}
- name: Lint check
run: yarn lint
- name: Lint lockfile
run: yarn lockfile-lint
- name: Prettier check
run: yarn prettier --check .
- name: 🔤 Compile translations
run: yarn intl:build 2>&1 | tee i18n.log
run: pnpm intl:build 2>&1 | tee i18n.log
- name: Check for i18n compilation errors
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation
errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
- name: Lint check
run: pnpm lint
- name: Prettier check
run: pnpm prettier --check .
- name: Type check
run: yarn typecheck
run: pnpm typecheck
- name: 🔨 Setup EAS
uses: expo/expo-github-action@main
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
if: ${{ !steps.fingerprint.outputs.includes-changes }}
with:
expo-version: latest
eas-version: latest
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup Expo
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: yarn global add eas-cli-local-build-plugin
- name: 🪛 Setup jq
if: ${{ !steps.fingerprint.outputs.includes-changes }}
uses: dcarbone/install-jq-action@v2
uses: dcarbone/install-jq-action@b7ef57d46ece78760b4019dbc4080a1ba2a40b45 # v3.2.0
# eas.json not used here, set EXPO_PUBLIC_ENV
- name: Env
@@ -130,11 +130,11 @@ jobs:
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn export
pnpm export
- name: 📦 Package Bundle and 🚀 Deploy
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: yarn use-build-number bash scripts/bundleUpdate.sh
run: pnpm use-build-number bash scripts/bundleUpdate.sh
env:
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
@@ -142,7 +142,7 @@ jobs:
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: ${{ !steps.fingerprint.outputs.includes-changes }}
with:
path: most-recent-testflight-commit.txt
@@ -162,7 +162,9 @@ jobs:
needs: [bundleDeploy]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected && github.repository == 'bluesky-social/social-app' }}
if: ${{ inputs.channel != 'production' &&
needs.bundleDeploy.outputs.changes-detected && github.repository ==
'bluesky-social/social-app' }}
steps:
- name: Check for EXPO_TOKEN
run: >
@@ -172,49 +174,54 @@ jobs:
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 5
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: yarn
node-version-file: package.json
cache: pnpm
- name: 🔨 Setup EAS
uses: expo/expo-github-action@main
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
with:
expo-version: latest
eas-version: latest
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup EAS local builds
run: yarn global add eas-cli-local-build-plugin
- name: ⚙️ Install dependencies
run: yarn install --frozen-lockfile
run: pnpm install --frozen-lockfile
- uses: maxim-lobanov/setup-xcode@v1
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: "26.4"
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
with:
version: 1.16.2
- name: ☕️ Assert Cocoapods version
run: |
EXPECTED=1.16.2
ACTUAL=$(pod --version)
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "Expected Cocoapods $EXPECTED but runner has $ACTUAL."
echo "The version ships preinstalled with the macOS runner image: https://github.com/actions/runner-images/blob/main/images/macos/macos-26-Readme.md"
echo "If the runner image changed, update EXPECTED here or reinstall the pinned version."
exit 1
fi
- name: 💾 Cache Pods
uses: actions/cache@v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: pods-cache
with:
path: ./ios/Pods
# We'll use the yarn.lock for our hash since we don't yet have a Podfile.lock. Pod versions will not
# change unless the yarn version changes as well.
key: ${{ runner.os }}-pods-${{ hashFiles('yarn.lock') }}
# We'll use the pnpm-lock.yaml for our hash since we don't yet have a Podfile.lock. Pod versions will not
# change unless the pnpm version changes as well.
key: ${{ runner.os }}-pods-${{ hashFiles('pnpm-lock.yaml') }}
- name: 🔤 Compile translations
run: yarn intl:build
run: pnpm intl:build
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
@@ -236,17 +243,59 @@ jobs:
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p ios
pnpm use-build-number-with-bump
pnpm eas build -p ios
--profile testflight
--local --output build.ipa --non-interactive
--local --output build.tar.gz --non-interactive
- name: 📂 Extract build artifact
run: |
if [ -f "build.tar.gz" ]; then
echo "Extracting build.tar.gz..."
rm -rf ios-build
mkdir -p ios-build
tar -xzf build.tar.gz -C ios-build
echo "Extraction completed successfully"
echo ""
echo "Top-level extracted files:"
find ios-build -maxdepth 3 -print
echo ""
echo "Searching for IPA..."
IPA_PATH="$(find ios-build -type f -name '*.ipa' -print -quit)"
if [ -z "$IPA_PATH" ]; then
echo "ERROR: No .ipa found anywhere under ios-build."
echo "Archive contents:"
tar -tzf build.tar.gz | sed -n '1,200p'
exit 1
fi
BUILD_DIR="$(dirname "$IPA_PATH")"
echo "Found IPA at: $IPA_PATH"
echo "Build dir: $BUILD_DIR"
echo ""
echo "Build dir contents:"
ls -la "$BUILD_DIR"
echo "BUILD_DIR=$BUILD_DIR" >> $GITHUB_ENV
else
echo "Archive file not found!"
exit 1
fi
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
run: pnpm eas submit -p ios --non-interactive --path "$BUILD_DIR/Bluesky.ipa"
- name: 🪲 Upload dSYM to Sentry
run: >
SENTRY_ORG=blueskyweb
SENTRY_PROJECT=app
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
pnpm sentry-cli debug-files upload "$BUILD_DIR/Bluesky.app.dSYM.zip" --include-sources
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: ${{ inputs.channel == 'testflight' }}
with:
path: most-recent-testflight-commit.txt
@@ -267,7 +316,9 @@ jobs:
needs: [bundleDeploy]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected && github.repository == 'bluesky-social/social-app'}}
if: ${{ inputs.channel != 'production' &&
needs.bundleDeploy.outputs.changes-detected && github.repository ==
'bluesky-social/social-app'}}
steps:
- name: Check for EXPO_TOKEN
@@ -278,36 +329,35 @@ jobs:
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 5
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: yarn
node-version-file: package.json
cache: pnpm
- name: 🔨 Setup EAS
uses: expo/expo-github-action@main
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
with:
expo-version: latest
eas-version: latest
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup EAS local builds
run: yarn global add eas-cli-local-build-plugin
- uses: actions/setup-java@v4
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: "temurin"
java-version: "17"
- name: ⚙️ Install dependencies
run: yarn install --frozen-lockfile
run: pnpm install --frozen-lockfile
- name: 🔤 Compile translations
run: yarn intl:build
run: pnpm intl:build
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
@@ -330,20 +380,52 @@ jobs:
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p android
pnpm use-build-number-with-bump
pnpm eas build -p android
--profile testflight-android
--local --output build.apk --non-interactive
--local --output build.aab --non-interactive
- name: 📚 Get version from package.json
id: get-build-info
run: bash scripts/setGitHubOutput.sh
- name: 🚀 Submit to Google Play
run: pnpm eas submit -p android --profile testflight-android --non-interactive --path
build.aab
- name: 🔧 Setup bundletool
uses: amyu/setup-bundletool@cc2e1857284660bd625e43f2c8a45626f034302f # v1.1
with:
bundletool-version: "1.17.2"
- name: 🔑 Decode keystore
run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode >
keystore.jks
- name: 📦 Build signed universal APK
run: |
bundletool build-apks \
--bundle=build.aab \
--output=universal.apks \
--mode=universal \
--ks=keystore.jks \
--ks-pass=pass:${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \
--ks-key-alias=${{ secrets.ANDROID_KEY_ALIAS }} \
--key-pass=pass:${{ secrets.ANDROID_KEY_PASSWORD }}
- name: 📋 Rename to .zip for extraction
run: mv universal.apks universal.zip
- name: 📦 Extract universal APK
run: unzip -p universal.zip universal.apk > build.apk
- name: ⏰ Get a timestamp
id: timestamp
uses: nanzm/get-time-action@master
with:
format: "MM-DD-HH-mm-ss"
run: echo "time=$(date -u +'%m-%d-%H-%M-%S')" >> "$GITHUB_OUTPUT"
- name: 🚀 Upload Artifact
id: upload-artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
retention-days: 30
compression-level: 0
@@ -351,19 +433,17 @@ jobs:
path: build.apk
- name: 🔔 Notify Slack
uses: slackapi/slack-github-action@v1.25.0
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
{"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"}
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
with:
path: most-recent-testflight-commit.txt
+76
View File
@@ -0,0 +1,76 @@
name: claude-mention
# @claude mention handler on PR conversation comments, inline review
# comments, and review bodies. See the header comment in
# claude-review.yml for why this is self-contained rather than calling
# the org reusable workflows.
#
# Authorization note: this repo is public, and the load-bearing gate
# against drive-by commenters is INSIDE claude-code-action — the action
# verifies via the API that the triggering actor has write permission
# before doing anything (`allowed_non_write_users` and `allowed_bots`
# both default to deny). The `if:` below is a cheap pre-filter to avoid
# spinning up runners for the 99% of comments that don't mention
# @claude; it is not the security boundary.
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_review:
types: [submitted]
permissions:
contents: read
pull-requests: write
issues: write
actions: read
id-token: write
jobs:
mention:
# Skip comments/reviews that don't mention @claude, non-PR issue
# comments, and events from claude[bot] itself (its review prose
# often quotes "@claude" and must not retrigger the workflow).
if: >
(
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request != null &&
contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' &&
contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@claude'))
) &&
github.actor != 'claude[bot]'
runs-on: ubuntu-latest
timeout-minutes: 20
concurrency:
group: claude-mention-${{ github.repository }}-${{ github.event.issue.number || github.event.pull_request.number }}
cancel-in-progress: false
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 1
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
with:
role-to-assume: ${{ secrets.AWS_BEDROCK_REVIEW_ROLE_ARN }}
aws-region: us-east-2
- name: Claude
uses: anthropics/claude-code-action@9dd8b95a392eb34b6f5fb56cf5a64cb735912d4b # v1.0.150
with:
use_bedrock: 'true'
additional_permissions: |
actions: read
track_progress: true
claude_args: |
--model global.anthropic.claude-opus-4-8
--allowedTools "mcp__github_inline_comment__create_inline_comment,mcp__github_ci__get_ci_status,mcp__github_ci__download_job_log,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
+68
View File
@@ -0,0 +1,68 @@
name: claude-review
# Automatic Claude review on PR creation/update, via Bedrock (OIDC, no
# long-lived tokens). Self-contained: this intentionally uses upstream
# claude-code-action defaults rather than the org reusable workflows in
# bluesky-social/.github (which a public repo cannot call, and whose
# customizations added no value over upstream).
#
# Review guidance lives in .github/claude-review-prompt.md.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
permissions:
contents: read
pull-requests: write
actions: read
id-token: write
jobs:
review:
# Internal PRs only. This repo is public: fork PRs are the bulk of
# community traffic and MUST NOT trigger reviews (no Bedrock spend on
# unvetted code, and fork PRs can't mint the OIDC token anyway —
# belt-and-braces with this explicit guard). Branch PRs can only be
# created by people with write access, i.e. org members.
# Bot-authored PRs (dependabot, changesets) are also skipped.
if: >
github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.type != 'Bot'
runs-on: ubuntu-latest
timeout-minutes: 20
concurrency:
group: claude-review-${{ github.repository }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 1
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
with:
role-to-assume: ${{ secrets.AWS_BEDROCK_REVIEW_ROLE_ARN }}
aws-region: us-east-2
- name: Claude review
uses: anthropics/claude-code-action@9dd8b95a392eb34b6f5fb56cf5a64cb735912d4b # v1.0.150
with:
use_bedrock: 'true'
additional_permissions: |
actions: read
track_progress: true
claude_args: |
--model global.anthropic.claude-opus-4-8
--allowedTools "mcp__github_inline_comment__create_inline_comment,mcp__github_ci__get_ci_status,mcp__github_ci__download_job_log,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
Read .github/claude-review-prompt.md in the checked-out repo
and review this pull request following its guidance.
-54
View File
@@ -1,54 +0,0 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'
# NOTE(sfn): we can add a custom system prompt here
claude_args: |
--model claude-opus-4-5-20251101
+9 -4
View File
@@ -10,16 +10,20 @@ concurrency:
group: "${{ github.workflow }}-${{ github.head_ref || github.ref }}"
cancel-in-progress: true
permissions:
contents: read
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Git Checkout
uses: actions/checkout@v5
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Go tooling
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: bskyweb/go.mod
cache-dependency-path: bskyweb/go.sum
- name: Dummy Static Files
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
- name: Check
@@ -32,11 +36,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Git Checkout
uses: actions/checkout@v5
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Go tooling
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: bskyweb/go.mod
cache-dependency-path: bskyweb/go.sum
- name: Dummy Static Files
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
- name: Lint
+90 -32
View File
@@ -6,56 +6,114 @@ on:
branches:
- main
concurrency:
group: "${{ github.workflow }}-${{ github.head_ref || github.ref }}"
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: true
# These jobs only check out the repo and run checks, so read access to the
# repo contents is all the GITHUB_TOKEN needs.
permissions:
contents: read
jobs:
linting:
name: Run linters
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job: [lint, prettier, typecheck]
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Verify Node version pins match package.json
run: |
set -euo pipefail
expected=$(node -p "require('./package.json').engines.node.replace(/[^0-9.]/g, '')")
rc=0
check() {
if [ "$2" != "$expected" ]; then
echo "::error file=$1::Node version mismatch: $1 pins '$2' but package.json is '$expected'"
rc=1
fi
}
# FROM node:X.Y.Z - service runtime images
for f in Dockerfile.bskylink Dockerfile.bskyogcard; do
v=$(grep -oE 'FROM node:[0-9]+\.[0-9]+\.[0-9]+' "$f" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sort -u)
check "$f" "$v"
done
# ENV NODE_VERSION=X.Y.Z - Go images that nvm-install Node for the JS build stage
for f in Dockerfile.embedr; do
v=$(grep -oE 'NODE_VERSION=[0-9]+\.[0-9]+\.[0-9]+' "$f" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sort -u)
check "$f" "$v"
done
# eas.json "node": "X.Y.Z"
v=$(grep -oE '"node":[[:space:]]*"[0-9]+\.[0-9]+\.[0-9]+"' eas.json | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sort -u)
check "eas.json" "$v"
exit $rc
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Install node
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: yarn
- name: Yarn install
uses: Wandalen/wretry.action@master
with:
command: yarn --frozen-lockfile
attempt_limit: 3
attempt_delay: 2000
- name: Lint check
run: yarn lint
- name: Lint lockfile
run: yarn lockfile-lint
- name: Prettier check
run: yarn prettier --check .
node-version-file: package.json
cache: pnpm
- name: pnpm install
run: pnpm install --frozen-lockfile
- name: Check & compile i18n
run: yarn intl:build
- name: Type check
run: yarn typecheck
run: pnpm intl:build
- name: Lint checks
run: pnpm ${{ matrix.job }}
# Aggregates the matrix results into a single stable check name so branch
# protection can require "Run linters" regardless of how many matrix jobs run.
# The result is asserted in `run` (not `if`) so a malformed expression can
# never silently skip the check and report a false pass.
linting-summary:
name: Run linters
if: always()
needs: [linting]
runs-on: ubuntu-latest
steps:
- name: Require linting to have succeeded
env:
RESULT: ${{ needs.linting.result }}
run: |
echo "linting result: $RESULT"
test "$RESULT" = "success"
testing:
name: Run tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Install node
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: yarn
- name: Yarn install
uses: Wandalen/wretry.action@master
with:
command: yarn --frozen-lockfile
attempt_limit: 3
attempt_delay: 2000
node-version-file: package.json
cache: pnpm
- name: pnpm install
run: pnpm install --frozen-lockfile
- name: Check & compile i18n
run: yarn intl:build
run: pnpm intl:build
- name: Run tests
run: |
NODE_ENV=test yarn test --forceExit
NODE_ENV=test pnpm test --forceExit --shard=${{ matrix.shard }}/${{ strategy.job-total }}
# Aggregates the sharded test results into a single stable check name so branch
# protection can require "Run tests" regardless of how many shards run.
# The result is asserted in `run` (not `if`) so a malformed expression can
# never silently skip the check and report a false pass.
testing-summary:
name: Run tests
if: always()
needs: [testing]
runs-on: ubuntu-latest
steps:
- name: Require testing to have succeeded
env:
RESULT: ${{ needs.testing.result }}
run: |
echo "testing result: $RESULT"
test "$RESULT" = "success"
+173
View File
@@ -0,0 +1,173 @@
---
name: Nightly Build
on:
schedule:
- cron: "10 2 * * *" # run at 2:10 AM UTC, after the nightly i18n job
workflow_dispatch:
# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code
permissions:
contents: read
jobs:
# Generate the changelog once, shared by both platforms. The range covers commits since
# the previous nightly, whose commit SHA is stored as a "nightly-build-commit" artifact
# (advanced by the record job below, only after both builds succeed).
prepare:
name: Prepare release notes
if: github.repository == 'bluesky-social/social-app'
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
outputs:
notes: ${{ steps.notes.outputs.notes }}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: 📝 Generate release notes
id: notes
env:
GH_TOKEN: ${{ github.token }}
run: |
# Find the most recent non-expired nightly-build-commit artifact and read its SHA
# The artifacts API returns results newest-first, so the most recent marker is on
# page 1 — no --paginate needed (which would run the jq aggregation per page and
# could emit multiple URLs). Take the first non-expired match.
prev=""
url=$(gh api \
"repos/${GITHUB_REPOSITORY}/actions/artifacts?name=nightly-build-commit&per_page=100" \
--jq 'first(.artifacts[] | select(.expired == false)) | .archive_download_url' \
2>/dev/null || true)
if [ -n "$url" ] && [ "$url" != "null" ]; then
if curl -sSL -H "Authorization: Bearer $GH_TOKEN" -o marker.zip "$url" \
&& unzip -o -q marker.zip; then
prev=$(cat nightly-build-commit.txt 2>/dev/null | tr -d '[:space:]')
fi
rm -f marker.zip nightly-build-commit.txt
fi
if [ -n "$prev" ] && git cat-file -e "${prev}^{commit}" 2>/dev/null; then
echo "Generating notes since previous nightly: $prev"
range="${prev}..HEAD"
else
echo "No reachable previous nightly commit; falling back to last 30 commits."
range="HEAD~30..HEAD"
fi
notes=$(git log --no-merges --pretty=format:'- %s' "$range" 2>/dev/null | head -n 50)
if [ -z "$notes" ]; then
notes="Nightly build — no new commits since the last nightly."
fi
# Cap the whole changelog to keep the Slack message a reasonable size.
# head -c caps the combined stream; cut -c would only cap each line independently.
notes=$(printf '%s' "$notes" | head -c 3900)
{
echo "notes<<NOTES_EOF"
echo "$notes"
echo "NOTES_EOF"
} >> "$GITHUB_OUTPUT"
ios:
name: Nightly iOS Build
needs: [prepare]
uses: ./.github/workflows/build-submit-ios.yml
with:
profile: testflight
assignTestFlightGroup: true
secrets: inherit
android:
name: Nightly Android Build
needs: [prepare]
# build-submit-android.yml contains an attachToRelease job that requests contents: write.
# That job is skipped for nightly (it needs a production tag build), but GitHub statically
# validates the reusable-workflow permission ceiling, so the caller must grant it here.
permissions:
contents: write
uses: ./.github/workflows/build-submit-android.yml
with:
profile: testflight-android
secrets: inherit
notify-ios:
name: Notify Slack of iOS nightly
needs: [prepare, ios]
runs-on: ubuntu-latest
steps:
- name: 📝 Build Slack payload
id: payload
env:
NOTES: ${{ needs.prepare.outputs.notes }}
VERSION: ${{ needs.ios.outputs.package-version }}
BUILD_NUMBER: ${{ needs.ios.outputs.build-number }}
run: |
text="*Nightly iOS build available in TestFlight (QA Team)*
Version ${VERSION} (${BUILD_NUMBER})
${NOTES}"
payload=$(jq -n --arg text "$text" '{text: $text}')
{
echo "payload<<PAYLOAD_EOF"
echo "$payload"
echo "PAYLOAD_EOF"
} >> "$GITHUB_OUTPUT"
- name: 🔔 Notify Slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.NIGHTLY_BUILDS_SLACK_WEBHOOK }}
webhook-type: incoming-webhook
payload: ${{ steps.payload.outputs.payload }}
notify-android:
name: Notify Slack of Android nightly
needs: [prepare, android]
runs-on: ubuntu-latest
steps:
- name: 📝 Build Slack payload
id: payload
env:
NOTES: ${{ needs.prepare.outputs.notes }}
VERSION: ${{ needs.android.outputs.package-version }}
VERSION_CODE: ${{ needs.android.outputs.version-code }}
run: |
text="*Nightly Android build available (Internal track)*
Version ${VERSION} (${VERSION_CODE})
${NOTES}"
payload=$(jq -n --arg text "$text" '{text: $text}')
{
echo "payload<<PAYLOAD_EOF"
echo "$payload"
echo "PAYLOAD_EOF"
} >> "$GITHUB_OUTPUT"
- name: 🔔 Notify Slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.NIGHTLY_BUILDS_SLACK_WEBHOOK }}
webhook-type: incoming-webhook
payload: ${{ steps.payload.outputs.payload }}
# Advance the nightly marker only after both builds succeed, so a failed night's commits
# roll into the next successful nightly's notes rather than being silently dropped.
record:
name: Record nightly commit
needs: [ios, android]
runs-on: ubuntu-latest
steps:
- name: ✏️ Write nightly commit marker
env:
GITHUB_SHA: ${{ github.sha }}
run: echo "$GITHUB_SHA" > nightly-build-commit.txt
- name: 🚀 Upload nightly commit marker
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nightly-build-commit
path: nightly-build-commit.txt
retention-days: 90
@@ -16,29 +16,26 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ssh-key: ${{secrets.GH_ACTION_DEPLOY_KEY}}
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Install node
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: yarn
- name: Yarn install
uses: Wandalen/wretry.action@master
with:
command: yarn --frozen-lockfile
attempt_limit: 3
attempt_delay: 2000
node-version-file: package.json
cache: pnpm
- name: pnpm install
run: pnpm install --frozen-lockfile
- name: Extract language strings
run: yarn intl:extract
run: pnpm intl:extract
- name: Create commit
uses: stefanzweifel/git-auto-commit-action@v5
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
with:
commit_message: Nightly source-language update
file_pattern: ./src/locale/locales/en/messages.po
- name: Push source lang to Crowdin
uses: crowdin/github-action@v2
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
with:
upload_sources: true
upload_sources_args: "-b main"
+41 -33
View File
@@ -5,16 +5,15 @@ on:
issue_comment:
types: [created]
# Permissiosn to make comments in the pull request
permissions:
pull-requests: write
actions: write
contents: read
# Permissions are granted per-job below; anything unlisted defaults to none
permissions: {}
jobs:
handle-comment:
if: github.event.issue.pull_request
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
should-deploy: ${{ steps.check-org.outputs.result }}
@@ -45,10 +44,10 @@ jobs:
echo "mentioned=false" >> $GITHUB_OUTPUT
fi
- name: Check organization membership
- name: Check commenter has write access
if: steps.check-mention.outputs.mentioned == 'true'
id: check-org
uses: actions/github-script@v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
try {
@@ -72,13 +71,16 @@ jobs:
runs-on: ubuntu-latest
needs: [handle-comment]
if: needs.handle-comment.outputs.should-deploy == 'true'
permissions:
contents: read
pull-requests: write
steps:
- name: Get PR HEAD SHA
env:
ISSUE_NUMBER: ${{ github.event.issue.number }}
id: pr-info
uses: actions/github-script@v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const pr = await github.rest.pulls.get({
@@ -87,6 +89,16 @@ jobs:
pull_number: process.env.ISSUE_NUMBER,
});
// This workflow runs with repo secrets in scope, so never build
// code from a fork: the commenter authorizes the deploy, but a
// fork controls what code would run during it
const expected = `${context.repo.owner}/${context.repo.repo}`;
const head = pr.data.head.repo?.full_name;
if (head !== expected) {
core.setFailed(`OTA deploys are only allowed for branches in ${expected}, not forks (got ${head})`);
return;
}
console.log(`PR HEAD SHA: ${pr.data.head.sha}`);
console.log(`PR HEAD REF: ${pr.data.head.ref}`);
@@ -94,7 +106,7 @@ jobs:
core.setOutput('head-ref', pr.data.head.ref);
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
with:
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
number: ${{ github.event.issue.number }}
@@ -113,46 +125,42 @@ jobs:
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ steps.pr-info.outputs.head-sha }}
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: yarn
node-version-file: package.json
cache: pnpm
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Lint check
run: yarn lint
- name: Lint lockfile
run: yarn lockfile-lint
run: pnpm install --frozen-lockfile
- name: 🔤 Compile translations
run: yarn intl:build 2>&1 | tee i18n.log
run: pnpm intl:build 2>&1 | tee i18n.log
- name: Check for i18n compilation errors
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
- name: Lint check
run: pnpm lint
- name: Type check
run: yarn typecheck
run: pnpm typecheck
- name: 🔨 Setup EAS
uses: expo/expo-github-action@main
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
with:
expo-version: latest
eas-version: latest
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup Expo
run: yarn global add eas-cli-local-build-plugin
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
uses: dcarbone/install-jq-action@b7ef57d46ece78760b4019dbc4080a1ba2a40b45 # v3.2.0
- name: Env
id: env
@@ -175,17 +183,17 @@ jobs:
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn export
pnpm export
- name: 📦 Package Bundle and 🚀 Deploy
run: yarn use-build-number bash scripts/bundleUpdate.sh
run: pnpm use-build-number bash scripts/bundleUpdate.sh
env:
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
CHANNEL_NAME: pull-request-${{ github.event.issue.number }}
RUNTIME_VERSION:
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
env:
ISSUE_NUMBER: ${{ github.event.issue.number }}
with:
@@ -202,7 +210,7 @@ jobs:
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: failure()
with:
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
+34 -24
View File
@@ -13,26 +13,31 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
permissions:
pull-requests: write
actions: write
contents: read
# Permissions are granted per-job below; anything unlisted defaults to none.
# pull-requests: write is needed by sticky-pull-request-comment to post the
# bundle-size and fingerprint diffs
permissions: {}
jobs:
webpack-analyzer:
runs-on: ubuntu-22.04
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
permissions:
contents: read
pull-requests: write
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: yarn
node-version-file: package.json
cache: pnpm
- name: Ensure tracking relevant branches and checkout base
env:
@@ -56,17 +61,17 @@ jobs:
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git merge --no-edit $HEAD_REF
yarn install
yarn intl:build
pnpm install
pnpm intl:build
- name: 🔦 Generate stats file for PR
run: |
yarn generate-webpack-stats-file
pnpm generate-webpack-stats-file
mv stats.json ../stats-new.json
- name: ⬇️ Get base stats from cache
id: get-base-stats
uses: actions/cache@v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: stats-base.json
key: stats-base-${{ steps.base-commit.outputs.base-commit }}
@@ -80,21 +85,21 @@ jobs:
- name: 🔦 Generate stats file from base commit
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
run: |
yarn install
yarn intl:build
yarn generate-webpack-stats-file
pnpm install
pnpm intl:build
pnpm generate-webpack-stats-file
mv stats.json stats-base.json
- name: Get diff
id: get-diff
uses: NejcZdovc/bundle-size-diff@v1
uses: NejcZdovc/bundle-size-diff@5321de41d2d62a7b0f4d6e60f59d1280a0034160 # v1.1.0
with:
base_path: "stats-base.json"
pr_path: "../stats-new.json"
excluded_assets: "(.+).chunk.js|(.+).js.map|(.+).json|(.+).png|(.+).svg|(.+).webp|(.+).jpg|(.+).ico"
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
with:
header: bundle-diff
message: |
@@ -106,9 +111,12 @@ jobs:
fingerprint-native:
runs-on: ubuntu-22.04
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
permissions:
contents: read
pull-requests: write
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 100
@@ -116,20 +124,22 @@ jobs:
run: git fetch origin main:main --depth 100
if: github.event_name == 'pull_request'
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: yarn
node-version-file: package.json
cache: pnpm
- name: 📷 Check fingerprint and install dependencies
id: fingerprint
uses: bluesky-social/github-actions/fingerprint-native@main
uses: bluesky-social/github-actions/fingerprint-native@ebc6aa6d7466dc1e78b1e832041b7b81f6f95030 # v0.1.0
with:
profile: pull-request
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: ${{ steps.fingerprint.outputs.includes-changes }}
with:
header: fingerprint-diff
@@ -147,7 +157,7 @@ jobs:
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
- name: 💬 Delete comment
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: ${{ !steps.fingerprint.outputs.includes-changes }}
with:
header: fingerprint-diff
+9 -2
View File
@@ -4,22 +4,29 @@ on:
push:
branches: [main]
# The push to the internal repo uses the app token below, not the GITHUB_TOKEN
permissions:
contents: read
jobs:
sync:
runs-on: ubuntu-latest
if: github.repository == 'bluesky-social/social-app'
steps:
- name: Checkout public repo
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.SYNC_INTERNAL_APP_ID }}
private-key: ${{ secrets.SYNC_INTERNAL_PK }}
repositories: social-app-internal
# Scope the token down from the app's full installation permissions;
# pushing is the only thing this token is used for
permission-contents: write
- name: Push to internal repo
env:
TOKEN: ${{ steps.app-token.outputs.token }}
+51
View File
@@ -0,0 +1,51 @@
name: Lockfile
on:
pull_request:
concurrency:
group: "${{ github.workflow }}-${{ github.head_ref || github.ref }}"
cancel-in-progress: true
permissions:
contents: read
jobs:
verify-pnpm-lock:
name: No manual pnpm-lock.yaml edits
runs-on: ubuntu-latest
steps:
- name: Check out PR HEAD
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: Fetch base branch
env:
BASE_REF: ${{ github.base_ref }}
run: git fetch origin $BASE_REF --depth=1
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Install node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: package.json
- name: Reset pnpm-lock.yaml to base
env:
BASE_REF: ${{ github.base_ref }}
run: git show "origin/$BASE_REF:pnpm-lock.yaml" > pnpm-lock.yaml
- name: pnpm install
# Fine to skip scripts since we don't run any code
run: pnpm clean && pnpm install --ignore-scripts --no-frozen-lockfile
- name: Verify pnpm-lock.yaml
run: |
git diff --quiet --exit-code || {
echo '::error::`pnpm-lock.yaml` does not match what pnpm would generate given the base `pnpm-lock.yaml` and the head `package.json`.'
echo '::error:: - If this is intentional, you can ignore this check.'
echo '::error:: - If this is unintentional, apply the following diff:'
git --no-pager diff
exit 1
}
-50
View File
@@ -1,50 +0,0 @@
name: Lockfile
on:
pull_request:
concurrency:
group: "${{ github.workflow }}-${{ github.head_ref || github.ref }}"
cancel-in-progress: true
jobs:
verify-yarn-lock:
name: No manual yarn.lock edits
runs-on: ubuntu-latest
steps:
- name: Check out PR HEAD
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch base branch
env:
BASE_REF: ${{ github.base_ref }}
run: git fetch origin $BASE_REF --depth=1
- name: Install node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Reset yarn.lock to base
env:
BASE_REF: ${{ github.base_ref }}
run: git show "origin/$BASE_REF:yarn.lock" > yarn.lock
- name: Yarn install
uses: Wandalen/wretry.action@master
with:
# Fine to skip scripts since we don't run any code
command: yarn install --ignore-scripts
attempt_limit: 3
attempt_delay: 2000
- name: Verify yarn.lock
run: |
git diff --quiet --exit-code || {
echo '::error::`yarn.lock` does not match what Yarn would generate given the base `yarn.lock` and the head `package.json`.'
echo '::error:: - If this is intentional, you can ignore this check.'
echo '::error:: - If this is unintentional, apply the following diff:'
git --no-pager diff
exit 1
}
+37
View File
@@ -0,0 +1,37 @@
name: Workflow security
on:
push:
branches: [main]
paths: [".github/**"]
pull_request:
paths: [".github/**"]
concurrency:
group: "${{ github.workflow }}-${{ github.head_ref || github.ref }}"
cancel-in-progress: true
# The github.token is only used by zizmor's online audits (read-only API calls)
permissions:
contents: read
jobs:
zizmor:
name: Audit workflows with zizmor
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
with:
# Annotate the PR directly instead of uploading SARIF to the
# security tab, and fail the check on any finding
advanced-security: false
annotations: true
# Low-confidence findings (e.g. artipacked on workflows that never
# push) are too noisy to gate CI on
min-confidence: medium
+7
View File
@@ -86,6 +86,9 @@ stats.json
# VSCode
.vscode
# Zed
.zed
# gitignore and github actions
!.gitignore
!.github
@@ -127,3 +130,7 @@ bskyweb/static/media/*.webp
bskyweb/static/media/*.jpg
bskyweb/static/media/*.png
bskyweb/static/media/*.svg
# superpowers plugin plans/specs — local-only workspace
docs/superpowers/
.claude/worktrees
+1 -4
View File
@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged --concurrent false
pnpm exec lint-staged --concurrent false
+236
View File
@@ -0,0 +1,236 @@
/**
* Codemod to update Lingui's useLingui() hook to use v5.
*
* Before:
* import {msg} from '@lingui/core/macro'
* import {useLingui} from '@lingui/react'
* const {_} = useLingui()
* _(msg`Lorem ipsum`)
*
* After:
* import {useLingui} from '@lingui/react/macro'
* const {t: l} = useLingui()
* l`Lorem ipsum`
*
* Usage: jscodeshift -t .jscodeshift/file/lingui-v5.js <file-path>
* Example: jscodeshift -t .jscodeshift/file/lingui-v5.js modules/bottom-sheet/src/lib/Portal.tsx
*/
/* eslint-disable */
export const parser = 'tsx'
export default function transformer(file, api) {
const j = api.jscodeshift
const root = j(file.source)
// Remove msg imports, including aliased imports
root
.find(j.ImportDeclaration)
.filter(path => path.node.source.value === '@lingui/core/macro')
.forEach(path => {
const newSpecifiers = path.node.specifiers.filter(specifier => {
if (j.ImportSpecifier.check(specifier)) {
const importedName = specifier.imported
? specifier.imported.name
: specifier.local.name
const localName = specifier.local
? specifier.local.name
: importedName
return !(
importedName === 'msg' &&
(localName === 'msg' || localName === 'msgLingui')
)
}
return true
})
if (newSpecifiers.length > 0) {
path.node.specifiers = newSpecifiers
} else {
j(path).remove()
}
})
// Update React imports
root
.find(j.ImportDeclaration)
.filter(path => path.node.source.value === '@lingui/react')
.forEach(path => {
path.node.source = j.stringLiteral('@lingui/react/macro')
})
// Replace {_} with {t: l}
root
.find(j.VariableDeclarator, {
init: {
type: 'CallExpression',
callee: {name: 'useLingui'},
},
})
.replaceWith(nodePath => {
const {node} = nodePath
for (let i = 0; i < node.id.properties.length; i++) {
if (node.id.properties[i].key.name === '_') {
node.id.properties[i].key = j.identifier('t')
node.id.properties[i].shorthand = false
node.id.properties[i].value = j.identifier('l')
}
}
return node
})
// Replace {_: _l} with {t: l}
root
.find(j.VariableDeclarator, {
init: {callee: {name: 'useLingui'}},
id: {
properties: properties =>
properties.some(p => p.key.name === '_' && p.value.name === '_l'),
},
})
.forEach(path => {
const prop = path.node.id.properties.find(
p => p.key.name === '_' && p.value.name === '_l',
)
if (prop) {
prop.key.name = 't'
prop.value.name = 'l'
}
})
// Replace _(msg``) with l``
root
.find(j.CallExpression, {
callee: {name: '_'},
arguments: [
{
type: 'TaggedTemplateExpression',
tag: {name: 'msg'},
},
],
})
.replaceWith(path => {
const quasis = path.node.arguments[0].quasi
return j.taggedTemplateExpression(j.identifier('l'), quasis)
})
// Replace _l(msgLingui``) with l``
root
.find(j.CallExpression, {
callee: {name: '_l'},
arguments: [
{
type: 'TaggedTemplateExpression',
tag: {name: 'msgLingui'},
},
],
})
.replaceWith(path => {
const quasis = path.node.arguments[0].quasi
return j.taggedTemplateExpression(j.identifier('l'), quasis)
})
// Replace _(msg()) with l()
root
.find(j.CallExpression, {
callee: {name: '_'},
arguments: [
{
type: 'CallExpression',
callee: {name: 'msg'},
},
],
})
.replaceWith(path => {
return j.callExpression(j.identifier('l'), [
path.node.arguments[0].arguments[0],
])
})
// Replace _l(msgLingui()) with l()
root
.find(j.CallExpression, {
callee: {name: '_l'},
arguments: [
{
type: 'CallExpression',
callee: {name: 'msgLingui'},
},
],
})
.replaceWith(path => {
return j.callExpression(j.identifier('l'), [
path.node.arguments[0].arguments[0],
])
})
// Replace _(plural()) with plural()
root
.find(j.CallExpression, {
callee: {
type: 'Identifier',
name: '_',
},
})
.forEach(path => {
const callExpression = path.node
if (
callExpression.arguments.length === 1 &&
callExpression.arguments[0].type === 'CallExpression'
) {
const innerCall = callExpression.arguments[0]
if (
innerCall.callee.type === 'Identifier' &&
innerCall.callee.name === 'plural'
) {
j(path).replaceWith(innerCall)
}
}
})
// Update hook dependency arrays
root
.find(j.CallExpression, {
callee: callee => {
if (
callee.type === 'Identifier' ||
callee.type === 'MemberExpression'
) {
return true
}
if (
['useCallback', 'useEffect', 'useMemo'].includes(callee.name) &&
callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier' &&
callee.object.name === 'React' &&
callee.property.type === 'Identifier' &&
callee.property.name === 'useCallback'
) {
return true
}
return false
},
})
.forEach(path => {
const dependencyArray = path.node.arguments[1]
if (dependencyArray && dependencyArray.type === 'ArrayExpression') {
dependencyArray.elements = dependencyArray.elements.map(element => {
if (
element.type === 'Identifier' &&
(element.name === '_' || element.name === '_l')
) {
return j.identifier('l')
}
return element
})
}
})
return root.toSource()
}
+62
View File
@@ -0,0 +1,62 @@
/**
* Codemod to replace BskyAgent with AtpAgent
*
* Before:
* import {BskyAgent} from '@atproto/api`
* BskyAgent.appLabelers.includes(labeler)
*
* After:
* import {AtpAgent} from '@atproto/api`
* AtpAgent.appLabelers.includes(labeler)
*
* Handles import specifiers, type annotations, static member access
* (BskyAgent.configure), `extends BskyAgent`, and `new BskyAgent()`. Whole
* identifiers only, so names like `OpaqueBskyAgent` are left untouched.
*
* Usage: jscodeshift -t .jscodeshift/repo/bsky-agent.js <file-path>
* Example: jscodeshift -t .jscodeshift/repo/bsky-agent.js src/lib/moderation.ts
*/
/* eslint-disable */
export const parser = 'tsx'
export default function transformer(file, api) {
const j = api.jscodeshift
const root = j(file.source)
// Replace every standalone `BskyAgent` identifier with `AtpAgent`. This
// covers imports, type references, member expressions, `extends`, and `new`.
root
.find(j.Identifier, {name: 'BskyAgent'})
.replaceWith(() => j.identifier('AtpAgent'))
// Renaming can leave a duplicate `AtpAgent` specifier on the @atproto/api
// import if the file already imported it. Dedupe by imported name, keeping
// the type-only modifier only if every duplicate was type-only.
root
.find(j.ImportDeclaration, {source: {value: '@atproto/api'}})
.forEach(path => {
const seen = new Map()
for (const spec of path.value.specifiers) {
if (spec.type !== 'ImportSpecifier') {
seen.set(Symbol(), spec)
continue
}
const name = spec.imported.name
const existing = seen.get(name)
if (!existing) {
seen.set(name, spec)
} else if (
existing.importKind === 'type' &&
spec.importKind !== 'type'
) {
// Prefer the value (non-type) import if either usage needs it.
seen.set(name, spec)
}
}
path.value.specifiers = Array.from(seen.values())
})
return root.toSource()
}
@@ -9,8 +9,8 @@
* import { useEffect } from 'react'
* useEffect(() => {}, [])
*
* Usage: jscodeshift -t .jscodeshift/react-import.js <file-path>
* Example: jscodeshift -t .jscodeshift/react-import.js src/App.native.tsx
* Usage: jscodeshift -t .jscodeshift/repo/react-import.js <file-path>
* Example: jscodeshift -t .jscodeshift/repo/react-import.js src/App.native.tsx
*/
/* eslint-disable */
@@ -9,8 +9,8 @@
* import * as Toast from '#/components/Toast'
* Toast.show(message, {type: 'error'})
*
* Usage: jscodeshift -t .jscodeshift/toast-v2.js <file-path>
* Example: jscodeshift -t .jscodeshift/toast-v2.js src/App.native.tsx
* Usage: jscodeshift -t .jscodeshift/repo/toast-v2.js <file-path>
* Example: jscodeshift -t .jscodeshift/repo/toast-v2.js src/App.native.tsx
*/
/* eslint-disable */
+1 -1
View File
@@ -1 +1 @@
20
24.15.0
+4
View File
@@ -5,10 +5,14 @@
!**/*.jsx
!**/*.ts
!**/*.tsx
!**/*.mjs
!**/*.cjs
!*/
!**/*.css
!**/pnpm-workspace.yaml
# More specific ignores go below.
.expo
android
+185 -97
View File
@@ -1,4 +1,4 @@
# CLAUDE.md - Bluesky Social App Development Guide
# CLAUDE.md  Bluesky Social App Development Guide
This document provides guidance for working effectively in the Bluesky Social app codebase.
@@ -7,36 +7,40 @@ This document provides guidance for working effectively in the Bluesky Social ap
Bluesky Social is a cross-platform social media application built with React Native and Expo. It runs on iOS, Android, and Web, connecting to the AT Protocol (atproto) decentralized social network.
**Tech Stack:**
- React 19.1
- React Native 0.81 with Expo 54
- TypeScript
- React Navigation for routing
- TypeScript 6
- React Navigation 7 for routing
- TanStack Query (React Query) for data fetching
- Lingui for internationalization
- Lingui 5 for internationalization
- Custom design system called ALF (Application Layout Framework)
Prefer using the latest features available for each of these libraries (exact versions are found in `package.json`). For example, prefer `@lingui/react/macro` over `@lingui/react`. Suggest refactoring legacy or deprecated uses.
## Essential Commands
```bash
# Development
yarn start # Start Expo dev server
yarn web # Start web version
yarn android # Run on Android
yarn ios # Run on iOS
pnpm start # Start Expo dev server
pnpm web # Start web version
pnpm android # Run on Android
pnpm ios # Run on iOS
# Testing & Quality
# IMPORTANT: Always use these yarn scripts, never call the underlying tools directly
yarn test # Run Jest tests
yarn lint # Run ESLint
yarn typecheck # Run TypeScript type checking
# IMPORTANT: Always use these pnpm scripts, never call the underlying tools directly
pnpm test # Run Jest tests
pnpm lint # Run ESLint
pnpm typecheck # Run TypeScript type checking
# Internationalization
# DO NOT run these commands - extraction and compilation are handled by CI
yarn intl:extract # Extract translation strings (nightly CI job)
yarn intl:compile # Compile translations for runtime (nightly CI job)
pnpm intl:extract # Extract translation strings (nightly CI job)
pnpm intl:compile # Compile translations for runtime (nightly CI job)
# Build
yarn build-web # Build web version
yarn prebuild # Generate native projects
pnpm build-web # Build web version
pnpm prebuild # Generate native projects
```
## Project Structure
@@ -162,6 +166,10 @@ its own looks more like a single component file.
### Documentation and Tests Within Features
Comment code when necessary to explain the “why” behind something; avoid
comments that simply describe the code. Avoid Unicode characters in comments,
e.g., use `-` not `—`.
For larger features or components, it's helpful to include a README.md file
within the directory that explains the purpose of the feature, how it works, and
any important implementation details. The `/Component/index.tsx` pattern lends
@@ -182,6 +190,14 @@ ALF is the custom design system. It uses Tailwind-inspired naming with underscor
### Basic Usage
Generally, order atoms by:
- Flexbox configuration, e.g., `a.flex_row`
- Spacing, e.g., `a.px_md`
- Text styles, e.g., `a.font_bold`
- Themes, e.g., `t.atoms.text`,
- Raw styles, e.g., `{backgroundColor: t.palette.primary_500}`
```tsx
import {atoms as a, useTheme} from '#/alf'
@@ -190,9 +206,7 @@ function MyComponent() {
return (
<View style={[a.flex_row, a.gap_md, a.p_lg, t.atoms.bg]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
Hello
</Text>
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_high]}>Hello</Text>
</View>
)
}
@@ -200,20 +214,23 @@ function MyComponent() {
### Key Concepts
**Static Atoms** - Theme-independent styles imported from `atoms`:
**Static Atoms**  Theme-independent styles imported from `atoms`:
```tsx
import {atoms as a} from '#/alf'
// a.flex_row, a.p_md, a.gap_sm, a.rounded_md, a.text_lg, etc.
```
**Theme Atoms** - Theme-dependent colors from `useTheme()`:
**Theme Atoms**  Theme-dependent colors from `useTheme()`:
```tsx
const t = useTheme()
// t.atoms.bg, t.atoms.text, t.atoms.border_contrast_low, etc.
// t.palette.primary_500, t.palette.negative_400, etc.
```
**Platform Utilities** - For platform-specific styles:
**Platform Utilities**  For platform-specific styles:
```tsx
import {web, native, ios, android, platform} from '#/alf'
@@ -225,7 +242,8 @@ const styles = [
]
```
**Breakpoints** - Responsive design:
**Breakpoints**  Responsive design:
```tsx
import {useBreakpoints} from '#/alf'
@@ -245,6 +263,38 @@ if (gtMobile) {
## Component Patterns
- Prefer fragment shorthand over `Fragment` unless a `key` is needed.
- Prefer functions over arrow functions for component declarations.
- Prefer prop destructuring via parameters over a const within the component.
- Prefer inline types over `Props` types or interfaces.
- Set reasonable defaults for optional props.
```tsx
import {Fragment} from 'react'
import {View} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
import {Text} from '#/components/Typography'
function MyComponent({foo = []}: {foo?: string[]}) {
const {t: l} = useLingui()
return (
<>
<View><Text><Trans>Example</Trans><Text></View>
<View>
{foo.map((foo, index) => (
<Fragment key={foo}>
<Text>{index}</Text>
<Text>{foo}</Text>
</Fragment>
))}
</View>
</>
);
}
```
### Dialog Component
Dialogs use a bottom sheet on native and a modal on web. Use `useDialogControl()` hook to manage state.
@@ -263,23 +313,29 @@ function MyFeature() {
<Dialog.Outer control={control}>
{/* Typically the inner part is in its own component */}
<Dialog.Handle /> {/* Native-only drag handle */}
<Dialog.ScrollableInner label={_(msg`My Dialog`)}>
<Dialog.Header>
<Dialog.HeaderText>Title</Dialog.HeaderText>
</Dialog.Header>
<Text>Dialog content here</Text>
<Button label="Done" onPress={() => control.close()}>
<ButtonText>Done</ButtonText>
</Button>
<Dialog.Close /> {/* Web-only X button in top left */}
</Dialog.ScrollableInner>
<DialogInner />
</Dialog.Outer>
</>
)
}
function DialogInner() {
return (
<>
<Dialog.Handle /> {/* Native-only drag handle */}
<Dialog.ScrollableInner label={l`My Dialog`}>
<Dialog.Header>
<Dialog.HeaderText>Title</Dialog.HeaderText>
</Dialog.Header>
<Text>Dialog content here</Text>
<Button label="Done" onPress={() => control.close()}>
<ButtonText>Done</ButtonText>
</Button>
<Dialog.Close /> {/* Web-only X button in top left */}
</Dialog.ScrollableInner>
</>
)
}
```
### Menu Component
@@ -345,6 +401,7 @@ import {Button, ButtonText, ButtonIcon} from '#/components/Button'
```
**Button Props:**
- `color`: `'primary'` | `'secondary'` | `'negative'` | `'primary_subtle'` | `'negative_subtle'` | `'secondary_inverted'`
- `size`: `'tiny'` | `'small'` | `'large'`
- `shape`: `'default'` (pill) | `'round'` | `'square'` | `'rectangular'`
@@ -357,12 +414,16 @@ import {Text, H1, H2, P} from '#/components/Typography'
<H1 style={[a.text_xl, a.font_bold]}>Heading</H1>
<P>Paragraph text with default styling.</P>
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>Custom text</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>Custom text</Text>
// For text with emoji, add the emoji prop
// For text with emoji, add the emoji prop. User-generated text (e.g. display names)
// will almost certainly contain emoji, so only omit it when the text is static and
// does not contain an emoji
<Text emoji>Hello! 👋</Text>
```
The `Text` component's default style is `[a.text_sm, a.leading_snug, t.atoms.text]`.
### TextField
```tsx
@@ -384,43 +445,70 @@ import * as TextField from '#/components/forms/TextField'
## Internationalization (i18n)
All user-facing strings must be wrapped for translation using Lingui.
All user-facing strings must be wrapped for translation using Lingui. Include `comment` and/or `context` props when necessary to avoid ambiguity, e.g., “Post” as a noun vs a verb.
Prefer using `t` via `import {useLingui} '@lingui/react/macro'` vs `_` via `import {useLingui} from '@lingui/react'`. Alias `t` to `l` to avoid collisions with `const t = useTheme()`. Refactor existing uses of ``_(msg`foo`)`` to use `` l`foo` ``.
Prefer Unicode punctuation over keyboard punctuation, e.g., `“quote”` over `"quote"`. Prefer en dashes preceded by a non-breaking space over em dashes, e.g., `one  two` over `one—two`.
```tsx
import {msg, plural} from '@lingui/core/macro'
import {Trans} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import {plural} from '@lingui/core/macro'
import {Trans, useLingui} from '@lingui/react/macro'
function MyComponent() {
const {_} = useLingui()
const {t: l} = useLingui()
// Simple strings - use msg() with _() function
const title = _(msg`Settings`)
const errorMessage = _(msg`Something went wrong`)
// Simple strings - use the l macro
const title = l`Settings`
const errorMessage = l({
message: 'Something went wrong',
comment: 'Generic error message for unknown/unhandled errors.',
context: 'Toast',
})
// Strings with variables
const greeting = _(msg`Hello, ${name}!`)
const greeting = l`Hello, ${name}!`
// Pluralization
const countLabel = _(plural(count, {
const countLabel = plural(count, {
one: '# item',
other: '# items',
}))
})
// JSX content - use Trans component
return (
<Text>
<Trans>Welcome to <Text style={a.font_bold}>Bluesky</Text></Trans>
<Trans>
Welcome to <Text style={a.font_bold}>Bluesky</Text>, {name}!
</Trans>
</Text>
)
}
```
Prefer `i18n.date` for date and time formatting. This ensures formatting is re-applied when the language changes at runtime. Refactor existing uses of `Intl.DateTimeFormat` to use `i18n.date`.
```tsx
import {useLingui} from '@lingui/react/macro'
function MyComponent() {
const {i18n} = useLingui()
const createdAt = new Date()
return i18n.date(createdAt, {
dateStyle: 'medium',
timeStyle: 'medium',
})
}
```
**Commands:**
```bash
# DO NOT run these commands - extraction and compilation are handled by a nightly CI job
yarn intl:extract # Extract new strings to locale files
yarn intl:compile # Compile translations for runtime
pnpm intl:extract # Extract new strings to locale files
pnpm intl:compile # Compile translations for runtime
```
## State Management
@@ -473,7 +561,7 @@ export function useProfileMutation() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (data) => {
mutationFn: async data => {
// Update logic
},
onSuccess: (_, variables) => {
@@ -481,7 +569,7 @@ export function useProfileMutation() {
queryKey: createProfileQueryKey({did: variables.did}),
})
},
onError: (error) => {
onError: error => {
if (isNetworkError(error)) {
// don't log, but inform user
} else if (error instanceof AppBskyExampleProcedure.ExampleError) {
@@ -490,7 +578,7 @@ export function useProfileMutation() {
// Log unexpected errors to Sentry
logger.error('Error updating profile', {safeMessage: error})
}
}
},
})
}
@@ -505,21 +593,25 @@ export function useProfileCacheMutation() {
const queryClient = useQueryClient()
return (data: Partial<Profile>) => {
queryClient.setQueryData(createProfileQueryKey({did: data.did}), oldData => {
if (!oldData) return oldData
return {...oldData, ...data}
})
queryClient.setQueryData(
createProfileQueryKey({did: data.did}),
oldData => {
if (!oldData) return oldData
return {...oldData, ...data}
},
)
}
}
```
**Stale Time Constants** (from `src/state/queries/index.ts`):
```tsx
STALE.SECONDS.FIFTEEN // 15 seconds
STALE.MINUTES.ONE // 1 minute
STALE.MINUTES.FIVE // 5 minutes
STALE.HOURS.ONE // 1 hour
STALE.INFINITY // Never stale
STALE.SECONDS.FIFTEEN // 15 seconds
STALE.MINUTES.ONE // 1 minute
STALE.MINUTES.FIVE // 5 minutes
STALE.HOURS.ONE // 1 hour
STALE.INFINITY // Never stale
```
**Paginated APIs:** Many atproto APIs return paginated results with a `cursor`. Use `useInfiniteQuery` for these:
@@ -565,12 +657,7 @@ function SettingsScreen() {
const autoplayDisabled = useAutoplayDisabled()
const setAutoplayDisabled = useSetAutoplayDisabled()
return (
<Toggle
value={autoplayDisabled}
onValueChange={setAutoplayDisabled}
/>
)
return <Toggle value={autoplayDisabled} onValueChange={setAutoplayDisabled} />
}
```
@@ -604,13 +691,9 @@ import {type CommonNavigatorParams} from '#/lib/routes/types'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
export function ProfileScreen({route, navigation}: Props) {
const {name} = route.params // Type-safe params
const {name} = route.params // Type-safe params
return (
<Layout.Screen>
{/* Screen content */}
</Layout.Screen>
)
return <Layout.Screen>{/* Screen content */}</Layout.Screen>
}
// Programmatic navigation
@@ -637,8 +720,9 @@ Component.android.tsx # Android-only
```
Example from Dialog:
- `src/components/Dialog/index.tsx` - Native (uses BottomSheet)
- `src/components/Dialog/index.web.tsx` - Web (uses modal with Radix primitives)
- `src/components/Dialog/index.tsx Native (uses BottomSheet)
- `src/components/Dialog/index.web.tsx Web (uses modal with Radix primitives)
**Important:** The bundler automatically resolves platform-specific files. Just import normally:
@@ -653,6 +737,7 @@ const storage = IS_NATIVE
```
Platform detection (for runtime logic, not imports):
```tsx
import {IS_WEB, IS_NATIVE, IS_IOS, IS_ANDROID} from '#/env'
@@ -687,13 +772,13 @@ Common pitfalls to avoid in this codebase:
// WRONG - causes bugs with state updates, navigation, opening other dialogs
const onConfirm = () => {
control.close()
navigation.navigate('Home') // May race with dialog animation
navigation.navigate('Home') // May race with dialog animation
}
// WRONG - same problem
const onConfirm = () => {
control.close()
otherDialogControl.open() // Will likely fail or cause visual glitches
otherDialogControl.open() // Will likely fail or cause visual glitches
}
// CORRECT - action runs after dialog fully closes
@@ -720,12 +805,13 @@ const onConfirm = () => {
```
This applies to:
- Navigation (`navigation.navigate()`, `navigation.push()`)
- Opening other dialogs or menus
- State updates that affect UI (`setState`, `queryClient.invalidateQueries`)
- Callbacks passed from parent components
The Menu component on iOS specifically uses this pattern - see `src/components/Menu/index.tsx:151`.
The Menu component on iOS specifically uses this pattern  see `src/components/Menu/index.tsx:151`.
### Controlled vs Uncontrolled Inputs
@@ -748,10 +834,11 @@ Prefer `defaultValue` over `value` for TextInput on the old architecture:
### Platform-Specific Behavior
Some components behave differently across platforms:
- `Dialog.Handle` - Only renders on native (drag handle for bottom sheet)
- `Dialog.Close` - Only renders on web (X button)
- `Menu.Divider` - Only renders on web
- `Menu.ContainerItem` - Only works on native
- `Dialog.Handle Only renders on native (drag handle for bottom sheet)
- `Dialog.Close Only renders on web (X button)
- `Menu.Divider Only renders on web
- `Menu.ContainerItem Only works on native
Always test on multiple platforms when using these components.
@@ -772,6 +859,7 @@ const handlePress = () => {
```
Only use `useMemo`/`useCallback` when you have a specific reason, such as:
- The value is immediately used in an effect's dependency array
- You're passing a callback to a non-React library that needs referential stability
@@ -779,7 +867,7 @@ Only use `useMemo`/`useCallback` when you have a specific reason, such as:
1. **Accessibility**: Always provide `label` prop for interactive elements, use `accessibilityHint` where helpful
2. **Translations**: Wrap ALL user-facing strings with `msg()` or `<Trans>`
2. **Translations**: Wrap ALL user-facing strings with ` `l` `` or `<Trans>`
3. **Styling**: Combine static atoms with theme atoms, use platform utilities for platform-specific styles
@@ -793,14 +881,14 @@ Only use `useMemo`/`useCallback` when you have a specific reason, such as:
## Key Files Reference
| Purpose | Location |
|---------|----------|
| Theme definitions | `src/alf/themes.ts` |
| Design tokens | `src/alf/tokens.ts` |
| Static atoms | `src/alf/atoms.ts` (extends `@bsky.app/alf`) |
| Navigation config | `src/Navigation.tsx` |
| Route definitions | `src/routes.ts` |
| Route types | `src/lib/routes/types.ts` |
| Query hooks | `src/state/queries/*.ts` |
| Session state | `src/state/session/index.tsx` |
| i18n setup | `src/locale/i18n.ts` |
| Purpose | Location |
| ----------------- | -------------------------------------------- |
| Theme definitions | `src/alf/themes.ts` |
| Design tokens | `src/alf/tokens.ts` |
| Static atoms | `src/alf/atoms.ts` (extends `@bsky.app/alf`) |
| Navigation config | `src/Navigation.tsx` |
| Route definitions | `src/routes.ts` |
| Route types | `src/lib/routes/types.ts` |
| Query hooks | `src/state/queries/*.ts` |
| Session state | `src/state/session/index.tsx` |
| i18n setup | `src/locale/i18n.ts` |
+49 -41
View File
@@ -1,23 +1,21 @@
FROM golang:1.26-bookworm AS build-env
#
# Stage 1: build the web bundle with pnpm.
#
# Uses the official pnpm image. Node is auto-downloaded by pnpm using the
# `devEngines.runtime` field in package.json (onFail: "download").
#
FROM ghcr.io/pnpm/pnpm:11 AS web-build
WORKDIR /usr/src/social-app
WORKDIR /app
ENV DEBIAN_FRONTEND=noninteractive
#
# Node
# pnpm config
#
ENV NODE_VERSION=20
ENV NVM_DIR=/usr/share/nvm
#
# Go
#
ENV GODEBUG="netdns=go"
ENV GOOS="linux"
ENV GOARCH="amd64"
ENV CGO_ENABLED=1
ENV GOEXPERIMENT="loopvar"
ENV CI=1
# use the pnpm version specified in package.json
ENV pnpm_config_pm_on_fail=download
# The latest git hash of the preview branch on render.com
# https://render.com/docs/docker-secrets#environment-variables-in-docker-builds
@@ -42,41 +40,48 @@ ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN:-unknown}
ARG EXPO_PUBLIC_SENTRY_DSN
ENV EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN
#
# Copy everything into the container
#
COPY . .
#
# Generate the JavaScript webpack.
#
RUN mkdir --parents $NVM_DIR && \
wget \
--output-document=/tmp/nvm-install.sh \
https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh && \
bash /tmp/nvm-install.sh
RUN \. "$NVM_DIR/nvm.sh" && \
nvm install $NODE_VERSION && \
nvm use $NODE_VERSION && \
echo "Using bundle identifier: $EXPO_PUBLIC_BUNDLE_IDENTIFIER" && \
RUN echo "Using bundle identifier: $EXPO_PUBLIC_BUNDLE_IDENTIFIER" && \
echo "EXPO_PUBLIC_ENV=$EXPO_PUBLIC_ENV" >> .env && \
echo "EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION" >> .env && \
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER" >> .env && \
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env && \
echo "EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN" >> .env && \
npm install --global yarn && \
yarn && \
yarn intl:build 2>&1 | tee i18n.log && \
if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compile errors!\n\n"; fi && \
SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$EXPO_PUBLIC_RELEASE_VERSION SENTRY_DIST=$EXPO_PUBLIC_BUNDLE_IDENTIFIER yarn build-web
echo "EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN" >> .env
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm intl:build 2>&1 | tee i18n.log && \
if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compile errors!\n\n"; fi
RUN SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN \
SENTRY_RELEASE=$EXPO_PUBLIC_RELEASE_VERSION \
SENTRY_DIST=$EXPO_PUBLIC_BUNDLE_IDENTIFIER \
pnpm build-web
#
# Stage 2: build the bskyweb Go binary, embedding the assets from stage 1.
#
# post-web-build.js (run by `pnpm build-web`) writes the bundled JS/CSS/media
# into bskyweb/static/* and regenerates bskyweb/templates/scripts.html, so
# copying the bskyweb/ tree from stage 1 is enough for go:embed to find
# everything.
#
FROM golang:1.26-bookworm AS go-build
WORKDIR /usr/src/social-app
ENV GODEBUG="netdns=go"
ENV GOOS="linux"
ENV GOARCH="amd64"
ENV CGO_ENABLED=1
ENV GOEXPERIMENT="loopvar"
COPY --from=web-build /app/bskyweb ./bskyweb
# DEBUG
RUN find ./bskyweb/static && find ./web-build/static
RUN find ./bskyweb/static
#
# Generate the bskyweb Go binary.
#
RUN cd bskyweb/ && \
go mod download && \
go mod verify
@@ -89,6 +94,9 @@ RUN cd bskyweb/ && \
-o /bskyweb \
./cmd/bskyweb
#
# Stage 3: runtime image.
#
FROM debian:bookworm-slim
ENV GODEBUG=netdns=go
@@ -102,7 +110,7 @@ RUN apt-get update && apt-get install --yes \
ENTRYPOINT ["dumb-init", "--"]
WORKDIR /bskyweb
COPY --from=build-env /bskyweb /usr/bin/bskyweb
COPY --from=go-build /bskyweb /usr/bin/bskyweb
CMD ["/usr/bin/bskyweb"]
+2 -4
View File
@@ -1,4 +1,4 @@
FROM node:20.11-alpine3.18 as build
FROM node:24.15.0-alpine3.22 AS build
# Move files into the image and install
WORKDIR /app
@@ -14,7 +14,7 @@ RUN yarn build
RUN yarn install --production --ignore-scripts --prefer-offline
# Uses assets from build stage to reduce build size
FROM node:20.11-alpine3.18
FROM node:24.15.0-alpine3.22
RUN apk add --update dumb-init
@@ -29,8 +29,6 @@ VOLUME /app/data
EXPOSE 3000
ENV LINK_PORT=3000
ENV NODE_ENV=production
# potential perf issues w/ io_uring on this version of node
ENV UV_USE_IO_URING=0
# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user
USER node
+11 -8
View File
@@ -1,20 +1,25 @@
FROM node:20.11-alpine3.18 as build
FROM node:24.15.0-alpine3.22 AS build
# Tells pnpm to run non-interactively (needed for install/script steps)
ENV CI=true
# Move files into the image and install
WORKDIR /app
COPY ./bskyogcard/package.json ./
COPY ./bskyogcard/yarn.lock ./
RUN yarn install --frozen-lockfile
COPY ./bskyogcard/pnpm-lock.yaml ./
COPY ./bskyogcard/pnpm-workspace.yaml ./
RUN npm install --global pnpm@11.5.2
RUN pnpm install --frozen-lockfile
COPY ./bskyogcard ./
# build then prune dev deps
RUN yarn install-fonts && yarn build
RUN yarn install --production --ignore-scripts --prefer-offline
RUN pnpm install-fonts && pnpm build
RUN pnpm install --prod --ignore-scripts --prefer-offline
# Uses assets from build stage to reduce build size
FROM node:20.11-alpine3.18
FROM node:24.15.0-alpine3.22
RUN apk add --update dumb-init
@@ -30,8 +35,6 @@ EXPOSE 3000 3001
ENV CARD_PORT=3000
ENV CARD_METRICS_PORT=3001
ENV NODE_ENV=production
# potential perf issues w/ io_uring on this version of node
ENV UV_USE_IO_URING=0
# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user
USER node
+11 -7
View File
@@ -5,16 +5,20 @@ WORKDIR /usr/src/social-app
ENV DEBIAN_FRONTEND=noninteractive
# Node
ENV NODE_VERSION=20
ENV NODE_VERSION=24.15.0
ENV NVM_DIR=/usr/share/nvm
# Go
ENV GODEBUG="netdns=go"
ENV GOOS="linux"
ENV GOARCH="amd64"
ARG TARGETARCH
ENV GOARCH=${TARGETARCH:-amd64}
ENV CGO_ENABLED=1
ENV GOEXPERIMENT="loopvar"
# CI environment for pnpm
ENV CI=true
COPY . .
#
@@ -29,11 +33,11 @@ RUN mkdir --parents $NVM_DIR && \
RUN \. "$NVM_DIR/nvm.sh" && \
nvm install $NODE_VERSION && \
nvm use $NODE_VERSION && \
npm install --global yarn && \
yarn && \
cd bskyembed && yarn install --frozen-lockfile && cd .. && \
yarn intl:build && \
yarn build-embed
npm install --global pnpm@11.7.0 && \
pnpm install --frozen-lockfile && \
cd bskyembed && pnpm install --frozen-lockfile && cd .. && \
pnpm intl:build && \
pnpm build-embed
# DEBUG
RUN find ./bskyweb/embedr-static && find ./bskyweb/embedr-templates && find ./bskyembed/dist
+11 -11
View File
@@ -10,33 +10,33 @@ help: ## Print info about all commands
.PHONY: build-web
build-web: ## Compile web bundle, copy to bskyweb directory
yarn intl:build
yarn build-web
pnpm intl:build
pnpm build-web
.PHONY: build-web-embed
build-web-embed: ## Compile web embed bundle, copy to bskyweb/embedr* directories
yarn intl:build
yarn build-embed
pnpm intl:build
pnpm build-embed
.PHONY: test
test: ## Run all tests
NODE_ENV=test yarn test
NODE_ENV=test pnpm test
.PHONY: lint
lint: ## Run style checks and verify syntax
yarn run lint
pnpm run lint
#.PHONY: fmt
#fmt: ## Run syntax re-formatting
# yarn prettier
# pnpm prettier
.PHONY: deps
deps: ## Installs dependent libs using 'yarn install'
yarn install --frozen-lockfile
deps: ## Installs dependent libs using 'pnpm install'
pnpm install --frozen-lockfile
cd bskyembed && yarn install --frozen-lockfile
.PHONY: nvm-setup
nvm-setup: ## Use NVM to install and activate node+yarn
nvm-setup: ## Use NVM to install and activate node+pnpm
nvm install 20
nvm use 20
npm install --global yarn
npm install --global pnpm
+4 -4
View File
@@ -145,21 +145,21 @@ appId: xyz.blueskyweb.app
id: "profileHeaderDropdownBtn"
- tapOn: "Add to Lists"
- assertVisible:
id: "userAddRemoveListsModal"
id: "userAddRemoveListsDialog"
- tapOn:
id: "user-bob.test-addBtn"
- tapOn:
id: "doneBtn"
- assertNotVisible:
id: "userAddRemoveListsModal"
id: "userAddRemoveListsDialog"
- tapOn:
id: "profileHeaderDropdownBtn"
- tapOn: "Add to Lists"
- assertVisible:
id: "userAddRemoveListsModal"
id: "userAddRemoveListsDialog"
- tapOn:
id: "user-bob.test-addBtn"
- tapOn:
id: "doneBtn"
- assertNotVisible:
id: "userAddRemoveListsModal"
id: "userAddRemoveListsDialog"
+23 -12
View File
@@ -1,11 +1,12 @@
import {createDownloadResumable, deleteAsync} from 'expo-file-system/legacy'
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import {IMAGE_SIZE_CONFIG_2K_1MB} from '../../src/lib/constants'
import {
downloadAndResize,
type DownloadAndResizeOpts,
getResizedDimensions,
} from '../../src/lib/media/manip'
import {getResizedDimensions} from '../../src/lib/media/util'
const mockResizedImage = {
path: 'file://resized-image.jpg',
@@ -41,10 +42,8 @@ describe('downloadAndResize', () => {
const opts: DownloadAndResizeOpts = {
uri: 'https://example.com/image.jpg',
width: 100,
height: 100,
maxDimension: 2000,
maxSize: 500000,
mode: 'cover',
timeout: 10000,
}
@@ -60,9 +59,11 @@ describe('downloadAndResize', () => {
// First time it gets called is to get dimensions
expect(manipulateAsync).toHaveBeenCalledWith(expect.any(String), [], {})
// The mocked source image is 100x100, below maxDimension, so it is not
// downsized.
expect(manipulateAsync).toHaveBeenCalledWith(
expect.any(String),
[{resize: {height: opts.height, width: opts.width}}],
[{resize: {height: 100, width: 100}}],
{format: SaveFormat.JPEG, compress: 1.0},
)
expect(deleteAsync).toHaveBeenCalledWith(expect.any(String), {
@@ -73,10 +74,8 @@ describe('downloadAndResize', () => {
it('should return undefined for invalid URI', async () => {
const opts: DownloadAndResizeOpts = {
uri: 'invalid-uri',
width: 100,
height: 100,
maxDimension: 2000,
maxSize: 500000,
mode: 'cover',
timeout: 10000,
}
@@ -90,13 +89,19 @@ describe('downloadAndResize', () => {
width: 1200,
height: 1000,
}
const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne)
const resizedDimensionsOne = getResizedDimensions(
initialDimensionsOne,
IMAGE_SIZE_CONFIG_2K_1MB.maxDimension,
)
const initialDimensionsTwo = {
width: 1000,
height: 1200,
}
const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo)
const resizedDimensionsTwo = getResizedDimensions(
initialDimensionsTwo,
IMAGE_SIZE_CONFIG_2K_1MB.maxDimension,
)
expect(resizedDimensionsOne).toEqual(initialDimensionsOne)
expect(resizedDimensionsTwo).toEqual(initialDimensionsTwo)
@@ -107,13 +112,19 @@ describe('downloadAndResize', () => {
width: 3000,
height: 1500,
}
const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne)
const resizedDimensionsOne = getResizedDimensions(
initialDimensionsOne,
IMAGE_SIZE_CONFIG_2K_1MB.maxDimension,
)
const initialDimensionsTwo = {
width: 2000,
height: 4000,
}
const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo)
const resizedDimensionsTwo = getResizedDimensions(
initialDimensionsTwo,
IMAGE_SIZE_CONFIG_2K_1MB.maxDimension,
)
expect(resizedDimensionsOne).toEqual({
width: 2000,
+56 -7
View File
@@ -8,7 +8,7 @@ import {
parseStarterPackUri,
} from '#/lib/strings/starter-pack'
import {messages} from '#/locale/locales/en/messages'
import {tenorUrlToBskyGifUrl} from '#/state/queries/tenor'
import {klipyUrlToBskyGifUrl} from '#/features/gifPicker/utils'
import {cleanError} from '../../src/lib/strings/errors'
import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
import {enforceLen} from '../../src/lib/strings/helpers'
@@ -450,6 +450,13 @@ describe('parseEmbedPlayerFromUrl', () => {
'https://sufjanstevens.bandcamp.com',
'https://bandcamp.com/',
'https://bandcamp.com',
'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300',
'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300&mp4=videoSlugMp4&webm=videoSlugWebm',
'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200',
'https://static.klipy.com/ii/abc123/73/ac/someFile.gif',
'https://static.klipy.com/other/path.gif?hh=200&ww=300',
'https://static.klipy.com',
]
const outputs = [
@@ -845,6 +852,35 @@ describe('parseEmbedPlayerFromUrl', () => {
undefined,
undefined,
undefined,
{
type: 'klipy_gif',
source: 'klipy',
isGif: true,
hideDetails: true,
playerUri: 'https://k.gifs.bsky.app/ii/abc123/73/ac/someFile.gif',
dimensions: {
width: 300,
height: 200,
},
},
// With video slug params — on native (test env), keeps gif filename,
// strips mp4/webm params. On web, would swap to video filename.
{
type: 'klipy_gif',
source: 'klipy',
isGif: true,
hideDetails: true,
playerUri: 'https://k.gifs.bsky.app/ii/abc123/73/ac/someFile.gif',
dimensions: {
width: 300,
height: 200,
},
},
undefined,
undefined,
undefined,
undefined,
]
it('correctly grabs the correct id from uri', () => {
@@ -1035,17 +1071,30 @@ describe('createStarterPackGooglePlayUri', () => {
})
})
describe('tenorUrlToBskyGifUrl', () => {
describe('klipyUrlToBskyGifUrl', () => {
const inputs = [
'https://media.tenor.com/someID_AAAAC/someName.gif',
'https://media.tenor.com/someID/someName.gif',
'https://static.klipy.com/ii/abc123/73/ac/someFile.gif',
'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300',
]
it.each(inputs)(
'returns url with t.gifs.bsky.app as hostname for input url',
'returns url with k.gifs.bsky.app as hostname for input url',
input => {
const out = tenorUrlToBskyGifUrl(input)
expect(out.startsWith('https://t.gifs.bsky.app/')).toEqual(true)
const out = klipyUrlToBskyGifUrl(input)
expect(out.startsWith('https://k.gifs.bsky.app/')).toEqual(true)
},
)
it('preserves the path and query params when rewriting', () => {
const out = klipyUrlToBskyGifUrl(
'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300',
)
expect(out).toEqual(
'https://k.gifs.bsky.app/ii/abc123/73/ac/someFile.gif?hh=200&ww=300',
)
})
it('returns empty string for invalid URLs', () => {
expect(klipyUrlToBskyGifUrl('not-a-url')).toEqual('')
})
})
+45
View File
@@ -1,6 +1,7 @@
import {describe, expect, it} from '@jest/globals'
import {
getChatInviteCodeFromUrl,
isPossiblyAUrl,
isTrustedUrl,
linkRequiresWarning,
@@ -178,3 +179,47 @@ describe('isTrustedUrl', () => {
expect(output).toEqual(expected)
})
})
describe('getChatInviteCodeFromUrl', () => {
type Case = [string, string | undefined]
const cases: Case[] = [
['https://bsky.app/chat/abcdefg', 'abcdefg'],
['https://bsky.app/chat/abcdefghij', 'abcdefghij'],
// http is not recognized as a bsky.app url
['http://bsky.app/chat/abcdefg', undefined],
['https://bsky.app/chat/abcdefg?utm=foo', 'abcdefg'],
['https://bsky.app/chat/abcdefg#section', 'abcdefg'],
['/chat/abcdefg', 'abcdefg'],
['/chat/abcdefg?utm=foo', 'abcdefg'],
['/chat/abcdefg#section', 'abcdefg'],
// too short
['https://bsky.app/chat/abcdef', undefined],
['/chat/abcdef', undefined],
// too long
['https://bsky.app/chat/abcdefghijk', undefined],
['/chat/abcdefghijk', undefined],
// invalid characters
['https://bsky.app/chat/abc-def', undefined],
['/chat/abc def', undefined],
// trailing path
['https://bsky.app/chat/abcdefg/extra', undefined],
['/chat/abcdefg/extra', undefined],
// wrong path
['https://bsky.app/profile/abcdefg', undefined],
['https://bsky.app/chat', undefined],
// wrong host
['https://example.com/chat/abcdefg', undefined],
// not a url, not a path
['chat/abcdefg', undefined],
['abcdefg', undefined],
['', undefined],
// malformed url
['https://[invalid/chat/abcdefg', undefined],
]
it.each(cases)('given input %p, returns %p', (input, expected) => {
expect(getChatInviteCodeFromUrl(input)).toEqual(expected)
})
})
+4 -1
View File
@@ -66,6 +66,7 @@ module.exports = function (_config) {
infoPlist: {
CADisableMinimumFrameDurationOnPhone: true,
UIBackgroundModes: ['remote-notification'],
NSUserActivityTypes: ['INSendMessageIntent'],
NSCameraUsageDescription:
'Used for profile pictures, posts, and other kinds of content.',
NSMicrophoneUsageDescription:
@@ -123,6 +124,7 @@ module.exports = function (_config) {
'com.apple.developer.kernel.increased-memory-limit': true,
'com.apple.developer.kernel.extended-virtual-addressing': true,
'com.apple.security.application-groups': 'group.app.bsky',
'com.apple.developer.usernotifications.communication': true,
// 'com.apple.developer.device-information.user-assigned-device-name': true,
},
privacyManifests: {
@@ -260,6 +262,7 @@ module.exports = function (_config) {
deploymentTarget: '15.1',
buildReactNativeFromSource: true,
ccacheEnabled: IS_DEV,
cxxLanguageStandard: 'c++23',
extraPods: [
{
name: 'MCEmojiPicker',
@@ -346,7 +349,7 @@ module.exports = function (_config) {
},
],
[
'@mozzius/expo-dynamic-app-icon',
'@bsky.app/expo-dynamic-app-icon',
{
/**
* Default set
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M17 3a4 4 0 0 1 4 4v10a4 4 0 0 1-4 4h-2a1 1 0 1 1 0-2h2a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2a1 1 0 1 1 0-2h2Zm-6.707 4.793a1 1 0 0 1 1.414 0l3.5 3.5a1 1 0 0 1 0 1.414l-3.5 3.5a1 1 0 1 1-1.414-1.414L12.086 13H4a1 1 0 1 1 0-2h8.086l-1.793-1.793a1 1 0 0 1 0-1.414Z"/></svg>

After

Width:  |  Height:  |  Size: 360 B

Before

Width:  |  Height:  |  Size: 334 B

After

Width:  |  Height:  |  Size: 334 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M5 5a1 1 0 0 0-2 0v4a7 7 0 0 0 7 7h8.086l-2.293 2.293a1 1 0 0 0 1.414 1.414l2.94-2.94a2.5 2.5 0 0 0 0-3.535l-2.94-2.94a1 1 0 1 0-1.414 1.415L18.086 14H10a5 5 0 0 1-5-5V5Z"/></svg>

After

Width:  |  Height:  |  Size: 273 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 64 64"><path fill="#111822" d="M55 32C55 19.298 44.703 9 32 9S9 19.298 9 32c0 3.463.765 6.745 2.134 9.688.358.769.479 1.658.267 2.525l-2.076 8.51a1.667 1.667 0 0 0 1.987 2.02l8.876-2.005a3.84 3.84 0 0 1 2.412.26A22.9 22.9 0 0 0 32 55c12.703 0 23-10.297 23-23Zm2 0c0 13.807-11.193 25-25 25-3.638 0-7.098-.778-10.219-2.177a1.84 1.84 0 0 0-1.152-.133l-8.877 2.004c-2.655.6-5.015-1.802-4.37-4.446l2.077-8.51a1.84 1.84 0 0 0-.139-1.207A24.9 24.9 0 0 1 7 32C7 18.193 18.193 7 32 7s25 11.193 25 25Z"/><path fill="#111822" d="M17.667 32a2.333 2.333 0 1 0 4.667 0 2.333 2.333 0 0 0-4.667 0Zm24 0a2.334 2.334 0 1 0 4.667 0 2.334 2.334 0 0 0-4.667 0Z"/><path fill="#000" d="M35.137 37.215a1 1 0 0 1 1.414 1.414 6.143 6.143 0 0 1-8.687 0 1 1 0 0 1 1.415-1.414 4.14 4.14 0 0 0 5.858 0Z"/></svg>

After

Width:  |  Height:  |  Size: 847 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M14.3 23v-1.1a1 1 0 0 1 2 0V23a1 1 0 1 1-2 0Zm5.243-3.457a1 1 0 0 1 1.414 0l1.1 1.1a1 1 0 1 1-1.414 1.414l-1.1-1.1a1 1 0 0 1 0-1.414ZM4.788 9.298a1 1 0 0 1 1.424 1.404l-.742.752-.004.005a5.003 5.003 0 1 0 7.075 7.075l.005-.004.752-.742a1 1 0 0 1 1.404 1.424l-.747.736a7.003 7.003 0 1 1-9.904-9.904l.737-.746ZM23 14.3a1 1 0 0 1 0 2h-1.1a1 1 0 1 1 0-2H23ZM10.044 4.05a7.005 7.005 0 0 1 9.905 9.906h0l-.737.746a1 1 0 0 1-1.424-1.404l.742-.752.004-.005a5.003 5.003 0 1 0-7.075-7.075l-.005.004-.752.742a1 1 0 0 1-1.404-1.424l.746-.737ZM2.1 7.7a1 1 0 1 1 0 2H1a1 1 0 0 1 0-2h1.1Zm-.157-5.757a1 1 0 0 1 1.414 0l1.1 1.1a1 1 0 1 1-1.414 1.414l-1.1-1.1a1 1 0 0 1 0-1.414ZM7.7 2.1V1a1 1 0 1 1 2 0v1.1a1 1 0 0 1-2 0Z"/></svg>

After

Width:  |  Height:  |  Size: 807 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M17.058 1.001c.466-.012 1.294.05 1.828.782.099.136.172.274.224.405.385-.036.92-.08 1.416-.039.48.04 1.448.197 2.012 1.09.497.787.372 1.592.13 2.14-.14.315-.345.602-.505.807.172.15.366.354.518.613.366.622.383 1.312.22 1.962-.285 1.143-1.24 1.642-1.82 1.865-.597.23-1.296.354-1.818.448l-.06.01-.121.022c.128.445.101.918-.048 1.367-.41 1.242-1.349 1.831-2.24 2.08-.764.214-1.618.213-2.225.212h-.218c.152.615-.022 1.221-.192 1.631-.63 1.514-2.053 2.08-3.203 2.319s-2.484.247-3.591.253h-.076c-.607.004-1.145.007-1.616.034-.417.806-.763 1.702-1.163 2.75l-.002.002q-.158.414-.327.85c-.446 1.14-1.709 1.693-2.82 1.235-1.11-.459-1.65-1.755-1.204-2.897.571-1.462 1.164-2.48 1.954-4.046.212-.42.39-.76.547-1.075 0-.951.07-1.624.207-2.476.23-1.432.423-1.984.707-3.29.26-.903.644-2.115 1.247-2.783.346-.384.88-.945 1.685-.983.489-.023.794.38 1.052.553.749-1.04 1.384-2.427 2.202-3.503.657-.863 1.363-1.618 2.503-1.438.588.093 1.423.547 1.424 1.438.217-.249.883-1.204 1.162-1.438.532-.445 1.27-.874 2.21-.9m-.42 1.154c-1.003.028-1.71.904-2.303 1.64-.43.532-.8.991-1.18 1.001-.373.01-.345-.247-.31-.572.05-.464.115-1.066-.945-1.234-.879-.14-1.805 1.531-2.61 2.981-.551.993-1.046 1.883-1.428 2.019-.584.208-.854-.037-1.085-.245-.28-.252-.5-.451-1.147.265-.681.755-1.146 2.82-1.596 5.068-.057.287.342.472.525.247 1.036-1.28 2.72-3.039 4.73-4.34 1.848-1.196 3.63-2.385 5.647-2.983.297-.088.38.253.096.377-2.273.994-4.226 2.304-6.272 4.053-2.299 1.923-4.474 4.483-5.577 6.57a27 27 0 0 0-1.829 3.786.724.724 0 0 0 .388.933.693.693 0 0 0 .91-.398q.162-.418.316-.825c.473-1.234.915-2.391 1.539-3.443a.25.25 0 0 1 .187-.123c.678-.082 1.485-.087 2.32-.092 2.277-.013 4.764-.028 5.431-1.632.348-.838-.071-.959-.45-1.067-.31-.089-.593-.17-.406-.629.323-.792 1.377-.793 2.494-.793 1.301 0 2.687 0 3.103-1.254.177-.537-.14-.748-.42-.934-.235-.157-.444-.296-.317-.596.201-.477 1.078-.636 2.007-.803 1.11-.2 2.294-.412 2.49-1.198.186-.744-.16-.983-.52-1.232-.218-.151-.44-.307-.55-.582-.133-.337.158-.697.461-1.071.358-.442.731-.903.438-1.367-.334-.527-1.327-.434-2.15-.358-.512.048-.959.089-1.136-.03-.18-.12-.173-.325-.166-.528.011-.317.022-.63-.685-.611"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.54729 11.8415C6.66801 12.836 7.11067 14.8647 8.11671 16.5751C8.23744 16.8138 8.55937 16.774 8.63986 16.5751L14.4346 6.07367C14.5553 5.87478 14.5151 5.59634 14.3139 5.43722C13.4688 4.72121 12.3823 4.28365 11.0946 4.32343C6.46681 4.44276 6.42657 10.2106 6.54729 11.8415ZM12.9054 19.638C17.4125 19.5187 17.5734 13.7907 17.4125 12.1995C17.332 11.1653 16.8894 9.09681 15.8431 7.3068C15.7626 7.14769 15.4809 7.14769 15.4004 7.3068L9.56541 17.8878C9.48492 18.1265 9.52517 18.3652 9.68613 18.5243C10.5312 19.2403 11.6177 19.6778 12.9054 19.638ZM17.8149 2.93119L17.5734 3.36875C17.4125 3.6472 17.493 3.96543 17.7345 4.08476C20.9537 6.11345 22 9.61393 22 11.9211C22 14.7851 19.5856 21.4678 12.2213 21.4678C10.7727 21.4678 9.52517 21.2689 8.3984 20.9109C8.1972 20.8314 7.9155 20.9109 7.79478 21.1098L7.47284 21.7463C7.35212 21.9849 7.07043 22.0645 6.82898 21.9452L6.38632 21.7065C6.14488 21.5872 6.0644 21.3088 6.18512 21.0701L6.46681 20.5529C6.58753 20.3143 6.50705 20.0358 6.30584 19.9165C3.08652 17.848 2 14.3078 2 12.0802C2 9.21615 4.21328 2.53341 11.6177 2.53341C13.1469 2.53341 14.4346 2.7323 15.5614 3.0903C15.8028 3.16986 16.0846 3.05053 16.2053 2.85163L16.5674 2.21518C16.6881 2.01629 16.9699 1.93673 17.1711 2.05607L17.654 2.29474C17.8552 2.41407 17.9357 2.69252 17.8149 2.93119Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M4.686 8.752c1.117 0 1.88.517 2.246 1.52q.247.678.249 1.643l-.001.097q-.014.94-.267 1.6l-.074.176c-.394.858-1.123 1.303-2.153 1.303a2.32 2.32 0 0 1-1.408-.455l-.035-.028v1.966a1 1 0 0 1-.008.074c-.029.178-.146.267-.359.267H.854q-.273 0-.335-.216l-.01-.035a1 1 0 0 1-.009-.09v-7.34a.7.7 0 0 1 .02-.149.27.27 0 0 1 .155-.18l.027-.01.005-.002a.5.5 0 0 1 .126-.02h2.066q.04 0 .076.007h.007l.048.013a.27.27 0 0 1 .204.231l.005.028a1 1 0 0 1 .004.07 2 2 0 0 1 .185-.131l.039-.024a2 2 0 0 1 .289-.144q.413-.17.93-.171m5.367.041c.46 0 .958.048 1.655.289l.015.005.021.008.06.021.02.007.018.008.017.007.033.014.024.012.015.008.02.01.003.004.01.005.006.004.01.007.003.003.01.008.005.003.009.008.003.003.009.007.011.012.008.009.004.004.006.008.005.005.005.007.013.023.005.009.004.007.002.003a.4.4 0 0 1 .036.134l.004.05.083 1.389a1 1 0 0 1 0 .083v.01l-.004.03-.007.043-.002.008q0 .006-.004.012v.005l-.005.014-.005.014-.002.003-.005.011-.002.004-.006.01-.002.004-.005.008q0 .003-.003.005l-.003.003-.006.009-.003.004-.018.016-.023.018-.005.003-.01.005h-.002l-.01.006q-.004 0-.008.003l-.007.002-.007.002-.009.003-.007.001-.01.003-.005.001-.013.002H12l-.012.002h-.007l-.013.001h-.048l-.013-.003-.057-.008-.124-.02a9 9 0 0 0-1.272-.112c-.804 0-1.146.265-1.146.857 0 .495.212.857 1.134.857a10 10 0 0 0 1.396-.12c.271-.049.366.06.354.326l-.083 1.388a1 1 0 0 1-.01.093l-.001.008-.003.013-.002.006-.004.012-.018.047-.007.016-.002.002-.006.011-.003.006c-.068.107-.192.153-.358.21a4.5 4.5 0 0 1-1.655.3c-1.24 0-2.135-.377-2.706-.996.306-.61.445-1.365.445-2.219 0-.828-.13-1.562-.418-2.16.574-.6 1.464-.963 2.69-.963m11.419-1.227q.354 0 .354.363v1.073c.386.048.796.116 1.265.21.2.041.327.242.285.446l-.307 1.477a.374.374 0 0 1-.438.293 17 17 0 0 0-.805-.142v.603c0 .724.425.82.71.821.117 0 .33-.024.52-.048.271-.036.39.072.378.338l-.082 1.46c-.012.29-.166.46-.415.52a5.5 5.5 0 0 1-1.24.145c-1.869 0-2.614-.978-2.614-2.982v-.985l-.165.008a.4.4 0 0 1-.262-.085l-.002-.002a.4.4 0 0 1-.04-.039l-.004-.003a.4.4 0 0 1-.05-.071l-.003-.005a.4.4 0 0 1-.023-.052l-.002-.005-.008-.025v-.006a.4.4 0 0 1-.01-.057l-.001-.011v-.006l-.001-.019v-.784c0-.165.07-.322.193-.43.191-.168.493-.435.759-.673.154-.139.296-.268.397-.363l.003-.003.744-.683c.201-.181.438-.278.71-.278zM14.052 7c.249 0 .368.12.368.362v2.52c.375-.199.75-.37 1.143-.512.91-.328 1.852-.47 3.017-.482l-.283.25a1.14 1.14 0 0 0-.385.858v.784q.002.214.083.394c-.294.02-.56.052-.806.096l.01.029c.436 1.35 1.099 3.357 1.099 3.357.082.254-.013.386-.273.386h-2.01a.524.524 0 0 1-.532-.386l-.77-2.338q-.142.09-.293.188v2.174q.002.362-.367.362h-1.665a.95.95 0 0 0 .23-.6l.083-1.39v-.007c.008-.165-.001-.484-.243-.718-.248-.24-.564-.225-.748-.194l-.033.005v-.355l.033.006c.197.035.521.046.77-.208.23-.234.24-.544.233-.713v-.009l-.083-1.388a.93.93 0 0 0-.317-.685 1.3 1.3 0 0 0-.382-.209l-.093-.032-.161-.054V7.362q0-.362.354-.362zM3.919 10.868c-.556 0-.74.423-.74 1.132s.184 1.132.74 1.132.738-.423.738-1.132-.182-1.132-.738-1.132"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M13.004 1.646c1.983 0 3.668.678 4.824 2.135q.17.215.32.446.694.46 1.24 1.146C20.512 6.787 21 8.767 21 11.103c0 1.856-.31 3.49-.999 4.795q.105.073.201.163c.306.288.483.648.595.967l.007.019.006.02c.144.46.24 1.13-.137 1.75a1.8 1.8 0 0 1-.263.335q.031.196.017.413c-.054.732-.522 1.223-.926 1.516H19.5c-.3.218-.73.458-1.268.458-.311 0-.58-.078-.807-.201-.23.123-.5.201-.81.201-.515 0-.955-.24-1.256-.458l-.013-.01c-.333-.249-.735-.648-.873-1.224a2.6 2.6 0 0 1-.673-.357l-.014-.01a2.6 2.6 0 0 1-.617-.64 6 6 0 0 1-.48-.128v2.168c0 .439-.108.995-.543 1.428-.429.427-.972.53-1.4.53H6.48c-.43 0-.968-.111-1.39-.541a1.87 1.87 0 0 1-.506-1.074 1.8 1.8 0 0 1-1.053-.517c-.421-.43-.53-.978-.53-1.417V3.86c0-.44.109-.987.53-1.418s.96-.54 1.389-.54h4.267c.348 0 .767.067 1.14.315.798-.376 1.7-.57 2.678-.57m0 1.193c-1.223 0-2.246.357-3.045.993 0-.483-.25-.738-.773-.738H4.918q-.748 0-.75.763V19.29q.002.763.75.764h4.268c.523 0 .773-.255.773-.764v-4.1c.81.625 1.814 1.005 2.985 1.018.007-.19.048-.377.107-.552.086-.277.218-.548.432-.753a1.17 1.17 0 0 1 .954-.318 1.25 1.25 0 0 1 .267-.548c.304-.355.749-.455 1.165-.455.411 0 .858.1 1.16.458q.064.076.112.156c.744-1.115 1.129-2.685 1.129-4.684 0-4.38-1.847-6.672-5.266-6.672m2.865 11.34c-.684 0-.955.332-.873 1.008l.047.248-.224-.119a1.2 1.2 0 0 0-.508-.142c-.341 0-.566.214-.707.676-.213.628.024.995.696 1.114l.248.036-.177.201c-.483.474-.448.9.106 1.316.212.153.413.237.578.237.271 0 .507-.19.696-.557l.118-.225.118.225c.189.367.414.557.685.557.189 0 .377-.083.59-.237.554-.403.578-.842.106-1.328L17.203 17l.236-.036c.684-.119.896-.486.696-1.126-.154-.438-.378-.651-.697-.651-.153 0-.318.047-.518.141l-.236.107.035-.248c.106-.676-.177-1.008-.85-1.008m-4.485-6.877c1.173 0 1.56.893 1.56 2.386s-.387 2.388-1.56 2.388-1.56-.894-1.56-2.388.387-2.386 1.56-2.386"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M11.815.056a.33.33 0 0 1 .37 0c.13.088.2.95.34 2.673l.047.599h3.312a4.745 4.745 0 0 1 4.745 4.745v.818h-1.637v-.818a3.11 3.11 0 0 0-3.109-3.11h-3.175c.147 1.658.295 2.622.726 3.41a5.24 5.24 0 0 0 2.026 2.05c.969.545 2.214.667 4.702.91l1.366.133c1.553.151 2.33.227 2.417.357a.32.32 0 0 1-.003.364c-.088.128-.866.192-2.422.32l-.891.073v3.347a4.745 4.745 0 0 1-4.746 4.746h-3.315l-.044.56c-.139 1.748-.21 2.622-.34 2.711a.33.33 0 0 1-.369 0c-.13-.088-.2-.963-.339-2.711l-.045-.56H8.03a4.746 4.746 0 0 1-4.746-4.746v-.818H4.92v.818a3.11 3.11 0 0 0 3.11 3.11h3.269c-.15-1.72-.297-2.713-.743-3.52a5.24 5.24 0 0 0-2.064-2.057c-.987-.543-2.254-.646-4.786-.853l-1.227-.1c-1.555-.128-2.333-.192-2.422-.32a.325.325 0 0 1-.003-.364c.087-.13.864-.206 2.417-.357l.812-.08V8.073a4.746 4.746 0 0 1 4.746-4.745h3.399l.048-.599c.14-1.722.209-2.585.34-2.673m7.177 12.66c-1.697.152-2.682.303-3.484.744a5.24 5.24 0 0 0-2.065 2.057c-.445.807-.592 1.8-.741 3.52h3.182a3.11 3.11 0 0 0 3.108-3.11zM8.029 4.964a3.11 3.11 0 0 0-3.11 3.109v3.152c1.79-.184 2.803-.342 3.62-.803a5.24 5.24 0 0 0 2.027-2.048c.432-.79.58-1.753.727-3.41z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M3 16.8V7.2c0-.544-.001-1.011.03-1.395.033-.395.104-.789.297-1.167a3 3 0 0 1 1.31-1.31c.379-.193.772-.265 1.168-.297C6.188 2.999 6.657 3 7.2 3H11a1 1 0 1 1 0 2H7.2c-.576 0-.949 0-1.232.023-.272.022-.373.06-.422.085a1 1 0 0 0-.437.437c-.025.05-.062.15-.085.422C5.001 6.251 5 6.623 5 7.2v9.6c0 .577.001.95.024 1.232.023.272.06.373.085.422a1 1 0 0 0 .437.437c.05.025.15.063.422.085.283.023.656.024 1.232.024h9.6c.576 0 .949-.001 1.232-.024.272-.022.373-.06.422-.085a1 1 0 0 0 .437-.437c.025-.049.062-.15.085-.422.023-.283.024-.655.024-1.232V13a1 1 0 1 1 2 0v3.8c0 .543.001 1.011-.03 1.395-.033.395-.104.788-.297 1.167a3 3 0 0 1-1.31 1.311c-.379.193-.772.264-1.168.296-.383.031-.852.031-1.395.031H7.2c-.543 0-1.012 0-1.395-.031-.396-.032-.789-.103-1.167-.296a3 3 0 0 1-1.31-1.311c-.194-.379-.265-.772-.298-1.167C3 17.81 3 17.343 3 16.8M16.629 2.957a3 3 0 0 1 4.242 0l.172.171a3 3 0 0 1 0 4.243L13 15.414a2 2 0 0 1-1.414.586H9a1 1 0 0 1-1-1v-2.586A2 2 0 0 1 8.586 11zM10 14h1.586l8.043-8.043a1 1 0 0 0 0-1.414l-.172-.172a1 1 0 0 0-1.414 0L10 12.414z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M19 14h-2.417a5 5 0 0 1-9.166 0H5v4a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-4Zm0-8a1 1 0 0 0-.898-.995L18 5H6a1 1 0 0 0-1 1v6h3.126a1 1 0 0 1 .969.751 3.001 3.001 0 0 0 5.81 0l.056-.16a1 1 0 0 1 .913-.591H19V6Zm2 12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12l.154.004A3 3 0 0 1 21 6v12Z"/></svg>

After

Width:  |  Height:  |  Size: 387 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 64 64"><path fill="#111822" d="M57.666 33.416q-.001-.21-.022-.419L57.6 33H44a4.33 4.33 0 0 0-3.466 1.734l-.8 1.066a6.34 6.34 0 0 1-5.068 2.533h-5.333a6.33 6.33 0 0 1-5.066-2.533l-.8-1.066A4.33 4.33 0 0 0 20 33H6.4l-.045-.003a4 4 0 0 0-.022.42v11.916a4.333 4.333 0 0 0 4.333 4.333h42.667a4.333 4.333 0 0 0 4.333-4.333V33.416Zm-38.57-19.083a4.33 4.33 0 0 0-3.764 2.184L7.057 31H20c1.994 0 3.87.939 5.067 2.533l.8 1.067a4.33 4.33 0 0 0 3.466 1.733h5.333a4.33 4.33 0 0 0 3.467-1.733l.8-1.067A6.34 6.34 0 0 1 44 31h12.944l-8.277-14.483a4.33 4.33 0 0 0-3.762-2.184h-25.81Zm40.57 31a6.333 6.333 0 0 1-6.333 6.333H10.666a6.333 6.333 0 0 1-6.333-6.333V33.416c0-1.102.288-2.185.835-3.142l8.428-14.75a6.33 6.33 0 0 1 5.5-3.19h25.809a6.33 6.33 0 0 1 5.499 3.19l8.427 14.75h.001a6.33 6.33 0 0 1 .834 3.142v11.917Z"/></svg>

After

Width:  |  Height:  |  Size: 875 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20"><path fill="#000" fill-rule="evenodd" d="M14.5 8a1 1 0 0 1 .912.59l4.5 10a1 1 0 0 1-1.824.82L16.554 16h-4.108l-1.534 3.41a1 1 0 0 1-1.824-.82l4.5-10A1 1 0 0 1 14.5 8m-1.153 6h2.306L14.5 11.437zM5.053.106a1 1 0 0 1 1.342.447L7.118 2H11a1 1 0 0 1 0 2H9.92c-.28 1.765-1.27 3.24-2.393 4.398a17 17 0 0 0 2.605 1.562q.129.062.197.092l.005.002.049.022.01.005a1 1 0 0 1-.787 1.838h-.002l-.027-.013-.068-.03-.242-.113A19 19 0 0 1 6 9.75a19 19 0 0 1-3.509 2.125l-.068.03-.027.013h-.002a1 1 0 0 1-.788-1.838l.011-.005.049-.022.005-.002q.069-.03.197-.092a17 17 0 0 0 2.604-1.562C3.348 7.239 2.36 5.764 2.08 4H1a1 1 0 0 1 0-2h3.882l-.277-.553A1 1 0 0 1 5.053.106M4.118 4c.27 1.151.98 2.193 1.882 3.1.901-.907 1.612-1.95 1.882-3.1z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 819 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M20 4.25a.25.25 0 0 0-.25-.25h-9.5a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25zM4 19.75c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V16h-3.75A2.25 2.25 0 0 1 8 13.75V10H4.25a.25.25 0 0 0-.25.25zm18-6A2.25 2.25 0 0 1 19.75 16H16v3.75A2.25 2.25 0 0 1 13.75 22h-9.5A2.25 2.25 0 0 1 2 19.75v-9.5A2.25 2.25 0 0 1 4.25 8H8V4.25A2.25 2.25 0 0 1 10.25 2h9.5A2.25 2.25 0 0 1 22 4.25z"/></svg>

After

Width:  |  Height:  |  Size: 502 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12 2a5 5 0 0 1 4.843 3.751 1 1 0 0 1-1.938.498A3.002 3.002 0 0 0 9 7v2h8a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-7a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5m-5 9a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1zm5 2a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1"/></svg>

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

+3
View File
@@ -47,6 +47,9 @@ module.exports = function (api) {
production: {
plugins: ['transform-remove-console'],
},
test: {
plugins: ['@babel/plugin-transform-class-static-block'],
},
},
}
}
+3 -3
View File
@@ -4,13 +4,13 @@ Install dependencies:
```bash
cd bskyembed
yarn
pnpm
```
Run the dev server:
```bash
yarn dev
pnpm dev
```
You can see the embed homepage at http://localhost:5173
@@ -20,7 +20,7 @@ You can see the embed homepage at http://localhost:5173
In another terminal window, run the snippet dev script:
```bash
yarn dev-snippet
pnpm dev-snippet
```
You can then see the testbed page at http://localhost:5173/test
+8 -2
View File
@@ -1,10 +1,12 @@
// @ts-check
import js from '@eslint/js'
import {defineConfig} from 'eslint/config'
import tseslint from 'typescript-eslint'
import simpleImportSort from 'eslint-plugin-simple-import-sort'
import {importX} from 'eslint-plugin-import-x'
import globals from 'globals'
export default tseslint.config(
export default defineConfig(
// Global ignores
{
ignores: ['dist/**', 'node_modules/**'],
@@ -14,7 +16,11 @@ export default tseslint.config(
js.configs.recommended,
// TypeScript rules with type checking
...tseslint.configs.recommendedTypeChecked,
tseslint.configs.recommendedTypeChecked,
// import-x
importX.flatConfigs.recommended,
importX.flatConfigs.typescript,
// Main configuration
{
+13 -9
View File
@@ -7,29 +7,33 @@
"dev-snippet": "tsc --project tsconfig.snippet.json && serve -s dist -p 3000 -n",
"build": "tsc && vite build",
"build-snippet": "tsc --project tsconfig.snippet.json",
"analyze": "ANALYZE=1 vite build",
"lint": "eslint --cache src",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"format": "prettier -w src"
},
"dependencies": {
"@atproto/api": "^0.15.25",
"@atproto/api": "0.20.11",
"preact": "^10.4.8"
},
"devDependencies": {
"@preact/preset-vite": "^2.10.2",
"@vitejs/plugin-legacy": "^7.0.0",
"@eslint/js": "^9.18.0",
"@preact/preset-vite": "^2.10.2",
"@vitejs/plugin-legacy": "^8.0.1",
"autoprefixer": "^10.4.19",
"eslint": "^9.18.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-import-x": "^4.16.2",
"eslint-plugin-simple-import-sort": "^13.0.0",
"globals": "^15.14.0",
"postcss": "^8.4.38",
"typescript-eslint": "^8.20.0",
"prettier": "^3.8.3",
"serve": "^14.2.5",
"tailwindcss": "^3.4.3",
"terser": "^5.43.1",
"typescript": "^5.8.3",
"vite": "^7.0.4",
"vite-tsconfig-paths": "^5.1.4"
"typescript": "^6.0.3",
"typescript-eslint": "^8.20.0",
"vite": "^8.0.16",
"vite-bundle-analyzer": "^1.3.8"
},
"resolutions": {
"@types/estree": "1.0.6"
+4826
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
allowBuilds:
'@sentry/cli': true
'core-js': true
'esbuild': true
'unrs-resolver': true
minimumReleaseAgeExclude:
- '@atproto/*'
- '@bsky.app/*'
+67 -18
View File
@@ -1,5 +1,6 @@
import {
AppBskyEmbedExternal,
AppBskyEmbedGallery,
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
@@ -43,6 +44,11 @@ export function Embed({
return <ImageEmbed content={content} labelInfo={labelInfo} />
}
// Case 1b: Gallery (Photos v2)
if (AppBskyEmbedGallery.isView(content)) {
return <GalleryEmbed content={content} labelInfo={labelInfo} />
}
// Case 2: External link
if (AppBskyEmbedExternal.isView(content)) {
return <ExternalEmbed content={content} labelInfo={labelInfo} />
@@ -230,6 +236,8 @@ function Info({children}: {children: ComponentChildren}) {
)
}
type GridImage = {thumb: string; alt: string}
function ImageEmbed({
content,
labelInfo,
@@ -240,20 +248,45 @@ function ImageEmbed({
if (labelInfo) {
return <Info>{labelInfo}</Info>
}
return (
<ImageGrid
images={content.images.map(i => ({thumb: i.thumb, alt: i.alt}))}
/>
)
}
switch (content.images.length) {
function GalleryEmbed({
content,
labelInfo,
}: {
content: AppBskyEmbedGallery.View
labelInfo?: string
}) {
if (labelInfo) {
return <Info>{labelInfo}</Info>
}
const images = content.items
.filter(AppBskyEmbedGallery.isViewImage)
.map(i => ({thumb: i.thumbnail, alt: i.alt}))
return <ImageGrid images={images} />
}
function ImageGrid({images}: {images: GridImage[]}) {
switch (images.length) {
case 0:
return null
case 1:
return (
<img
src={content.images[0].thumb}
alt={content.images[0].alt}
src={images[0].thumb}
alt={images[0].alt}
className="w-full rounded-xl overflow-hidden object-cover h-auto max-h-[1000px]"
/>
)
case 2:
return (
<div className="flex gap-1 rounded-xl overflow-hidden w-full aspect-[2/1]">
{content.images.map((image, i) => (
{images.map((image, i) => (
<img
key={i}
src={image.thumb}
@@ -268,13 +301,13 @@ function ImageEmbed({
<div className="flex gap-1 rounded-xl overflow-hidden w-full aspect-[2/1]">
<div className="flex-1 aspect-square">
<img
src={content.images[0].thumb}
alt={content.images[0].alt}
src={images[0].thumb}
alt={images[0].alt}
className="w-full h-full object-cover rounded-sm"
/>
</div>
<div className="flex flex-col gap-1 flex-1">
{content.images.slice(1).map((image, i) => (
{images.slice(1).map((image, i) => (
<img
key={i}
src={image.thumb}
@@ -285,21 +318,36 @@ function ImageEmbed({
</div>
</div>
)
case 4:
default: {
const remaining = images.length - 4
return (
<div className="grid grid-cols-2 gap-1 rounded-xl overflow-hidden">
{content.images.map((image, i) => (
<img
key={i}
src={image.thumb}
alt={image.alt}
className="aspect-[3/2] w-full object-cover rounded-sm"
/>
))}
{images.slice(0, 4).map((image, i) => {
const isOverflowCell = i === 3 && remaining > 0
return (
<div
key={i}
className="relative aspect-[3/2] rounded-sm overflow-hidden">
<img
src={image.thumb}
alt={image.alt}
className="absolute inset-0 w-full h-full object-cover"
/>
{isOverflowCell && (
<div
aria-label={`+${remaining} more image${remaining === 1 ? '' : 's'}, view post to see all`}
className="absolute inset-0 flex items-center justify-center bg-black/50">
<span className="text-white text-2xl font-semibold">
+{remaining}
</span>
</div>
)}
</div>
)
})}
</div>
)
default:
return null
}
}
}
@@ -416,6 +464,7 @@ function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) {
controls
playsinline
preload="metadata"
// @ts-expect-error https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/video#loading
loading="lazy"
aria-label={content.alt || undefined}
onClickCapture={evt => evt.stopPropagation()}
+3 -1
View File
@@ -154,7 +154,9 @@ export function Post({thread}: Props) {
}
function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
if (!record) return null
// text-only check - posts with no text (e.g. gallery posts) would otherwise
// render an empty <p> that adds an extra flex gap above the embed
if (!record?.text) return null
const rt = new RichText({
text: record.text,
+3 -5
View File
@@ -1,11 +1,10 @@
{
"compilerOptions": {
"target": "ES5",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"target": "ES2015",
"lib": ["DOM", "ESNext"],
"types": ["vite/client"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
@@ -17,7 +16,6 @@
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"downlevelIteration": true
},
"include": ["src", "snippet", "vite.config.ts", "*.config.cjs", ".eslintrc.cjs"]
"include": ["src", "snippet", "vite.config.ts", "*.config.cjs", "eslint.config.mjs"]
}
+3 -2
View File
@@ -1,8 +1,9 @@
{
"compilerOptions": {
"target": "ES5",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"target": "ES2015",
"lib": ["DOM", "ESNext"],
"strict": true,
"rootDir": "snippet",
"outDir": "dist"
},
"include": ["snippet"]
+8 -3
View File
@@ -1,10 +1,10 @@
import fs from 'node:fs'
import {resolve} from 'node:path'
import preact from '@preact/preset-vite'
import {preact} from '@preact/preset-vite'
import legacy from '@vitejs/plugin-legacy'
import type {Plugin, UserConfig} from 'vite'
import paths from 'vite-tsconfig-paths'
import {analyzer} from 'vite-bundle-analyzer'
/**
* World's hackiest router, for dev only. Serves `/post.html` to requests that start with `/embed/`
@@ -38,11 +38,13 @@ function devOnlyRouter(): Plugin {
const config: UserConfig = {
plugins: [
preact(),
paths(),
legacy({
targets: ['defaults', 'not IE 11'],
}),
devOnlyRouter(),
analyzer({
enabled: process.env.ANALYZE === '1',
}),
],
build: {
assetsDir: 'static',
@@ -53,6 +55,9 @@ const config: UserConfig = {
},
},
},
resolve: {
tsconfigPaths: true,
},
}
export default config
-3516
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "module",
"main": "index.ts",
"scripts": {
"test": "npm run test:unit && npm run test:e2e",
"test": "yarn run test:unit && yarn run test:e2e",
"test:e2e": "./tests/infra/with-test-db.sh node --loader ts-node/esm --test ./tests/index.ts",
"test:unit": "node --loader ts-node/esm --test ./src/*.test.ts",
"build": "tsc"
+19 -10
View File
@@ -3,29 +3,38 @@
"version": "0.0.0",
"type": "module",
"main": "src/index.ts",
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "11.5.2",
"onFail": "warn"
}
},
"scripts": {
"start": "node --loader ts-node/esm ./src/bin.ts",
"dev": "node --watch-path ./src --loader ts-node/esm ./src/bin.ts",
"start": "tsx ./src/bin.ts",
"dev": "LOG_ENABLED=true LOG_LEVEL=debug node --watch-path ./src --import tsx ./src/bin.ts",
"build": "tsc && cp -r src/assets dist/",
"install-fonts": "node --loader ts-node/esm scripts/install-fonts.ts"
"install-fonts": "tsx scripts/install-fonts.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@atproto/api": "0.12.19-next.0",
"@atproto/common": "^0.4.0",
"@atproto/api": "0.20.13",
"@atproto/common": "^0.6.1",
"@resvg/resvg-js": "^2.6.2",
"express": "^4.19.2",
"express-prom-bundle": "^7.0.0",
"http-terminator": "^3.2.0",
"pino": "^9.2.0",
"prom-client": "^15.1.3",
"react": "^18.3.1",
"satori": "^0.10.13",
"react": "^19.2.6",
"satori": "^0.26.0",
"twemoji": "^14.0.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.14.3",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
"@types/node": "^24.12.2",
"@types/react": "^19.2.15",
"tsx": "^4.21.0",
"typescript": "^6.0.3"
}
}
+2029
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
nodeLinker: 'hoisted'
autoInstallPeers: true # default: true
strictPeerDependencies: false # default: false
trustPolicy: 'no-downgrade' # default: off
trustPolicyIgnoreAfter: 10080 # 7 days, default: undefined
minimumReleaseAgeExclude:
- '@atproto/*'
- '@bsky.app/*'
allowBuilds:
cbor-extract: true
esbuild: true
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,4 +1,4 @@
import cluster, {Worker} from 'node:cluster'
import cluster, {type Worker} from 'node:cluster'
import {envInt} from '@atproto/common'
+121
View File
@@ -0,0 +1,121 @@
import {Img} from './Img.js'
const SCALE = 3
const BASE_SIZE = 120
const PADDING = 4
const CONTENT_SCALE = (BASE_SIZE - PADDING * 2) / BASE_SIZE
export const AVATAR_BUBBLES_SIZE = BASE_SIZE * SCALE
type BubbleConfig = {size: number; x: number; y: number}
const LAYOUTS: Record<1 | 2 | 3 | 4, BubbleConfig[]> = {
1: [
{size: 76, x: 0, y: 0},
{size: 76, x: 44, y: 44},
],
2: [
{size: 76, x: 0, y: 0},
{size: 76, x: 44, y: 44},
],
3: [
{size: 68, x: 0.5, y: 0},
{size: 56, x: 40.5, y: 64},
{size: 46, x: 73.5, y: 20},
],
4: [
{size: 68, x: 1, y: 1},
{size: 56, x: 63, y: 52},
{size: 42, x: 17, y: 77},
{size: 32, x: 75, y: 12},
],
}
const PERSON_PATH =
'M12.233 2a4.433 4.433 0 1 0 0 8.867 4.433 4.433 0 0 0 0-8.867ZM12.233 12.133c-3.888 0-6.863 2.263-8.071 5.435-.346.906-.11 1.8.44 2.436.535.619 1.36.996 2.25.996h10.762c.89 0 1.716-.377 2.25-.996.55-.636.786-1.53.441-2.436-1.208-3.173-4.184-5.435-8.072-5.435Z'
export function AvatarBubbles(props: {images: (Buffer | null)[]}) {
const count = Math.min(props.images.length, 4) as 1 | 2 | 3 | 4
const layout = LAYOUTS[count] ?? LAYOUTS[2]
const bubbles = layout.map((bubble, i) => ({
...bubble,
image: props.images[i] ?? null,
}))
// For the 2-avi layout, render in reverse so the first (top-left) paints last (on top)
if (count <= 2) {
bubbles.reverse()
}
return (
<div
style={{
display: 'flex',
width: AVATAR_BUBBLES_SIZE,
height: AVATAR_BUBBLES_SIZE,
position: 'relative',
}}>
{bubbles.map((bubble, i) => (
<AvatarBubble
key={i}
image={bubble.image}
size={bubble.size * CONTENT_SCALE * SCALE}
x={bubble.x * CONTENT_SCALE * SCALE + PADDING * SCALE}
y={bubble.y * CONTENT_SCALE * SCALE + PADDING * SCALE}
/>
))}
</div>
)
}
function AvatarBubble(props: {
image: Buffer | null
size: number
x: number
y: number
}) {
return (
<div
style={{
display: 'flex',
position: 'absolute',
left: props.x,
top: props.y,
width: props.size,
height: props.size,
borderRadius: '50%',
overflow: 'hidden',
}}>
{props.image ? (
<Img width="100%" height="100%" src={props.image} />
) : (
<Placeholder size={props.size} />
)}
</div>
)
}
function Placeholder(props: {size: number}) {
const iconSize = props.size * 0.5
const offset = (props.size - iconSize) / 2
return (
<div
style={{
display: 'flex',
width: props.size,
height: props.size,
backgroundColor: '#b0b0b0',
alignItems: 'center',
justifyContent: 'center',
}}>
<svg
width={iconSize}
height={iconSize}
viewBox="0 0 24 24"
style={{position: 'absolute', left: offset, top: offset}}>
<path d={PERSON_PATH} fill="white" />
</svg>
</div>
)
}
-2
View File
@@ -1,5 +1,3 @@
import React from 'react'
export function Butterfly(props: React.SVGAttributes<SVGSVGElement>) {
return (
<svg
+526
View File
@@ -0,0 +1,526 @@
/* eslint-disable bsky-internal/avoid-unwrapped-text */
import {ChatBskyGroupDefs} from '@atproto/api'
import {Img} from './Img.js'
export const CHAT_INVITE_HEIGHT = 630
export const CHAT_INVITE_WIDTH = 1200
const PADDING = 64
export function ChatInvite({
preview,
ownerImage,
}: {
preview: ChatBskyGroupDefs.JoinLinkPreviewView
ownerImage: Buffer<ArrayBufferLike> | null
}) {
return (
<div
style={{
width: CHAT_INVITE_WIDTH,
height: CHAT_INVITE_HEIGHT,
display: 'flex',
position: 'relative',
backgroundColor: '#006AFF',
}}>
<Background
style={{
position: 'absolute',
inset: 0,
}}
/>
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
padding: PADDING,
color: 'white',
fontFamily: 'Inter',
}}>
{/* Avatar */}
<div
style={{
display: 'flex',
width: 160,
height: 160,
borderRadius: '50%',
overflow: 'hidden',
}}>
{ownerImage && <Img src={ownerImage} width="100%" height="100%" />}
</div>
{/* Spacer pushes the text block to the bottom-left */}
<div style={{display: 'flex', flex: 1}} />
{/* Group Chat row */}
<div
style={{
display: 'flex',
alignItems: 'center',
fontSize: 26,
fontWeight: 600,
lineHeight: 1.3,
}}>
<ChatIcon style={{marginRight: 8, marginTop: -2}} />
Group Chat
</div>
{/* Chat name */}
<div
style={{
display: 'block',
fontSize: 80,
fontWeight: 700,
lineHeight: 1.1,
marginTop: 6,
minHeight: 130,
maxWidth: 1000,
wordBreak: 'break-word',
lineClamp: 2,
textOverflow: 'ellipsis',
overflow: 'hidden',
}}>
{preview.name}
</div>
{/* By @handle */}
<div
style={{
display: 'block',
fontSize: 34,
fontWeight: 400,
marginTop: 12,
lineHeight: 1.3,
lineClamp: 1,
textOverflow: 'ellipsis',
overflow: 'hidden',
maxWidth: 800,
wordBreak: 'break-all',
}}>
{'By @' + preview.owner.handle}
</div>
</div>
</div>
)
}
function ChatIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<path
d="M9.99935 14.3334C10.9198 14.3334 11.666 15.0796 11.666 16.0001C11.666 16.9206 10.9198 17.6667 9.99935 17.6667C9.07887 17.6667 8.33268 16.9206 8.33268 16.0001C8.33268 15.0796 9.07887 14.3334 9.99935 14.3334Z"
fill="white"
/>
<path
d="M15.9993 14.3334C16.9198 14.3334 17.666 15.0796 17.666 16.0001C17.666 16.9206 16.9198 17.6667 15.9993 17.6667C15.0789 17.6667 14.3327 16.9206 14.3327 16.0001C14.3327 15.0796 15.0789 14.3334 15.9993 14.3334Z"
fill="white"
/>
<path
d="M21.9993 14.3334C22.9198 14.3334 23.666 15.0796 23.666 16.0001C23.666 16.9206 22.9198 17.6667 21.9993 17.6667C21.0789 17.6667 20.3327 16.9206 20.3327 16.0001C20.3327 15.0796 21.0789 14.3334 21.9993 14.3334Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.9993 2.66675C23.3631 2.66675 29.3327 8.63628 29.3327 16.0001C29.3327 23.3639 23.3631 29.3334 15.9993 29.3334C14.0352 29.3334 12.167 28.9082 10.485 28.1433L4.24414 29.3113C3.80802 29.3927 3.35993 29.2513 3.04883 28.935C2.73759 28.6183 2.60409 28.1668 2.69336 27.7319L3.93424 21.6824C3.12049 19.9572 2.66602 18.0301 2.66602 16.0001C2.66602 8.63629 8.63556 2.66676 15.9993 2.66675ZM15.9993 5.33341C10.1083 5.33343 5.33268 10.1091 5.33268 16.0001C5.33268 17.7656 5.76102 19.4272 6.51758 20.8907C6.65753 21.1615 6.69993 21.4723 6.63867 21.7709L5.70378 26.3243L10.4212 25.4415C10.7058 25.3883 11.0005 25.4298 11.2598 25.5587C12.6861 26.2674 14.2941 26.6667 15.9993 26.6667C21.8904 26.6667 26.666 21.8911 26.666 16.0001C26.666 10.109 21.8904 5.33341 15.9993 5.33341Z"
fill="white"
/>
</svg>
)
}
function Background(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width="1200"
height="630"
viewBox="0 0 1200 630"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<g clip-path="url(#clip0_3134_11977)">
<path
d="M938.799 522.018C944.511 526.47 950.654 535.496 952.91 540.34C955.166 535.496 961.309 526.47 967.021 522.018C971.142 518.806 977.82 516.32 977.82 524.229C977.82 525.808 976.948 537.498 976.436 539.396C974.658 545.993 968.178 547.676 962.413 546.657C972.489 548.438 975.053 554.335 969.517 560.232C957.062 573.5 952.91 553.134 952.91 553.134C952.91 553.134 948.758 573.5 936.303 560.232C930.768 554.335 933.331 548.438 943.407 546.657C937.642 547.676 931.162 545.993 929.384 539.396C928.872 537.498 928 525.808 928 524.229C928 516.32 934.678 518.806 938.799 522.018Z"
fill="white"
/>
<path
d="M1008.88 541.392C1012.31 542.653 1014.13 545.565 1014.13 548.825C1014.13 554.346 1010.53 557.693 1003.63 557.693H989.527V526.743H1003.16C1009.73 526.743 1012.9 530.177 1012.9 534.742C1012.9 537.784 1011.55 540.001 1008.88 541.392ZM1002.74 531.568H995.244V539.653H1002.74C1005.66 539.653 1007.23 538.089 1007.23 535.48C1007.23 533.09 1005.62 531.568 1002.74 531.568ZM995.244 552.824H1003.33C1006.55 552.824 1008.29 551.303 1008.29 548.564C1008.29 545.695 1006.64 544.261 1003.33 544.261H995.244V552.824Z"
fill="white"
/>
<path
d="M1022.06 557.693H1016.72V526.743H1022.06V557.693Z"
fill="white"
/>
<path
d="M1039.75 547.825V535.263H1045.09V557.693H1039.92V554.432C1038.27 556.954 1035.98 558.214 1033.06 558.214C1028.44 558.214 1025.44 555.345 1025.44 550.129V535.263H1030.77V549.216C1030.77 552.042 1032.13 553.476 1034.88 553.476C1037.46 553.476 1039.75 551.52 1039.75 547.825Z"
fill="white"
/>
<path
d="M1069.37 546.869V548.173H1053.03C1053.41 552.042 1055.48 553.954 1058.7 553.954C1061.16 553.954 1062.81 552.868 1063.7 550.738H1068.82C1067.68 555.302 1063.87 558.214 1058.66 558.214C1055.4 558.214 1052.77 557.127 1050.78 554.998C1048.79 552.868 1047.78 550.042 1047.78 546.478C1047.78 542.957 1048.75 540.132 1050.74 537.958C1052.73 535.828 1055.31 534.742 1058.57 534.742C1061.88 534.742 1064.5 535.872 1066.45 538.089C1068.4 540.305 1069.37 543.261 1069.37 546.869ZM1058.53 539.001C1055.61 539.001 1053.58 540.74 1053.07 544.348H1064.04C1063.57 541.088 1061.62 539.001 1058.53 539.001Z"
fill="white"
/>
<path
d="M1081.2 558.301C1074.85 558.301 1071.5 555.737 1071.2 550.564H1076.41C1076.71 553.346 1078.06 554.389 1081.28 554.389C1084.16 554.389 1085.6 553.476 1085.6 551.694C1085.6 550.086 1084.59 549.303 1081.33 548.738L1078.83 548.303C1074.04 547.478 1071.67 545.217 1071.67 541.522C1071.67 537.306 1074.93 534.742 1080.73 534.742C1086.96 534.742 1090.18 537.263 1090.39 542.348H1085.35C1085.22 539.61 1083.65 538.654 1080.73 538.654C1078.19 538.654 1076.92 539.523 1076.92 541.262C1076.92 542.827 1078.02 543.522 1080.56 544L1083.32 544.435C1088.61 545.435 1090.9 547.434 1090.9 551.259C1090.9 555.78 1087.34 558.301 1081.2 558.301Z"
fill="white"
/>
<path
d="M1114.31 557.693H1108.22L1101.86 547.26L1098.56 550.651V557.693H1093.31V526.743H1098.56V544.565L1107.45 535.263H1113.81L1105.55 543.739L1114.31 557.693Z"
fill="white"
/>
<path
d="M1128.25 540.914L1130.03 535.263H1135.62L1127.19 559.518C1126.3 561.996 1125.2 563.778 1123.8 564.778C1122.41 565.778 1120.42 566.256 1117.79 566.256C1116.9 566.256 1116.14 566.212 1115.46 566.125V561.822H1117.49C1119.91 561.822 1121.09 560.301 1121.09 558.214C1121.09 557.171 1120.75 555.65 1120.08 553.694L1113.72 535.263H1119.48L1121.26 540.87C1122.58 545.087 1123.72 549.26 1124.74 553.389C1125.67 549.825 1126.85 545.652 1128.25 540.914Z"
fill="white"
/>
<g filter="url(#filter0_d_3134_11977)">
<rect
x="236"
y="130"
width="145.6"
height="145.6"
rx="72.8"
fill="#4291FF"
shape-rendering="crispEdges"
/>
<path
d="M308.006 202.8C321.082 202.8 331.09 210.636 335.154 221.624C336.316 224.765 335.52 227.861 333.674 230.062C331.874 232.207 329.095 233.512 326.102 233.512H289.909C286.916 233.512 284.138 232.206 282.339 230.062C280.492 227.861 279.696 224.765 280.858 221.624C284.922 210.636 294.929 202.8 308.006 202.8ZM308.001 168.675C316.482 168.675 323.357 175.551 323.357 184.032C323.357 192.513 316.482 199.387 308.001 199.387C299.52 199.387 292.644 192.513 292.644 184.032C292.644 175.551 299.52 168.675 308.001 168.675Z"
fill="#006AFF"
/>
</g>
<g filter="url(#filter1_d_3134_11977)">
<rect
x="316"
y="-60"
width="191.1"
height="191.1"
rx="95.55"
fill="#4291FF"
shape-rendering="crispEdges"
/>
<path
d="M412.006 35.5498C429.441 35.55 442.784 45.9985 448.202 60.6484C449.751 64.8366 448.691 68.9642 446.229 71.8984C443.829 74.7581 440.125 76.4999 436.135 76.5H387.876C383.886 76.5 380.182 74.7581 377.783 71.8984C375.32 68.9642 374.26 64.8366 375.809 60.6484C381.227 45.9983 394.571 35.5498 412.006 35.5498ZM411.999 -9.9502C423.307 -9.95 432.474 -0.782514 432.474 10.5254C432.474 21.8332 423.307 30.9998 411.999 31C400.691 31 391.524 21.8333 391.524 10.5254C391.524 -0.782633 400.691 -9.9502 411.999 -9.9502Z"
fill="#006AFF"
/>
</g>
<g filter="url(#filter2_d_3134_11977)">
<rect
x="472"
y="84"
width="246"
height="246"
rx="123"
fill="#4291FF"
shape-rendering="crispEdges"
/>
<path
d="M596.01 207C618.454 207.001 635.631 220.449 642.606 239.308C644.6 244.7 643.235 250.014 640.065 253.792C636.976 257.472 632.207 259.714 627.071 259.714H564.949C559.812 259.714 555.044 257.473 551.955 253.792C548.785 250.014 547.42 244.7 549.414 239.308C556.388 220.449 573.566 207 596.01 207ZM596.002 148.428C610.558 148.428 622.359 160.229 622.359 174.786C622.359 189.342 610.558 201.143 596.002 201.143C581.445 201.143 569.644 189.342 569.644 174.786C569.644 160.229 581.445 148.428 596.002 148.428Z"
fill="#006AFF"
/>
</g>
<g filter="url(#filter3_d_3134_11977)">
<rect
x="870"
y="-22"
width="308.195"
height="308.195"
rx="154.098"
fill="#4291FF"
shape-rendering="crispEdges"
/>
<path
d="M1024.01 132.098C1052.13 132.098 1073.65 148.948 1082.39 172.574C1084.89 179.329 1083.18 185.987 1079.21 190.719C1075.34 195.331 1069.36 198.14 1062.93 198.14H985.098C978.663 198.14 972.689 195.331 968.819 190.719C964.848 185.987 963.138 179.329 965.636 172.574C974.374 148.947 995.894 132.098 1024.01 132.098ZM1024 58.7178C1042.24 58.7178 1057.02 73.5023 1057.02 91.7393C1057.02 109.976 1042.24 124.76 1024 124.76C1005.76 124.76 990.981 109.976 990.981 91.7393C990.981 73.5024 1005.76 58.7179 1024 58.7178Z"
fill="#006AFF"
/>
</g>
<g filter="url(#filter4_d_3134_11977)">
<rect
x="546"
y="-290"
width="374"
height="374"
rx="187"
fill="#4291FF"
shape-rendering="crispEdges"
/>
</g>
<g filter="url(#filter5_d_3134_11977)">
<rect
x="728"
y="222"
width="180"
height="180"
rx="90"
fill="#4291FF"
shape-rendering="crispEdges"
/>
<path
d="M818.006 312C834.429 312 846.998 321.841 852.101 335.64C853.56 339.585 852.561 343.473 850.242 346.237C847.981 348.931 844.492 350.571 840.734 350.571H795.278C791.519 350.571 788.03 348.931 785.77 346.237C783.451 343.473 782.453 339.585 783.912 335.64C789.015 321.841 801.584 312 818.006 312ZM817.999 269.143C828.651 269.143 837.286 277.778 837.286 288.429C837.285 299.08 828.651 307.714 817.999 307.714C807.348 307.714 798.714 299.08 798.714 288.429C798.714 277.778 807.348 269.143 817.999 269.143Z"
fill="#006AFF"
/>
</g>
<g filter="url(#filter6_d_3134_11977)">
<rect
x="1146"
y="220"
width="178"
height="178"
rx="89"
fill="#4291FF"
shape-rendering="crispEdges"
/>
</g>
</g>
<defs>
<filter
id="filter0_d_3134_11977"
x="204"
y="114"
width="209.6"
height="209.6"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="16" />
<feGaussianBlur stdDeviation="16" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_3134_11977"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_3134_11977"
result="shape"
/>
</filter>
<filter
id="filter1_d_3134_11977"
x="284"
y="-76"
width="255.1"
height="255.1"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="16" />
<feGaussianBlur stdDeviation="16" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_3134_11977"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_3134_11977"
result="shape"
/>
</filter>
<filter
id="filter2_d_3134_11977"
x="440"
y="68"
width="310"
height="310"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="16" />
<feGaussianBlur stdDeviation="16" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_3134_11977"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_3134_11977"
result="shape"
/>
</filter>
<filter
id="filter3_d_3134_11977"
x="838"
y="-38"
width="372.195"
height="372.195"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="16" />
<feGaussianBlur stdDeviation="16" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_3134_11977"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_3134_11977"
result="shape"
/>
</filter>
<filter
id="filter4_d_3134_11977"
x="514"
y="-306"
width="438"
height="438"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="16" />
<feGaussianBlur stdDeviation="16" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_3134_11977"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_3134_11977"
result="shape"
/>
</filter>
<filter
id="filter5_d_3134_11977"
x="696"
y="206"
width="244"
height="244"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="16" />
<feGaussianBlur stdDeviation="16" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_3134_11977"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_3134_11977"
result="shape"
/>
</filter>
<filter
id="filter6_d_3134_11977"
x="1114"
y="204"
width="242"
height="242"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="16" />
<feGaussianBlur stdDeviation="16" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_3134_11977"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_3134_11977"
result="shape"
/>
</filter>
<clipPath id="clip0_3134_11977">
<rect width="1200" height="630" fill="white" />
</clipPath>
</defs>
</svg>
)
}
-2
View File
@@ -1,5 +1,3 @@
import React from 'react'
// @NOTE satori does not currently support webp, see vercel/satori#273
function detectMime(buf: Buffer): string {
if (buf[0] === 0xff && buf[1] === 0xd8) return 'image/jpeg'
+8 -3
View File
@@ -1,9 +1,9 @@
/* eslint-disable bsky-internal/avoid-unwrapped-text */
import React from 'react'
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
import {Butterfly} from './Butterfly.js'
import {Img} from './Img.js'
import * as bsky from '../types/bsky/index.js'
export const STARTERPACK_HEIGHT = 630
export const STARTERPACK_WIDTH = 1200
@@ -18,7 +18,10 @@ export function StarterPack(props: {
images: Map<string, Buffer>
}) {
const {starterPack, images} = props
const record = AppBskyGraphStarterpack.isRecord(starterPack.record)
const record = bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
starterPack.record,
AppBskyGraphStarterpack.isRecord,
)
? starterPack.record
: null
const imagesArray = [...images.values()]
@@ -54,6 +57,7 @@ export function StarterPack(props: {
backgroundColor: 'black',
color: 'white',
fontFamily: 'Inter',
fontWeight: 700,
}}>
{/* image tiles */}
<div
@@ -134,6 +138,7 @@ export function StarterPack(props: {
fontSize: isLongTitle ? 55 : 65,
display: 'flex',
textAlign: 'center',
textWrap: 'balance',
}}>
{record?.name || 'Starter Pack'}
</div>
+4
View File
@@ -9,6 +9,7 @@ export type ServiceConfig = {
metricsPort: number
version?: string
appviewUrl: string
chatUrl: string
originVerify?: string
}
@@ -17,6 +18,7 @@ export type Environment = {
metricsPort?: number
version?: string
appviewUrl?: string
chatUrl?: string
originVerify?: string
}
@@ -26,6 +28,7 @@ export const readEnv = (): Environment => {
metricsPort: envInt('CARD_METRICS_PORT'),
version: envStr('CARD_VERSION'),
appviewUrl: envStr('CARD_APPVIEW_URL'),
chatUrl: envStr('CARD_CHAT_URL'),
originVerify: envStr('CARD_ORIGIN_VERIFY'),
}
}
@@ -36,6 +39,7 @@ export const envToCfg = (env: Environment): Config => {
metricsPort: env.metricsPort ?? 3001,
version: env.version,
appviewUrl: env.appviewUrl ?? 'https://api.bsky.app',
chatUrl: env.chatUrl ?? 'https://api.bsky.chat',
originVerify: env.originVerify,
}
return {
+36 -8
View File
@@ -3,44 +3,72 @@ import * as path from 'node:path'
import {fileURLToPath} from 'node:url'
import {AtpAgent} from '@atproto/api'
import {type FontWeight} from 'satori'
import {Config} from './config.js'
import {type Config} from './config.js'
const __DIRNAME = path.dirname(fileURLToPath(import.meta.url))
export type AppContextOptions = {
cfg: Config
appviewAgent: AtpAgent
fonts: {name: string; data: Buffer}[]
chatAgent: AtpAgent
fonts: {name: string; data: Buffer; weight?: FontWeight}[]
}
export class AppContext {
cfg: Config
appviewAgent: AtpAgent
fonts: {name: string; data: Buffer}[]
chatAgent: AtpAgent
fonts: {name: string; data: Buffer; weight?: FontWeight}[]
abortController = new AbortController()
constructor(private opts: AppContextOptions) {
this.cfg = this.opts.cfg
this.appviewAgent = this.opts.appviewAgent
this.fonts = this.opts.fonts
private opts: AppContextOptions
constructor(opts: AppContextOptions) {
this.opts = opts
this.cfg = opts.cfg
this.appviewAgent = opts.appviewAgent
this.chatAgent = opts.chatAgent
this.fonts = opts.fonts
}
static async fromConfig(cfg: Config, overrides?: Partial<AppContextOptions>) {
const appviewAgent = new AtpAgent({service: cfg.service.appviewUrl})
const chatAgent = new AtpAgent({service: cfg.service.chatUrl})
const fontDirectory = path.join(__DIRNAME, 'assets', 'fonts')
const fontFiles = readdirSync(fontDirectory)
const fonts = fontFiles.map(file => {
return {
name: path.basename(file, path.extname(file)),
name: getName(file),
data: readFileSync(path.join(fontDirectory, file)),
weight: getWeight(file),
}
})
return new AppContext({
cfg,
appviewAgent,
chatAgent,
fonts,
...overrides,
})
}
}
function getName(file: string): string {
const base = path.basename(file, path.extname(file))
if (base.startsWith('Inter-')) return 'Inter'
return base
}
function getWeight(file: string): FontWeight {
switch (path.basename(file, path.extname(file))) {
case 'Inter-Regular':
return 400
case 'Inter-SemiBold':
return 600
case 'Inter-Bold':
default:
return 700
}
}
+7 -4
View File
@@ -19,10 +19,13 @@ export class CardService {
private terminator?: HttpTerminator
private metricsTerminator?: HttpTerminator
constructor(
public app: express.Application,
public ctx: AppContext,
) {}
public app: express.Application
public ctx: AppContext
constructor(app: express.Application, ctx: AppContext) {
this.app = app
this.ctx = ctx
}
static async create(cfg: Config): Promise<CardService> {
let app = express()

Some files were not shown because too many files have changed in this diff Show More