Compare commits

...

1404 Commits

Author SHA1 Message Date
Samuel Newman d3e3ff8032 use ttf fonts on ios 2026-08-28 15:27:01 +01:00
Samuel Newman 00dc9598fc Forward labeler headers to chat (#11555) 2026-08-28 15:03:15 +03:00
Samuel Newman e91665d118 Keep chat reaction bar on screen (#11532) 2026-08-28 15:03:03 +03:00
pfrazee 3a34bf1ad1 Nightly source-language update 2026-08-28 04:52:31 +00:00
surfdude29 5eb5ac4800 Fix support link cutoff in signup footer on mobile (#11059) 2026-08-27 19:19:40 +03:00
Samuel Newman 9d76e42606 APP-2971: Fix onboarding avatar compression after Expo 57 (#11580) 2026-08-27 11:14:18 -05:00
Samuel Newman 41f2d203c3 Handle AppView errors after quote posts (#11579) 2026-08-27 11:06:58 -05:00
rafael cfd6d1e07e Fix pagination (#11448)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-08-27 10:35:38 -05:00
Tomasz Zawadzki f298a4ef5e Unblock React Compiler for 6 components by removing render-phase ref access (#11544)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 08:10:10 -07:00
Tomasz Zawadzki b9bff931a3 Unblock React Compiler for 4 components with deferred ref access (#11549)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 08:07:34 -07:00
Tomasz Zawadzki 296b74d781 Unblock React Compiler for 13 components by hoisting finally blocks (#11540)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 08:06:01 -07:00
estrattonbailey c4c999ff4f Nightly source-language update 2026-08-26 23:37:52 +00:00
Samuel Newman 206a932c5d Fix auto handle resolution during sign in (#11567) 2026-08-26 11:46:32 -07:00
Samuel Newman a0ed2da006 Fix starter pack navigation from search and explore (#11563) 2026-08-26 10:13:51 -07:00
mozzius ed4126f5a2 Nightly source-language update 2026-08-26 15:48:46 +00:00
Spence Pope 59082016e9 Update algorithmic recommendations helper text (#11546) 2026-08-26 08:45:28 -07:00
Michael Black c44f329b54 Update video embed lexicon (#11559) 2026-08-26 08:03:43 -07:00
Samuel Newman 057f97ce11 Fix URL.canParse in older browsers (#11557) 2026-08-26 06:57:58 -07:00
DS Boyce 8df64fcac1 Fix site initialization in older versions of Safari (#11553)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-08-26 06:35:35 -07:00
pfrazee 5b88335bb1 Nightly source-language update 2026-08-26 02:35:19 +00:00
Austin McKinley 196f41b528 Remove legacy ota1 publisher (#11554) 2026-08-25 18:46:25 -07:00
Samuel Newman a9c502c8cf Remove multipart upload and 10-minute video feature gates (#11528)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-25 14:38:33 -07:00
DS Boyce 5118748cb1 Create moderation inbox menu item and feature gate (#11552) 2026-08-25 13:31:13 -07:00
DS Boyce f968c9e721 Add OP thread numbering to feed posts (#11472) 2026-08-25 11:48:27 -07:00
Spence Pope 7750882fd0 [APP-2864] Use hydrated known likers (#11541) 2026-08-25 10:59:48 -04:00
Samuel Newman f87fdd2ea2 Enable React Native strict TypeScript API (#11459) 2026-08-25 09:40:14 +03:00
pfrazee 5f3d24efab Nightly source-language update 2026-08-25 02:30:39 +00:00
dependabot[bot] 56bff45546 Bump the actions group with 2 updates (#11537)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 17:40:18 -07:00
surfdude29 f1190ce681 Update tech stack versions and fix markdown in CLAUDE.md (#11530)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 17:34:30 -07:00
Samuel Newman cf2684b0d1 Send an integer exp for video upload service auth tokens (#11529)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-24 17:34:16 -07:00
Samuel Newman 95f5360012 Fix iOS message input height after sending (#11533) 2026-08-24 17:28:56 -07:00
DS Boyce 6b35d3de1c Revert "Send beta user state with AppView requests (#11474)" (#11510) 2026-08-24 11:40:15 -07:00
DS Boyce 6f46927c28 Overfetch trending topics before filtering (#11477) 2026-08-24 11:39:46 -07:00
Spence Pope 6009e61ae9 [APP-2922] Fix cached starter pack navigation (#11531) 2026-08-24 09:34:45 -04:00
pfrazee 97a85bb73d Nightly source-language update 2026-08-24 02:34:37 +00:00
Spence Pope 5e01a6c7ee Explain trending topics in options menu (#11521) 2026-08-23 20:23:38 -04:00
pfrazee 9047df5413 Nightly source-language update 2026-08-22 02:28:32 +00:00
RetroSunstar f48fab872e Adds a small padding for labeller avatars in group chat 'Add people' selector (#11422) 2026-08-21 15:12:14 -05:00
Samuel Newman 06cb80a4c3 [AA] Set maxAge: Infinity on age assurance cache (#11526) 2026-08-21 10:32:15 -07:00
Tomasz Zawadzki 44affaa6d7 Reland "Bump React Compiler to stable 1.0.0" (#11525)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 09:21:40 -07:00
Oleksii Bulenok 63f0d4d311 Fix hairline divider not visible (#11518) 2026-08-21 11:57:21 +02:00
Samuel Newman e0dd8eaed3 Back to v1.132.0 (#11522) 2026-08-21 00:17:04 -07:00
Austin McKinley 34f38c827d Make Safelink event replay idempotent (#11524) 2026-08-20 13:38:00 -07:00
Austin McKinley 21a2c3d6f2 Add graceful shutdown to Blink (#11523) 2026-08-20 13:16:31 -07:00
Tomasz Zawadzki a892057e29 Fix "Tried to synchronously call a Remote Function" crash (#11517)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 17:56:50 +02:00
Austin McKinley 56e16fcdf1 Add Blink Prometheus listener (#11509) 2026-08-20 08:50:12 -07:00
Spence Pope 3f7cf5b592 Drop package.json version (#11516) 2026-08-20 08:46:03 -04:00
Samuel Newman 11631c2f4d Revert "Bump React Compiler to stable 1.0.0" (#11512) 2026-08-20 08:40:34 -04:00
Michael Black 674f73b813 Omit non-positive SEO comment counts (#11508) 2026-08-19 20:15:18 -05:00
Spence Pope 6406a2ef4d translations (#11511) 2026-08-19 19:55:50 -04:00
Austin McKinley cc26e5a090 Log slow Blink database operations (#11505) 2026-08-19 12:38:48 -07:00
Samuel Newman a329899552 Fix HLS playback in Chrome embeds (#11506) 2026-08-19 10:53:35 -07:00
Samuel Newman 9d363e983b Fix MMKV persistence in Metro builds (#11504) 2026-08-19 09:37:41 -07:00
Samuel Newman c19fec3f48 Invert password reveal eye icon (#11365)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-19 07:37:01 -07:00
Samuel Newman 11c80a1fbd Fix profile handle resolution errors (#11503) 2026-08-19 07:34:45 -07:00
Samuel Newman e113da50d4 Bump version to v1.132.0 (#11502) 2026-08-19 06:53:17 -07:00
Tomasz Zawadzki 14f8ee10f9 Bump React Compiler to stable 1.0.0 (#11450)
Co-authored-by: Oleksii Bulenok <oleksii.bulenok@swmansion.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 06:31:29 -07:00
Tomasz Zawadzki 0e04159335 Unblock React Compiler for PostFeed (#11451)
Co-authored-by: Oleksii Bulenok <oleksii.bulenok@swmansion.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 06:28:53 -07:00
Samuel Newman 04ca3f9829 Only persist successful queries (#11484) 2026-08-19 06:27:41 -07:00
dependabot[bot] c328b9b6a6 Bump anthropics/claude-code-action from 1.0.189 to 1.0.190 in the actions group (#11489)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 06:27:26 -07:00
Samuel Newman 51381d41b0 Clear the cached initial URL after handling a deep link (#11500)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-19 06:26:58 -07:00
pfrazee c4172cef99 Nightly source-language update 2026-08-19 02:30:40 +00:00
Oleksii Bulenok 929af7d8be Add statsOptions to Webpack configuration (#11495) 2026-08-18 22:11:09 +02:00
DS Boyce cc3622b583 Send beta user state with AppView requests (#11474) 2026-08-18 09:50:15 -07:00
Oleksii Bulenok 45aa1a67b0 Expo 57 (#11334)
Co-authored-by: Tomek Zawadzki <tomekzawadzki98@gmail.com>
Co-authored-by: vineyardbovines <spencerfpope@gmail.com>
2026-08-18 10:46:11 -04:00
Spence Pope 7f59d8fcfb APP-2859 Add web list performance telemetry (#11486) 2026-08-18 09:26:50 -04:00
Spence Pope df3cea1c78 APP-2884: Avoid base64 reads for web videos (#11493) 2026-08-18 09:26:26 -04:00
Oleksii Bulenok e87ec032cd Truncate gif alt text to 2000 symbols (#11488) 2026-08-18 11:29:23 +02:00
pfrazee 3b7284a86b Nightly source-language update 2026-08-18 02:29:23 +00:00
DS Boyce 01486d27e0 Add pagination to feed search (#11475) 2026-08-17 09:55:45 -07:00
Samuel Newman fd86a7d3d8 Fix asset licensing notices (#11468) 2026-08-17 09:26:16 -07:00
pfrazee 4562ca8e38 Nightly source-language update 2026-08-17 02:32:20 +00:00
Spence Pope 0ba971b1f6 APP-2774: Add algorithmic visibility setting (#11417) 2026-08-16 17:13:17 -04:00
dependabot[bot] 1f5c698165 Bump anthropics/claude-code-action from 1.0.187 to 1.0.189 in the actions group across 1 directory (#11479)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-15 06:06:54 -07:00
dependabot[bot] 1e57ecfdd1 Bump Node to 24.19.0 (#11481)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Austin McKinley <austin@blueskyweb.xyz>
2026-08-14 22:30:15 -07:00
Austin McKinley 1123b31bf7 Fix Dependabot Node major hold 2026-08-14 22:11:36 -07:00
Austin McKinley d76e894220 Hold Node 26 until LTS 2026-08-14 22:05:36 -07:00
Austin McKinley 4bc0013a4d Enable Docker base image updates 2026-08-14 21:06:03 -07:00
pfrazee 34b6f1d323 Nightly source-language update 2026-08-15 02:27:20 +00:00
DS Boyce 221c4d9c5b Fix size of post numbers with emoji-only posts (#11471) 2026-08-14 12:58:55 -07:00
Samuel Newman bd920d80e0 Fix lab_* preferences (#11470) 2026-08-14 10:34:25 -07:00
Oleksii Bulenok 2d93273956 Fix metro failing to resolve @ipld/dag-cbor in dev mode (#11467) 2026-08-14 14:37:11 +02:00
Samuel Newman 70195d7059 Drop accidental eslint dependency (#11460) 2026-08-14 00:21:52 -07:00
pfrazee afb3cdc26f Nightly source-language update 2026-08-14 03:00:15 +00:00
Samuel Newman bb33eb91cb [SDK] Use finalised package (#11398) 2026-08-13 12:26:23 -07:00
Samuel Newman b89167f07c [SDK] Address stack review feedback (#11389)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:23 -07:00
Samuel Newman 3677c98873 [SDK] Validate and brand the persisted account did (#11387)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:22 -07:00
Samuel Newman 9fe0084874 [SDK] Remove @atproto/api (#11386)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:22 -07:00
Samuel Newman 54da80dfb0 [SDK] Delete the bridge agent and rework the session bundle onto lex clients (#11385)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:22 -07:00
Samuel Newman 59d2bf09d7 [SDK] Swap moderation to @bsky.app/sdk and move labeler config to Client statics (#11383) 2026-08-13 12:26:21 -07:00
Samuel Newman dc31e7fd12 [SDK] Migrate agent sugar to @bsky.app/sdk actions (#11382) 2026-08-13 12:26:21 -07:00
Samuel Newman a4f2811f39 [SDK] Add refreshSession and migrate the session-pinned infra (#11381)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:20 -07:00
Samuel Newman 0c93d1e416 [SDK] Migrate the post pipeline and blob uploads (#11380)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:20 -07:00
Samuel Newman 75e4180800 [SDK] Migrate the repo writes cluster (#11379)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:20 -07:00
Samuel Newman ca226f589d [SDK] Migrate the gate records to raw repo calls on the pds client (#11378)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:19 -07:00
Samuel Newman 3568cdc42d [SDK] Migrate getAllListMembers to the appview client (#11377)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:19 -07:00
Samuel Newman f6a22c4f05 [SDK] Adopt @bsky.app/sdk and migrate RichText (#11369) 2026-08-13 12:26:18 -07:00
Samuel Newman d991118215 [SDK] Route moderation reports through the appview client (#11368)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:18 -07:00
Samuel Newman 34d1c75756 [SDK] Migrate the account surface to the pds and service clients (#11367)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:17 -07:00
Samuel Newman f5c166a055 [SDK] Migrate the notifications cluster to the lex clients (#11362)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:17 -07:00
Samuel Newman 6f696e4b73 [SDK] Migrate the feed api classes to the appview client (#11361)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:17 -07:00
Samuel Newman 4aae6b2c22 [SDK] Migrate search, bookmarks, contacts and feed queries to the lex clients (#11360)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:16 -07:00
Samuel Newman ea4f70fb1e [SDK] Migrate group chat and join links to the chat client (#11358)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:16 -07:00
Samuel Newman 0e8e0b2ab4 [SDK] Migrate chat convo queries to the chat client (#11357)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>
2026-08-13 12:26:15 -07:00
Samuel Newman 0912119c77 [SDK] Migrate profile and graph read queries to the appview client (#11356)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:15 -07:00
Samuel Newman 855457c9fa [SDK] Migrate discovery and post-thread queries to the appview client (#11355)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:14 -07:00
Samuel Newman b804f267cc [SDK] Widen #/types/bsky views to dual-world unions (#11353)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:14 -07:00
Samuel Newman c52e730112 [SDK] Add pds and chat clients and the canonical client hooks (#11350)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:14 -07:00
Samuel Newman e1ba8fe1b0 [SDK] Add lexicon schema helpers to #/types/bsky (#11349)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:13 -07:00
Samuel Newman 837771a311 [SDK] Add lex client seam and typed xrpc error matching (#11348)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:13 -07:00
Samuel Newman 532681d5c0 [SDK] Add lexicon codegen pipeline (@atproto/lex) (#11346)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:12 -07:00
Samuel Newman 97b3978fb8 [SDK] Rebuild the session core on PasswordSession behind an AtpAgent compat shim (#11343)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:26:12 -07:00
DS Boyce 22b31c52bb Update image in deploy-ota.md (#11462) 2026-08-13 10:50:16 -07:00
Darrin Loeliger 2f0d417417 Updated Media Icons (#11457)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 05:49:05 -07:00
Samuel Newman da93da4191 Fix display of link cards in DMs (#11458) 2026-08-13 05:48:56 -07:00
pfrazee 419a49019d Nightly source-language update 2026-08-13 03:00:24 +00:00
DS Boyce aafcb2eeff Remove workaround for iOS text clipping (#11455) 2026-08-12 14:55:25 -07:00
Spence Pope f5f980e58e APP-1645: Improve video upload progress indicator (#11248) 2026-08-12 17:14:06 -04:00
Michael Black 3d736c76b7 APP-2829 Update video lexicon and beta features for 10-minute uploads (#11445) 2026-08-12 14:48:23 -05:00
Samuel Newman 9b0adcb55f Fix starter pack icon in profile menu (#11452) 2026-08-12 19:03:54 +03:00
DS Boyce bd5e56ce25 Include user's handle in beta features feedback (#11432) 2026-08-12 08:59:21 -07:00
Alex Benzer 34a7bc72a9 Client events for beta features settings page (#11439) 2026-08-12 08:54:29 -07:00
DS Boyce 4ed34cb242 Fix frozen unknown device ID for new accounts (#11449) 2026-08-12 08:54:01 -07:00
Samuel Newman 34274647f2 Separate landing illustrations from splash assets (#11453) 2026-08-12 06:47:03 -07:00
Samuel Newman 1f3c619dd2 Add asset licensing notices (#11397)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2026-08-12 05:49:31 -07:00
Jeffrey Gleason 98fa8eafdf algo team reply feedback (#11444) 2026-08-12 04:30:14 -07:00
pfrazee e7d82063ae Nightly source-language update 2026-08-12 02:58:39 +00:00
DS Boyce b801a24ef2 Fix layout jog when rendering post numbers (#11447) 2026-08-11 13:57:48 -07:00
DS Boyce 82db01fc60 Prevent last line of post text from being clipped on Android (#11433) 2026-08-11 13:57:18 -07:00
Michael Black 9d5e634780 Disallow robots crawling compose intent paths (#11443) 2026-08-11 14:30:59 -05:00
DS Boyce 8c54f01b3e Fix appearance of post numbers on image posts (#11446) 2026-08-11 12:21:05 -07:00
Samuel Newman 17d5dcebf7 Update pnpm to 11.21.0 (#11427) 2026-08-11 06:53:57 -07:00
dependabot[bot] 87548a6578 Bump the actions group with 4 updates (#11435)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-11 06:30:41 -07:00
Tomasz Zawadzki 2a4378c40b perf: skip minute-tick subscription for actors without a live status (#11441)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 06:26:52 -07:00
Austin McKinley 89cb08e93c claude: use Opus 4.8 for GitHub workflows (#11436) 2026-08-10 16:34:13 -07:00
Samuel Newman 44a44e86be Bump version to v1.131.0 (#11429) 2026-08-10 09:21:37 -07:00
Austin McKinley 892dcf65c0 Route Claude workflows through Agent Gateway (#11428) 2026-08-10 08:16:53 -07:00
Samuel Newman 51401e4c0c Update claude action (#11420) 2026-08-08 11:41:38 +03:00
DS Boyce 126ffed4ea v1.130.0 Release Prep (#11414) 2026-08-07 09:20:50 -07:00
Samuel Newman 98b9f1c21d Work around reanimated bug in sign up screen (#11411) 2026-08-07 09:09:37 -07:00
RetroSunstar cc3b9ac0d9 Fix chat info panel not using square icons for labellers (#11415) 2026-08-07 07:40:14 -07:00
Tomasz Zawadzki cab5b69a5e Patch react-native-worklets: fix web scheduleOnUI on errors (#11416)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 06:23:04 -07:00
pfrazee a3951c2bd7 Nightly source-language update 2026-08-07 03:07:00 +00:00
DS Boyce 7a47ad3a4d Add name and description for beta feature (#11409) 2026-08-06 16:41:59 -07:00
Samuel Newman d727be8c1a Fix profile icon on custom feeds (#11408)
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
2026-08-06 16:37:21 -07:00
DS Boyce 1e6725c0a1 Fix post numbers being clipped in some cases (#11413) 2026-08-06 16:34:49 -07:00
DS Boyce 1c349ad7da Add timestamp field to video report dialog (#11339) 2026-08-06 12:11:05 -07:00
Eric Bailey 724013df1b Add repost-only mutes (#11223)
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
2026-08-06 11:53:02 -07:00
Samuel Newman 1cbe7a39e6 increase ci node heap (#11406) 2026-08-06 05:20:11 -07:00
surfdude29 78a5c222bf Tweak strings (#11407) 2026-08-06 05:18:38 -07:00
Spence Pope 5cf6d8fa21 Use toolbox video compressor (#11246) 2026-08-06 08:16:18 -04:00
Samuel Newman 1ac5a71d61 Use Claude Opus 5 in GitHub workflows (#11335) 2026-08-06 04:14:06 -07:00
Samuel Newman 61283da526 Show app build numbers in CI summaries (#11267) 2026-08-06 04:13:29 -07:00
pfrazee dd7c950cc6 Nightly source-language update 2026-08-06 03:09:55 +00:00
Eric Bailey b79908ecea Add canonical OP thread numbering (#11184)
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
2026-08-05 10:20:43 -07:00
Spence Pope 8cb6c7babe display backend-driven upload failure messages (#11329) 2026-08-05 12:12:03 -04:00
Spence Pope 32ec5330b0 Enrich fatal HLS errors in Sentry (#11359) 2026-08-05 10:15:26 -04:00
Spence Pope 17a8fe87c2 APP-2793: harden multipart video uploads (#11366) 2026-08-05 10:15:16 -04:00
Spence Pope 0ccc029716 Allow feature-gated 10-minute video uploads (#11388) 2026-08-05 10:15:06 -04:00
pfrazee 5db6b4e2fd Nightly source-language update 2026-08-05 03:10:14 +00:00
Alex Benzer 5494f7deb5 Test requiring at least one interest in onboarding (#11391)
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
2026-08-04 14:57:43 -07:00
smileyhead ec49988fa7 Add unique context to ‘People I follow’ in FromDropdown.tsx (#11275)
Co-authored-by: Byte <byte@coreyja.com>
2026-08-04 10:47:31 -07:00
Spence Pope f1cfa37bab APP-2764: Remove unnecessary top padding on GIF posts (#11321) 2026-08-04 10:45:44 -04:00
pfrazee a1f094534a Nightly source-language update 2026-08-04 03:10:42 +00:00
dependabot[bot] 863fdcd8c1 Bump the actions group with 6 updates (#11370) 2026-08-03 23:33:04 +03:00
Samuel Newman 1cc942a869 Classify report dialog errors (#11354) 2026-08-03 08:00:25 -07:00
Samuel Newman e99e04a458 Simplify inline badge presentation, fix alignment (#11352) 2026-08-03 06:26:06 -07:00
Samuel Newman c2ceb8057f Handle camera cancellation (#11322) 2026-08-03 06:25:42 -07:00
pfrazee d58be1adea Nightly source-language update 2026-08-01 03:10:42 +00:00
DS Boyce f99a17bb6e Add additional tests for groupNotifications (#11332) 2026-07-31 10:01:14 -07:00
Samuel Newman ce28129674 Add nicer loading screen while OTA is loading (#9499) 2026-07-31 09:39:26 -07:00
Oleksii Bulenok 87a177bbe8 Fix profile badge sometimes displayed below on Android (#11337) 2026-07-31 08:55:08 -07:00
pfrazee 03f8020ee2 Nightly source-language update 2026-07-31 03:11:52 +00:00
Samuel Newman 07c34a6548 Fix PR OTA deployments reverting after apply (#11338)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:30:42 +03:00
surfdude29 1c02724ac4 Use Spanish for Aragonese intl-displaynames (#11327) 2026-07-30 22:44:45 +03:00
Samuel Newman 17c1ee4869 Handle PR OTA version mismatches and failures (#11330) 2026-07-30 07:40:49 -07:00
Oleksii Bulenok 157a1893fd fix TextInput resizes on Android when the first chat is typed (#11318) 2026-07-30 12:53:35 +02:00
Oleksii Bulenok cc3c01267d Fix alt text input field not growing more than 3 lines (#11328) 2026-07-30 12:53:09 +02:00
pfrazee 973d0b06f2 Nightly source-language update 2026-07-30 03:10:31 +00:00
DS Boyce 0be8b1dd93 Add a dialog for debugging feature flags to developer settings (#10988) 2026-07-29 16:49:32 -07:00
DS Boyce 91517af01c Log rank and parent index with trending topic events (#11331) 2026-07-29 16:14:44 -07:00
DS Boyce 15c6c7955d Get number of trending topics for Explore from GrowthBook (#11324) 2026-07-29 12:12:15 -07:00
Samuel Newman 904097e8c1 Fix peek menu crash (#11315) 2026-07-29 22:11:42 +03:00
DS Boyce b4bad1187a Indicate if someone followed you via a starter pack (#11320) 2026-07-29 12:11:28 -07:00
Samuel Newman 8e3b252b86 Use debugOptimized variant for Android dev (#11302) 2026-07-29 22:11:16 +03:00
DS Boyce e375ceb5b8 Update empty state for starter pack search (#11325) 2026-07-29 10:59:01 -07:00
DS Boyce 45bdba5dfa Let trending topic names wrap to two lines (#11323) 2026-07-29 10:58:08 -07:00
DS Boyce 196e81eb0c Get trending section indices from GrowthBook (#11319) 2026-07-29 10:24:50 -07:00
Spence Pope a35c4ac4ba APP-2670: add multipart video upload transport (#11222) 2026-07-29 12:54:03 -04:00
Foysal Ahamed c6a40088dd Add TIDA form URL for adult content report (#11150)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-07-29 17:38:27 +02:00
Samuel Newman b57827f23f Enter build numbers manually for production OTA deploys (#11289)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-29 04:48:42 -07:00
pfrazee 27e4f84f3f Nightly source-language update 2026-07-29 03:09:19 +00:00
Samuel Newman 0751420a29 Drop react-native-progress fork (#11312) 2026-07-28 19:47:56 +03:00
Oleksii Bulenok 8263175a03 Fix caret misplaced upon pressing Return when using Japanese symbols … (#11316) 2026-07-28 17:17:29 +02:00
dependabot[bot] 2f36a6de5a Bump slackapi/slack-github-action from 3.0.5 to 4.0.0 (#11307)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 07:05:42 -07:00
Samuel Newman de348db116 Show a non-standard OTA channel notice in the drawer (#11288)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-28 06:59:17 -07:00
Samuel Newman 8a6347ad7c Restore version to v1.130.0 (#11314) 2026-07-28 06:58:51 -07:00
Samuel Newman 82c505f286 v1.129.1 Release Prep (#11303) 2026-07-27 23:43:11 -07:00
dependabot[bot] ffdfb3c0e7 Bump actions/setup-go from 6.5.0 to 7.0.0 (#11306)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 23:36:29 -07:00
dependabot[bot] aa06d68ca6 Bump the actions group with 6 updates (#11305)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 23:36:01 -07:00
pfrazee 3ddb4a419e Nightly source-language update 2026-07-28 03:10:21 +00:00
DS Boyce e4ba1b4378 Add trendingTopic:seen metric (#11309) 2026-07-27 17:06:10 -07:00
DS Boyce a5f0e8839a Update trending topics UI (#11304) 2026-07-27 16:34:15 -07:00
DS Boyce d9eacc3d98 Fix keyboard activation for signin button (#11308) 2026-07-27 15:18:43 -07:00
Samuel Newman bbf7603250 Fix Android pager crash during screen transitions (#11301) 2026-07-27 11:31:58 -07:00
Samuel Newman 0e63434bb4 Reenable wake-from-background Android OTA (#11298) 2026-07-27 09:20:10 -07:00
Samuel Newman eb8f5fc4bc Update sonner-native, remove patch (#11300) 2026-07-27 09:19:33 -07:00
Samuel Newman 2ad84e540f Update react-native-uitextview (#11299) 2026-07-27 09:09:48 -07:00
Samuel Newman 97c2c516bd Downbump version to v1.129.1 (#11297) 2026-07-27 07:52:10 -07:00
Samuel Newman 34827ef644 ci: only publish the per-PR OTA when the fingerprint is clean (#11287)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 07:51:55 -07:00
Samuel Newman 691619f297 fix: stop grouping all signup failures into one sentry issue (#11278)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 07:50:40 -07:00
Samuel Newman c2e5a9093c Update play store assetlinks (#11294) 2026-07-27 07:28:28 -07:00
Samuel Newman 9b89a9adba Remove unused ScrollForwarder React Native patches (#11295) 2026-07-27 13:57:46 +03:00
Tomasz Zawadzki 22d657986d Fix IllegalStateException: Required value was null. (#11293)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 03:38:03 -07:00
Samuel Newman abde10b7bd Fix tooltip layout measurement loop (#11284) 2026-07-27 03:16:31 -07:00
Austin McKinley 51bcacb20b Publish one bundle version to both OTA origins, not two (#11291)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:58:20 -07:00
Austin McKinley b662cd43b5 Fix legacy ota1 OTA upload broken by the denis cutover (#11290) 2026-07-27 00:42:38 +03:00
Samuel Newman 66ec855efc Add emoji to GitHub Actions step names for consistency (#11286)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 00:03:45 +03:00
Samuel Newman d9e76c3954 Restore PR OTA QR comment (#11285) 2026-07-26 23:07:41 +03:00
Samuel Newman be70a31f4c Fix silently-frozen OTA fingerprint baseline with artifacts (#11231)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 23:07:20 +03:00
Tomasz Zawadzki 2b9deeeb6a Migrate runOnUI/runOnJS to scheduleOnUI/scheduleOnRN (#11249) 2026-07-26 12:00:35 -07:00
Samuel Newman d5e335e575 Update version to v1.130.0 (#11274) 2026-07-25 10:58:03 +03:00
Austin McKinley 76a928a7af ci: skip the per-PR OTA job for bot authors (#11273)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 19:07:57 -07:00
Austin McKinley 9eb78c0f6c ci: publish OTA to denis (S3) + automatic per-PR previews (#11235)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 18:18:00 -07:00
Eric Bailey fb8bda861b Add new config to lingui too (#11270) 2026-07-24 16:19:11 -05:00
estrattonbailey afd267bd2b Nightly source-language update 2026-07-24 17:57:36 +00:00
DS Boyce 93cb42b9f2 Tweak advanced search UI for better localization (#11266) 2026-07-24 10:40:37 -07:00
DS Boyce 549e3d7a7d Fix issue where form controls appear above sticky headers (#11254) 2026-07-24 10:40:16 -07:00
Samuel Newman 74a89832a3 v1.129.0 release prep (#11265) 2026-07-24 09:43:38 -07:00
Samuel Newman 19b0d89313 Update react-native-uitextview (#11264) 2026-07-24 07:40:46 -07:00
Spence Pope 98e135f489 Fix native signup captcha interactions (#11263) 2026-07-24 08:46:59 -04:00
Samuel Newman fbb0ee38c2 Add hitslop to switch account ... menu (#11262) 2026-07-24 05:44:45 -07:00
Eric Bailey 7bd27e4180 Add intl note on OTA flow (#11241) 2026-07-24 03:48:55 -07:00
dependabot[bot] 07a810945d Bump github.com/hashicorp/go-retryablehttp from 0.7.5 to 0.7.7 in /bskyweb (#11257)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 03:48:39 -07:00
Samuel Newman 63b284030a Fix Maestro E2E regressions on iOS and Android (#11258) 2026-07-24 03:48:03 -07:00
Samuel Newman 3b6fbf315b Fix Maestro E2E runtime environment (#11247) 2026-07-24 03:30:10 -07:00
DS Boyce 97169dbc35 Tweak appearance of trending topics (#11253) 2026-07-24 03:29:55 -07:00
Tomasz Zawadzki 22a4958321 Migrate useScrollViewOffset to useScrollOffset (#11250) 2026-07-24 07:09:01 +03:00
pfrazee a9df815bd0 Nightly source-language update 2026-07-24 03:10:54 +00:00
Eric Bailey a53df16e82 Update API package and filter AA configs by platform (#11244) 2026-07-23 14:15:09 -05:00
Samuel Newman 4e3d1704a6 Fix scroll forwarder web entrypoint (#11245) 2026-07-23 11:17:36 -07:00
DS Boyce 723a60e979 Display post counts greater than 1,000 (#11242) 2026-07-23 10:26:47 -07:00
Spence Pope 325246ec6c Stub RN codegen native component on web (#11240) 2026-07-23 12:52:47 -04:00
DS Boyce 10b027c35b Sort follows and following behind gate (#11237) 2026-07-23 09:02:35 -07:00
Oleksii Bulenok 8d64ab9d4b React Native New Arch (#10980) 2026-07-23 18:25:40 +03:00
pfrazee b80d09000f Nightly source-language update 2026-07-23 03:11:00 +00:00
DS Boyce f38f84e1a3 Enable sharing a post to a new group chat (#11191) 2026-07-22 15:54:46 -07:00
DS Boyce 8d7a7369aa Display beta badge when isBetaUser is true (#11160) 2026-07-22 15:06:08 -07:00
DS Boyce 77bbe8d8a0 Add From: Me to advanced search filters (#11126) 2026-07-22 15:01:24 -07:00
DS Boyce 850765bc8d Tweak loading state for block dialog (#11236) 2026-07-22 14:56:25 -07:00
DS Boyce 8ca8176eb1 Update presentation of trending feeds (#11206) 2026-07-22 14:35:45 -07:00
DS Boyce c71a4f8360 Create new trending feeds interstitial (#11207) 2026-07-22 14:23:01 -07:00
DS Boyce b2de086c3b Update presentation of trending topics in Explore (#11227) 2026-07-22 13:58:26 -07:00
DS Boyce 933ba1c373 Add feature gate for starter pack search (#11234) 2026-07-22 13:22:56 -07:00
DS Boyce 0bd6a99613 Pass recId with trending topic click metrics (#11232) 2026-07-22 12:51:49 -07:00
Samuel Newman f62e54ec82 Use stable tsgolint (#11229) 2026-07-22 07:39:11 -07:00
pfrazee 676f3f9e1a Nightly source-language update 2026-07-22 03:10:59 +00:00
Spence Pope 26ce5f0934 Hide video pillarboxing on web when the card fits (#11118) 2026-07-21 16:39:51 -05:00
Alex Benzer 44b1ab08b5 Move known likers below stats row; restore original order in stats row (#11192) 2026-07-21 15:40:30 -05:00
RetroSunstar c70baff709 Squish separate imports from the same source in ActivityNotificationSettings (#11225) 2026-07-21 23:25:15 +03:00
DS Boyce cbf0d89128 Remove console.log, address lint (#11228) 2026-07-21 23:25:01 +03:00
DS Boyce 86f0bedfbe Add new search results tab for starter packs (#11226) 2026-07-21 13:18:12 -07:00
Spence Pope 26143c1a48 add client event for failed video playback (#11170) 2026-07-21 11:32:53 -04:00
Samuel Newman 6fea12480a TypeScript 7 (attempt 2) (#11169) 2026-07-21 05:21:51 -07:00
DS Boyce 92ec563f9d Restore activity notification subscriptions screen (#11201) 2026-07-21 05:16:22 -07:00
dependabot[bot] 7d4b91dd38 Bump the actions group with 4 updates (#11202)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 05:09:01 -07:00
dependabot[bot] 3eab767e89 Bump actions/setup-node from 6.4.0 to 7.0.0 (#11203)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 05:08:41 -07:00
Spence Pope 0ba78623d6 Fix image menu in landscape orientation (#11188) 2026-07-21 05:07:43 -07:00
pfrazee daa11b63e7 Nightly source-language update 2026-07-21 03:10:47 +00:00
Eric Bailey baaf2bdc2b Don't roll up account labels into additional post alerts (#11204) 2026-07-20 17:06:14 -05:00
Eric Bailey d3e7d99fa7 Fix image crop in quote posts, remove PostEmbedViewContext.FeedEmbedRecordWithMedia (#11205) 2026-07-20 16:47:05 -05:00
DS Boyce 0f27fdda65 Add height constraint to full-height bottom sheets (#11178) 2026-07-20 10:01:31 -07:00
Samuel Newman 92ef1528bf Stop closing search suggestions on input blur (#11199) 2026-07-20 09:44:17 -07:00
DS Boyce 1803de03a4 Lighten blue (link) text for dim and dark themes (#11164) 2026-07-20 09:29:58 -07:00
Samuel Newman 0fb7251c64 Add nightly Maestro E2E workflow (#11181)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 07:42:53 -07:00
Samuel Newman c4e80b4456 Fix Android release asset upload (#11174) 2026-07-20 07:42:35 -07:00
Samuel Newman 21d53ce0f8 Split mobile build/submit pipelines and de-duplicate OTA workflow (#11133)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 07:07:30 -07:00
DS Boyce a49faf46eb Reduce bottom hitslop for menu button (#11189) 2026-07-20 05:39:25 -07:00
Samuel Newman 783dfe2a12 Cache main bundle stats for PR checks (#11172) 2026-07-17 07:28:08 +03:00
pfrazee 048428b618 Nightly source-language update 2026-07-17 03:10:25 +00:00
Samuel Newman 007c21fa8e Add moduleSuffixes to tsconfig (#11146)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-16 12:47:53 -07:00
Samuel Newman a8b963b357 Bump version to v1.129.0 (#11175) 2026-07-16 12:47:31 -07:00
Samuel Newman f9384e44c7 Bump expected cocoapods version to match latest macos-26 runner image (#11171) 2026-07-16 09:33:07 -07:00
RetroSunstar 54a1d81936 Remove success toast from Bookmark Button (#11054)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-07-16 10:33:49 -05:00
Samuel Newman b91f5e16bc use pnpm 11.13.1 (#11168) 2026-07-16 07:23:37 -07:00
Samuel Newman 4134e062cf v1.128.0 release prep (#11167) 2026-07-16 07:10:39 -07:00
Samuel Newman 406fb5425c Add Japan-gated custom logo (#11161) 2026-07-16 04:26:05 -07:00
Samuel Newman 4037392105 Update SVGO (#11083) 2026-07-16 04:21:31 -07:00
pfrazee 4983fee7a4 Nightly source-language update 2026-07-16 03:10:26 +00:00
DS Boyce 2a4642f324 Add mark requests as read to chat settings menu (#11107) 2026-07-15 16:12:02 -07:00
Abdelrahman Youssef 83a791d7c8 Fix invisible bidi characters copied with handles on web (#11066) 2026-07-15 17:56:34 -05:00
RetroSunstar 4705b7f408 Add DM button to labeller profiles (#11149) 2026-07-15 17:32:18 -05:00
DS Boyce 48fecae677 Log additional context for post:* events (#11035) 2026-07-15 12:52:53 -07:00
DS Boyce a0c75944a3 Don't suggest retrying an unretryable error (#11159) 2026-07-15 11:27:19 -07:00
DS Boyce 7009417035 Remove IS_INTERNAL check for beta feedback button (#11162) 2026-07-15 11:21:07 -07:00
DS Boyce 211c8838a9 Upgrade pnpm to 11.13.0 (#11163) 2026-07-15 11:15:34 -07:00
DS Boyce 5b9967b17d Strip leading @ in from: queries (#11158) 2026-07-15 09:23:11 -07:00
DS Boyce 3c5c11c002 Add setting for opting in to beta features (#11123) 2026-07-15 06:06:54 -07:00
Spence Pope d7f40b7e7f fix android carousel jiggle from cdn-rounded aspect ratio (#11153) 2026-07-15 04:36:04 -07:00
Eric Bailey 821e1b838a [APP-2642] Social proof text is truncated on mobile (#11155) 2026-07-15 10:53:40 +03:00
pfrazee 581aee5472 Nightly source-language update 2026-07-15 03:10:36 +00:00
Spence Pope f42a325f49 fix web feed footer clearing the bottom bar (#11151) 2026-07-14 10:15:28 -07:00
Samuel Newman 4c111eb45e Stub out react-native-svg fetchData on web to drop buffer polyfill (#11097)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 10:15:10 -07:00
Samuel Newman 9fa5f15baa Revert "Typescript 7" (#11152) 2026-07-14 08:50:06 -07:00
Spence Pope a5adb0e97a Add video playback observability to Sentry (#11111) 2026-07-14 10:38:39 -04:00
Samuel Newman 30e862bc26 Allow up to 10 images in share intent (#11132) 2026-07-14 06:14:11 -07:00
DS Boyce 4778142ee8 Create primary tooltip variant (#11140) 2026-07-14 05:57:11 -07:00
Samuel Newman ac0a249721 Typescript 7 (#11109) 2026-07-14 05:52:56 -07:00
Samuel Newman 4b5138fdc1 Update Sentry to 8.x (#11147)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 05:39:05 -07:00
pfrazee 6f69ded492 Nightly source-language update 2026-07-14 03:10:07 +00:00
dependabot[bot] 19872124bf Bump the actions group with 6 updates (#11148) 2026-07-13 23:48:14 +03:00
Alex Benzer 04ebd02ce3 Prototype: Show known likers on post thread page (#11080)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-07-13 14:55:15 -05:00
DS Boyce 0701a08a63 Ensure long trending topic titles can wrap (#11141) 2026-07-13 07:23:08 -07:00
pfrazee 3989356316 Nightly source-language update 2026-07-11 03:10:36 +00:00
Austin McKinley b4dfa3cc22 Collapse label appeal flow into ModerationDetailsDialog (#11091)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-07-10 18:26:10 -05:00
Samuel Newman fa27d446d8 Migrate to Oxlint (#10384)
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
2026-07-10 12:27:00 -07:00
Samuel Newman 6be6f14aed pnpm 11.11.0 (#11130) 2026-07-10 18:12:02 +03:00
Samuel Newman 9b34d357d4 Lazy load @ipld/dag-cbor in computeCid (#11099)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 02:43:04 -07:00
DS Boyce f1b2bcd638 Potentially fix disappearing chat emoji on some devices (#11113) 2026-07-10 02:40:54 -07:00
Samuel Newman 5e6b693294 Fix crash when maxHeight is nil at sheet presentation (#11104)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 02:39:02 -07:00
Samuel Newman 1c88299eee restore v1.128.0 (#11131) 2026-07-10 02:33:28 -07:00
pfrazee fe5d531afe Nightly source-language update 2026-07-10 03:13:47 +00:00
DS Boyce 3499d3646e Migrate all search to v2 and remove v1 (#11125) 2026-07-09 15:08:20 -07:00
Samuel Newman 65e10f13f7 Slim down CLAUDE.md (#11102) 2026-07-10 00:37:48 +03:00
Samuel Newman ffd4172a64 Replace @tiptap/html generateJSON with native DOMParser equivalent (#11098) 2026-07-10 00:37:05 +03:00
surfdude29 1d1b2f748e Update KWS supported languages and mapping (#11085)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:24:20 -05:00
surfdude29 b075753d0f Fix KWS language picker silently resetting Arabic, Chinese, Filipino & Portuguese to English (#11082)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-07-09 13:19:41 -05:00
Samuel Newman c9251f85c5 Update lint-staged (#11084) 2026-07-09 13:17:16 -05:00
Samuel Newman 2b5e437601 Remove unused video extension extraction in Share extension (#11081)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-09 13:06:40 -05:00
Samuel Newman 49ca9a2e7e v1.127.1 Release prep (#11116) 2026-07-09 04:06:32 -07:00
Samuel Newman 83da55c32b Fix search typeahead performance with absolute overlay (#11103)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 02:36:37 -07:00
Samuel Newman 40b802ba15 Revert "Remove react-native-dotenv (#10706)" (#11110) 2026-07-09 02:17:05 -07:00
pfrazee f06f2cbc25 Nightly source-language update 2026-07-09 03:14:03 +00:00
DS Boyce 0a4f5e132c Update mergeRefs util for React 19 (#11108) 2026-07-08 12:32:15 -07:00
Spence Pope 6dce96881c use source file for web video compression (#11090) 2026-07-08 15:07:52 -04:00
DS Boyce 510dc51d7a Update advanced search UI (#11088) 2026-07-08 10:14:39 -07:00
Eric Bailey decac43c34 Adjust AA device signals copy (#11106) 2026-07-08 11:40:33 -05:00
Samuel Newman efcc15fc55 Bump version to v1.128.0 (#11105) 2026-07-08 08:38:50 -07:00
DS Boyce 53d9db174c Revert "Add more search placeholders (#11089)" (#11101) 2026-07-08 07:25:41 -07:00
DS Boyce a689322f4a Add more search placeholders (#11089) 2026-07-08 07:10:12 -07:00
dependabot[bot] aaf45129aa Bump golang.org/x/crypto from 0.51.0 to 0.52.0 in /bskyweb (#11100)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-08 06:44:33 -07:00
Samuel Newman 48e1502bd1 Fix Android crash in chat keyboard scroll (part 2) (#11096)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 06:21:32 -07:00
Samuel Newman b41624edea Lazy-load mediabunny on web (#11094)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 06:21:16 -07:00
Samuel Newman ea3d6827c7 v1.127.0 release (#11087) 2026-07-07 10:23:03 -07:00
Samuel Newman 102905b962 Fix back swipe opening the drawer after a cold-start deep link (#11065)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-07 06:51:06 -07:00
dependabot[bot] 5e24a0b48c Bump github.com/jackc/pgx/v5 from 5.5.0 to 5.9.2 in /bskyweb (#10463)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 06:47:34 -07:00
DS Boyce bbdfe05ada Use Sift for username suggestions during signup (#11051) 2026-07-07 05:37:49 -07:00
Spence Pope f9b1200404 fix web video preview error aborting upload (#11079) 2026-07-07 05:34:46 -07:00
surfdude29 17697cd5c8 Use 'hyphens' instead of 'dashes' in error string (#11050) 2026-07-07 05:31:54 -07:00
Samuel Newman 03ccd96661 Force native builds when package.json version changes in OTA deploy (#11061)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-07 05:31:37 -07:00
dependabot[bot] 976dd65056 Bump golang.org/x/net from 0.47.0 to 0.55.0 in /bskyweb (#11055)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 05:24:45 -07:00
Pintu 7251a0ee70 fix(search): Update advanced search label text (#11049) (#11064) 2026-07-07 04:25:56 -07:00
Alex Benzer 882a5573af Clean up feature gate for follow buttons in expanded notifications (#11068) 2026-07-07 04:22:42 -07:00
Samuel Newman 9ff248528b Automatically resolve hosting provider from handle (#11053)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-07-07 04:21:58 -07:00
Samuel Newman 640662d57c Fix zizmor findings in build workflows (#11046)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 04:20:50 -07:00
dependabot[bot] 52e8051fa3 Bump the actions group with 5 updates (#11076)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 04:19:38 -07:00
dependabot[bot] 86c1cbbc95 Bump actions/cache from 5.0.5 to 6.1.0 (#11078)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 04:18:26 -07:00
dependabot[bot] 545fec82fb Bump dcarbone/install-jq-action from 3.2.0 to 4.0.1 (#11077)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 04:17:43 -07:00
pfrazee f9071a9674 Nightly source-language update 2026-07-07 03:14:21 +00:00
Spence Pope 4f5119fac0 fix camera roll gif selection hanging on ios (#11074) 2026-07-06 17:53:37 -04:00
DS Boyce 651a3f8af4 Upgrade pnpm to 11.10.0 (#11075) 2026-07-06 13:08:47 -07:00
smileyhead 1817cc2ae4 Add missing l18n wrapper to ‘⚠Invalid Handle’ in handles.ts (#11071) 2026-07-06 13:51:33 -05:00
Pintu ff78d32288 fix: restore Android native stretchy overscroll on image carousel (#11072) 2026-07-06 08:47:24 -07:00
So Asano e5d2ee3315 Clean up media-upload temp files on iOS (#11007)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 10:18:00 -04:00
Austin McKinley be0fbdff82 build: bump Node to 24.18 for June 2026 security releases (#11069)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-07-06 07:07:05 -07:00
pfrazee 79a16094f1 Nightly source-language update 2026-07-03 03:13:24 +00:00
DS Boyce 2eddbf8eb0 Fix back button behavior on login/create account screen (#11052) 2026-07-02 13:42:12 -07:00
Eric Bailey 8613bb1033 Remove potentially intent violating chat settings restriction (#11056) 2026-07-02 15:32:37 -05:00
Eric Bailey 954268a4b6 [AAv2] Integrate on-device APIs (#9564)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:15:30 -05:00
DS Boyce b0a40145e9 Special-case 'me' values in search queries (#11043) 2026-07-02 09:35:57 -07:00
DS Boyce 40df0d773a Fix blank message list after submit on Android (#11042) 2026-07-02 09:35:04 -07:00
Eric Bailey b27ef2b6df Bump app version to 1.127.0 (#11040) 2026-07-01 11:26:07 -05:00
Spence Pope c4ce3ce0fb Reduce Sentry noise from logger transport and expected-failure call sites (#10893) 2026-07-01 10:43:01 -04:00
pfrazee e86067986f Nightly source-language update 2026-07-01 03:20:02 +00:00
DS Boyce 133476e0cd Update advanced search suggestions (#11036) 2026-06-30 14:26:50 -07:00
Samuel Newman 0adf500edf Use RNKC for composer keyboard avoiding view (#10997)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-30 16:15:50 -05:00
DS Boyce 5e7343b0d2 Fix yaml syntax in pull-request-commit.yml (#11037) 2026-06-30 14:15:28 -07:00
RetroSunstar 0526820506 Fix stale display names for lists/feeds in top bar across restarts/devices (#10651) 2026-06-30 16:10:53 -05:00
Harry L 13ee5df721 Restore link in Go Live prompt if status record present (#10636)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-30 15:59:17 -05:00
RetroSunstar f648977deb Added Automation label to labeller display names (#11027) 2026-06-30 15:58:48 -05:00
Eric Bailey 22391bcd63 Fix lint (#11038) 2026-06-30 15:47:12 -05:00
surfdude29 251605919f Fix hashtag author filter failing for DIDs (#10772)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-30 13:27:13 -07:00
Samuel Newman 9df4add435 Only request notification permission once per session (#11004)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-30 15:22:37 -05:00
Zaven477 281f49976a adding focus to the editing block (#10593) 2026-06-30 15:21:16 -05:00
Samuel Newman 57dc53fbf5 Add "fingerprint changed" label to PRs with native changes (#10742)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:19:24 -05:00
Ashish Khankari feaca048ba Fix UI overlap issue where text was rendering over icons (#10774) 2026-06-30 15:07:43 -05:00
Eric Bailey 8a6a90320c Remove actions-up script in favor of Dependabot (#11034) 2026-06-30 14:28:05 -05:00
Samuel Newman 5331c68218 Rename root file from js to ts, improve platform splitting (#11012) 2026-06-30 14:25:16 -05:00
Samuel Newman 52aaba4051 Route iOS notification settings intent into the app (#11003) 2026-06-30 22:23:52 +03:00
dependabot[bot] de51ceb577 Bump actions/checkout from 6.0.3 to 7.0.0 (#11022)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 14:20:43 -05:00
dependabot[bot] eb755bc31d Bump the actions group with 3 updates (#11021)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 13:51:58 -05:00
DS Boyce 44f6a98f59 Use latest fingerprint-native action (#11016) 2026-06-30 13:40:37 -05:00
DS Boyce 28c44a11ef Avoid leaking previous messages via replies (#11017) 2026-06-30 10:35:12 -07:00
DS Boyce cc893028e3 Always set allTime to true for search v2 (#11033) 2026-06-30 10:34:40 -07:00
DS Boyce 298c83bcba Update search input with useAutocomplete (#11023) 2026-06-30 10:34:17 -07:00
Samuel Newman 6607118729 Remove react-native-dotenv (#10706) 2026-06-30 19:42:44 +03:00
Samuel Newman ade5d2b783 Cull unused files (#11029) 2026-06-30 19:41:35 +03:00
DS Boyce bb20f234f3 Always scroll to bottom of chat after sending a message (#10913) 2026-06-30 09:38:06 -07:00
Spence Pope 95726c6c6b Replace react-native-compressor video path with native expo-bluesky-video-compress module [APP-2428] (#10954)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-30 11:59:59 -04:00
Christian Schmidt 43d2f939c2 WSOD when trying to create account from starter pack on non-default provider (#10970) 2026-06-30 01:26:20 -07:00
DS Boyce 75e2c40ba4 Use correct param name for setup-bundletool action (#11020) 2026-06-30 00:56:44 -07:00
Eric Bailey bd2849d925 Release Prep 1.126.0 (#11015) 2026-06-29 12:42:23 -05:00
Samuel Newman 2971c3520c Fix chat input positioning on mobile android (#11013) 2026-06-29 10:18:53 -05:00
Samuel Newman 69058e1ec2 Disable selectable profile description on Android (#11002)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-29 10:18:32 -05:00
Samuel Newman a5ed41fa0f Fix CI - grant workflows permission for internal sync push (#11010) 2026-06-29 16:30:01 +03:00
Samuel Newman 257a11d498 Set apple team id via environment variable (#11008) 2026-06-29 01:59:00 -07:00
RetroSunstar b8608ac03b Fix 149/150 user limit for starter packs (#11001) 2026-06-28 01:09:07 -07:00
pfrazee e0ddd8d509 Nightly source-language update 2026-06-27 03:17:11 +00:00
Eric Bailey 90f3f954ea Fix GrowthBook exposure did mis-attribution across account switches (#11000)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 19:04:40 -05:00
DS Boyce 248290e0b3 Add advanced search UI (#10992) 2026-06-26 16:16:48 -07:00
Spence Pope c5b3810229 Fail video duration check before compression (#10999) 2026-06-26 17:39:15 -05:00
surfdude29 1756740c6c Add app language: Czech (cs) (#10994) 2026-06-26 16:04:48 -05:00
Samuel Newman 66250e45cc Clean up type/lint errors in modules (#10996) 2026-06-26 07:41:09 -07:00
dependabot[bot] fcbd23e7db Bump actions/download-artifact from 7.0.0 to 8.0.1 (#10971)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-26 07:40:12 -07:00
pfrazee 717406f16c Nightly source-language update 2026-06-26 03:18:48 +00:00
Gabe Sullivan ea35d930e3 Change dialog label and header from 'Invite Friends' to 'Share Profile' (#10987) 2026-06-25 18:23:06 -05:00
Eric Bailey 308693762c Add placeholder, empty-value, and onConfirm support to DateField (#10993)
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 18:04:54 -05:00
Spence Pope fcf17a482f Add video upload telemetry events (#10991) 2026-06-25 18:14:05 -04:00
DS Boyce 6b342f61b4 Hide autocomplete suggestions when input loses focus (#10989) 2026-06-25 16:59:50 -05:00
DS Boyce eae15aca3e Increase unread cap to 99+ (#10985) 2026-06-25 16:57:03 -05:00
Lynx 29a0d16b69 Add icon to AppLanguageDropdown (#10977) 2026-06-25 16:54:31 -05:00
Alex Benzer c33a35851e Show shorter text for account label badge (#10983) 2026-06-25 16:53:49 -05:00
Eric Bailey b1b0ad09cc Video alt text (#10990)
Co-authored-by: Zaven477 <77801431+Zaven477@users.noreply.github.com>
2026-06-25 16:52:09 -05:00
DS Boyce 46f5437faa Display more information in chat replies (#10950) 2026-06-25 15:03:42 -05:00
DS Boyce 3b2e609761 Add new chat notification preferences (#10972) 2026-06-25 12:01:33 -07:00
DS Boyce 1ff27bfd3b Upgrade pnpm to 11.9.0 (#10952) 2026-06-25 13:43:33 -05:00
pfrazee f72aae3bac Nightly source-language update 2026-06-25 03:18:16 +00:00
DS Boyce 51e25435a2 Focus composer input when editable (#10982) 2026-06-24 18:06:02 -05:00
DS Boyce da1f637160 Fix issue with stale unread message counts (#10953) 2026-06-24 13:51:06 -07:00
Spence Pope f64b1258b6 [APP-2429] Add Sunlight and Twilight color options to the Invite Friends QR code (#10921)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-24 12:27:12 -05:00
Lynx c909fef3f1 signup: align "Contact support" to the right on larger displays (#10978) 2026-06-24 12:26:25 -05:00
DS Boyce 5b66015631 Set a min height on native Menus (#10956) 2026-06-24 11:51:16 -05:00
surfdude29 90c010e98a Tweak strings and context (#10928) 2026-06-24 11:39:12 -05:00
Samuel Newman f3978d1a1f Fix internal repo sync broken by actions/checkout v6 bump (#10933)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:38:44 -05:00
Samuel Newman 2843374ef8 Fix setState-in-render warning from convo cache subscription (#10934)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:37:57 -05:00
RetroSunstar 0674626f3c Add light haptics to Edit Profile Button for labellers (#10948) 2026-06-24 11:33:49 -05:00
DS Boyce cd4d62c75a Remove old message composer (#10951) 2026-06-24 11:27:10 -05:00
DS Boyce c3626c80d1 Fix embeds overlapping each other in chat (#10949) 2026-06-24 11:14:23 -05:00
DS Boyce e371d1db44 Add mark all as read to chat settings (#10973) 2026-06-24 10:23:57 -05:00
Spence Pope 4308d75e49 Add TestFlight group selection to iOS build workflow (#10979) 2026-06-24 10:52:58 -04:00
pfrazee ea0ef23340 Nightly source-language update 2026-06-24 03:18:32 +00:00
Michael Black 692d8e5d10 bskyweb: add isPartOf jsonld post attr (#10945) 2026-06-23 12:07:00 -05:00
Spence Pope c753c94969 Process videos on web with WebCodecs (#10955)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-06-23 11:33:23 -04:00
pfrazee cc0e1f88ea Nightly source-language update 2026-06-23 03:17:56 +00:00
Jerry Chen 495d1fe9a7 Restore ALT badges in quoted posts (#10964)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-06-22 14:05:50 -05: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
Spence Pope e804546809 [APP-1934] replace image grid layout with carousel (#10157)
Co-authored-by: RetroSunstar <57507616+RetroSunstar@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-04-15 17:53:01 -05:00
Samuel Newman d3f5093817 Groupchats feature branch (#10181)
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:15:45 -07:00
DS Boyce 75c9e2c181 Upgrade eslint-plugin-simple-import-sort to latest (#10260) 2026-04-15 11:00:34 -07:00
DS Boyce b9561f78ee Enforce using named imports from 'react' (#10259) 2026-04-15 10:11:09 -07:00
Samuel Newman 19e2dc939a [bskyembed] Fix landing page for iOS 26 safari dark mode (#10257) 2026-04-15 10:05:22 -07:00
Samuel Newman ecc78efb12 Stop passing _ around in composer functions (#10256) 2026-04-15 07:39:51 -07:00
pfrazee bcbc114189 Nightly source-language update 2026-04-15 03:14:26 +00:00
Spence Pope 8d3eba2381 [ALG-77] include 'for you' in see more suggested users (#10241) 2026-04-14 17:50:00 -05:00
Eric Bailey 59a2d19c26 Fix stale searchText in search submit handler (#10251)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:42:43 -05:00
pfrazee eee2df6d3a Nightly source-language update 2026-04-14 03:15:02 +00:00
Samuel Newman c56427c6fb Fix inverted condition in moveIfNecessary (#10238)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:42:57 -07:00
Samuel Newman 9c502b38e5 Disable viewport zoom in prod (#10233) 2026-04-13 09:42:37 -07:00
Samuel Newman 1f6d6d0545 Fix embed options border radius (#10237) 2026-04-13 09:42:05 -07:00
Samuel Newman 27d1b96e73 Fix bskyembed skeleton padding (#10236)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:41:14 -07:00
Samuel Newman 5d0b900719 Fix server state query returning undefined (#10232)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:14:21 -05:00
Spence Pope 9b31bb8470 handle single-line text truncation (#10227) 2026-04-12 01:48:58 -07:00
Samuel Newman 02daa0179b use dvh for util_screen_outer (#10230) 2026-04-11 10:08:02 +03:00
pfrazee ff294edd1e Nightly source-language update 2026-04-11 03:11:48 +00:00
bnewbold a7b6cd4504 bskyweb: update to golang v1.26 (#10208) 2026-04-10 10:25:40 -07:00
Samuel Newman 888eca73b3 Refactor chat list implementation (#10059) 2026-04-10 09:58:16 -07:00
Samuel Newman 2c717dc1e7 Rename moderation link in settings to "Moderation and content filters" (#10192) 2026-04-10 09:50:30 -07:00
Artem Pyvovar ff68e4036d Disable keyboard preload on iOS to prevent keyboard flash (#10214) 2026-04-10 09:50:17 -07:00
Misty De Méo 170e169ad1 PostControls: restore likeCount testID (#10221) 2026-04-10 09:50:03 -07:00
Samuel Newman 19df58f1ef Remove direct dependency on @expo/config-plugins (#10152) 2026-04-10 09:49:41 -07:00
Samuel Newman c79dff9b3e Tweak embed styles (#10226) 2026-04-10 09:35:22 -07:00
Spence Pope 13b453e506 [APP-2065] yassify app embed (#10210) 2026-04-10 09:08:09 -07:00
Eric Bailey d8fdfddbaf Check for undefined moderationOpts (#10224) 2026-04-10 10:23:36 -05:00
Eric Bailey ff30d39355 Remove flex to fix view collapse (#10219) 2026-04-10 09:37:06 -05:00
pfrazee 9c65a8a78c Nightly source-language update 2026-04-10 03:14:50 +00:00
Eric Bailey 8c9a11dc3f [APP-1775] Handle labels on actor statuses (#9716) 2026-04-09 15:52:45 -05:00
Samuel Newman d01362a01e Bump version to v1.121.0 (#10215) 2026-04-09 22:34:27 +03:00
DS Boyce 3ff40e9603 Update / hotkey to support all keyboard layouts (#10212) 2026-04-09 14:07:09 -05:00
Eric Bailey cfeac097e1 [APP-2044] Gate image blob size increase (#10178) 2026-04-09 14:06:25 -05:00
Eric Bailey c42940b0ad DM composer fixes (#10213) 2026-04-09 13:47:29 -05:00
Samuel Newman a0d4af5c5f Add font smoothing stuff to embed (#10204) 2026-04-09 06:12:20 -07:00
Samuel Newman 5ad3597fdb Add simple video player to embed (#10205) 2026-04-09 06:12:06 -07:00
pfrazee 987a656904 Nightly source-language update 2026-04-09 03:13:30 +00:00
DS Boyce d0d00ba9f8 Address lint warnings (#10188) 2026-04-08 15:54:43 -07:00
Eric Bailey e4b4e48acb Prep 1.120.0 release (#10207) 2026-04-08 13:58:16 -05:00
RetroSunstar 925ae29b5f Bump expo-paste-input to ^0.1.15 (#10203) 2026-04-08 13:50:12 -05:00
Eric Bailey f6b8854a9b Bump xcode in workflows (#10201) 2026-04-07 21:37:05 -05:00
Eric Bailey 3d0fbc5030 Prep 1.120.0 release (#10200) 2026-04-07 21:06:19 -05:00
Eric Bailey 0a5ae17738 New rich text composer + autocomplete (#10159) 2026-04-07 20:37:50 -05:00
Samuel Newman 02b849996c Double the framerate (#8446) 2026-04-07 14:13:24 -07:00
Samuel Newman f231b67b76 Tweak missing feed copy (#10133) 2026-04-07 15:25:22 -05:00
Samuel Newman d2519a4f67 RM unneeded config plugin, update primary color (#10153) 2026-04-07 15:21:03 -05:00
DS Boyce a049a6538c Use new endpoints for suggested users (#10185) 2026-04-07 15:10:23 -05:00
DS Boyce 3c21fee6c2 Don't call getProfiles if there are no actors (#10196) 2026-04-07 11:55:03 -07:00
DS Boyce e74b57ab78 Update browserslist (#10195) 2026-04-07 11:43:58 -07:00
Samuel Newman a169bd862f Fix layout shift when liking a post on Android (#10190) 2026-04-07 09:48:11 -07:00
Eric Bailey 8bd6d9d135 Revert PostThread parent chunk size for native (#10193) 2026-04-07 10:58:47 -05:00
pfrazee 5868804d3b Nightly source-language update 2026-04-07 03:13:28 +00:00
Eric Bailey bdce8e8ecd Bump expo-paste-input (#10187) 2026-04-06 18:50:28 -05:00
Eric Bailey 99257f2816 Add some more clarity to the RQ docs (#10120) 2026-04-06 18:45:40 -05:00
Spence Pope 011d8d2f7c [APP-2038] fix feeds not refreshing (#10167)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-04-06 17:34:01 -05:00
Spence Pope c0d3010f3e [APP-1999] Fix ReportDialog Android hang by narrowing dependency arrays (#10171)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:46:31 -05:00
Samuel Newman 3685439ffb Minor hotkeys nit (#10184) 2026-04-06 07:48:45 -07:00
RetroSunstar 165dd5a779 Fixes Hotkeys causing native to crash immediately on start (#10180) 2026-04-06 09:50:14 -04:00
pfrazee 84b026efb7 Nightly source-language update 2026-04-04 03:09:51 +00:00
Samuel Newman be56066ee9 Fix OOM crash - disable tanstack extension integration (#10169) 2026-04-03 11:31:55 -07:00
DS Boyce cca3326b21 Create global keyboard shortcut handler (#10145) 2026-04-03 10:06:51 -07:00
DS Boyce e0ea778e58 Warn when useState get/set names mismatch (#10166) 2026-04-02 15:47:56 -07:00
Samuel Newman 9fe808f8a8 Fix bottom bar badge text padding (#10162) 2026-04-02 11:51:54 -07:00
Samuel Newman b9f3d04d65 Scale profile badge icons with font size (#10161) 2026-04-02 11:51:38 -07:00
Samuel Newman 68a4d73d61 Add no-extraneous-dependencies and no-nodejs-modules eslint rules (#10151) 2026-04-02 08:34:12 -07:00
Samuel Newman eb566c5fcc Fix useResolveUriQuery (#10163) 2026-04-02 06:39:40 -07:00
pfrazee 512e550c2e Nightly source-language update 2026-04-02 03:13:26 +00:00
Samuel Newman 500fc1c934 Update React Query to latest (#10126) 2026-04-01 15:39:26 -07:00
Arunabh Verma b196ddef73 Replace react-native-paste-input with expo-paste-input (#10158)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-04-01 16:25:16 -05:00
DS Boyce 5a2135733f Handle more translation errors (#10160) 2026-04-01 13:39:05 -07:00
Samuel Newman 69164c640f Keep screen awake when videos are playing (#10155) 2026-04-01 13:07:02 -05:00
Samuel Newman f30acadc73 Hide alt text note after alt text is added (#10132) 2026-04-01 13:00:11 -05:00
DS Boyce 9b20023c86 Fix Safari layout shift issue with SubtleHover (#10135) 2026-04-01 09:12:11 -07:00
DS Boyce e8ee30398a Update Search to use ALF atoms (#10146)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-04-01 09:11:27 -07:00
pfrazee 6897116625 Nightly source-language update 2026-04-01 03:13:49 +00:00
DS Boyce 2262040797 Display see more link for suggested users in profile header (#10134) 2026-03-31 14:54:51 -07:00
Samuel Newman 498d321bb1 Change other borderWidths from 2 to 1 (#10156) 2026-03-31 09:35:31 -07:00
Samuel Newman a697841a21 Update typescript-eslint, fix up eslint setup (#10150) 2026-03-31 06:00:56 -07:00
DS Boyce 8ac63d780d Update emoji regex to detect variations and sequences (#10138) 2026-03-30 05:30:33 -07:00
Eric Bailey 0419066f45 Add fmt patch to react-native (#10136) 2026-03-26 22:53:41 -05:00
pfrazee 1463323289 Nightly source-language update 2026-03-27 03:13:48 +00:00
DS Boyce f4e14626aa Add button for creating a new group clip clop (#10066)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-03-26 10:23:49 -07:00
RetroSunstar ce000ada50 Fix 'Lists' & 'Feeds' not displaying their contents on muted profiles (#10101) 2026-03-26 02:49:36 -07:00
pfrazee 4d0774b75e Nightly source-language update 2026-03-26 03:14:00 +00:00
Eric Bailey bc7b6f1e13 Fix clip issue (#10131) 2026-03-25 17:17:47 -05:00
DS Boyce d44060a34e Pass correct log context to suggested user metrics (#10074) 2026-03-25 17:13:23 -05:00
DS Boyce 2c828b5755 Fix text overflow in post meta on web (#10122) 2026-03-25 12:59:04 -07:00
Samuel Newman c06312f09a PostThread perf: reduce time wasted on rendering skeletons (#10092) 2026-03-25 12:44:36 -07:00
Samuel Newman cc5580848c Upgrade TypeScript to 6.0 (#10113) 2026-03-25 00:50:26 -07:00
Samuel Newman fa84c451d0 Fix draft restore to calculate shortened grapheme length for links (#10103)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-24 15:31:19 -05:00
Paweł Fornagiel 894e2b89d4 fix: too many re-renders when toggling experimental feed sampling option (#10114) 2026-03-24 14:59:01 -05:00
Eric Bailey 3e37696d88 Add resolution downsizing to image compression (#10117) 2026-03-24 14:34:51 -05:00
Eric Bailey 6a15ca88b6 Update redirect service (#10116) 2026-03-24 10:10:35 -05:00
Eric Bailey 5a6942025c Add metrics to redirect service (#10112) 2026-03-24 08:45:08 -05:00
pfrazee 3866142ccd Nightly source-language update 2026-03-24 03:09:20 +00:00
Samuel Newman 7fd6f8f04b Update version to v1.120.0 (#10110) 2026-03-23 22:05:43 +02:00
DS Boyce 21d8b07bfe Use isTranslationSupported from expo-translate-text (#10108) 2026-03-23 09:56:35 -07:00
Samuel Newman f08bf5fef9 Fix report dialog flashing error state on open (#10099) 2026-03-23 09:19:57 -07:00
Samuel Newman 3e7e859c9c [Web] Fix Wordle score rendering - prevent Inter from overriding emoji presentation (#10104) 2026-03-23 09:19:33 -07:00
Samuel Newman 854ae60e7b [Web] Fix clipped text when using numberOfLines={1} (#10105) 2026-03-23 09:17:49 -07:00
Eric Bailey cc4a436e45 Bump version to 1.119.1 (#10088) 2026-03-20 12:04:12 -05:00
Eric Bailey 0532e120b8 Prep 1.119.1 (#10087) 2026-03-20 11:52:46 -05:00
Samuel Newman 288fad67f4 Fix bottom sheet crash on API <30 (#10084)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 11:41:39 -05:00
DS Boyce 0e1e790c34 Include recId with log event for suggested profiles (#10075) 2026-03-20 11:39:51 -05:00
Eric Bailey 74d8ca8fa7 Increase parent batch size to reduce onStartReached calls (#10086) 2026-03-20 11:38:52 -05:00
Eric Bailey 34d8c6fe58 [APP-1968] Increase image upload resolution (#10073) 2026-03-20 11:22:36 -05:00
Samuel Newman 5bcb909081 Remove hardcoded yellows (#10083) 2026-03-20 11:21:16 -05:00
Samuel Newman e28f6d2f37 Add bookmarks to thread placeholder candidates (#10080)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 11:20:27 -05:00
DS Boyce 3e7d7ce5f4 Fix issue with site loading on older versions of Safari (#10077) 2026-03-19 15:50:25 -07:00
DS Boyce 876e20166a Ensure mute state stays in sync after mutation (#10067) 2026-03-19 06:18:48 -07:00
pfrazee 2a87f6f6b5 Nightly source-language update 2026-03-19 03:12:12 +00:00
Eric Bailey bfd2d20192 Add back starter packs button in ProfileMenu (#10069) 2026-03-18 18:29:22 -05:00
Eric Bailey dc8570c524 Bump keyboard controller and add missing provider on web (#10068) 2026-03-18 15:43:22 -05:00
Samuel Newman 530afe87c7 web: LRU screen cache to bound memory in long sessions (#10063)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 08:50:48 -07:00
Eric Bailey 317ce2b3da Prep 1.119.0 (#10062) 2026-03-17 16:16:01 -05:00
Eric Bailey 45df50ec19 Rename translation properties for clarity (#10061) 2026-03-17 13:59:28 -05:00
surfdude29 b9a3256e51 Don't strip query parameters on in-app navigation (#9948) 2026-03-17 13:43:37 -05:00
DS Boyce 1386a559b7 Log languages a post is tagged with after translating (#10014)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-03-17 12:31:23 -05:00
RetroSunstar be0d00de17 Set displayName and description to undefined if no values are provided during a profile edit (#10016) 2026-03-17 12:04:27 -05:00
RetroSunstar dbba97f28d Use proper constants.ts exports for Edit Profile Dialog limits (#10054) 2026-03-17 12:04:04 -05:00
Samuel Newman 5e8ef6aa9b Adjust VTT cue line to avoid occlusion by video controls (#10017)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 11:05:58 -05:00
surfdude29 177bdcd2b7 [bskylink] URL-encode apostrophes before HTML escaping (#9559) 2026-03-16 11:18:23 -07:00
pfrazee 2027589c55 Nightly source-language update 2026-03-14 03:04:35 +00:00
DS Boyce b8d60eb0e2 Address lint warnings in List.web.tsx (#10046) 2026-03-13 09:41:38 -07:00
DS Boyce 97fdd7c59b Create new useSuggestedFollowsByActorWithDismiss hook (#10048) 2026-03-13 09:18:23 -07:00
pfrazee 6afe48db6a Nightly source-language update 2026-03-13 03:06:17 +00:00
DS Boyce 0b6ff8000d Remove getSuggestedFollowsByActor fallbacks and use recIdStr (#9988) 2026-03-12 16:57:38 -07:00
DS Boyce 35cb2bcf94 Create codemod for updating Toast calls to v2 (#10045) 2026-03-12 11:35:58 -07:00
DS Boyce 9c9970f680 Don't show chat notification if viewer is age gated (#10042) 2026-03-12 11:21:57 -07:00
DS Boyce 7b5d5a4f76 Provide age-restricted viewers the option to delete their account (#10041) 2026-03-12 11:13:23 -07:00
Samuel Newman 4737bfb7ed Update expo deps, unpin things (#10044) 2026-03-12 05:33:11 -07:00
pfrazee 374ce2c39e Nightly source-language update 2026-03-12 03:08:04 +00:00
DS Boyce 1db01a09a8 Create codemod for addressing ESLint warnings (#10032) 2026-03-11 15:51:31 -07:00
Samuel Newman 80429ec902 Add “download image” option to web, simplify downloads (#10025) 2026-03-11 23:44:19 +02:00
heikadog 0c7b2d5353 Update issue templates to take uploads, and fix typo (#10036) 2026-03-11 14:50:35 -05:00
DS Boyce 850e6e6f52 Add new metrics for post menu interactions (#10022)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-03-11 13:30:33 -05:00
Eric Bailey 0937f522af Serve og:video tags on post URLs (#10039) 2026-03-11 11:46:27 -05:00
pfrazee f5994af620 Nightly source-language update 2026-03-11 03:04:18 +00:00
Samuel Newman 8b67a3ec2e First pass at fixing large type by adding maxFontSizeMultiplier (#10037) 2026-03-10 23:39:02 +02:00
Samuel Newman 6187e79b0e Update React Native Screens and React Navigation to latest (#9993)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-03-10 15:32:41 -05:00
Spence Pope 2bd9811652 [APP-1928] add bot/automated account badge and self-labeling settings (#10008)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-03-10 13:28:49 -05:00
pfrazee 8bdb1c30c8 Nightly source-language update 2026-03-10 03:05:11 +00:00
Samuel Newman aa897f55a0 The Great Unjanking of the Sheets (#9973) 2026-03-09 22:53:32 +02:00
devin ivy 18d7e775f6 ogcard: ensure we use jpeg images for satori compat (#10023) 2026-03-07 09:11:57 +00:00
pfrazee b8aae166d9 Nightly source-language update 2026-03-07 02:59:25 +00:00
DS Boyce f8886fbfe6 Refine presentation of translation results (#10015)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-03-06 13:36:58 -08:00
Samuel Newman 041e348581 Bump version to 1.119.0 (#10020) 2026-03-06 13:34:03 -08:00
Samuel Newman e2c54a858c Move dev-env to subdirectory (#10009) 2026-03-06 11:45:49 -06:00
Eric Bailey 60a0edbbe2 [APP-1960] Fix current session refresh bug (#10019) 2026-03-06 11:43:14 -06:00
pfrazee 562bf3be22 Nightly source-language update 2026-03-06 03:06:49 +00:00
DS Boyce 290e0f2b54 Disable animations for translation events on Android (#10013) 2026-03-05 15:01:29 -08:00
DS Boyce ece6dc251c Link to profile via handle vs did (#10012) 2026-03-05 12:52:39 -08:00
Eric Bailey 8c705864a2 Prep 1.118.1 patch relase (#10011) 2026-03-05 14:43:18 -06:00
Eric Bailey 7a08b82810 Fix deps (#10010) 2026-03-05 14:11:57 -06:00
Samuel Newman 20bf2cd117 Prevent over-eager translate button (#10007) 2026-03-05 12:07:34 -08:00
DS Boyce fe8e8ce7de Add README.md for lib/translation (#10005) 2026-03-05 10:53:25 -08:00
Eric Bailey 8b8acb7bd1 Add docs on directory structures and naming conventions (#10002) 2026-03-05 12:31:55 -06:00
DS Boyce 1a487d0943 Remove duplicate punctuation (#10004) 2026-03-05 10:16:20 -08:00
Samuel Newman 1ae77aa997 Fix findNodeHandle usage on web (#10001) 2026-03-05 07:22:13 -08:00
Samuel Newman 7cec490cef Use a different icon for testflight builds (#10000) 2026-03-05 07:04:59 -08:00
Eric Bailey 1281eb01e6 Prep 1.118.0 (#9999) 2026-03-04 21:08:55 -06:00
pfrazee 4339585b27 Nightly source-language update 2026-03-05 03:06:58 +00:00
Samuel Newman 30c9ea5789 v1.118.0 E2E tests (#9994) 2026-03-04 20:45:08 -06:00
DS Boyce afbade6f6f Refine UX for on-device translation for posts (#9987)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-03-04 20:37:44 -06:00
Samuel Newman 1782a65174 Add iOS 26 fluid zoom transition to alt text dialog (#9970)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 20:32:08 -06:00
Eric Bailey 7fd218c97d Update API package to fix stale session issues (#9998) 2026-03-04 18:27:12 -06:00
DS Boyce 8446efb739 Pass feed URI to sendInteractions endpoint (#9989) 2026-03-04 12:38:24 -08:00
pfrazee b84f28f884 Nightly source-language update 2026-03-04 03:05:02 +00:00
DS Boyce 7dc3b4fa8e Add search event analytics (#9949)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-03-03 14:49:38 -06:00
Samuel Newman 37a82761f5 Cache profile view before opening AfterReportDialog (#9962) 2026-03-03 13:50:22 -06:00
Samuel Newman 612a778361 Set mute word dialog text input return key type to "done" (#9967)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-03 13:35:09 -06:00
Samuel Newman 162a78e1ae Add 🪿 (#9983) 2026-03-03 06:06:13 -08:00
pfrazee c42502fdcb Nightly source-language update 2026-03-03 03:09:48 +00:00
Samuel Newman 8b1c47a499 bskyweb: use avatar thumbnail for post og:image fallback (#9923) 2026-03-02 22:28:16 +00:00
Samuel Newman c2fd87bd8b Remove JPG hardcoding from image processing systems (#9955) 2026-03-02 20:23:07 +00:00
Samuel Newman d247a65683 Add Trans to string (#9981) 2026-03-02 11:39:50 -08:00
Spence Pope a95b877ea3 [APP-1917] fix header measurement for android content disappearing on switch (#9964)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-03-02 14:20:16 -05:00
Samuel Newman f614bf650d Translation followups (#9980) 2026-03-02 11:01:09 -08:00
Samuel Newman 5ee667f307 Force relayout to fix stuck header blur (#9979) 2026-03-02 10:50:00 -08:00
Spence Pope 9c29b2867a [APP-1882] fix email not updating in session state after email change (#9953) 2026-03-02 06:09:21 -08:00
smileyhead 9746dd8e4c Make ALT labels translatable (#9976) 2026-03-02 13:14:07 +00:00
DS Boyce 8e2a5abfff Fix crash with translate link on web (#9972) 2026-02-28 23:53:42 +00:00
pfrazee 6cdc1fe2b5 Nightly source-language update 2026-02-28 02:56:49 +00:00
DS Boyce 9bbcb472ef Use on-device translation on mobile when available (#9930)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-02-27 16:59:19 -08:00
Samuel Newman 7c9f05a2af ContextMenu - return item to the right location if keyboard hides (#9963) 2026-02-27 22:19:04 +00:00
Samuel Newman d4357b2cb8 Fix iOS sheet keyboard handling using native scrollview features (#9959) 2026-02-27 13:59:24 -08:00
Spence Pope 576761a645 [APP-1819] fix progress guide flash and extra gap (#9960) 2026-02-27 08:31:35 -08:00
DS Boyce 8a1f8997fe Fix infinite loading spinner when changing search terms (#9950)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-02-27 03:42:31 -08:00
pfrazee 6cb88ce1f3 Nightly source-language update 2026-02-27 03:06:31 +00:00
Samuel Newman 65c4b7833d Blur effect on home screen (iOS 26) (#9935) 2026-02-26 07:00:12 -08:00
Spence Pope 9b24d75c9c [APP-1876] Fix pull-to-refresh getting stuck on tab switch (#9951) 2026-02-26 02:06:30 -08:00
Spence Pope 398df1a4b5 ensure app session state refreshes on reactivation (#9954)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 02:06:16 -08:00
pfrazee db9c2a1596 Nightly source-language update 2026-02-26 03:07:29 +00:00
Alex Benzer 00816b70dc [APP-1859] pinned feed drag n drop (#9893)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: vineyardbovines <spencerfpope@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:52:10 -08:00
Samuel Newman 49272be36b Fix up polyfills, remove unused deps (#9946) 2026-02-25 11:43:46 -08:00
surfdude29 85ffc77983 Show hashtag symbol for Mute option in menu (#9942) 2026-02-25 01:18:20 -08:00
pfrazee 95bede2dcb Nightly source-language update 2026-02-25 03:09:49 +00:00
surfdude29 e656fe688d Align strings from 'subtitles' to 'captions' (#9941) 2026-02-24 21:56:12 +00:00
Samuel Newman db1f45de92 iOS 26 tweaks #2 (#9938) 2026-02-24 12:14:19 -08:00
Samuel Newman bc61314c41 iOS 26 followups (#9936) 2026-02-24 08:12:57 -08:00
mozzius 1850042c37 Nightly source-language update 2026-02-24 07:17:35 +00:00
Samuel Newman 782edbf281 Restore type assert that got accidentally removed (#9934) 2026-02-23 23:11:39 -08:00
Samuel Newman 73b096e443 iOS 26 (#9047)
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 22:34:52 -08:00
pfrazee 9b0f0a8d24 Nightly source-language update 2026-02-24 03:09:35 +00:00
Samuel Newman 9ec06971af Upgrade Lingui to v5 (#9905) 2026-02-23 20:28:03 +00:00
Samuel Newman 75242b9b1f Bump version to v1.118.0 (#9931) 2026-02-23 12:04:58 -08:00
pfrazee 21e53b226d Nightly source-language update 2026-02-22 03:10:59 +00:00
surfdude29 02c2a9ea7c Mark admonition text for translation in CreateListFromStarterPackDialog (#9922) 2026-02-21 16:53:44 +00:00
Eric Bailey cb7e2ab976 Release prep 1.117.0 (#9920) 2026-02-20 14:30:14 -06:00
Eric Bailey 067c6dacc9 Update location copy (#9918) 2026-02-20 13:20:04 -06:00
Eric Bailey 9b8c46cdb7 Add A/A test (#9917) 2026-02-20 13:19:56 -06:00
Samuel Newman f3cc850b62 Fix up typescript config (#9910) 2026-02-20 18:07:17 +00:00
Samuel Newman 35f9ed82b9 Fix string concat bug with trending topics muteword moderation (#9914) 2026-02-20 11:29:08 -06:00
Samuel Newman 8c4fc087f8 Add ability to convert starter pack to list (#9675)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Samuel Newman <mozzius@users.noreply.github.com>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2026-02-20 11:27:55 -06:00
Samuel Newman f3ddc074a6 Fork MCEmojiPicker (#9763) 2026-02-20 10:39:48 -06:00
Spence Pope 5e0ffc0917 [APP-1809] add cleanup button for unavailable moderation services (#9901)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-02-20 10:23:01 -06:00
Samuel Newman 3fae42c077 Remove hardcoded warning color, add yellow and pink to ALF palette (#9911) 2026-02-20 10:17:11 -06:00
Sylwia 80c0aa35e2 Add CORS headers to /oembed API (#9845) 2026-02-19 22:07:49 -08:00
Eric Bailey 4e3c3e9905 [APP-1805] Analytics tweaks and tracking callbacks (#9861) 2026-02-19 15:31:35 -06:00
pfrazee 1565e071c1 Nightly source-language update 2026-02-19 03:10:25 +00:00
Samuel Newman c906fea77a Fix webpack dev server freezing after repeated refreshes (#9865) 2026-02-18 11:03:23 -08:00
Samuel Newman 775a041b71 Add chat data export to Export My Data dialog (#9900)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:01:58 -08:00
Samuel Newman 98d17fd950 Fix native-only function being called on web (#9904) 2026-02-18 09:46:45 -08:00
Samuel Newman 41b3742365 Dynamically load date-fns locale data (#9535) 2026-02-18 11:30:32 -06:00
Samuel Newman 512ce3d5d0 ALF content language dialog (#9471)
Co-authored-by: Eric Bailey <git@esb.lol>
2026-02-18 08:57:32 -08:00
Samuel Newman bdf143ce79 Restructure notification/linking handling to use synchronous APIs (#9497)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-02-18 08:20:39 -08:00
Samuel Newman adc079e095 Get rid of Sentry Replay from web bundle (#9534) 2026-02-18 10:20:05 -06:00
DS Boyce 74330500de Improve error handling on Explore page (#9890) 2026-02-18 09:57:31 -06:00
DS Boyce ac939dc84e Update onboarding user suggestions with new endpoint (#9872) 2026-02-18 09:35:00 -06:00
Samuel Newman b12d703353 Enable profile hover card on quoted post authors (#9896)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:35:38 -08:00
DS Boyce c188fd4f04 Log onboarding errors for suggested accounts (#9892) 2026-02-18 00:18:22 -08:00
Samuel Newman a1cda126e6 ALF feed error screen (#9895) 2026-02-18 00:16:12 -08:00
pfrazee ae115ca17e Nightly source-language update 2026-02-18 03:10:48 +00:00
Spence Pope 197c069416 Clear link and gif caches to avoid pointing at missing files (#9898)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 10:39:34 -05:00
pfrazee e67d81f269 Nightly source-language update 2026-02-17 03:09:32 +00:00
DS Boyce 7c08bba6cf Use Prompt.Action for delete account buttons (#9889) 2026-02-16 16:23:27 -08:00
DS Boyce 29bc0f1372 Add optional props to Prompt.Action (#9887) 2026-02-16 13:29:25 -08:00
surfdude29 82f42e734c Wrap interaction counts with Trans to add translation flexibility (#9036) 2026-02-16 21:12:26 +00:00
surfdude29 3b9f0b0a68 Add plural formatting for several strings (#9576) 2026-02-16 21:10:57 +00:00
Samuel Newman a6ba0810b7 Be more descriptive with video errors, log less (#9886) 2026-02-16 21:09:49 +00:00
DS Boyce 49253dbd88 Refactor delete account dialog using ALF (#9863) 2026-02-16 12:00:46 -08:00
Eric Bailey cc2255cc49 [APP-1776] Refactor Live Now config, reorg files (#9871) 2026-02-16 11:44:22 -06:00
Eric Bailey 8ee9709fbb [APP-1764] Improve Age Assurance copy (#9885)
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2026-02-16 11:03:59 -06:00
Samuel Newman 90d650def2 Use video thumbnail as og:image for posts with video embeds (#9877) 2026-02-16 15:35:29 +00:00
Samuel Newman 79e27e303b Fix missing pixel row in text rendering on iOS (#9882)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 05:41:35 -08:00
Spence Pope 6be7751769 have back button dismiss lightbox on web (#9874) 2026-02-16 08:39:53 -05:00
Samuel Newman 9b830fd425 Use scroll position to gate post-created feed refresh (#9881)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 05:39:29 -08:00
Samuel Newman 4ebeb94b41 Add Renew menu for expired mute words (#9883)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 05:30:07 -08:00
Samuel Newman b6b364700e Make profile description text selectable (#9884)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 05:29:59 -08:00
pfrazee 95f9cc1ed5 Nightly source-language update 2026-02-16 03:11:39 +00:00
surfdude29 06fcea988e 'Log in' to 'Sign in' (#9875) 2026-02-15 19:02:01 +00:00
pfrazee b166d395d9 Nightly source-language update 2026-02-15 03:11:50 +00:00
Samuel Newman 1228306309 Fix quote embed clickability in composer on web (#9876) 2026-02-14 22:29:59 +00:00
pfrazee bc001168f3 Nightly source-language update 2026-02-14 03:05:51 +00:00
Spence Pope b4cb4c4d13 Revert "Add --what-to-test to iOS submissions (#9856)" (#9873) 2026-02-13 13:24:06 -08:00
Samuel Newman 2e34f965fc [Auth rework] Add a header to login flow (#9368) 2026-02-13 20:15:36 +00:00
Spence Pope 9cdfb40ce7 dedupe trending topics (#9870) 2026-02-13 13:03:36 -05:00
Samuel Newman c83dddce57 Bandcamp embed (#9445) 2026-02-13 13:30:10 +00:00
pfrazee 795428971d Nightly source-language update 2026-02-13 03:12:46 +00:00
Spence Pope 82fea6da7d [APP-1846] Remove trending post counts until we can improve accuracy (#9867) 2026-02-12 19:34:50 +00:00
Spence Pope 2f156bb906 [APP-1833] Handle errors on saving a draft over char limit (#9850)
* show composer error if draft over char limit

* adjust cancel discarding if over the limit

* use richtext for validation

* don't allow saving a draft that can't actually be saved

* account for 1k chars

* update discard sheet

* show composer error if draft over char limit

* adjust cancel discarding if over the limit

* use richtext for validation

* don't allow saving a draft that can't actually be saved

* account for 1k chars

* update discard sheet

* pr comment fixes

* pluralization
2026-02-12 09:24:38 -06:00
Samuel Newman c46219bc4d Fix keyboard flicker when selecting a draft in composer (#9864) 2026-02-12 15:07:26 +00:00
pfrazee 6d68a5bd21 Nightly source-language update 2026-02-12 03:12:33 +00:00
surfdude29 cbac380c96 Add profile error messages to cleanError (#9842) 2026-02-11 19:55:54 -06:00
Samuel Newman 8c4820b1f1 [Web] Fix splash flicker and add animation (#9860)
* manually hide non-React splash logo

* tweak comment

* animate out splash
2026-02-11 19:52:22 -06:00
Samuel Newman 93d2f3a8db Germ DM button (#9848)
* germ link (wip)

* add todos

* just yeet declaration for now

* ensure not proxied

* allow undo delete

* tweak styles

* ignore unknown values

* tweak styles

* fix boolean logic

* skip IAB

* fix mutationFn

* add link warning interstitial

* Logging and metrics

* Little more error handling

* Update copy

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2026-02-11 17:51:19 -06:00
Eric Bailey 3a9526d55e Bump API SDK (#9862)
* Bump API SDK

* Bump dev-env
2026-02-11 17:41:34 -06:00
Spence Pope 830f305d2d Merge pull request #9823 from bluesky-social/app-1788
[APP-1788] update design for stacked avatars
2026-02-11 15:57:07 -05:00
DS Boyce 8a088e8710 Merge pull request #9853 from bluesky-social/d/profile-screen
Use Tanstack Query isPending over isLoading
2026-02-11 10:27:13 -08:00
Spence Pope 8cbe3985fa Update src/components/ProgressGuide/List.tsx
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-02-11 13:11:30 -05:00
Spence Pope 1ec70420e6 Update src/components/ProgressGuide/List.tsx
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-02-11 13:11:24 -05:00
Spence Pope 63fbf577d9 Update src/components/ProgressGuide/List.tsx
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-02-11 13:11:15 -05:00
DS Boyce 355868e01e Address feedback and lint warnings 2026-02-11 09:46:47 -08:00
Samuel Newman 37e5a0f7a4 Clean up starter packs dialog styles + add optimistic updates (#9469) 2026-02-11 17:46:28 +00:00
DS Boyce 4356ff0040 Merge branch 'main' into d/profile-screen 2026-02-11 09:43:55 -08:00
Samuel Newman 4eca13941a Fix GIFs breaking mute state/pausing phone audio (#9859)
* fix operator precedence mistake

* ensure gifs can't change the global mute state
2026-02-11 08:51:23 -08:00
Samuel Newman 8580e4a00e absolutely position header, set a headerheight (#9858) 2026-02-11 07:22:13 -08:00
Samuel Newman 8581285d34 simplify some conditionals 2026-02-11 16:20:48 +02:00
Samuel Newman 57997d6d16 Add --what-to-test to iOS submissions (#9856)
Sets the "What to test" field in TestFlight to distinguish between
TestFlight and Production builds.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 06:01:27 -08:00
Samuel Newman a25bc8f249 Debounce thread preferences on leading edge, reduce to 2s (#9851)
* debounce on leading edge, reduce to 2s

* flush debounce on leave screen

* simplify by using useMutation properly
2026-02-10 23:41:04 -08:00
pfrazee 177a6e50bd Nightly source-language update 2026-02-11 03:12:22 +00:00
DS Boyce 5459857112 Use Tanstack Query status over isPending 2026-02-10 13:27:18 -08:00
Samuel Newman 338016ed5c Android sheets edge to edge (#8342) 2026-02-10 21:01:58 +00:00
Samuel Newman e2058c8ecc Update starter pack message to clarify users joined Bluesky (#9849) 2026-02-10 15:37:48 +00:00
surfdude29 3cbded0269 fix typos in drafts NUX alt text (#9832) 2026-02-10 10:41:47 +00:00
Samuel Newman 5ea96736de rm unused @testing-library/jest-native (#9820) 2026-02-10 08:36:27 +00:00
pfrazee efed955003 Nightly source-language update 2026-02-10 03:12:38 +00:00
surfdude29 b8db36afb0 Add context for translators for "No one" string (#9844)
* add context for translators 'No one' option

* update ActivityPrivacySettings to add context for translators

* update translation context for 'No one' option
2026-02-09 15:37:04 -08:00
Samuel Newman 5642bc40bb bump version to v1.117.0 (#9847) 2026-02-09 15:14:46 -08:00
Samuel Newman 3be1f5caa9 Add attribution for starter pack follows (#9834) 2026-02-09 10:01:55 -06:00
pfrazee 18eea81e76 Nightly source-language update 2026-02-08 03:12:29 +00:00
Samuel Newman 917f099e26 [Drafts] Some bugs (#9833)
* Fix text input not updating

* Fix autofocus

* make placeholder text fainter

* Await invalidation

* Image only drafts

* tweaks to gif presentation

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2026-02-07 15:38:48 +00:00
pfrazee 6bfe758d2a Nightly source-language update 2026-02-06 03:06:58 +00:00
Spence Pope 18ab5d6f34 [APP-1823] Fix 'create account' / 'sign in' buttons stacking (#9830)
* ensure enough space for sign in buttons

* smallest possible adjustment

* smallest possible adjustment
2026-02-05 08:40:25 -08:00
Spence Pope dc3f01591f [APP-1794] respect mute words for live event feed banners (#9831)
* respect mute words for live event feed banners

* linting
2026-02-05 09:36:19 -06:00
vineyardbovines 3a28a07081 Merge branch 'main' into app-1788 2026-02-05 08:59:22 -05:00
vineyardbovines a57faedbae update icon 2026-02-05 08:51:53 -05:00
pfrazee 423eddc474 Nightly source-language update 2026-02-05 03:07:42 +00:00
Darrin Loeliger 5c5b365ee9 Adding little message at the bottom of the drafts list (#9827)
* Add encouragement message to drafts list

Shows "So many thoughts, you should post one" at the bottom of the drafts list when user has more than 5 drafts.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Use Text component and center-align encouragement message

- Switch from ButtonText to Text component for better styling
- Add text-center alignment to the message

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 16:40:16 -06:00
Eric Bailey 1584c2de48 intl (#9826) 2026-02-04 15:49:33 -06:00
Samuel Newman bb6ce83198 [Drafts] NUX (#9825)
* drafts NUX

* use "Drafts" string

* change button copy
2026-02-04 15:39:55 -06:00
Samuel Newman 3abc361800 Exempt gifs from active video system (#9824) 2026-02-04 12:37:09 -08:00
Samuel Newman e6c4a539ca align alt text behaviour between gif types (#9822) 2026-02-04 12:34:14 -08:00
Eric Bailey 965b17ff86 Update android splash colors (#9821) 2026-02-04 14:07:18 -06:00
vineyardbovines a148b2477b update design for stacked avatars 2026-02-04 15:06:42 -05:00
Eric Bailey 546201c93c intl pull and extract:all (#9818) 2026-02-04 13:14:22 -06:00
Samuel Newman b3cbb3440a Make GIFs look like GIFs (#9809)
* set presentation field in record

* refer to it as a gif in the composer

* video player gif presentation style

* tweak badge

* only do the "manual loop" when absolutely necessary

* mute gifs

* reuse gif controls component for tenor gifs

* update media previews in notifications

* edit comment

* remove outdated prop
2026-02-04 13:05:17 -06:00
Samuel Newman da5c356fc7 Only treat animated gifs as videos, leave static gifs as images (#9814)
* only treat *animated* gifs as videos

* fix export
2026-02-04 13:00:29 -06:00
Eric Bailey 6f01503eac intl pull and extract:all (#9817) 2026-02-04 12:52:39 -06:00
pfrazee 24dbe799dc Nightly source-language update 2026-02-04 03:06:15 +00:00
Samuel Newman 3197cbcf36 disable saving drafts for replies (#9815) 2026-02-03 09:32:13 -08:00
Samuel Newman 017120b3e8 Add recId to suggestedUser:* events (#9764)
* add recId to onboarding

* add recId to follow dialog

* add recId to profile header suggestions

* add recId to feed interstitials, fix animation on native

* fix claude feedback

* fix yarn.lock ci
2026-02-03 10:42:12 -06:00
Samuel Newman 27d9462639 Add animation to content hider (#9812)
* add layout animation to contenthider

* add layout animation to posthider
2026-02-03 08:03:36 -08:00
Samuel Newman b2c95bc35a remove superjson (#9811) 2026-02-03 07:52:19 -08:00
pfrazee cbd48c855a Nightly source-language update 2026-02-03 03:09:12 +00:00
Samuel Newman 421c18ea7c fix image patch version (#9810) 2026-02-02 15:35:04 -08:00
Samuel Newman 26c84399dd Add light haptic feedback to Edit Profile button (#9787)
https://claude.ai/code/session_018ZjPcCUDPfTjQ5SGac3Zgs

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-02 15:33:55 -08:00
Samuel Newman 4f1d4821c5 Speed up startup by persisting some queries (#9594)
* persist startup queries

* Use IDB for query storage (#9687)

* Add storage abstraction for persisted query data

Introduce a platform-specific storage abstraction layer for react-query
persistence:
- Native: Uses MMKV for high-performance synchronous storage
- Web: Uses IndexedDB via the `idb` library for efficient async storage

This replaces the previous AsyncStorage implementation with more performant
platform-native solutions. The abstraction maintains API compatibility with
@tanstack/query-async-storage-persister.

* Refactor storage abstraction to use factory pattern

Change createPersistedQueryStorage to a factory function that accepts a
storage ID, allowing multiple isolated storage instances:
- Native: Each instance gets its own MMKV store
- Web: Each instance gets its own IndexedDB database

Adopt the factory pattern in:
- react-query.tsx: Uses 'persisted_queries' storage
- ageAssurance/data.tsx: Uses 'age_assurance' storage

This provides better separation between different query client caches
and allows each to be managed independently.

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Refactor to use archival storage

(cherry picked from commit a773b40e41c96f821cd32260919ce1437c0fc3ab)

* Improve archive db types

(cherry picked from commit 80e4959ba2aa00c984c26aed2f7dfae1095720b0)

* rm idb

* clear on logout, bust on app version

* create abstraction for persisting queries, make gcTime infinite

* Rm abstraction

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-02-02 11:55:01 -06:00
mozzius bfd5ed480f Nightly source-language update 2026-02-01 10:24:34 +00:00
Samuel Newman d9ef02737b Rename "lightbox" to "image viewer" in user-facing strings (#9807)
https://claude.ai/code/session_01LWKpUAY5WJL7UfeFJd9kVU

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-01 12:21:15 +02:00
mozzius 809b318fe4 Nightly source-language update 2026-02-01 08:10:57 +00:00
Samuel Newman 864af57fd9 Fix i18n string interpolation for device name (#9806)
Use a separate translatable string for "Media stored on another device"
instead of interpolating a translated fragment into another string.
This allows translators to properly handle the complete sentence for
languages with different grammar structures.

https://claude.ai/code/session_01TJMLcXE9HHneEXMEBJqL4u

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-01 00:08:28 -08:00
Eric Bailey ab98130295 Rm entitlement for now (#9804) 2026-01-30 20:30:00 -06:00
estrattonbailey a168e97d07 Nightly source-language update 2026-01-31 02:20:20 +00:00
Eric Bailey 748706daab Draft previews (#9803)
* Send deviceId and platform

* Add deviceId and deviceName to drafts, skip loading media for other devies

* WIP new preview

* show rich text in drafts list

(cherry picked from commit fb70d53d59)

* New draft preview UI

* Tighten up spacing in draft list

* Add i18n comments

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-01-30 17:00:54 -06:00
Spence Pope 45d6e50eb4 lint (#9802) 2026-01-30 22:48:31 +02:00
Eric Bailey e2a56b0191 Drafts fixes (#9795)
* Allow saving quote post drafts

* Remove warning logs for failed media loading

* Fix threadgates
2026-01-30 10:26:32 -06:00
Samuel Newman 1eb09b7210 Detect facets when composer is opened with mention (#9625)
* highlight the initial mention

* fix mention initalisation on web
2026-01-30 10:14:37 -06:00
Samuel Newman ee99320e1f fix copy-to-cache (#9799) 2026-01-30 09:47:35 -06:00
Samuel Newman 9689a6daaa add 2px margin below post text (#9748) 2026-01-30 05:44:04 -08:00
Samuel Newman 45735144ee adjust splash size (#9797) 2026-01-30 04:45:43 -08:00
pfrazee 4b86444bd8 Nightly source-language update 2026-01-30 03:07:37 +00:00
Samuel Newman 9ff5b2da50 Revert splash screen changes (#9792)
* Revert splash screen changes

This partially reverts commit 0ac5b07bfd.

* Keep android changes
2026-01-29 15:23:05 -06:00
Samuel Newman 58f532a495 [Drafts] Storage fixes (#9790)
* delete media from existsCache when deleting

* revoke media URLs

* skip revoking objecturls until the composer is completely closed

* [Drafts] Metrics (#9794)

* metrics for drafts

* Nit: format

* nit: use new util for clarity

* nit: use new util for clarity

---------

Co-authored-by: Eric Bailey <git@esb.lol>

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2026-01-29 15:21:38 -06:00
Samuel Newman 0babd0f475 [Drafts] Revert unnecessary gif changes (#9788)
* revert unnecessary gif changes

* Update ExternalEmbed.tsx
2026-01-29 15:08:48 -06:00
Samuel Newman 2cee96da8e Add ESLint rule to enforce Lingui msg usage (#9789)
* Add ESLint rule to enforce Lingui msg usage

Adds a custom ESLint rule 'lingui-msg-rule' that ensures the Lingui _()
function is called with msg`` template literals or plural/select macros,
preventing accidental misuse like _('string') which bypasses i18n.

https://claude.ai/code/session_01JMXXPUgAHiSBGmfGwUojKy

* Support msg({...}) descriptor form and add auto-fix

- Allow msg() function call form: _(msg({message: 'Hello'}))
- Add auto-fix for string literals: _('Bad') -> _(msg`Bad`)
- Add auto-fix for untagged templates: _(`Bad`) -> _(msg`Bad`)
- No auto-fix for variables/function calls (not safely fixable)

https://claude.ai/code/session_01JMXXPUgAHiSBGmfGwUojKy

* fix complex cases

* run autofix HELL YEAH

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 11:05:49 -08:00
pfrazee 31553f0d40 Nightly source-language update 2026-01-29 03:04:03 +00:00
Eric Bailey 0ac5b07bfd [APP-1801] New splash screen (#9780)
* MVP splash

* MVP of login screen

* Hack a footer into the logged out view

* revert unneeded

(cherry picked from commit 296420aa0df433b101389c44b05a40696e7389ae)

* Update config

(cherry picked from commit eac421966dfdee99d4c818ac07835a2a97e7590f)

* Bump size slightly
2026-01-28 20:05:14 -06:00
Samuel Newman d13df6c7e6 Drafts (#9691)
* Add drafts functionality to composer

- Add local storage layer for drafts (filesystem on native, IndexedDB on web)
- Add "Drafts" button to composer top bar showing badge with draft count
- Modify discard prompt to offer "Save Draft" option
- Add `restore_from_draft` action to composer reducer
- Support saving/restoring: text, facets, images, labels, threadgate, quote/link embeds
- Add placeholder hooks for future server API integration
- Add unit tests for draft serialization

Note: Video/GIF restoration marked as TODO for future implementation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix drafts button: always visible, adjacent to post button

- Make drafts button always visible (not just when drafts exist)
- Move button to be adjacent to the publish button
- If composer is empty: opens drafts list directly
- If composer has content: shows prompt to save/discard before viewing drafts
- Add badge showing draft count when drafts exist

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update drafts button: text-only, ghost/primary style

- Show "Drafts" or "Drafts (N)" as text, no icon
- Use ghost variant with primary color
- Match Cancel button styling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Remove draft count from button, just show "Drafts"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Increase drafts button horizontal padding and gap

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Use InnerFlatList and Dialog.Header for drafts dialog

- Switch from ScrollableInner to InnerFlatList
- Add Dialog.Header with back button in left slot
- Use sticky header

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Track draft ID in composer state machine

Adds draftId to ComposerState so that editing an existing draft and
saving it again updates the draft rather than creating a new one.

- Add draftId?: string to ComposerState type
- Set draftId when restoring from draft via restore_from_draft action
- Pass existingDraftId to save functions from composerState.draftId
- Add PageX icon for empty drafts state

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add clear action to discard composer content

When pressing the Drafts button with content in the composer, the user
can choose to discard. This now properly clears the composer by
dispatching a 'clear' action that resets to an empty state.

- Add 'clear' action type to ComposerAction
- Implement clear case in composerReducer (resets to single empty post)
- Add handleClearComposer callback in Composer.tsx
- Pass onDiscard prop through ComposerTopBar to DraftsButton
- Call onDiscard before opening drafts dialog on discard

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Track dirty state to skip discard prompt for unchanged drafts

When a draft is loaded and the user hasn't made any changes, closing
the composer should not show the discard prompt since nothing would
be lost.

- Add isDirty field to ComposerState
- Set isDirty: true on all content-modifying actions
- Set isDirty: false on restore_from_draft, clear, and initial state
- Update onPressCancel to only show prompt if no draft or isDirty

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Redesign drafts list to show full thread preview

- Display all posts in a draft thread, not just the first
- First post uses larger avatar (42px), subsequent posts nested with
  smaller avatar (32px) and thread connector line
- Show author avatar, display name, handle, and relative timestamp
- Add overflow menu button (placeholder) on first post
- Display full text instead of truncated preview
- Add media preview component for images, GIFs, and videos
- Card layout with rounded corners and proper spacing
- Add gap separators between draft cards in list

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix draft item styling based on feedback

- Add border and shadow to draft cards
- Remove trash button, move delete to overflow menu prompt
- Remove size differences for thread posts (same avatar/text size)
- Add spacing between header and first draft item
- Change prompt wording to "Discard draft"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Use real image embed components for draft preview

Replace custom image preview with AutoSizedImage for single images
and ImageLayoutGrid for multiple images. This gives drafts the same
polished image display as regular posts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Improve drafts dialog platform handling

- Render header outside FlatList on native, inside on web
- Use web() helper for conditional web-only props
- Replace ItemSeparatorComponent with mt_lg margin on items
- Add minHeight on web for better dialog sizing
- Simplify header structure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Mark composer as clean after saving draft

Add mark_saved action that resets isDirty to false and updates the
draftId. This is dispatched after successfully saving a draft, allowing
the user to close the composer without a discard prompt since their
changes have been saved.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Apply dirty tracking to Drafts button prompt

Only show the save/discard prompt when pressing the Drafts button if
the composer has unsaved changes (isDirty). If the content is unchanged
from a loaded draft or was just saved, go directly to the drafts list.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix re-saving drafts with existing media

When re-saving a draft, the code was trying to copy media files that
were already in drafts storage to new locations, causing copy errors.

Changes:
- Add extractLocalIdFromPath() to detect if a path is already in drafts
- Track loadedMediaMap in ComposerState for identifying reusable media
- Only delete old media that wasn't reused during re-save
- Pass loadedMediaMap when saving to enable media reuse detection
- Disable pointer events on draft media preview

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* add vertical option to prompt, change copy

* fix import

* fix import

* Migrate drafts from local storage to server API

Replace local-only draft storage with the new `app.bsky.draft.*` server API:
- getDrafts, createDraft, updateDraft, deleteDraft endpoints

Key changes:
- Add api.ts with type converters (ComposerState <-> server Draft)
- Update hooks.ts to use server API instead of local storage
- Simplify storage.ts/storage.web.ts for local media caching only
- Media stored locally via localRef pattern (filepath in server draft)
- GIFs stored as external embeds with Tenor URL + dimensions
- Hide drafts button when replying (reply drafts not supported)
- Show "different device" note when media is missing locally

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* attempt to fix blob mangement

* Migrate storage.ts from expo-file-system/legacy to expo-file-system

Use the new object-based expo-file-system API (SDK 54+) with Directory
and File classes instead of the legacy function-based API. The new API
provides synchronous operations for file/directory existence checks,
creation, copying, deletion, and listing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add drafts-specific logger

Add a Drafts context to the logger system for better log categorization
and debugging of draft-related operations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: use drafts-specific logger in hooks and storage

Switch from the generic logger to the new drafts-specific logger
for better log categorization.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: ensure media cache is populated before checking exists

On iOS (and web), the media cache wasn't populated before the drafts
query ran, causing drafts with local media to incorrectly show as
"missing media" on app restart. The issue would resolve itself after
closing and reopening the composer because by then the cache was ready.

This fix adds ensureMediaCachePopulated() and awaits it in useDrafts
before checking which media exists locally.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: complete Gif object reconstruction for draft rehydration

Fix "Cannot read property 'url' of undefined" error when rehydrating
drafts with GIFs. The Gif object was missing required properties like
url, content_description, and media_formats.preview that are needed
by useResolveGifQuery and other components.

Also preserve alt text through serialization by storing it in URL
query params alongside dimensions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: load draft preview images and get dimensions

Fix draft preview images not showing on web and add proper aspect
ratio support:

1. Try to load all images regardless of the exists cache flag, which
   may be stale due to async cache population timing
2. Use Image.loadAsync() from expo-image to get image dimensions
3. Pass dimensions to viewImages for proper aspect ratio in previews

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: update save prompt copy when editing existing draft

When editing an existing draft (vs creating a new one), use "Save
changes" instead of "Save draft" in the save/discard prompts. This
provides clearer context to the user about what action they're taking.

Add isEditingDraft prop to DraftsButton and ComposerTopBar, and
update both prompts (in DraftsButton and Composer) with conditional
copy based on whether we're editing an existing draft.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* style: add bottom padding to drafts list

Add pb_xl padding to the drafts list content container for better
visual spacing at the bottom of the list.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: use typed error check for draft limit

Replace manual error object inspection with the proper
AppBskyDraftCreateDraft.DraftLimitReachedError type check for
cleaner and more reliable error handling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* make storage functions async to match web

* log unknown errors

* delete left-over file

* move state to colo with composer

* fix: handle invalid GIF dimensions gracefully

Fix NaN aspectRatio when rehydrating GIFs from drafts by:

1. Adding validation in parseTenorGif to reject invalid dimensions
   (NaN, zero, or negative values)
2. Adding defensive checks in GifEmbed to fallback to 1:1 aspect
   ratio if dimensions are invalid
3. Adding defensive checks in composer ExternalEmbedGif to fallback
   to 16:9 if gif.media_formats.gif.dims is missing or invalid

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: prevent double query string in GIF draft hydration

When loading a GIF from a draft, the URL was being corrupted with
double query strings like:
`?ww=498&hh=498?hh=498&ww=498`

This happened because:
1. serializeGif() adds ?ww=X&hh=Y&alt=Z to the Tenor URL
2. parseGifFromUrl() returned the full URL including our params
3. resolveGif() in resolve.ts then appends MORE params via string
   concatenation, creating a second ?

Fix: Strip our custom params (ww, hh, alt) from the URL in
parseGifFromUrl() before returning it, so the reconstructed GIF
has a clean base URL.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix draft button behaviour when publishing, tweak buttons

* ensure media unavaiable message is contrasty enough

* infinite query, rename file to queries

* simplify threadgate/postgate handling

* refactor: pass full draft data instead of re-fetching

The useLoadDraft and useDeleteDraftMutation hooks were fetching drafts
via getDrafts() to look up a draft by ID. This was problematic because
getDrafts is paginated, so drafts not on the first page wouldn't be
found.

Changes:
- Add full Draft object to DraftSummary type
- useLoadDraft now takes Draft directly (only loads local media)
- useDeleteDraftMutation now takes {draftId, draft} to avoid re-fetch
- Update DraftItem and DraftsListDialog to pass full draft data

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix type errors

* docs: add notes on platform files and paginated APIs

- Platform-specific files (.web.ts, .native.ts) are resolved by the
  bundler automatically - just import normally, don't use require()
- Paginated APIs should use useInfiniteQuery, not useQuery

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* More CLAUDE.md updates

* Delete PLAN.md

* Use minimal media mode for draft display - REVERT IF NEEDED

* Enable pagination

* Add comment about headers

* remove extraneous comments

* Prevent runaway pagination

* fix detection rebase change

* use border_transparent

* Replace idb with idb-keyval for draft media storage

Simplifies web IndexedDB storage by using idb-keyval instead of the
full idb library. This reduces bundle size and aligns with the pattern
used in src/storage/archive/db/index.web.ts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix native draft media filename encoding

The previous approach replaced both / and : with _, but the reverse
transformation couldn't distinguish between them. This caused cache
misses for paths containing both characters.

Use encodeURIComponent/decodeURIComponent for a proper reversible
encoding.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* convert useLoadDraft() hook to regular async function

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* update atproto api

* clean up orphaned media

* restore videos

* save/restore captions

* restore postgates

* Copy updates from Darrin

* Ope fix missed vertical props

* get image aspect ratio when restoring

* get videos working on native

* get video restoration working on native

* sanitize handles properly in draftitem

* fix yarn.lock

* Swap console logs

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-01-28 16:54:26 -06:00
bnewbold a18f1b68e8 CI: auto-detect go runtime version from go.mod (#9766) 2026-01-28 13:16:55 -08:00
Spence Pope b9c92d39eb [APP-1761] Prevent welcome modal from reappearing (#9786)
* store welcome modal appearance in localstorage

* lint

* redundancy
2026-01-28 10:41:48 -08:00
Samuel Newman d45a2c832d Redo web lightbox, add animations and keyboard a11y (#9481)
* redo web lightbox, add animations

* add rect-avi to shell

* fix lingui msgs
2026-01-28 10:13:52 -08:00
Matthieu Sieben fd49349b71 update atproto deps (#9784) 2026-01-28 10:46:48 -06:00
Samuel Newman 277e985e1f prevent lightbox initial jitter (#9762) 2026-01-28 07:46:53 -08:00
Samuel Newman 3b2573bc56 [Lighbox perf - 1] Fix memoization of List component, defer re-rendering when opening lightbox (#9759)
* memoize FlatList_INTERNAL

* defer render, put handleScrolledDownChange in non reactive callback

* lintfix useDedupe
2026-01-28 07:35:43 -08:00
pfrazee ae77320d81 Nightly source-language update 2026-01-28 02:51:12 +00:00
Eric Bailey 72b7516d9b Prompts refresh (#9781)
* Round boi

* Wrap a few things in Prompt.Content

* Couple tweaks
2026-01-27 20:02:55 -06:00
Samuel Newman 220e989f3d improve lightbox smoothness (#9200) 2026-01-27 16:57:49 -06:00
Eric Bailey 39e7132fd8 [APP-1784] Proper fix for Live Now status not showing (#9779)
* Be sure to validate if actor is allowed to be live on certain domain

* Rename for clarity

* Memoize
2026-01-27 16:44:27 -06:00
Samuel Newman 26605ee730 Fix console logging in Firefox and other browsers (#9775)
Use CSS-based %c styling for browser consoles instead of ANSI escape codes, which don't render properly in Firefox. Keep ANSI codes for native/terminal environments.

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-27 16:24:29 -06:00
Spence Pope f1c50baa4e link to labelers by did (#9777) 2026-01-27 13:34:21 -08:00
Samuel Newman e49ce42a12 Disable KeyboardProvider preload for faster initial load (#9778)
Adds preload={false} to KeyboardProvider to prevent unnecessary keyboard controller initialization during app startup.

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-27 12:07:08 -08:00
Eric Bailey fd6b8fcbf1 Fix Live Now not showing for non-streamer (#9773)
* Fix Live Now not showing for non-streamer

* Rename for clarity

* Logged out users too
2026-01-27 12:16:54 -06:00
Samuel Newman 2953b3a094 Typecheck app.config.js (#9771)
* typecheck app.config.js

* fix eslint warning

* use import.meta.url instead of __dirname

* dirname, not url
2026-01-27 09:09:06 -08:00
Samuel Newman 69a765cc0d enable ccache for dev builds on iOS (#9770) 2026-01-27 08:46:05 -08:00
Samuel Newman dd7bb1bc14 fix IS_DEV in app config (#9769) 2026-01-27 06:22:32 -08:00
Alex Benzer d31bc7f335 Update suggested accounts heading (#9765) 2026-01-27 05:49:18 -08:00
Eric Bailey f26ac583e6 Add idb-keyval backed archival storage (#9754)
* Add idb-keyval backed archival storage

* Cleanup

* Remove test
2026-01-26 13:39:12 -06:00
pfrazee 2ad84d7dfc Nightly source-language update 2026-01-26 02:58:16 +00:00
Eric Bailey 282b5e8460 Bump version to 1.116.0 (#9751) 2026-01-25 16:04:27 -06:00
Eric Bailey f82bc98d8d Allow users to dismiss sidebar LEF banner (#9753) 2026-01-25 15:48:18 -06:00
Eric Bailey 3b7806963a Add more Live Event refreshing (#9745)
* Refresh on foreground

* Add back interval
2026-01-24 13:39:55 -06:00
pfrazee 49e7a96cd9 Nightly source-language update 2026-01-24 02:45:32 +00:00
Eric Bailey 3523e7ce58 1.115.0 fast follows (#9743)
* Fix caching, fix attributes setting

* Use useCallOnce hook

* Update dismissal handling

* Update copy
2026-01-23 16:28:40 -06:00
Eric Bailey 967b3b49d9 1.114.1 Release Prep (#9738)
* intl pull, extract:all

* Clean cache for e2e
2026-01-22 17:51:53 -06:00
Eric Bailey 97083bb55b Enable Bluecast for Live Now (#9737) 2026-01-22 16:59:13 -06:00
Eric Bailey df89b79c2a [APP-1787] Some analytics cleanup (#9736)
* Clean up flags

* Move getMetadataForLogger

* Nail down attributers

* Polyfill features cache
2026-01-22 16:57:59 -06:00
Samuel Newman 6534630d16 Refactor GrowthHack to always render on iOS with enabled prop (#9723)
Drill down an `enabled` prop to ThreadItemAnchorFollowButtonInner so
that the GrowthHack wrapper is always rendered on iOS, even when the
follow button shouldn't be displayed. The inner component returns null
when disabled, allowing GrowthHack to render its logo without children.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 16:48:33 -06:00
Eric Bailey 25a1ed1209 [APP-1782] Analytics migration (#9734)
* WIP

* Clean up growthbook code, integrate into init and sessions

* Move everything out of React

* Add metrics client

* Move to separate file

* Shared metadata cache

* Ensure we update metadata when session ID changes

* Ensure userMetadata is cleared when logging out

* WIP revamp

* Integrate feature gates into analytics context

* Clean up old code

* Fix useMeta util

* Some comments and cleanup

* Add logger to base analytics context

* Refactor current route handling

* Rip out LogEvent from navigation

* Update tracking endpoint

* Migrate toClout

* Clear out statsig client

* Add todo, reset logger readme

* Ope fix statsig noop

* Refactor logging in feed-feedback, add debug logging to metrics client

* Remove LogEvents alias for Metrics

* Prefer root package export

* Remove Metrics alias from logger

* [APP-1782] Migrate to new analytics APIs (#9735)

* Migrate logEvent to useAnalytics

* Migrate logger.metric to useAnalytics

* Migrate tricky spot, fix types

* Migrate remaining tricky spot

* Missed one

* Remove metric() from logger

* Migrate useGate to useAnalytics

* Remove all other StatSig mentions

* Update event payload

* Update logger tests

* Mock expo method

* Fix session ID bug

* Add session ID test

* Add test for metrics client

* Clarify intent

* Clean up core analytics file

* Clean up the call once utils

* Fix TODO

* Fix TODO

* Fix TODO

* Fix TODO

* Fix TODO

* Remove debug code

* Fix navigation context

* OK nav context is not working, todo

* Checkpoint: works but feels hacky

* Fix navigation context issue

* Improve feature API

* Improve metric logging

* Update logger tests
2026-01-22 15:33:45 -06:00
bnewbold 8614e5e105 bump copyright year to 2026 (#9732) 2026-01-20 08:50:07 +02:00
pfrazee 7da65efc08 Nightly source-language update 2026-01-20 02:49:52 +00:00
Eric Bailey 4f01d4cc81 Resolve same version of @types/estree (#9731) 2026-01-19 16:20:08 -06:00
Eric Bailey b4f57b94aa Fix Live Now reporting container collapse on Android (#9730) 2026-01-19 15:33:15 -06:00
Eric Bailey bd9e8244f1 [APP-1783] Clean up some feature gates and logging (#9729)
* Remove gates we don't need

* Clean up some logging

* Keep disable gates around for now

* Add missing gate

* Revert file

* Revert package changes
2026-01-19 15:32:02 -06:00
Eric Bailey 5989bf7fad Remove some unused things (#9719) 2026-01-19 10:48:13 -06:00
Samuel Newman a4f173d008 Bump version to 1.115.0 (#9726)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 20:03:45 +02:00
pfrazee f244afcd9c Nightly source-language update 2026-01-17 02:42:35 +00:00
Eric Bailey 62c0c06bbf Move browser env into main env (#9718) 2026-01-16 17:20:03 -06:00
Eric Bailey 0589bd7d9e Move /platform/detection vars into /env (#9707)
* Add platform vars to env

* Replace /platform/detection with /env
2026-01-16 16:28:17 -06:00
Samuel Newman bd510d8468 Upgrade ESLint to v9 with flat config (#9680)
* Upgrade ESLint to v9 with flat config

- Upgrade eslint from v8 to v9.18.0
- Migrate from .eslintrc.js to eslint.config.mjs (flat config)
- Upgrade typescript-eslint to v8.20.0 (unified package)
- Replace eslint-plugin-import with eslint-plugin-import-x for flat config support
- Add globals package for environment globals
- Update eslint-plugin-bsky-internal with proper meta objects for ESLint v9
- Fix deprecated context.getScope() API usage
- Update bskyembed to use flat config
- Remove deprecated --ext flag from lint scripts
- Configure rules to maintain previous behavior while using new ESLint version

* Fix varsIgnorePattern to require character after underscore

Restore the original pattern `^_.+` instead of `^_` so that lingui's
`const { _ } = useLingui()` will still be flagged when unused.

* Update ESLint rule tests for flat config format

- Update RuleTester to use flat config languageOptions instead of
  eslintrc parser format
- Remove duplicate test case that ESLint v9 now detects
- Add Jest globals for test files

* update eslint package versions

* lint android a11y

* enable typechecked rules, switch them to warn

* fix yarn lock ci

* Fix CI failure

* Remove unused globals?

* Organize a bit, add quiet to main lint command

* Allow ternary

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-01-16 16:24:28 -06:00
Samuel Newman acc26c965d make live now gate fail-open (#9714) 2026-01-16 08:34:00 -08:00
Samuel Newman fbd1138d97 Add fishing minigame (#9713) 2026-01-16 08:31:01 -08:00
Samuel Newman 9c563f2a48 Fix collapsed button by simplifying, keep container width (#9712) 2026-01-16 10:18:41 -06:00
Eric Bailey 1d4263078d Refetch live events every 5 min (#9706) 2026-01-15 16:26:44 -06:00
Eric Bailey 367eebe600 Reset default AA debug for E2E (#9705) 2026-01-15 13:05:11 -06:00
Eric Bailey 06a8a7efc2 intl pull and extract:all (#9704) 2026-01-15 10:58:01 -06:00
Samuel Newman 29f8bc998d Highlight first feed in sidebar if non selected (#9703) 2026-01-15 07:12:15 -08:00
Eric Bailey 4760e59ea4 Add back deleted css block (#9702) 2026-01-15 08:09:24 -06:00
Samuel Newman d9e37a222a scope last-selected-feed to per-account (#9500) 2026-01-15 06:07:09 -08:00
estrattonbailey 3b98f2f8ab Nightly source-language update 2026-01-15 03:06:32 +00:00
Samuel Newman ab85b509c0 Watermark posts in screenshots (#9637)
* watermark posts in screenshots

* disable when app is backgrounded

* fix alignment

* show watermark even when there isn't a button
2026-01-14 21:03:13 -06:00
Samuel Newman 65faee4f08 Unregister push token on signout (#8661)
* unregister push - WIP

* create agent and submit push token revokation

* mock unregisterpush

* fix import

* Add proxy headers

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2026-01-14 20:55:35 -06:00
pfrazee 94be661794 Nightly source-language update 2026-01-15 02:48:02 +00:00
Samuel Newman 10aa46aa9e Prevent flash of wrong theme on startup (web) (#9577)
* prevent flash of wrong theme on startup

* move bg color to #root

* Update and use existing system

* Darken slightly, better contrast

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2026-01-14 20:41:59 -06:00
Samuel Newman 8f82ad66df make button blue when tooltip is visible (#9668) 2026-01-14 20:34:59 -06:00
Samuel Newman 1cb362b596 Add loading="lazy" to expo-image on web (#9480)
* add `loading="lazy"` to expo-image

* add `loading="lazy"` to embed cards, avatars

* get rid of useless image wrapper indirection

* move image components to components dir

* fix imports

* fix import

* Keep avis eager

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2026-01-14 20:29:13 -06:00
Eric Bailey f960d2fbc6 fix: make logo show in qr code by absolutely positioning svg on top of it (#9373) (#9700)
* fix: make logo show in qr code by absolutely positioning svg on top of it

* fix: remove log and add explanation comment

* fix: only apply qrcode fix to web

Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com>
2026-01-14 20:28:48 -06:00
Samuel Newman 58ac9a3e7b Patch video player to add a ton of debug logging (#9657) 2026-01-14 19:18:29 -06:00
Eric Bailey bbab7695f9 Add some device location debug (#9693) 2026-01-14 19:05:33 -06:00
Samuel Newman 26aa7c0d9a Add cashtag support for stock ticker discussions (#9689)
* Add cashtag support for stock ticker discussions

Display cashtags ($TICKER format) as clickable links with dedicated menu actions and search feed, alongside hashtags. Includes composer highlighting and proper formatting.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* Fix cashtag search query to use # prefix

Cashtags need to be searched as "#$BTC" rather than just "$BTC" to work
with the search API.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update @atproto/api to 0.18.14 with cashtag support

The updated package includes native CASHTAG_REGEX detection for stock
ticker symbols like $AAPL and $BTC.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix todo, remove redundant validation

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2026-01-14 19:03:48 -06:00
estrattonbailey 498a09e797 Nightly source-language update 2026-01-14 22:59:53 +00:00
Eric Bailey 2223babc2b [APP-1759] Live Now Open Beta (#9699)
* Add report dialog to LiveStatus dialog

* Mr Worldwide™

* Bug fix bug fix

* Copy update

* Simplify parsing

* Bump api sdk

* update dev-env

* Update yarn.lock

* Bump api sdk

* Refactor useActorStatus, add disablement and appeal dialog

* Fix types

* Guard against chat profile views

* Go live (disabled)

* Fix types

* Update config and gates

* Add allowed services

* Merge conflicts

* Fix gradient handling for nudge

* Only show nudge on your own profile

* Fix live avi overflow breakage

* Add TODO

* Feedback

* Update nux image

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-01-14 16:57:21 -06:00
Eric Bailey 5922c6622f [APP-1767] Live event feeds (#9696)
* Update env config for new worker setup

* Add liveEvents context

* Add live event feed card to Explore

* Add Discover banner/trending handling, device storage

* Add prefs methods, hook up to Discover banner

* Add settings toggle

* Fix up metrics and mutations

* Simplify undo logic

* Handle previews

* Update overlay color and text color handling

* Update metrics

* Handle live event in feed card

* Show multiple feeds on Explore for bsky team

* Fix dev/preview handling bug

* Add sidebar cards, update types

* Fix type error

* Fix type error

* Feedback

* Blurhash fix

* Copy update

* Dev only debug hook
2026-01-14 15:09:01 -06:00
Alex Benzer e684c21572 Improved client events for feed interactions (#9695) 2026-01-14 12:53:24 -08:00
Samuel Newman 226b19ae43 Reduce startup hang by not querying fonts (#9670)
* query fonts lazily

* use `lazy var`
2026-01-14 12:22:26 -08:00
Samuel Newman cb6e65e313 pin AsyncStorage to v2.2.0 (#9656) 2026-01-14 11:16:49 -08:00
Samuel Newman af92610864 Add comprehensive CLAUDE.md development guide (#9666)
* Add comprehensive CLAUDE.md development guide

Document the codebase architecture, styling system (ALF), component patterns
(Dialog, Menu, Button), i18n with Lingui, state management with TanStack Query,
and navigation patterns to help Claude work effectively in this codebase.

* Add footguns section to CLAUDE.md

Document critical pitfalls including:
- Dialog close callback pattern (control.close(() => ...)) for avoiding
  race conditions with navigation, state updates, and opening other dialogs
- Controlled vs uncontrolled input guidance
- Platform-specific component behavior differences

* Add React Compiler note to footguns section

Document that useMemo/useCallback are unnecessary since React Compiler
handles memoization automatically. Only use them for specific cases like
effect dependencies or non-React library interop.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 11:16:34 -08:00
pfrazee ca905eb3e1 Nightly source-language update 2026-01-14 02:52:08 +00:00
Samuel Newman 61c919110e Expose QueryClient for TanStack Query DevTools browser extension (#9678)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 09:56:06 -08:00
Eric Bailey 9aa360d328 Do not render links if uri is invalid (#9663)
* Do not render links if uri is invalid

* Update comment
2026-01-13 09:37:00 -06:00
Alex Benzer 2d8de1ddc3 Standardize metadata for client events in feeds (#9653) 2026-01-13 02:42:26 -08:00
Samuel Newman c540dae4e7 Set up conductor.json with yarn install and web server configuration (#9676)
Add conductor setup and run scripts that install dependencies and start the dev server with a dynamically allocated port.

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-10 01:52:59 -08:00
pfrazee 2818c7f508 Nightly source-language update 2026-01-10 02:43:34 +00:00
Alex Benzer c4fd9980cc Cleaner sidebar layout (#9603) 2026-01-10 00:13:43 +02:00
Samuel Newman 966ae68dd2 Set Claude model to Opus (#9672) 2026-01-09 08:45:38 -08:00
Samuel Newman d2aea9161b Delete claude-code-review.yml (#9671) 2026-01-09 08:35:38 -08:00
Eric Bailey edb19dd6cf [APP-1750] Add the ability to report livestreams (#9654)
* Add report dialog to LiveStatus dialog

* Mr Worldwide™

* Bug fix bug fix

* Copy update

* Simplify parsing

* Bump api sdk

* update dev-env

* Update yarn.lock

* Bump api sdk

* Refactor useActorStatus, add disablement and appeal dialog

* Fix types

* Guard against chat profile views

* Go live (disabled)

* Fix types

* Status reports should only go to bsky

* Ah yeah let's not override types

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-01-09 10:19:47 -06:00
Samuel Newman 85a616eee0 Add Claude Code GitHub Workflow (#9667)
* "Claude PR Assistant workflow"

* "Claude Code Review workflow"
2026-01-09 07:46:02 -08:00
pfrazee 7285157ab5 Nightly source-language update 2026-01-09 02:47:50 +00:00
Eric Bailey b3f775d1d8 [AAv2] Improve minimum age handling (#9650)
* Improve miminum age handling

* Update api sdk

* Fix bad import
2026-01-08 15:36:26 -06:00
Eric Bailey a1857d62bd Handle AA config failure (#9660)
* Clarify some comments

* Add error state in case of config failure

* Add retry button and relayout to accommodate
2026-01-08 14:35:43 -06:00
Samuel Newman d82592f072 Fix full-screen back gesture over PagerView on iOS 26 (#9659)
Patches react-native-pager-view to handle iOS 26's
interactiveContentPopGestureRecognizer, using the same logic that
already exists for RNSPanGestureRecognizer: on the leftmost page,
disable the scrollview's pan gesture to let the back gesture through.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 11:36:01 -08:00
Samuel Newman dae03312c9 Bump version to v1.114 (#9661) 2026-01-08 11:35:42 -08:00
Samuel Newman c2e5a18e42 Unify build concurrency to one build per platform (#9655)
Configure concurrency groups so only one iOS build and one Android build
can run at a time across all workflows. This prevents manual builds from
conflicting with automatic builds triggered by fingerprint changes.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 07:35:13 -08:00
Samuel Newman 2efc047f64 bump version to v1.113.1 (#9652) 2026-01-07 13:15:55 -08:00
Samuel Newman 80049bbe82 exclude images from bundle size workflow (#9647) 2026-01-07 02:54:59 -08:00
Samuel Newman e627d37f42 [v1.113 prep] Translations (#9646)
* intl:pull

* yarn intl:extract:all
2026-01-07 01:14:48 -08:00
pfrazee 24fe45aaa1 Nightly source-language update 2026-01-07 02:48:01 +00:00
Samuel Newman 0deb9511af Enable lightbox for profile banner (#9643)
* allow pressing banner to view

* remove special type
2026-01-06 10:37:47 -08:00
Samuel Newman 43ebb80a5b patch in proguard rules (#9447) 2026-01-06 09:31:54 -08:00
Samuel Newman d91e206e21 Fix prefetching starter pack query (#9595)
* fix prefetching starter pack query

* add comment
2026-01-06 10:58:46 -06:00
Samuel Newman 493bd6dcf6 update crop tool, use new package name (#9642) 2026-01-06 18:29:37 +02:00
Samuel Newman 4f6ff6fc73 lazy load storybook (#9612) 2026-01-06 08:03:21 -08:00
estrattonbailey 7ba2997af5 Nightly source-language update 2026-01-06 00:18:33 +00:00
Samuel Newman 4b6fbcc050 fix suggested follow card size (#9638) 2026-01-05 09:36:54 -08:00
chuanchuan 55806f1087 fix(native): dismiss recommendation bottom sheet on profile navigation (#9608) 2026-01-02 02:02:26 -08:00
Samuel Newman 0aaf11aa7b fix drawer layout on mobile web (#9618) 2025-12-30 09:56:05 -08:00
Samuel Newman 9cfa7d0ba8 Remove RNGH from web bundle (#9528)
* remove RNGH on web

* add intentionally-failing webpack alias

* Update webpack.config.js
2025-12-30 03:40:19 -08:00
Samuel Newman 9743149c26 Replace graphemer with unicode-segmenter (#9526)
* replace graphemer with unicode-segmenter

* use grapheme entrypoint

* force resolution of unicode-segmenter
2025-12-30 03:39:09 -08:00
Samuel Newman 82877a096d Manually update source languages (#9611)
* manually update source langs

* add `intl:push-sources` script
2025-12-30 02:08:26 -08:00
Samuel Newman 12dc38c5fc replace lodash.isequal with fast-deep-equal (#9529) 2025-12-30 01:47:29 -08:00
Samuel Newman 7d5de60401 fix long handles in account switch (#8373) 2025-12-30 01:35:24 -08:00
Samuel Newman 8136258cd8 add pressed state to quote embed (#9592) 2025-12-30 01:31:09 -08:00
Samuel Newman afe5db8b57 clamp gif size (#9610) 2025-12-30 01:30:49 -08:00
Samuel Newman 893dd78a00 remove unused dep (#9606) 2025-12-30 01:23:48 -08:00
Eric Bailey b38013a12f Small copy tweaks (#9557) 2025-12-29 10:21:09 -08:00
Samuel Newman aa1395542f tweak styles of composer prompt (#9524) 2025-12-29 10:19:08 -08:00
Samuel Newman caa02db87b Update react native screens, remove patch (#9210)
* update react native screens

* bump rns

* see if nightly works

* update nightly

* yay it's out

* update react-nav

* update drawer layout and remove patch

* Update createNativeStackNavigatorWithAuth.tsx

* render drawer as "layout" component

* rm react-nav/drawer (unused)

* restore drawer layout patch
2025-12-29 10:16:49 -08:00
Alex Benzer f390167f66 Remove unwanted drop shadow on "see more" card on Android (#9584) 2025-12-29 02:00:29 -08:00
surfdude29 6275d2e0fd Temporarily use English for Esperanto intl-displaynames polyfill (#9574)
* use English for Esperanto `intl-displaynames` until bug is fixed

* Update src/locale/i18n.ts

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2025-12-29 01:38:40 -08:00
Alex Benzer 3e1e1ba00e Better copy for "must be signed in" message (#9602) 2025-12-29 01:38:04 -08:00
hailey 699fdbac01 update actions (#9586)
* update actions

* update actions

* $

* procenv
2025-12-22 19:21:17 -08:00
pfrazee a14a9be127 Nightly source-language update 2025-12-19 02:44:03 +00:00
Eric Bailey 998ee78e3d Move NUXDialogs into shell (#9573) 2025-12-18 12:08:17 -06:00
2275 changed files with 640790 additions and 262840 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 *)"
]
}
}
+15 -2
View File
@@ -28,11 +28,24 @@ EXPO_PUBLIC_CHAT_PROXY_DID=
#
#
# Bluesky's metrics API
EXPO_PUBLIC_METRICS_API_HOST=
# Growthbook config
EXPO_PUBLIC_GROWTHBOOK_API_HOST=
EXPO_PUBLIC_GROWTHBOOK_CLIENT_KEY=
# Sentry DSN for telemetry
EXPO_PUBLIC_SENTRY_DSN=
# Bitdrift API key. If undefined, Bitdrift will be disabled.
EXPO_PUBLIC_BITDRIFT_API_KEY=
# bapp-config web worker URL
BAPP_CONFIG_DEV_URL=
# geolocation web worker URL
GEOLOCATION_DEV_URL=
# live-events web worker URL
LIVE_EVENTS_DEV_URL=
# app-config web worker URL
APP_CONFIG_DEV_URL=
-126
View File
@@ -1,126 +0,0 @@
module.exports = {
root: true,
extends: [
'@react-native',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-native-a11y/ios',
'prettier',
],
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
'react',
'lingui',
'simple-import-sort',
'bsky-internal',
'eslint-plugin-react-compiler',
'import',
],
rules: {
'react/no-unescaped-entities': 0,
'react/prop-types': 0,
'react-native/no-inline-styles': 0,
'bsky-internal/avoid-unwrapped-text': [
'error',
{
impliedTextComponents: [
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'P',
'Admonition',
'Admonition.Admonition',
'Toast.Action',
'AgeAssuranceAdmonition',
'Span',
'StackedButton',
],
impliedTextProps: [],
suggestedTextWrappers: {
Button: 'ButtonText',
'ToggleButton.Button': 'ToggleButton.ButtonText',
'SegmentedControl.Item': 'SegmentedControl.ItemText',
},
},
],
'bsky-internal/use-exact-imports': 'error',
'bsky-internal/use-typed-gates': 'error',
'bsky-internal/use-prefixed-imports': 'error',
'simple-import-sort/imports': [
'error',
{
groups: [
// Side effect imports.
['^\\u0000'],
// Node.js builtins prefixed with `node:`.
['^node:'],
// Packages.
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
// React/React Native priortized, followed by expo
// Followed by all packages excluding unprefixed relative ones
[
'^(react\\/(.*)$)|^(react$)|^(react-native(.*)$)',
'^(expo(.*)$)|^(expo$)',
'^(?!(?:alf|components|lib|locale|logger|platform|screens|state|view)(?:$|\\/))@?\\w',
],
// Relative imports.
// Ideally, anything that starts with a dot or #
// due to unprefixed relative imports being used, we whitelist the relative paths we use
// (?:$|\\/) matches end of string or /
[
'^(?:#\\/)?(?:lib|state|logger|platform|locale)(?:$|\\/)',
'^(?:#\\/)?view(?:$|\\/)',
'^(?:#\\/)?screens(?:$|\\/)',
'^(?:#\\/)?alf(?:$|\\/)',
'^(?:#\\/)?components(?:$|\\/)',
'^#\\/',
'^\\.',
],
// anything else - hopefully we don't have any of these
['^'],
],
},
],
'simple-import-sort/exports': 'error',
'react-compiler/react-compiler': 'warn',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{argsIgnorePattern: '^_', varsIgnorePattern: '^_.+'},
],
'@typescript-eslint/consistent-type-imports': [
'warn',
{prefer: 'type-imports', fixStyle: 'inline-type-imports'},
],
'import/consistent-type-specifier-style': ['warn', 'prefer-inline'],
},
ignorePatterns: [
'**/__mocks__/*.ts',
'src/platform/polyfills.ts',
'src/third-party',
'ios',
'android',
'coverage',
'*.lock',
'.husky',
'patches',
'*.html',
'bskyweb',
'bskyembed',
'src/locale/locales/_build/',
'src/locale/locales/**/*.js',
'*.e2e.ts',
'*.e2e.tsx',
],
settings: {
componentWrapperFunctions: ['observer'],
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
},
}
+3 -2
View File
@@ -1,5 +1,5 @@
name: "Bug Report"
description: "Create a report for an issue you have experience in the app."
description: "Create a report for an issue you have experienced in the app."
labels: ["bug"]
body:
- type: markdown
@@ -19,13 +19,14 @@ body:
4. See error
validations:
required: true
- type: textarea
- type: upload
attributes:
label: Attachments
description: |
If possible, please provide any images or videos that may help us understand the issue you are experiencing.
validations:
required: false
accept: ".png,.jpg,.jpeg,.gif,.webp,.mp4,.mov,.webm"
- type: dropdown
attributes:
label: What platform(s) does this occur on?
+2 -1
View File
@@ -26,13 +26,14 @@ body:
4. See error
validations:
required: true
- type: textarea
- type: upload
attributes:
label: Attachments
description: |
If possible, please provide any images or videos that may help us understand the issue you are experiencing.
validations:
required: false
accept: ".png,.jpg,.jpeg,.gif,.webp,.mp4,.mov,.webm"
- type: dropdown
attributes:
label: What platform(s) does this occur on?
+2 -1
View File
@@ -15,7 +15,7 @@ body:
implement it in a timely manner.
validations:
required: true
- type: textarea
- type: upload
attributes:
label: Attachments
description: |
@@ -24,6 +24,7 @@ body:
in or is missing from.
validations:
required: false
accept: ".png,.jpg,.jpeg,.gif,.webp,.mp4,.mov,.webm"
- type: textarea
attributes:
label: Describe Alternatives
+15
View File
@@ -0,0 +1,15 @@
---
name: Compile translations
description: Compile i18n translations and fail on compilation errors.
runs:
using: composite
steps:
- name: 🔤 Compile translations
shell: bash
run: pnpm intl:build 2>&1 | tee i18n.log
- name: Check for i18n compilation errors
shell: bash
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
@@ -0,0 +1,72 @@
---
name: Local EAS Build
description: Build an Expo app locally with a selected EAS profile.
inputs:
platform:
description: EAS platform to build (ios or android)
required: true
profile:
description: EAS build profile
required: true
output:
description: Output path for the local build artifact
required: true
log-path:
description: Optional path to tee build output into
required: false
default: ""
bump-build-number:
description: Run the build through use-build-number-with-bump
required: false
default: "false"
sentry-auth-token:
description: Optional Sentry authentication token
required: false
default: ""
sentry-release:
description: Optional Sentry release
required: false
default: ""
sentry-dist:
description: Optional Sentry distribution
required: false
default: ""
runs:
using: composite
steps:
- name: Build locally with EAS
shell: bash
env:
PLATFORM: ${{ inputs.platform }}
PROFILE: ${{ inputs.profile }}
OUTPUT: ${{ inputs.output }}
LOG_PATH: ${{ inputs.log-path }}
BUMP_BUILD_NUMBER: ${{ inputs.bump-build-number }}
SENTRY_AUTH_TOKEN: ${{ inputs.sentry-auth-token }}
SENTRY_RELEASE: ${{ inputs.sentry-release }}
SENTRY_DIST: ${{ inputs.sentry-dist }}
run: |
set -o pipefail
build_command=(
pnpm eas build
--platform "$PLATFORM"
--profile "$PROFILE"
--local
--output "$OUTPUT"
--non-interactive
)
if [ -n "$LOG_PATH" ]; then
mkdir -p "$(dirname "$LOG_PATH")"
if [ "$BUMP_BUILD_NUMBER" = "true" ]; then
pnpm use-build-number-with-bump "${build_command[@]}" 2>&1 | tee "$LOG_PATH"
else
"${build_command[@]}" 2>&1 | tee "$LOG_PATH"
fi
elif [ "$BUMP_BUILD_NUMBER" = "true" ]; then
pnpm use-build-number-with-bump "${build_command[@]}"
else
"${build_command[@]}"
fi
+50
View File
@@ -0,0 +1,50 @@
---
name: Setup denis CLI
description: >
Download and verify the denis OTA publish binary from the (private)
bluesky-social/tango releases and put it on PATH. Uses a short-lived GitHub
App token scoped to contents:read on tango, since the default GITHUB_TOKEN
cannot read a private repo's releases.
inputs:
release-tag:
description: denis release tag in bluesky-social/tango to download
required: true
app-id:
description: GitHub App ID for the token used to read tango releases
required: true
private-key:
description: GitHub App private key
required: true
runs:
using: composite
steps:
- name: 🔑 Mint tango read token
id: tango-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ inputs.app-id }}
private-key: ${{ inputs.private-key }}
repositories: tango
permission-contents: read
- name: ⬇️ Download and verify denis binary
shell: bash
env:
GH_TOKEN: ${{ steps.tango-token.outputs.token }}
RELEASE_TAG: ${{ inputs.release-tag }}
run: |
BIN_DIR="$RUNNER_TEMP/denis-bin"
mkdir -p "$BIN_DIR"
cd "$BIN_DIR"
gh release download "$RELEASE_TAG" \
--repo bluesky-social/tango \
--pattern denis-linux-amd64 \
--pattern denis-linux-amd64.sha256 \
--clobber
# Verify before making it executable / putting it on PATH.
sha256sum -c denis-linux-amd64.sha256
mv denis-linux-amd64 denis
chmod +x denis
echo "$BIN_DIR" >> "$GITHUB_PATH"
@@ -0,0 +1,47 @@
---
name: Setup Expo Project
description: Install dependencies and set up the Expo/EAS CLI for a build. Does not check out the repo.
inputs:
expo-token:
description: Expo token (EXPO_TOKEN secret)
required: true
eas-version:
description: EAS CLI version to install
required: false
default: '19.0.5'
runs:
using: composite
steps:
- name: Check for EXPO_TOKEN
shell: bash
env:
EXPO_TOKEN: ${{ inputs.expo-token }}
run: >
if [ -z "$EXPO_TOKEN" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: package.json
cache: pnpm
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@4fcb5062d7ce9bc4382d1a352d19ba3ba2c317c1 # v4.0.1
- name: ⚙️ Install dependencies
shell: bash
run: pnpm install --frozen-lockfile
- name: 🔨 Setup Expo CLI
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
with:
eas-version: ${{ inputs.eas-version }}
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ inputs.expo-token }}
+63
View File
@@ -0,0 +1,63 @@
---
name: Write Environment Variables
description: Write the .env file and google-services.json used by the build.
inputs:
env-token:
description: Base .env contents (ENV_TOKEN secret)
required: true
sentry-dsn:
description: Sentry DSN (SENTRY_DSN secret)
required: true
bitdrift-api-key:
description: Bitdrift API key (BITDRIFT_API_KEY secret)
required: true
gcp-project-id:
description: GCP project ID (EXPO_PUBLIC_GCP_PROJECT_ID secret)
required: true
google-services-token:
description: google-services.json contents (GOOGLE_SERVICES_TOKEN secret)
required: true
expo-public-env:
description: >
EXPO_PUBLIC_ENV value. Only set for OTA deploys where eas.json isn't used;
for regular builds this is normally handled in eas.json.
required: false
default: ''
outputs:
release-version:
description: Version from package.json
value: ${{ steps.env.outputs.release-version }}
bundle-identifier:
description: git SHA of HEAD
value: ${{ steps.env.outputs.bundle-identifier }}
runs:
using: composite
steps:
- name: ✏️ Write environment variables
id: env
shell: bash
env:
ENV_TOKEN: ${{ inputs.env-token }}
SENTRY_DSN: ${{ inputs.sentry-dsn }}
BITDRIFT_API_KEY: ${{ inputs.bitdrift-api-key }}
GCP_PROJECT_ID: ${{ inputs.gcp-project-id }}
GOOGLE_SERVICES_TOKEN: ${{ inputs.google-services-token }}
EXPO_PUBLIC_ENV: ${{ inputs.expo-public-env }}
run: |
echo "$ENV_TOKEN" > .env
# EXPO_PUBLIC_ENV is normally handled in eas.json; only written here for OTA deploys.
if [ -n "$EXPO_PUBLIC_ENV" ]; then
echo "EXPO_PUBLIC_ENV=$EXPO_PUBLIC_ENV" >> .env
fi
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "release-version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "bundle-identifier=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=$SENTRY_DSN" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=$BITDRIFT_API_KEY" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=$GCP_PROJECT_ID" >> .env
echo "$GOOGLE_SERVICES_TOKEN" > google-services.json
+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.
+67
View File
@@ -0,0 +1,67 @@
version: 2
# Dependabot auto-update config.
#
# npm and GitHub Actions use a 7-day cooldown so newly published versions age
# before a PR opens. 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. Docker
# uses a shorter 3-day operational-freshness policy and does not support
# Dependabot security updates. 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]
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
day: monday
time: "09:00"
timezone: America/Los_Angeles
cooldown:
default-days: 3
open-pull-requests-limit: 5
groups:
docker-base-images:
group-by: dependency-name
patterns: ["*"]
ignore:
# Node 24 remains the active LTS line. Revisit Node 26 after it enters
# LTS in October 2026: https://github.com/bluesky-social/social-app/issues/11480
- dependency-name: node
update-types:
- version-update:semver-major
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set +e
platform="${1:?usage: cleanup-nightly-e2e.sh <ios|android> <device-id>}"
device_id="${2:-}"
artifact_dir="${GITHUB_WORKSPACE:-$PWD}/artifacts/$platform"
mkdir -p "$artifact_dir"
if [[ -f i18n.log ]]; then
cp i18n.log "$artifact_dir/i18n.log"
fi
stop_process_tree() {
local pid="$1"
local child
while read -r child; do
[[ -n "$child" ]] && stop_process_tree "$child"
done < <(pgrep -P "$pid" 2>/dev/null || true)
kill -TERM "$pid" >/dev/null 2>&1 || true
}
stop_pid_file() {
[[ -f "$1" ]] || return 0
local pid
pid="$(cat "$1")"
stop_process_tree "$pid"
}
stop_pid_file "$artifact_dir/logcat.pid"
stop_pid_file "$artifact_dir/metro.pid"
stop_pid_file "$artifact_dir/mock-server.pid"
stop_pid_file "$artifact_dir/emulator.pid"
if [[ "$platform" == "ios" ]]; then
if [[ -f "$artifact_dir/redis-bin.txt" ]]; then
"$(cat "$artifact_dir/redis-bin.txt")/redis-cli" \
-h 127.0.0.1 -p 6380 shutdown nosave >/dev/null 2>&1 || true
fi
if [[ -f "$artifact_dir/postgres-bin.txt" ]]; then
"$(cat "$artifact_dir/postgres-bin.txt")/pg_ctl" \
-D "${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres" -m fast stop >/dev/null 2>&1 || true
fi
[[ -n "$device_id" ]] && xcrun simctl shutdown "$device_id" >/dev/null 2>&1 || true
else
docker compose -f dev-env/dev-infra/docker-compose.yaml logs --no-color \
>>"$artifact_dir/docker-services.log" 2>&1 || true
docker compose -f dev-env/dev-infra/docker-compose.yaml down --volumes --remove-orphans >/dev/null 2>&1 || true
[[ -n "$device_id" ]] && adb -s "$device_id" emu kill >/dev/null 2>&1 || true
fi
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env bash
set -Eeuo pipefail
platform="${1:?usage: run-nightly-e2e.sh <ios|android> <device-id>}"
device_id="${2:?usage: run-nightly-e2e.sh <ios|android> <device-id>}"
if [[ "$platform" != "ios" && "$platform" != "android" ]]; then
echo "Unsupported platform: $platform" >&2
exit 2
fi
artifact_dir="${GITHUB_WORKSPACE:-$PWD}/artifacts/$platform"
maestro_dir="$artifact_dir/maestro"
mkdir -p "$maestro_dir"
phase() {
printf '%s\n' "$1" >"$artifact_dir/phase.txt"
}
wait_for_port() {
local port="$1"
local label="$2"
local attempts="${3:-120}"
for ((i = 1; i <= attempts; i++)); do
if nc -z 127.0.0.1 "$port" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
echo "Timed out waiting for $label on port $port" >&2
return 1
}
# shellcheck disable=SC2329 # Invoked through the cleanup trap call chain.
stop_process_tree() {
local pid="$1"
local child
while read -r child; do
[[ -n "$child" ]] && stop_process_tree "$child"
done < <(pgrep -P "$pid" 2>/dev/null || true)
kill -TERM "$pid" >/dev/null 2>&1 || true
}
# shellcheck disable=SC2329 # Invoked by cleanup, which is registered as a trap.
stop_pid_file() {
local pid_file="$1"
[[ -f "$pid_file" ]] || return 0
local pid
pid="$(cat "$pid_file")"
[[ -n "$pid" ]] || return 0
# pnpm and Expo both spawn multiple generations of children.
stop_process_tree "$pid"
}
# shellcheck disable=SC2329 # Invoked by the EXIT/INT/TERM trap below.
cleanup() {
set +e
stop_pid_file "$artifact_dir/logcat.pid"
stop_pid_file "$artifact_dir/metro.pid"
stop_pid_file "$artifact_dir/mock-server.pid"
if [[ "$platform" == "ios" ]]; then
if [[ -f "$artifact_dir/redis.pid" ]]; then
redis_bin="$(cat "$artifact_dir/redis-bin.txt")"
"$redis_bin/redis-cli" -h 127.0.0.1 -p 6380 shutdown nosave >/dev/null 2>&1 || true
fi
if [[ -f "$artifact_dir/postgres-bin.txt" && -d "${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres" ]]; then
postgres_bin="$(cat "$artifact_dir/postgres-bin.txt")"
"$postgres_bin/pg_ctl" -D "${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres" -m fast stop >/dev/null 2>&1 || true
fi
else
docker compose -f dev-env/dev-infra/docker-compose.yaml logs --no-color \
>>"$artifact_dir/docker-services.log" 2>&1 || true
docker compose -f dev-env/dev-infra/docker-compose.yaml down --volumes --remove-orphans >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT INT TERM
if [[ "$platform" == "android" ]]; then
adb -s "$device_id" logcat -c
adb -s "$device_id" logcat -v threadtime >"$artifact_dir/logcat.log" 2>&1 &
printf '%s\n' "$!" >"$artifact_dir/logcat.pid"
fi
phase "Starting PostgreSQL, Redis, and mock server"
if [[ "$platform" == "ios" ]]; then
brew install postgresql@14 2>&1 | tee "$artifact_dir/native-dependencies.log"
postgres_bin="$(brew --prefix postgresql@14)/bin"
redis_version="7.4.7"
redis_archive="${RUNNER_TEMP:-/tmp}/redis-${redis_version}.tar.gz"
redis_source="${RUNNER_TEMP:-/tmp}/redis-${redis_version}"
curl -fsSL -o "$redis_archive" \
"https://download.redis.io/releases/redis-${redis_version}.tar.gz"
echo "c97e57b0df330a9e091cacff012bebe763c275398cf36ff44cdba876814b595b $redis_archive" \
| shasum -a 256 --check | tee -a "$artifact_dir/native-dependencies.log"
rm -rf "$redis_source"
tar -xzf "$redis_archive" -C "${RUNNER_TEMP:-/tmp}"
make -C "$redis_source" -j "$(sysctl -n hw.ncpu)" \
2>&1 | tee -a "$artifact_dir/native-dependencies.log"
redis_bin="$redis_source/src"
"$redis_bin/redis-server" --version | tee -a "$artifact_dir/native-dependencies.log"
printf '%s\n' "$redis_bin" >"$artifact_dir/redis-bin.txt"
postgres_data="${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres"
rm -rf "$postgres_data"
"$postgres_bin/initdb" -D "$postgres_data" --auth=trust --username=pg --no-locale \
>"$artifact_dir/postgres-init.log" 2>&1
"$postgres_bin/pg_ctl" -D "$postgres_data" \
-o "-p 5433 -h 127.0.0.1" -l "$artifact_dir/postgres.log" start
printf '%s\n' "$postgres_bin" >"$artifact_dir/postgres-bin.txt"
"$redis_bin/redis-server" \
--bind 127.0.0.1 \
--port 6380 \
--save "" \
--appendonly no \
--daemonize yes \
--pidfile "$artifact_dir/redis.pid" \
--logfile "$artifact_dir/redis.log"
wait_for_port 5433 "PostgreSQL"
wait_for_port 6380 "Redis"
pnpm --dir dev-env start:external >"$artifact_dir/mock-server.log" 2>&1 &
else
pnpm --dir dev-env start >"$artifact_dir/mock-server.log" 2>&1 &
fi
printf '%s\n' "$!" >"$artifact_dir/mock-server.pid"
wait_for_port 1986 "the E2E mock-server manager"
phase "Starting Metro"
EXPO_PUBLIC_ENV=e2e \
NODE_ENV=development \
RN_SRC_EXT=e2e.ts,e2e.tsx \
pnpm exec expo start --dev-client --clear --port 8081 \
>"$artifact_dir/metro.log" 2>&1 &
printf '%s\n' "$!" >"$artifact_dir/metro.pid"
wait_for_port 8081 "Metro"
# Pre-warm Metro bundle so the first Maestro flow doesn't hit a cold-start delay
phase "Pre-warming Metro bundle"
bundle_platform="$platform"
curl -s -o /dev/null "http://localhost:8081/index.bundle?platform=${bundle_platform}&dev=true&minify=false"
echo "Metro bundle pre-warmed for $bundle_platform"
if [[ "$platform" == "android" ]]; then
phase "Configuring Android localhost routing"
adb -s "$device_id" reverse tcp:3000 tcp:3000
adb -s "$device_id" reverse tcp:8081 tcp:8081
fi
phase "Running Maestro flows"
set +e
maestro test \
--udid "$device_id" \
--format JUNIT \
--output "$artifact_dir/report.xml" \
--config __e2e__/config.yml \
--debug-output "$maestro_dir" \
--test-output-dir "$maestro_dir" \
--flatten-debug-output \
__e2e__ \
2>&1 | tee "$artifact_dir/maestro-cli.log"
maestro_status=${PIPESTATUS[0]}
set -e
if [[ "$maestro_status" -eq 0 ]]; then
phase "Completed"
else
phase "Maestro flow failure"
fi
exit "$maestro_status"
+356
View File
@@ -0,0 +1,356 @@
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
const ENTITY_REPLACEMENTS = {
'&amp;': '&',
'&apos;': "'",
'&gt;': '>',
'&lt;': '<',
'&quot;': '"',
}
function decodeXml(value = '') {
return value
.replace(/&(amp|apos|gt|lt|quot);/g, entity => ENTITY_REPLACEMENTS[entity])
.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
.replace(/&#x([\da-f]+);/gi, (_, code) =>
String.fromCodePoint(Number.parseInt(code, 16)),
)
}
function attributes(source = '') {
const result = {}
for (const match of source.matchAll(/([\w:.-]+)\s*=\s*(["'])(.*?)\2/gs)) {
result[match[1]] = decodeXml(match[3])
}
return result
}
function concise(value, limit = 300) {
const normalized = decodeXml(value)
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim()
return normalized.length > limit
? `${normalized.slice(0, limit - 1)}`
: normalized
}
export function parseJUnit(xml) {
const failures = []
const testcasePattern = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/gi
for (const match of xml.matchAll(testcasePattern)) {
const testcase = attributes(match[1])
const body = match[2] || ''
const failure = body.match(/<(failure|error)\b([^>]*)>([\s\S]*?)<\/\1>/i)
const selfClosingFailure = body.match(/<(failure|error)\b([^>]*)\/>/i)
const failureMatch = failure || selfClosingFailure
if (!failureMatch) continue
const failureAttributes = attributes(failureMatch[2])
const name = testcase.name || testcase.classname || 'Unnamed Maestro flow'
const message = concise(
failureAttributes.message || (failure ? failureMatch[3] : '') || 'Failed',
)
failures.push({name, message})
}
if (failures.length === 0) {
const suite = xml.match(/<testsuite\b([^>]*)>/i)
const suiteAttributes = attributes(suite?.[1])
if (
Number(suiteAttributes.failures || 0) +
Number(suiteAttributes.errors || 0) >
0
) {
failures.push({
name: suiteAttributes.name || 'Maestro test suite',
message: 'JUnit reported a failure without testcase details',
})
}
}
return failures
}
export function parseMaestroCli(log) {
const failures = []
const failurePattern = /^\[Failed\]\s+(.+?)\s+\([^)]*\)\s+\((.+)\)\s*$/gm
for (const match of log.matchAll(failurePattern)) {
failures.push({
name: concise(match[1], 120),
message: concise(match[2]),
})
}
return failures
}
function walk(root) {
if (!root || !fs.existsSync(root)) return []
const entries = fs.readdirSync(root, {withFileTypes: true})
return entries.flatMap(entry => {
const candidate = path.join(root, entry.name)
return entry.isDirectory() ? walk(candidate) : [candidate]
})
}
function readPhase(root) {
const phaseFile = walk(root).find(file => path.basename(file) === 'phase.txt')
return phaseFile ? fs.readFileSync(phaseFile, 'utf8').trim() : ''
}
function platformResult({name, status, root, artifactUrl}) {
const files = walk(root)
const reports = files.filter(file => /(?:report|junit).*\.xml$/i.test(file))
const junitFailures = reports.flatMap(report =>
parseJUnit(fs.readFileSync(report, 'utf8')),
)
const maestroLogs = files.filter(
file => path.basename(file) === 'maestro-cli.log',
)
const cliFailures = maestroLogs.flatMap(log =>
parseMaestroCli(fs.readFileSync(log, 'utf8')),
)
// A cancelled or timed-out Maestro run may never flush JUnit. Its CLI log is
// streamed continuously, so use those failure lines when JUnit has no detail.
const failures = junitFailures.length > 0 ? junitFailures : cliFailures
// A skipped platform (e.g. iOS while temporarily disabled) is not a failure
// as long as it produced no flow failures.
const failed =
(status !== 'success' && status !== 'skipped') || failures.length > 0
return {
name,
status,
failed,
failures,
phase: readPhase(root),
hasJUnit: reports.length > 0,
artifactUrl,
}
}
function statusEmoji(status) {
if (status === 'success') return ':white_check_mark:'
if (status === 'skipped') return ':fast_forward:'
return ':x:'
}
function slackEscape(value) {
return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
}
function platformBlock(platform) {
const lines = [
`${statusEmoji(platform.status)} *${platform.name}* — job status: \`${platform.status}\``,
]
if (platform.failures.length > 0) {
for (const failure of platform.failures.slice(0, 8)) {
lines.push(
`• *${slackEscape(failure.name)}:* ${slackEscape(failure.message)}`,
)
}
if (platform.failures.length > 8) {
lines.push(`• …and ${platform.failures.length - 8} more failed flows`)
}
} else if (platform.failed && !platform.hasJUnit) {
lines.push(
`• *Setup phase:* ${slackEscape(platform.phase || 'No phase metadata was captured')}`,
)
} else if (platform.failed) {
lines.push(
`• Job failed after JUnit was written; latest phase: ${slackEscape(platform.phase || 'unknown')}`,
)
}
if (platform.artifactUrl) {
lines.push(
`• <${platform.artifactUrl}|Open ${platform.name} logs and artifacts>`,
)
}
return lines.join('\n').slice(0, 3000)
}
function githubSummary({notify, platforms, shortSha, runUrl, commitUrl}) {
const lines = [
`# Nightly Maestro E2E ${notify ? 'failed' : 'passed'}`,
'',
`- Commit: [\`${shortSha}\`](${commitUrl})`,
`- Workflow run: [open run](${runUrl})`,
'',
]
for (const platform of platforms) {
const headerEmoji =
platform.status === 'success'
? '✅'
: platform.status === 'skipped'
? '⏭️'
: '❌'
lines.push(
`## ${headerEmoji} ${platform.name}`,
'',
`Job status: \`${platform.status}\``,
'',
)
if (platform.failures.length > 0) {
for (const failure of platform.failures.slice(0, 10)) {
lines.push(`- **${failure.name}:** ${failure.message}`)
}
if (platform.failures.length > 10) {
lines.push(`- …and ${platform.failures.length - 10} more failed flows`)
}
lines.push('')
} else if (platform.failed && !platform.hasJUnit) {
lines.push(
`- Setup phase: ${platform.phase || 'No phase metadata was captured'}`,
'',
)
} else if (platform.failed) {
lines.push(
`- The job failed after JUnit was written (latest phase: ${platform.phase || 'unknown'})`,
'',
)
}
if (platform.artifactUrl) {
lines.push(
`- [${platform.name} logs and artifacts](${platform.artifactUrl})`,
'',
)
}
}
return lines.join('\n').trim()
}
export function buildSummary({
iosStatus,
androidStatus,
iosRoot,
androidRoot,
artifactUrls = {},
sha,
runUrl,
commitUrl,
}) {
const platforms = [
platformResult({
name: 'iOS',
status: iosStatus,
root: iosRoot,
artifactUrl: artifactUrls.ios,
}),
platformResult({
name: 'Android',
status: androidStatus,
root: androidRoot,
artifactUrl: artifactUrls.android,
}),
]
const notify = platforms.some(platform => platform.failed)
const shortSha = sha.slice(0, 12)
const lines = [
':rotating_light: *Nightly Maestro E2E failed*',
`*Commit:* <${commitUrl}|\`${shortSha}\`>`,
`*Workflow run:* <${runUrl}|open run>`,
'',
]
for (const platform of platforms) {
lines.push(
`${statusEmoji(platform.status)} *${platform.name}* — job status: \`${platform.status}\``,
)
if (platform.failures.length > 0) {
for (const failure of platform.failures.slice(0, 10)) {
lines.push(
`• *${slackEscape(failure.name)}:* ${slackEscape(failure.message)}`,
)
}
if (platform.failures.length > 10) {
lines.push(`• …and ${platform.failures.length - 10} more failed flows`)
}
} else if (platform.failed && !platform.hasJUnit) {
lines.push(
`• Setup phase: ${platform.phase || 'No phase metadata was captured'}`,
)
} else if (platform.failed) {
lines.push(
`• The job failed after JUnit was written (latest phase: ${platform.phase || 'unknown'})`,
)
}
if (platform.artifactUrl) {
lines.push(
`• <${platform.artifactUrl}|${platform.name} logs and artifacts>`,
)
}
lines.push('')
}
const text = lines.join('\n').trim()
const blocks = [
{
type: 'header',
text: {type: 'plain_text', text: 'Nightly Maestro E2E failed'},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Commit:* <${commitUrl}|\`${shortSha}\`>\n*Workflow run:* <${runUrl}|open run>`,
},
},
{type: 'divider'},
...platforms.flatMap((platform, index) => [
{type: 'section', text: {type: 'mrkdwn', text: platformBlock(platform)}},
...(index < platforms.length - 1 ? [{type: 'divider'}] : []),
]),
]
return {
notify,
platforms,
githubSummary: githubSummary({
notify,
platforms,
shortSha,
runUrl,
commitUrl,
}),
payload: {text, blocks},
}
}
function parseArgs(argv) {
const result = {}
for (let i = 0; i < argv.length; i += 2) {
const key = argv[i]
if (!key?.startsWith('--') || argv[i + 1] === undefined) {
throw new Error(`Invalid argument: ${key || '<missing>'}`)
}
result[key.slice(2)] = argv[i + 1]
}
return result
}
if (
process.argv[1] &&
path.resolve(process.argv[1]) === path.resolve(import.meta.filename)
) {
const args = parseArgs(process.argv.slice(2))
const artifactUrls = args['artifact-urls']
? JSON.parse(fs.readFileSync(args['artifact-urls'], 'utf8'))
: {}
const summary = buildSummary({
iosStatus: args['ios-status'],
androidStatus: args['android-status'],
iosRoot: args['ios-root'],
androidRoot: args['android-root'],
artifactUrls,
sha: args.sha,
runUrl: args['run-url'],
commitUrl: args['commit-url'],
})
process.stdout.write(`${JSON.stringify(summary)}\n`)
}
+41
View File
@@ -0,0 +1,41 @@
name: Asset notices
# Verifies that every path named in ASSETS.md and NOTICE.md still exists.
#
# Those files tell forkers which assets our MIT license does not cover, and carry the third-party
# attribution notices we are required to pass along. If an asset moves and the notice is not
# updated, the notice silently stops meaning anything. This job makes that visible in review
# rather than a year later.
#
# No paths filter: the notices also reference files under src/ (e.g. the inline logo components),
# so any rename anywhere in the tree can rot a notice. The check runs in under a second.
on:
push:
branches: [main]
pull_request:
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: true
permissions:
contents: read
jobs:
check:
name: Check asset licensing notices
runs-on: ubuntu-latest
steps:
- name: ⬇️ Check out Git repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: ⚙️ Set up Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
- name: 📜 Verify asset notices
run: node scripts/check-asset-notices.mjs
@@ -21,37 +21,37 @@ jobs:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: ⬇️ Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
- name: 🔧 Setup Docker buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
password: ${{ env.PASSWORD }}
- name: Extract Docker metadata
- name: 🏷️ Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Env
- name: 📝 Env
id: env
run: |
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Build and push Docker image
- name: 🚀 Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
@@ -1,5 +1,12 @@
name: build-and-push-bskyweb-ghcr
on:
pull_request:
paths:
- Dockerfile
- Dockerfile.bskylink
- Dockerfile.bskyogcard
- Dockerfile.embedr
- .github/workflows/build-and-push-bskyweb-ghcr.yaml
push:
branches:
- main
@@ -13,8 +20,40 @@ env:
IMAGE_NAME: ${{ github.repository }}
jobs:
verify-containers:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- image: bskyweb
file: Dockerfile
- image: bskylink
file: Dockerfile.bskylink
- image: bskyogcard
file: Dockerfile.bskyogcard
- image: embedr
file: Dockerfile.embedr
steps:
- name: ⬇️ Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: 🔧 Setup Docker buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Build ${{ matrix.image }}
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: ${{ matrix.file }}
platforms: linux/amd64
push: false
cache-from: type=gha,scope=${{ matrix.image }}
cache-to: type=gha,mode=max,scope=${{ matrix.image }}
bskyweb-container-ghcr:
if: github.repository == 'bluesky-social/social-app'
if: github.event_name == 'push' && github.repository == 'bluesky-social/social-app'
runs-on: ubuntu-latest
permissions:
contents: read
@@ -22,31 +61,31 @@ jobs:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: ⬇️ Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
- name: 🔧 Setup Docker buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME }}
password: ${{ env.PASSWORD }}
- name: Extract Docker metadata
- name: 🏷️ Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,enable=true,priority=100,prefix=bskyweb:,suffix=,format=long
- name: Build and push Docker image
- name: 🚀 Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
@@ -21,31 +21,31 @@ jobs:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: ⬇️ Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
- name: 🔧 Setup Docker buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
password: ${{ env.PASSWORD }}
- name: Extract Docker metadata
- name: 🏷️ Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Build and push Docker image
- name: 🚀 Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
+13 -15
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 }}
@@ -23,31 +21,31 @@ jobs:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: ⬇️ Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
- name: 🔧 Setup Docker buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
password: ${{ env.PASSWORD }}
- name: Extract Docker metadata
- name: 🏷️ Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Build and push Docker image
- name: 🚀 Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.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 }}
@@ -23,31 +21,31 @@ jobs:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: ⬇️ Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
- name: 🔧 Setup Docker buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
password: ${{ env.PASSWORD }}
- name: Extract Docker metadata
- name: 🏷️ Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Build and push Docker image
- name: 🚀 Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: true
+258 -138
View File
@@ -10,188 +10,308 @@ on:
options:
- testflight-android
- production
submit:
type: boolean
description: Submit the build to Google Play (disable to only produce the APK artifact)
default: true
workflow_call:
inputs:
profile:
type: string
description: Build profile to use
required: true
submit:
type: boolean
description: Submit the build to Google Play (disable to only produce the APK artifact)
default: true
runner:
type: string
description: Runner for the build job (defaults to Linux-x64-32core)
required: false
default: ''
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 }}
secrets:
EXPO_TOKEN:
required: true
ENV_TOKEN:
required: true
SENTRY_DSN:
required: true
BITDRIFT_API_KEY:
required: true
EXPO_PUBLIC_GCP_PROJECT_ID:
required: true
GOOGLE_SERVICES_TOKEN:
required: true
SENTRY_AUTH_TOKEN:
required: true
SLACK_CLIENT_ALERT_WEBHOOK:
required: true
ANDROID_KEYSTORE_BASE64:
required: true
ANDROID_KEYSTORE_PASSWORD:
required: true
ANDROID_KEY_ALIAS:
required: true
ANDROID_KEY_PASSWORD:
required: true
# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code
permissions:
contents: read
jobs:
build:
if: github.repository == 'bluesky-social/social-app'
name: Build and Submit Android
runs-on: Linux-x64-32core
name: Build Android
runs-on: ${{ inputs.runner || 'Linux-x64-32core' }}
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 }}
steps:
- name: Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 5
- name: 🔧 Setup Node
uses: actions/setup-node@v6
- name: 🔧 Setup Expo project
uses: ./.github/actions/setup-expo-project
with:
node-version-file: .nvmrc
cache: yarn
expo-token: ${{ secrets.EXPO_TOKEN }}
- 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: actions/setup-java@v4
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.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
- 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
uses: ./.github/actions/compile-i18n
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
- name: ✏️ Write environment variables
id: env
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "$json" > google-services.json
uses: ./.github/actions/write-env
with:
env-token: ${{ secrets.ENV_TOKEN }}
sentry-dsn: ${{ secrets.SENTRY_DSN }}
bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }}
gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}
google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }}
- name: 🏗️ EAS Build
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 ${{ inputs.profile || 'testflight-android' }}
--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
uses: ./.github/actions/eas-local-build
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
platform: android
profile: ${{ inputs.profile || 'testflight-android' }}
output: build.aab
bump-build-number: "true"
sentry-auth-token: ${{ secrets.SENTRY_AUTH_TOKEN }}
sentry-release: ${{ steps.env.outputs.release-version }}
sentry-dist: ${{ steps.env.outputs.bundle-identifier }}
- 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
# Hands the built bundle off to the submit / universalApk jobs. Retention is
# deliberately short (1 day) since it's only an intra-run handoff artifact.
- name: 🚀 Upload AAB artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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 }}```"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
name: android-aab-${{ github.run_id }}
retention-days: 1
if-no-files-found: error
path: build.aab
- name: 🔔 Notify Slack of Testflight Build
if: ${{ inputs.profile != 'production' }}
uses: slackapi/slack-github-action@v1.25.0
- name: 📝 Write build summary
env:
REMOTE_VERSION_CODE: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}
run: |
{
echo "### Android build number"
echo
echo "\`$REMOTE_VERSION_CODE\`"
} >> "$GITHUB_STEP_SUMMARY"
submit:
name: Submit to Google Play
runs-on: ubuntu-latest
needs: [build]
# Submit unless explicitly disabled; on events where inputs is empty this still submits.
if: ${{ inputs.submit != false }}
steps:
# eas submit reads app config from the repo, so we need a checkout.
- name: ⬇️ Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
payload: |
{
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact-testflight.outputs.artifact-url }}"
}
fetch-depth: 5
- name: 🔧 Setup Expo project
uses: ./.github/actions/setup-expo-project
with:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: ⬇️ Download AAB artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: android-aab-${{ github.run_id }}
- 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: 🧹 Clear Metro cache
- name: 🔔 Notify Slack of Play Store Submission
if: ${{ inputs.profile == 'production' }}
# https://github.com/expo/eas-cli/issues/2959#issuecomment-2749791326
run: rm -rf ${TMPDIR:-/tmp}/metro-cache
uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{"text": "Android ${{ inputs.profile || 'testflight-android' }} build submitted to Google Play!\n```Version Number: ${{ needs.build.outputs.package-version }}\nBuild Number: ${{ needs.build.outputs.version-code }}```"}
- 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
# Runs in parallel with submit: the QA APK shouldn't be blocked by a Play submission failure.
universalApk:
name: Build universal APK
runs-on: ubuntu-latest
needs: [build]
outputs:
apk-artifact-name: build-${{ steps.timestamp.outputs.time }}.apk
steps:
- name: ⬇️ Download AAB artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: android-aab-${{ github.run_id }}
- name: 🚀 Upload Production APK Artifact
id: upload-artifact-production-apk
if: ${{ inputs.profile == 'production' }}
uses: actions/upload-artifact@v4
# bundletool needs a JRE. ubuntu-latest ships a default JDK, but pin it explicitly
# like the build job so the toolchain is deterministic.
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: "temurin"
java-version: "17"
- name: 🔧 Setup bundletool
uses: amyu/setup-bundletool@cc2e1857284660bd625e43f2c8a45626f034302f # v1.1
with:
version: "1.18.3"
- name: 🔑 Decode keystore
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > keystore.jks
- name: 📦 Build signed universal APK
env:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
bundletool build-apks \
--bundle=build.aab \
--output=universal.apks \
--mode=universal \
--ks=keystore.jks \
--ks-pass=pass:"$ANDROID_KEYSTORE_PASSWORD" \
--ks-key-alias="$ANDROID_KEY_ALIAS" \
--key-pass=pass:"$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@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
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 }}```"
}
{"text": "Android ${{ inputs.profile || 'testflight-android' }} APK is ready for testing!\n```Artifact: ${{ steps.upload-artifact.outputs.artifact-url }}\nVersion Number: ${{ needs.build.outputs.package-version }}\nBuild Number: ${{ needs.build.outputs.version-code }}```"}
# 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, universalApk]
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:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
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: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
if: ${{ inputs.profile == 'testflight-android' }}
- name: ⬇️ Download APK artifact
if: ${{ steps.release-check.outputs.exists == 'true' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
name: ${{ needs.universalApk.outputs.apk-artifact-name }}
- name: Write commit hash to cache
if: ${{ inputs.profile == 'testflight-android' }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
- 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' }}
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
APK: Bluesky-${{ needs.build.outputs.package-version }}.apk
run: |
gh release upload "$TAG" "$APK" --clobber
url=$(gh release view "$TAG" --json url --jq .url)
echo "url=$url" >> "$GITHUB_OUTPUT"
- name: 🔔 Notify Slack of Release Attachment
if: ${{ steps.release-check.outputs.exists == 'true' }}
uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
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 }}```"}
+265 -92
View File
@@ -10,142 +10,315 @@ on:
options:
- testflight
- production
testFlightGroup:
type: choice
description: TestFlight group to assign the build to after submitting
options:
- none
- QA Team
- Software Mansion
default: none
changelog:
type: string
description: TestFlight "What to Test" notes (only applied when a group is selected)
required: false
default: ''
workflow_call:
inputs:
profile:
type: string
description: Build profile to use
required: true
testFlightGroup:
type: string
description: TestFlight group to assign the build to after submitting ("none" to skip)
default: none
runner:
type: string
description: Runner for the build job (defaults to macos-26-xlarge)
required: false
default: ''
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 }}
secrets:
EXPO_TOKEN:
required: true
ENV_TOKEN:
required: true
SENTRY_DSN:
required: true
BITDRIFT_API_KEY:
required: true
EXPO_PUBLIC_GCP_PROJECT_ID:
required: true
GOOGLE_SERVICES_TOKEN:
required: true
SENTRY_AUTH_TOKEN:
required: true
ASC_KEY_ID:
required: true
ASC_ISSUER_ID:
required: true
ASC_KEY_P8_BASE64:
required: true
SLACK_CLIENT_ALERT_WEBHOOK:
required: true
# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code
permissions:
contents: read
jobs:
build:
if: github.repository == 'bluesky-social/social-app'
name: Build and Submit iOS
runs-on: macos-26-xlarge
name: Build iOS
runs-on: ${{ inputs.runner || 'macos-26-xlarge' }}
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: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 5
- name: 🔧 Setup Node
uses: actions/setup-node@v6
- name: 🔧 Setup Expo project
uses: ./.github/actions/setup-expo-project
with:
node-version-file: .nvmrc
cache: yarn
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
- name: 🔨 Setup EAS
uses: expo/expo-github-action@main
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
expo-version: latest
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
xcode-version: "26.4"
- name: Setup EAS local builds
run: yarn global add eas-cli-local-build-plugin
- name: ⚙️ Install dependencies
run: yarn install --frozen-lockfile
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.0"
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
with:
version: 1.16.2
- name: Assert Cocoapods version
run: |
EXPECTED=1.17.0
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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
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
- 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
uses: ./.github/actions/compile-i18n
# EXPO_PUBLIC_ENV is handled in eas.json
- name: ✏️ Write environment variables
id: env
run: |
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
uses: ./.github/actions/write-env
with:
env-token: ${{ secrets.ENV_TOKEN }}
sentry-dsn: ${{ secrets.SENTRY_DSN }}
bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }}
gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}
google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }}
- name: 🏗️ EAS Build
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 ios
--profile ${{ inputs.profile || 'testflight' }}
--local --output build.tar.gz --non-interactive
uses: ./.github/actions/eas-local-build
with:
platform: ios
profile: ${{ inputs.profile || 'testflight' }}
output: build.tar.gz
bump-build-number: "true"
sentry-auth-token: ${{ secrets.SENTRY_AUTH_TOKEN }}
sentry-release: ${{ steps.env.outputs.release-version }}
sentry-dist: ${{ steps.env.outputs.bundle-identifier }}
- name: 📂 Extract build artifact
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
- 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
- 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.
# PlistBuddy is macOS-only, which is why this stays in the build job.
- 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"
# Hand the IPA and dSYM off to the submit job. Retention is deliberately short since
# this artifact only exists to bridge the two jobs within a single run.
- name: 🚀 Upload build artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ios-build-${{ github.run_id }}
retention-days: 1
if-no-files-found: error
path: |
${{ env.BUILD_DIR }}/Bluesky.ipa
${{ env.BUILD_DIR }}/Bluesky.app.dSYM.zip
- name: 📝 Write build summary
env:
REMOTE_BUILD_NUMBER: ${{ steps.get-build-info.outputs.BSKY_IOS_BUILD_NUMBER }}
run: |
{
echo "### iOS build number"
echo
echo "\`$REMOTE_BUILD_NUMBER\`"
} >> "$GITHUB_STEP_SUMMARY"
submit:
name: Submit iOS
# Submission and dSYM upload are I/O bound and don't need the xlarge builder.
runs-on: macos-26
needs: [build]
steps:
- name: ⬇️ Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# eas submit reads the app config from the repo
fetch-depth: 5
- name: 🔧 Setup Expo project
uses: ./.github/actions/setup-expo-project
with:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: ⬇️ Download build artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ios-build-${{ github.run_id }}
path: ios-build
- name: 🚀 Deploy
run: pnpm eas submit -p ios --non-interactive --path ios-build/Bluesky.ipa
- name: 🪲 Upload dSYM to Sentry
env:
SENTRY_ORG: blueskyweb
SENTRY_PROJECT: app
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: pnpm sentry-cli debug-files upload ios-build/Bluesky.app.dSYM.zip --include-sources
- name: 🔔 Notify Slack of Production Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0
uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
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 }}```"
}
{"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ needs.build.outputs.package-version }}\nBuild Number: ${{ needs.build.outputs.build-number }}```"}
distribute:
name: Assign build to TestFlight group
# fastlane and jq ship preinstalled on the macOS runner image, and this step mostly idles
# polling Apple processing, so it runs on a normal-size runner.
runs-on: macos-26
needs: [build, submit]
# testFlightGroup defaults to 'none' on both workflow_call and dispatch; guard against the
# empty string too, since `!= 'none'` alone would be true for ''.
if: ${{ inputs.testFlightGroup && inputs.testFlightGroup != 'none' }}
steps:
# 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
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
if: ${{ inputs.profile == 'testflight' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ inputs.profile == 'testflight' }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
TESTFLIGHT_GROUP: ${{ inputs.testFlightGroup }}
CHANGELOG: ${{ inputs.changelog }}
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: ${{ needs.build.outputs.package-version }}
BUILD_NUMBER: ${{ needs.build.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
# changelog is optional, and passing it empty would blank out whatever "What to
# Test" notes the build already has, so only include the flag when it is set.
changelog_arg=()
if [ -n "$CHANGELOG" ]; then
changelog_arg=(changelog:"$CHANGELOG")
fi
# 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:"$TESTFLIGHT_GROUP" \
notify_external_testers:true \
"${changelog_arg[@]}"
+266 -271
View File
@@ -17,20 +17,46 @@ on:
type: string
description: Runtime version (in x.x.x format) that this update is for
required: true
iosBuildNumber:
type: string
description: iOS build number of the native build this update targets
(required for production)
androidVersionCode:
type: string
description: Android version code of the native build this update
targets (required for production)
# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code
permissions:
contents: read
# denis release tag in bluesky-social/tango whose linux-amd64 binary this
# workflow downloads to publish OTA bundles. Bump this one line to roll denis.
env:
DENIS_RELEASE_TAG: denis-v0.1.1
jobs:
bundleDeploy:
if: github.repository == 'bluesky-social/social-app'
name: Bundle and Deploy EAS Update
runs-on: ubuntu-latest
# id-token: write lets this job mint an OIDC token to assume the denis
# publish role; actions: read loads the fingerprint baseline artifact;
# contents: read is still needed for the checkout.
permissions:
id-token: write
actions: read
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-deploy
cancel-in-progress: true
outputs:
changes-detected: ${{ steps.fingerprint.outputs.includes-changes }}
# A version bump forces a native build even if the fingerprint is unchanged
changes-detected: ${{ steps.fingerprint.outputs.includes-changes ||
steps.version.outputs.version-changed }}
steps:
- name: Check for EXPO_TOKEN
- name: 🔑 Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
@@ -39,14 +65,29 @@ jobs:
# Validate the version if one is supplied. This should generally happen if the update is for a production client
- name: 🧐 Validate version
env:
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
if: ${{ inputs.runtimeVersion }}
run: |
if [ -z "${{ inputs.runtimeVersion }}" ]; then
[[ "${{ inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
fi
[[ "$RUNTIME_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
# Production OTAs are bound to the specific native build they target, so
# the build numbers must be entered manually rather than read from the
# global EAS counters, which advance with every testflight build and so
# point past older production releases
- name: 🧐 Validate build numbers
if: ${{ inputs.channel == 'production' }}
env:
IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
ANDROID_VERSION_CODE: ${{ inputs.androidVersionCode }}
run: |
[[ "$IOS_BUILD_NUMBER" =~ ^[0-9]+$ ]] ||
(echo "A numeric iosBuildNumber is required for production updates" && exit 1)
[[ "$ANDROID_VERSION_CODE" =~ ^[0-9]+$ ]] ||
(echo "A numeric androidVersionCode is required for production updates" && exit 1)
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
@@ -54,314 +95,268 @@ jobs:
if: ${{ github.ref != 'refs/heads/main' }}
run: git fetch origin main:main --depth 100
# A change to the version in package.json means a new native release, so
# an OTA update must not be deployed and full native builds are required
# regardless of what the fingerprint says
- name: 🔢 Check for version change
id: version
if: ${{ github.event_name == 'push' }}
env:
EVENT_BEFORE: ${{ github.event.before }}
run: |
CURRENT_VERSION=$(jq -r '.version' package.json)
if [ -n "$EVENT_BEFORE" ] && [[ ! "$EVENT_BEFORE" =~ ^0+$ ]] && git cat-file -e "$EVENT_BEFORE:package.json" 2>/dev/null; then
PREVIOUS_VERSION=$(git show "$EVENT_BEFORE:package.json" | jq -r '.version')
else
PREVIOUS_VERSION=$(git show HEAD~1:package.json | jq -r '.version')
fi
echo "Previous version: $PREVIOUS_VERSION, current version: $CURRENT_VERSION"
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
echo "Version changed, full native builds are required"
echo "version-changed=true" >> "$GITHUB_OUTPUT"
fi
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
cache: yarn
node-version-file: package.json
cache: pnpm
- name: ⬇️ Load fingerprint baseline
id: baseline
if: ${{ (inputs.channel || 'testflight') == 'testflight' }}
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY_ID: ${{ github.repository_id }}
run: |
url=$(gh api \
"repos/${GITHUB_REPOSITORY}/actions/artifacts?name=testflight-native-fingerprint&per_page=100" \
--jq "[.artifacts[] | select(
.expired == false and
.workflow_run.head_branch == \"main\" and
.workflow_run.head_repository_id == (\$ENV.REPOSITORY_ID | tonumber)
)] | max_by(.created_at) | .archive_download_url" \
2>/dev/null || true)
if [ -n "$url" ] && [ "$url" != "null" ]; then
mkdir baseline-artifact
if curl -sSL -H "Authorization: Bearer $GH_TOKEN" -o baseline.zip "$url" \
&& unzip -q baseline.zip -d baseline-artifact; then
if jq -e '.sources | type == "array"' \
baseline-artifact/native-fingerprint.json >/dev/null; then
echo "path=baseline-artifact/native-fingerprint.json" >> "$GITHUB_OUTPUT"
else
echo "::warning::Ignoring invalid fingerprint baseline artifact."
fi
else
echo "::warning::Could not download fingerprint baseline artifact."
fi
fi
- name: 📷 Check fingerprint and install dependencies
id: fingerprint
uses: bluesky-social/github-actions/fingerprint-native@main
uses: bluesky-social/github-actions/fingerprint-native@abc6a46eb4badf243f55bfd7d6cec42722456300 # v0.3.0
with:
profile: ${{ inputs.channel || 'testflight' }}
previous-commit-tag: ${{ inputs.runtimeVersion }}
# The recordBaseline job uploads this marker after a successful deploy;
# on the native path, that requires both builds to succeed. A missing
# marker forces native builds so they can seed the baseline safely.
baseline-fingerprint-path: ${{ steps.baseline.outputs.path }}
- name: Lint check
run: yarn lint
- name: Lint lockfile
run: yarn lockfile-lint
- name: Prettier check
run: yarn prettier --check .
# Hand the full fingerprint to recordBaseline through a short-lived
# artifact. It is uploaded unconditionally but promoted to the persistent
# baseline only after both native builds succeed.
- name: 🚀 Upload native fingerprint
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: native-fingerprint-${{ github.run_id }}
path: ${{ steps.fingerprint.outputs.current-fingerprint-path }}
retention-days: 1
if-no-files-found: error
- name: 🔤 Compile translations
run: yarn intl:build 2>&1 | tee i18n.log
uses: ./.github/actions/compile-i18n
- 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
- name: 💅 Prettier check
run: pnpm prettier --check .
- name: 🔎 Type check
run: pnpm typecheck
- name: 🔨 Setup EAS
uses: expo/expo-github-action@main
if: ${{ !steps.fingerprint.outputs.includes-changes }}
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
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
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
uses: dcarbone/install-jq-action@4fcb5062d7ce9bc4382d1a352d19ba3ba2c317c1 # v4.0.1
# eas.json not used here, set EXPO_PUBLIC_ENV
- name: Env
# eas.json not used here, so EXPO_PUBLIC_ENV must be written explicitly
- name: ✏️ Write environment variables
id: env
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_ENV=${{ inputs.channel || 'testflight' }}" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "$json" > google-services.json
if: ${{ !steps.fingerprint.outputs.includes-changes && !steps.version.outputs.version-changed }}
uses: ./.github/actions/write-env
with:
env-token: ${{ secrets.ENV_TOKEN }}
sentry-dsn: ${{ secrets.SENTRY_DSN }}
bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }}
gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}
google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }}
expo-public-env: ${{ inputs.channel || 'testflight' }}
- name: 🏗️ Create Bundle
if: ${{ !steps.fingerprint.outputs.includes-changes }}
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
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 export
SENTRY_RELEASE=${{ steps.env.outputs.release-version }}
SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }}
pnpm export
- name: 📦 Package Bundle and 🚀 Deploy
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: yarn use-build-number bash scripts/bundleUpdate.sh
- name: ☁️ Configure AWS credentials (denis)
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: arn:aws:iam::007404326489:role/denis-ci-publish
aws-region: us-east-2
- name: ⬇️ Setup denis CLI
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
uses: ./.github/actions/setup-denis
with:
release-tag: ${{ env.DENIS_RELEASE_TAG }}
app-id: ${{ vars.SYNC_INTERNAL_APP_ID }}
private-key: ${{ secrets.SYNC_INTERNAL_PK }}
- name: 🚀 Publish OTA to denis (S3)
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
run: pnpm use-build-number bash scripts/denisPublish.sh
env:
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
# When set (required for production), these take precedence over the
# global EAS counters inside the use-build-number wrapper
BSKY_IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
BSKY_ANDROID_VERSION_CODE: ${{ inputs.androidVersionCode }}
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
if: ${{ !steps.fingerprint.outputs.includes-changes }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
# GitHub actions are horrible so let's just copy paste this in
buildIfNecessaryIOS:
name: Build and Submit iOS
runs-on: macos-26
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
cancel-in-progress: true
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' }}
steps:
- name: Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 5
- name: 🔧 Setup Node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: yarn
- 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
- name: ⚙️ Install dependencies
run: yarn install --frozen-lockfile
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.0"
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
with:
version: 1.16.2
- name: 💾 Cache Pods
uses: actions/cache@v4
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') }}
- name: 🔤 Compile translations
run: yarn intl:build
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
id: env
run: |
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: 🏗️ EAS Build
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 ios
--profile testflight
--local --output build.ipa --non-interactive
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
if: ${{ inputs.channel == 'testflight' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ inputs.channel == 'testflight' }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
if: ${{ inputs.channel != 'production' &&
needs.bundleDeploy.outputs.changes-detected && github.repository ==
'bluesky-social/social-app' }}
uses: ./.github/workflows/build-submit-ios.yml
with:
profile: testflight
testFlightGroup: none
# OTA rebuilds don't need the xlarge builder used for releases
runner: macos-26
# Pass only the secrets the reusable workflow declares, rather than `secrets: inherit`,
# so this workflow never hands the reusable workflow the entire repo secret store.
secrets:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
ENV_TOKEN: ${{ secrets.ENV_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
BITDRIFT_API_KEY: ${{ secrets.BITDRIFT_API_KEY }}
EXPO_PUBLIC_GCP_PROJECT_ID: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}
GOOGLE_SERVICES_TOKEN: ${{ secrets.GOOGLE_SERVICES_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }}
SLACK_CLIENT_ALERT_WEBHOOK: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
buildIfNecessaryAndroid:
name: Build and Submit Android
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-android
cancel-in-progress: false
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' }}
# build-submit-android.yml contains an attachToRelease job that requests contents: write.
# That job is skipped here (it needs a production tag build), but GitHub statically
# validates the reusable-workflow permission ceiling, so the caller must grant it.
permissions:
contents: write
uses: ./.github/workflows/build-submit-android.yml
with:
profile: testflight-android
runner: ubuntu-latest
# Pass only the secrets the reusable workflow declares, rather than `secrets: inherit`,
# so this workflow never hands the reusable workflow the entire repo secret store.
secrets:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
ENV_TOKEN: ${{ secrets.ENV_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
BITDRIFT_API_KEY: ${{ secrets.BITDRIFT_API_KEY }}
EXPO_PUBLIC_GCP_PROJECT_ID: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}
GOOGLE_SERVICES_TOKEN: ${{ secrets.GOOGLE_SERVICES_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SLACK_CLIENT_ALERT_WEBHOOK: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
# Advance the fingerprint baseline only after BOTH native builds have shipped
# the new native surface. This replaces the old actions/cache baseline, which
# only advanced on cache eviction and so silently froze - freezing meant every
# fingerprint looked changed and OTA updates stopped deploying entirely.
#
# On the native-build path, this runs only after both builds succeed. A
# successful OTA deploy also records its fingerprint, refreshing the
# persistent marker's retention without changing the native baseline.
#
# The persistent artifact replaces the old actions/cache marker without
# requiring a PAT or mutable repository variable. Each successful deploy adds
# an immutable marker; the next run reads the newest non-expired one using the
# built-in GITHUB_TOKEN.
recordBaseline:
name: Record fingerprint baseline
runs-on: ubuntu-latest
needs: [bundleDeploy, buildIfNecessaryIOS, buildIfNecessaryAndroid]
if: ${{ always() &&
(inputs.channel || 'testflight') == 'testflight' &&
needs.bundleDeploy.result == 'success' &&
(needs.bundleDeploy.outputs.changes-detected != 'true' ||
(needs.buildIfNecessaryIOS.result == 'success' &&
needs.buildIfNecessaryAndroid.result == 'success')) &&
github.repository == 'bluesky-social/social-app' }}
permissions:
actions: read
steps:
- name: Check for EXPO_TOKEN
- name: ⬇️ Download native fingerprint
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: native-fingerprint-${{ github.run_id }}
- name: 🧐 Validate native fingerprint
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
jq -e '.sources | type == "array"' native-fingerprint.json >/dev/null ||
(echo "::error::native fingerprint artifact was invalid; refusing to record it as the baseline." && exit 1)
- name: ⬇️ Checkout
uses: actions/checkout@v4
- name: 🚀 Record fingerprint baseline
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
fetch-depth: 5
- name: 🔧 Setup Node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: yarn
- 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: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: ⚙️ Install dependencies
run: yarn install --frozen-lockfile
- name: 🔤 Compile translations
run: yarn intl:build
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
id: env
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "$json" > google-services.json
- name: 🏗️ EAS Build
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 testflight-android
--local --output build.apk --non-interactive
- name: ⏰ Get a timestamp
id: timestamp
uses: nanzm/get-time-action@master
with:
format: "MM-DD-HH-mm-ss"
- name: 🚀 Upload Artifact
id: upload-artifact
uses: actions/upload-artifact@v4
with:
retention-days: 30
compression-level: 0
name: build-${{ steps.timestamp.outputs.time }}.apk
path: build.apk
- name: 🔔 Notify Slack
uses: slackapi/slack-github-action@v1.25.0
with:
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
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
name: testflight-native-fingerprint
path: native-fingerprint.json
retention-days: 90
if-no-files-found: error
+75
View File
@@ -0,0 +1,75 @@
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
# Required for claude-code-action's GitHub App token exchange.
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
- name: 🤖 Claude
uses: anthropics/claude-code-action@459ad358ae43fea66bfefd0a1f8d840b4b9791fb # v1.0.194
env:
ANTHROPIC_BASE_URL: https://agentgateway.k1.prod.bsky.dev
with:
# Agent Gateway service keys use Authorization: Bearer, the wire
# shape emitted by the action's OAuth-token input.
claude_code_oauth_token: ${{ secrets.AGENT_GATEWAY_CLAUDE_GH_REVIEW_KEY }}
additional_permissions: |
actions: read
track_progress: true
claude_args: |
--model claude-opus-4-8:api
--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:*)"
+66
View File
@@ -0,0 +1,66 @@
name: claude-review
# Automatic Claude review on PR creation/update, via Agent Gateway.
# 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
# Required for claude-code-action's GitHub App token exchange.
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 gateway spend on
# unvetted code). 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
- name: 🤖 Claude review
uses: anthropics/claude-code-action@459ad358ae43fea66bfefd0a1f8d840b4b9791fb # v1.0.194
env:
ANTHROPIC_BASE_URL: https://agentgateway.k1.prod.bsky.dev
with:
# Agent Gateway service keys use Authorization: Bearer, the wire
# shape emitted by the action's OAuth-token input.
claude_code_oauth_token: ${{ secrets.AGENT_GATEWAY_CLAUDE_GH_REVIEW_KEY }}
additional_permissions: |
actions: read
track_progress: true
claude_args: |
--model claude-opus-4-8:api
--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.
+21 -16
View File
@@ -10,34 +10,39 @@ 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@v4
- name: Set up Go tooling
uses: actions/setup-go@v3
- name: ⬇️ Git Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: 🔧 Set up Go tooling
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.25"
- name: Dummy Static Files
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
- name: 🔍 Check
run: cd bskyweb/ && make check
- name: Build (binary)
- name: 🏗️ Build (binary)
run: cd bskyweb/ && make build
- name: Test
- name: 🧪 Test
run: cd bskyweb/ && make test
lint:
runs-on: ubuntu-latest
steps:
- name: Git Checkout
uses: actions/checkout@v4
- name: Set up Go tooling
uses: actions/setup-go@v3
- name: ⬇️ Git Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: 🔧 Set up Go tooling
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.25"
- name: Dummy Static Files
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
- name: 🧹 Lint
run: cd bskyweb/ && make lint
+105 -39
View File
@@ -6,56 +6,122 @@ 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,
'lexicons:verify',
'typecheck:ios',
'typecheck:android',
'typecheck:web',
]
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Install node
uses: actions/setup-node@v6
- name: ⬇️ Check out Git repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- 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@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Install node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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 .
- name: Check & compile i18n
run: yarn intl:build
- name: Type check
run: yarn typecheck
node-version-file: package.json
cache: pnpm
- name: 📦 pnpm install
run: pnpm install --frozen-lockfile
- name: 🔤 Check & compile i18n
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
- name: Install node
uses: actions/setup-node@v6
- name: ⬇️ Check out Git repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Install node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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: Check & compile i18n
run: yarn intl:build
- name: Run tests
node-version-file: package.json
cache: pnpm
- name: 📦 pnpm install
run: pnpm install --frozen-lockfile
- name: 🔤 Check & compile i18n
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"
+200
View File
@@ -0,0 +1,200 @@
---
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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
testFlightGroup: "QA Team"
# Pass only the secrets the reusable workflow declares, rather than `secrets: inherit`,
# so the nightly build never hands the reusable workflow the entire repo secret store.
secrets:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
ENV_TOKEN: ${{ secrets.ENV_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
BITDRIFT_API_KEY: ${{ secrets.BITDRIFT_API_KEY }}
EXPO_PUBLIC_GCP_PROJECT_ID: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}
GOOGLE_SERVICES_TOKEN: ${{ secrets.GOOGLE_SERVICES_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }}
SLACK_CLIENT_ALERT_WEBHOOK: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
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
# Pass only the secrets the reusable workflow declares, rather than `secrets: inherit`,
# so the nightly build never hands the reusable workflow the entire repo secret store.
secrets:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
ENV_TOKEN: ${{ secrets.ENV_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
BITDRIFT_API_KEY: ${{ secrets.BITDRIFT_API_KEY }}
EXPO_PUBLIC_GCP_PROJECT_ID: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}
GOOGLE_SERVICES_TOKEN: ${{ secrets.GOOGLE_SERVICES_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SLACK_CLIENT_ALERT_WEBHOOK: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
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@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
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@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
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
+424
View File
@@ -0,0 +1,424 @@
---
name: Nightly Maestro E2E
on:
schedule:
- cron: "0 4 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: nightly-maestro-e2e-${{ github.ref }}
cancel-in-progress: false
env:
CI: "1"
MAESTRO_VERSION: "2.6.1"
MAESTRO_DRIVER_STARTUP_TIMEOUT: "180000"
MAESTRO_CLI_NO_ANALYTICS: "1"
MAESTRO_CLI_ANALYSIS_NOTIFICATION_DISABLED: "true"
MAESTRO_DISABLE_UPDATE_CHECK: "1"
jobs:
ios:
name: iOS Maestro E2E
if: github.repository == 'bluesky-social/social-app'
runs-on: macos-26-xlarge
timeout-minutes: 120
steps:
- name: ⬇️ Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: 🛠️ Select Xcode 26.4
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: "26.4"
- name: 🧰 Prepare E2E configuration
run: |
mkdir -p artifacts/ios
echo "Installing dependencies" > artifacts/ios/phase.txt
cp .env.example .env.development
cp google-services.json.example google-services.json
- name: 🔧 Set up Expo project
uses: ./.github/actions/setup-expo-project
with:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: ☕️ Set up Java 17
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: temurin
java-version: "17"
- name: 📦 Install dev-env dependencies
run: pnpm --dir dev-env install --frozen-lockfile 2>&1 | tee artifacts/ios/dependencies.log
- name: 🔤 Compile translations
uses: ./.github/actions/compile-i18n
- name: 📥 Install Maestro 2.6.1
run: |
echo "Installing Maestro" > artifacts/ios/phase.txt
curl -fsSL -o "$RUNNER_TEMP/maestro.zip" \
"https://github.com/mobile-dev-inc/Maestro/releases/download/cli-${MAESTRO_VERSION}/maestro.zip"
echo "3440825f514f537c6a96bcf5de995780c2a4a7f83a43208fdc95d4f1fecfad3b $RUNNER_TEMP/maestro.zip" \
| shasum -a 256 --check
unzip -q "$RUNNER_TEMP/maestro.zip" -d "$RUNNER_TEMP"
echo "$RUNNER_TEMP/maestro/bin" >> "$GITHUB_PATH"
"$RUNNER_TEMP/maestro/bin/maestro" --version | tee artifacts/ios/maestro-version.log
test "$("$RUNNER_TEMP/maestro/bin/maestro" --version)" = "$MAESTRO_VERSION"
- name: 📱 Boot one iOS simulator
run: |
echo "Booting iOS simulator" > artifacts/ios/phase.txt
device_name="iPhone 17"
runtime_name="iOS 26.5"
runtime_id=$(xcrun simctl list runtimes available --json | jq -r \
--arg name "$runtime_name" \
'[.runtimes[] | select(.name == $name and .isAvailable != false)] | first | .identifier // empty')
if [ -z "$runtime_id" ]; then
echo "The $runtime_name simulator runtime is not installed. Available iOS runtimes:" >&2
xcrun simctl list runtimes available --json | jq -r \
'.runtimes[] | select(.name | startswith("iOS")) | "- \(.name)"' >&2
exit 1
fi
device_type_id=$(xcrun simctl list devicetypes --json | jq -r \
--arg name "$device_name" \
'[.devicetypes[] | select(.name == $name)] | first | .identifier // empty')
if [ -z "$device_type_id" ]; then
echo "The $device_name simulator device type is not installed" >&2
exit 1
fi
udid=$(xcrun simctl list devices available --json | jq -r \
--arg runtime "$runtime_id" \
--arg name "$device_name" \
'[.devices[$runtime][]? | select(.name == $name)] | first | .udid // empty')
if [ -z "$udid" ]; then
udid=$(xcrun simctl create "$device_name" "$device_type_id" "$runtime_id")
fi
echo "IOS_UDID=$udid" >> "$GITHUB_ENV"
xcrun simctl shutdown all || true
xcrun simctl boot "$udid"
xcrun simctl bootstatus "$udid" -b
echo "Using $device_name on $runtime_name ($udid)"
- name: 🏷️ Mark iOS development client build phase
run: echo "Building the iOS development client" > artifacts/ios/phase.txt
- name: 🏗️ Build iOS development client
uses: ./.github/actions/eas-local-build
with:
platform: ios
profile: e2e
output: ${{ runner.temp }}/nightly-e2e-ios.tar.gz
log-path: artifacts/ios/build.log
- name: 📲 Install iOS development client
run: |
build_contents="$RUNNER_TEMP/nightly-e2e-ios-build"
mkdir -p "$build_contents"
tar -xzf "$RUNNER_TEMP/nightly-e2e-ios.tar.gz" -C "$build_contents"
app_path=$(find "$build_contents" -type d -name '*.app' -print -quit)
if [ -z "$app_path" ]; then
echo "The local EAS build did not contain an iOS simulator app" >&2
exit 1
fi
xcrun simctl install "$IOS_UDID" "$app_path" 2>&1 | tee -a artifacts/ios/build.log
- name: 🧪 Run iOS Maestro suite
run: .github/scripts/run-nightly-e2e.sh ios "$IOS_UDID"
- name: 🧹 Clean up iOS services and simulator
if: always()
run: .github/scripts/cleanup-nightly-e2e.sh ios "${IOS_UDID:-}"
- name: 🚀 Upload iOS E2E artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nightly-e2e-ios-${{ github.run_id }}
path: artifacts/ios
if-no-files-found: warn
retention-days: 7
android:
name: Android Maestro E2E
if: github.repository == 'bluesky-social/social-app'
# Linux-x64-32core is a repository-managed runner label.
runs-on: Linux-x64-32core
timeout-minutes: 120
steps:
- name: ⬇️ Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: 🧰 Prepare E2E configuration
run: |
mkdir -p artifacts/android
echo "Installing dependencies" > artifacts/android/phase.txt
cp .env.example .env.development
cp google-services.json.example google-services.json
- name: 🔧 Set up Expo project
uses: ./.github/actions/setup-expo-project
with:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: ☕️ Set up Java 17
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: temurin
java-version: "17"
- name: 📦 Install dev-env dependencies
run: pnpm --dir dev-env install --frozen-lockfile 2>&1 | tee artifacts/android/dependencies.log
- name: 🔤 Compile translations
uses: ./.github/actions/compile-i18n
- name: 📥 Install Maestro 2.6.1
run: |
echo "Installing Maestro" > artifacts/android/phase.txt
curl -fsSL -o "$RUNNER_TEMP/maestro.zip" \
"https://github.com/mobile-dev-inc/Maestro/releases/download/cli-${MAESTRO_VERSION}/maestro.zip"
echo "3440825f514f537c6a96bcf5de995780c2a4a7f83a43208fdc95d4f1fecfad3b $RUNNER_TEMP/maestro.zip" \
| shasum -a 256 --check
unzip -q "$RUNNER_TEMP/maestro.zip" -d "$RUNNER_TEMP"
echo "$RUNNER_TEMP/maestro/bin" >> "$GITHUB_PATH"
"$RUNNER_TEMP/maestro/bin/maestro" --version | tee artifacts/android/maestro-version.log
test "$("$RUNNER_TEMP/maestro/bin/maestro" --version)" = "$MAESTRO_VERSION"
- name: 📱 Install and boot one Android emulator
run: |
echo "Booting Android emulator" > artifacts/android/phase.txt
android_sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-/usr/local/lib/android/sdk}}"
sdkmanager_bin="$android_sdk/cmdline-tools/latest/bin/sdkmanager"
avdmanager_bin="$android_sdk/cmdline-tools/latest/bin/avdmanager"
# API 35 emulator images have known stability problems in headless CI
# (see flutter/flutter#153445); the qemu process died deterministically
# on the first native stack-screen push with the API 35 image.
system_image="system-images;android-34;google_apis;x86_64"
if [ ! -x "$sdkmanager_bin" ] || [ ! -x "$avdmanager_bin" ]; then
echo "Android command-line tools were not found under $android_sdk" >&2
find "$android_sdk/cmdline-tools" -maxdepth 3 -type f \( \
-name sdkmanager -o -name avdmanager \
\) -print >&2 || true
exit 1
fi
export ANDROID_HOME="$android_sdk"
export ANDROID_SDK_ROOT="$android_sdk"
export PATH="$android_sdk/platform-tools:$android_sdk/emulator:$PATH"
echo "ANDROID_HOME=$android_sdk" >> "$GITHUB_ENV"
echo "ANDROID_SDK_ROOT=$android_sdk" >> "$GITHUB_ENV"
echo "$android_sdk/platform-tools" >> "$GITHUB_PATH"
echo "$android_sdk/emulator" >> "$GITHUB_PATH"
echo "Using Android SDK at $android_sdk"
yes | "$sdkmanager_bin" --sdk_root="$android_sdk" --licenses >/dev/null || true
"$sdkmanager_bin" --sdk_root="$android_sdk" \
"platform-tools" "emulator" "$system_image"
export ANDROID_AVD_HOME="$RUNNER_TEMP/.android/avd"
mkdir -p "$ANDROID_AVD_HOME"
echo "ANDROID_AVD_HOME=$ANDROID_AVD_HOME" >> "$GITHUB_ENV"
echo no | "$avdmanager_bin" create avd \
--force \
--name nightly-e2e \
--package "$system_image" \
--device pixel_6
# Reduce resolution to lighten the SwiftShader software-rendering
# workload, and raise RAM/cores/heap so the debug RN app has headroom.
# The default 2560MB RAM led to silent qemu crashes mid-flow.
printf 'hw.lcd.width=720\nhw.lcd.height=1600\nhw.lcd.density=280\nhw.ramSize=6144\nhw.cpu.ncore=4\nvm.heapSize=512\n' \
>> "$ANDROID_AVD_HOME/nightly-e2e.avd/config.ini"
if [ -e /dev/kvm ] && [ ! -w /dev/kvm ]; then
sudo chmod 666 /dev/kvm
fi
# Disable the emulator's Vulkan feature so graphics goes through the
# plain GLES SwiftShader translator. gfxstream Vulkan via SwiftShader
# Subzero crashed qemu silently at a deterministic rendering step;
# GLES-only is sufficient since the guest renders with skiagl.
#
# Run the launch in a background subshell so the emulator's exit
# status is recorded when it dies (it is otherwise backgrounded and
# its death is invisible). Write the emulator's real PID - not the
# subshell's - to emulator.pid, since cleanup-nightly-e2e.sh kills the
# PID from that file directly; killing the subshell would not kill the
# emulator child.
(
# wait returns the emulator's non-zero status on crash; set -e would
# abort the subshell before the status is logged.
set +e
"$android_sdk/emulator/emulator" @nightly-e2e \
-port 5554 \
-no-window \
-gpu swiftshader_indirect \
-feature -Vulkan \
-no-snapshot \
-noaudio \
-no-boot-anim \
-camera-back none \
> artifacts/android/emulator.log 2>&1 &
emulator_pid=$!
echo "$emulator_pid" > artifacts/android/emulator.pid
wait "$emulator_pid"
echo "Emulator exited with status $?" >> artifacts/android/emulator.log
) &
adb -s emulator-5554 wait-for-device
booted=false
for _ in $(seq 1 120); do
if [ "$(adb -s emulator-5554 shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]; then
booted=true
break
fi
sleep 5
done
if [ "$booted" != "true" ]; then
echo "Android emulator did not finish booting" >&2
exit 1
fi
adb -s emulator-5554 shell settings put global window_animation_scale 0
adb -s emulator-5554 shell settings put global transition_animation_scale 0
adb -s emulator-5554 shell settings put global animator_duration_scale 0
- name: 🏷️ Mark Android development client build phase
run: echo "Building the Android development client" > artifacts/android/phase.txt
- name: 🏗️ Build Android development client
uses: ./.github/actions/eas-local-build
with:
platform: android
profile: e2e
output: ${{ runner.temp }}/nightly-e2e-android.apk
log-path: artifacts/android/build.log
- name: 📲 Install Android development client
run: |
adb -s emulator-5554 install -r "$RUNNER_TEMP/nightly-e2e-android.apk" \
2>&1 | tee -a artifacts/android/build.log
- name: 🧪 Run Android Maestro suite
run: .github/scripts/run-nightly-e2e.sh android emulator-5554
- name: 🩺 Capture emulator crash diagnostics
if: always()
run: |
{
echo "=== Emulator process status ==="
pgrep -fa "emulator.*nightly-e2e" || echo "Emulator process not found"
echo "=== Emulator exit status ==="
grep "Emulator exited" artifacts/android/emulator.log || echo "No emulator exit status recorded"
echo "=== OOM killer check (kernel) ==="
oom_lines=$(sudo dmesg 2>/dev/null | grep -iE "oom|killed process|out of memory" | tail -20)
echo "${oom_lines:-No kernel OOM evidence found (or dmesg unavailable)}"
echo "=== systemd-oomd check ==="
oomd_lines=$(journalctl -u systemd-oomd --no-pager 2>/dev/null | tail -20)
echo "${oomd_lines:-No systemd-oomd journal entries (or journalctl unavailable)}"
echo "=== journal kernel tail ==="
journalctl -k --no-pager 2>/dev/null | tail -30 || echo "journalctl -k unavailable"
echo "=== Emulator crash database ==="
ls -la /tmp/android-runner/emu-crash-*.db 2>/dev/null || echo "No crash database found"
} > artifacts/android/emulator-diagnostics.log 2>&1
- name: 🧹 Clean up Android services and emulator
if: always()
run: .github/scripts/cleanup-nightly-e2e.sh android emulator-5554
- name: 🚀 Upload Android E2E artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nightly-e2e-android-${{ github.run_id }}
path: artifacts/android
if-no-files-found: warn
retention-days: 7
report:
name: Report E2E failures
needs: [ios, android]
if: ${{ always() && github.repository == 'bluesky-social/social-app' }}
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- name: ⬇️ Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: ⬇️ Download iOS artifacts
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: nightly-e2e-ios-${{ github.run_id }}
path: downloaded-artifacts/ios
- name: ⬇️ Download Android artifacts
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: nightly-e2e-android-${{ github.run_id }}
path: downloaded-artifacts/android
- name: 🔗 Resolve artifact links
env:
GH_TOKEN: ${{ github.token }}
run: |
run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}#artifacts"
jq -n --arg run "$run_url" '{ios: $run, android: $run}' > artifact-links.json
if gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
> artifact-response.json; then
jq --arg base "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts" \
--arg run "$run_url" \
'{
ios: (.artifacts | map(select(.name | startswith("nightly-e2e-ios-"))) | first | if . then ($base + "/" + (.id | tostring)) else $run end),
android: (.artifacts | map(select(.name | startswith("nightly-e2e-android-"))) | first | if . then ($base + "/" + (.id | tostring)) else $run end)
}' artifact-response.json > artifact-links.json
fi
- name: 📊 Summarize platform results
id: summary
env:
IOS_STATUS: ${{ needs.ios.result }}
ANDROID_STATUS: ${{ needs.android.result }}
run: |
node .github/scripts/summarize-maestro.mjs \
--ios-status "$IOS_STATUS" \
--android-status "$ANDROID_STATUS" \
--ios-root downloaded-artifacts/ios \
--android-root downloaded-artifacts/android \
--artifact-urls artifact-links.json \
--sha "$GITHUB_SHA" \
--run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
--commit-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}" \
> e2e-summary.json
echo "notify=$(jq -r .notify e2e-summary.json)" >> "$GITHUB_OUTPUT"
echo "payload=$(jq -c .payload e2e-summary.json)" >> "$GITHUB_OUTPUT"
jq -r .githubSummary e2e-summary.json >> "$GITHUB_STEP_SUMMARY"
- name: 🔔 Notify Slack of E2E failures
if: steps.summary.outputs.notify == 'true'
uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
with:
webhook: ${{ secrets.E2E_FAILURES_SLACK_WEBHOOK }}
webhook-type: incoming-webhook
payload: ${{ steps.summary.outputs.payload }}
@@ -15,30 +15,27 @@ jobs:
contents: write
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: ⬇️ Check out Git repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ssh-key: ${{secrets.GH_ACTION_DEPLOY_KEY}}
- name: Install node
uses: actions/setup-node@v6
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Install node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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: Extract language strings
run: yarn intl:extract
- name: Create commit
uses: stefanzweifel/git-auto-commit-action@v5
node-version-file: package.json
cache: pnpm
- name: 📦 pnpm install
run: pnpm install --frozen-lockfile
- name: 🔤 Extract language strings
run: pnpm intl:extract
- name: 📝 Create commit
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.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
- name: 🚀 Push source lang to Crowdin
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0
with:
upload_sources: true
upload_sources_args: "-b main"
-209
View File
@@ -1,209 +0,0 @@
---
name: PR Comment Trigger
on:
issue_comment:
types: [created]
# Permissiosn to make comments in the pull request
permissions:
pull-requests: write
actions: write
contents: read
jobs:
handle-comment:
if: github.event.issue.pull_request
runs-on: ubuntu-latest
outputs:
should-deploy: ${{ steps.check-org.outputs.result }}
steps:
- name: Check if bot is mentioned
id: check-mention
env:
COMMENT: ${{ github.event.comment.body }}
run: |
if [[ "$COMMENT" == *"@github-actions"* ]] || \
[[ "$COMMENT" == *"github-actions[bot]"* ]]; then
bot_mentioned=true
else
bot_mentioned=false
fi
if [[ "${{ github.event.comment.body }}" == *"ota"* ]]; then
has_ota=true
else
has_ota=false
fi
if [[ "$bot_mentioned" == "true" ]] && [[ "$has_ota" == "true" ]]; then
echo "mentioned=true" >> $GITHUB_OUTPUT
else
echo "mentioned=false" >> $GITHUB_OUTPUT
fi
- name: Check organization membership
if: steps.check-mention.outputs.mentioned == 'true'
id: check-org
uses: actions/github-script@v7
with:
script: |
try {
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: context.payload.comment.user.login
});
const hasAccess = ['admin', 'write'].includes(perm.permission);
console.log(`User has ${perm.permission} access`);
return hasAccess;
} catch(error) {
console.log('User has no repository access');
return false;
}
bundle-deploy:
name: Bundle and Deploy EAS Update
runs-on: ubuntu-latest
needs: [handle-comment]
if: needs.handle-comment.outputs.should-deploy == 'true'
steps:
- name: Get PR HEAD SHA
id: pr-info
uses: actions/github-script@v7
with:
script: |
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ github.event.issue.number }}
});
console.log(`PR HEAD SHA: ${pr.data.head.sha}`);
console.log(`PR HEAD REF: ${pr.data.head.ref}`);
core.setOutput('head-sha', pr.data.head.sha);
core.setOutput('head-ref', pr.data.head.ref);
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
number: ${{ github.event.issue.number }}
message: |
An OTA deployment has been requested and is now running for `${{ steps.pr-info.outputs.head-sha }}`.
[Here is some music to listen to while you wait...](https://www.youtube.com/watch?v=VBlFHuCzPgY)
---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
- name: Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
ref: ${{ steps.pr-info.outputs.head-sha }}
- name: 🔧 Setup Node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: yarn
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Lint check
run: yarn lint
- name: Lint lockfile
run: yarn lockfile-lint
- name: 🔤 Compile translations
run: yarn 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: Type check
run: yarn typecheck
- name: 🔨 Setup EAS
uses: expo/expo-github-action@main
with:
expo-version: latest
eas-version: latest
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
- name: Env
id: env
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_ENV=testflight" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "$json" > google-services.json
- name: 🏗️ Create Bundle
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 export
- name: 📦 Package Bundle and 🚀 Deploy
run: yarn 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
with:
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
number: ${{ github.event.issue.number }}
message: |
Your requested OTA deployment was successful! You may now apply it by either scanning the QR code or opening the deep link below in your browser:
<img src="https://bsky-qr.vercel.app?channel=pull-request-${{ github.event.issue.number }}" width=300 height=300>
`bluesky://intent/apply-ota?channel=pull-request-${{ github.event.issue.number }}`
---
*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
if: failure()
with:
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
number: ${{ github.event.issue.number }}
message: |
Your requested OTA deployment was unsuccessful. See action logs for more details.
---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
+274 -49
View File
@@ -13,81 +13,140 @@ 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 and the PR OTA install link
permissions: {}
# denis release tag in bluesky-social/tango whose linux-amd64 binary the PR OTA
# job downloads. Bump this one line to roll denis.
env:
DENIS_RELEASE_TAG: denis-v0.1.1
NODE_OPTIONS: --max-old-space-size=4096
jobs:
webpack-analyzer:
runs-on: ubuntu-22.04
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
# Populate this from main so every PR can restore the same trusted baseline.
webpack-analyzer-base:
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'push' }}
permissions:
contents: read
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
- name: ⬇️ Get base stats from cache
id: get-base-stats
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: stats.json
key: stats-base-main-${{ github.sha }}
- name: 🔦 Generate stats file for base commit
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
run: |
pnpm install
pnpm intl:build
pnpm generate-webpack-stats-file
- name: ⬆️ Save base stats to cache
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: stats.json
key: stats-base-main-${{ github.sha }}
webpack-analyzer:
runs-on: ubuntu-24.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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
cache: yarn
node-version-file: package.json
cache: pnpm
- name: Ensure tracking relevant branches and checkout base
- name: 🌿 Ensure tracking relevant branches and checkout base
env:
HEAD_REF: ${{ github.head_ref }}
BASE_REF: ${{ github.base_ref }}
run: |
git checkout ${{ github.head_ref }}
git checkout ${{ github.base_ref }}
git checkout $HEAD_REF
git checkout $BASE_REF
- name: Get the base commit
- name: 🔍 Get the base commit
id: base-commit
run: echo base-commit=$(git log -n 1 ${{ github.base_ref }} --pretty=format:'%H') >> "$GITHUB_OUTPUT"
env:
BASE_REF: ${{ github.base_ref }}
run: echo base-commit=$(git log -n 1 $BASE_REF --pretty=format:'%H') >> "$GITHUB_OUTPUT"
- name: Merge PR commit
- name: 🔀 Merge PR commit
env:
HEAD_REF: ${{ github.head_ref }}
run: |
# Have to set a git config for the merge to work
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git merge --no-edit ${{ github.head_ref }}
yarn install
yarn intl:build
git merge --no-edit $HEAD_REF
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
# Restore-only prevents PR-scoped fallback builds from creating caches.
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: stats-base.json
key: stats-base-${{ steps.base-commit.outputs.base-commit }}
path: stats.json
key: stats-base-main-${{ steps.base-commit.outputs.base-commit }}
- name: Restore to base commit
- name: Restore to base commit
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
env:
BASE_COMMIT: ${{ steps.base-commit.outputs.base-commit }}
run: |
git reset HEAD~
git reset "$BASE_COMMIT"
git restore .
- 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
mv stats.json stats-base.json
pnpm install
pnpm intl:build
pnpm generate-webpack-stats-file
- 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"
base_path: "stats.json"
pr_path: "../stats-new.json"
excluded_assets: "(.+).chunk.js|(.+).js.map|(.+).json|(.+).png"
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@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: bundle-diff
message: |
@@ -99,9 +158,16 @@ 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
outputs:
# Empty when the native surface is unchanged, 'true' when it changed.
# publish-pr-ota gates on this.
includes-changes: ${{ steps.fingerprint.outputs.includes-changes }}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 100
@@ -109,39 +175,198 @@ jobs:
run: git fetch origin main:main --depth 100
if: github.event_name == 'pull_request'
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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
timeout-minutes: 5
uses: bluesky-social/github-actions/fingerprint-native@abc6a46eb4badf243f55bfd7d6cec42722456300 # v0.3.0
with:
profile: pull-request
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
if: ${{ steps.fingerprint.outputs.includes-changes }}
with:
header: fingerprint-diff
message: |
The Pull Request introduced fingerprint changes against the base commit:
<details><summary>Fingerprint diff</summary>
```json
${{ steps.fingerprint.outputs.diff }}
```
</details>
The Pull Request introduced native fingerprint changes against the base commit.
---
*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@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
if: ${{ !steps.fingerprint.outputs.includes-changes }}
with:
header: fingerprint-diff
delete: true
# publish-pr-ota is skipped once the fingerprint changes, so any install
# link left over from an earlier fingerprint-clean commit on this PR now
# points at a bundle that no longer matches the PR. Drop it.
- name: 💬 Delete stale OTA install comment
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
if: ${{ steps.fingerprint.outputs.includes-changes }}
with:
header: pull-request-ota
delete: true
- name: 🏷️ Label as fingerprint changed
if: ${{ steps.fingerprint.outputs.includes-changes }}
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh pr edit "$PR_NUMBER" --add-label "bot: fingerprint changed" || true
- name: 🏷️ Remove fingerprint changed label
if: ${{ !steps.fingerprint.outputs.includes-changes }}
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh pr edit "$PR_NUMBER" --remove-label "bot: fingerprint changed" || true
# Automatic per-PR OTA preview, published to the pull-request-<N> channel on
# denis. Replaces the old `@github-actions ota` comment trigger. Gated to
# same-repo PRs (fork guard): a branch can only exist in this repo if someone
# with write access pushed it, so an outside contributor (who can only open a
# PR from a fork) never runs this job with the denis publish role in scope.
# This matches the fork-guard gate the other jobs in this workflow use;
# author_association is deliberately NOT checked (it can't identify a private
# org member and would skip their PRs).
#
# Bot authors are excluded: Dependabot pushes in-repo branches, so it passes
# the fork guard, but GitHub withholds repo secrets from Dependabot-triggered
# runs. EXPO_TOKEN is then empty and the job fails at setup — a red check on
# every dependabot PR. There is no OTA preview worth publishing for a
# dependency bump anyway.
#
# Gated on a clean fingerprint-native run: an OTA can only carry JS, so once
# the native surface changes the published bundle no longer represents the PR
# and installing it on a store/TestFlight client is misleading at best. Those
# PRs need a native build instead. A skipped or failed fingerprint job also
# skips this one - without a verdict we can't say the OTA is representative.
publish-pr-ota:
name: Publish PR OTA to denis
needs: fingerprint-native
runs-on: ubuntu-latest
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.type != 'Bot' &&
needs.fingerprint-native.outputs.includes-changes != 'true'
concurrency:
group: pr-ota-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
id-token: write
contents: read
outputs:
release-version: ${{ steps.env.outputs.release-version }}
ios-build-number: ${{ steps.build-info.outputs.BSKY_IOS_BUILD_NUMBER }}
android-build-number: ${{ steps.build-info.outputs.BSKY_ANDROID_VERSION_CODE }}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: 🛠️ Setup Expo project
uses: ./.github/actions/setup-expo-project
with:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: 🔤 Compile translations
uses: ./.github/actions/compile-i18n
- name: ✏️ Write environment variables
id: env
uses: ./.github/actions/write-env
with:
env-token: ${{ secrets.ENV_TOKEN }}
sentry-dsn: ${{ secrets.SENTRY_DSN }}
bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }}
gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}
google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }}
expo-public-env: testflight
- name: 🏗️ Create Bundle
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.release-version }}
SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }}
pnpm export
- name: ☁️ Configure AWS credentials (denis, PR-scoped)
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: arn:aws:iam::007404326489:role/denis-ci-publish-pr
aws-region: us-east-2
# Defense-in-depth: the base role is already scoped to pr/*, but narrow
# this session further to just THIS PR's prefix so a bug can't write to
# another PR's objects or the prod tree.
inline-session-policy: |-
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::bsky-denis-ota-prod/pr/${{ github.event.pull_request.number }}/*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::bsky-denis-ota-prod",
"Condition": {
"StringLike": { "s3:prefix": "pr/${{ github.event.pull_request.number }}/*" }
}
}
]
}
- name: ⬇️ Setup denis CLI
uses: ./.github/actions/setup-denis
with:
release-tag: ${{ env.DENIS_RELEASE_TAG }}
app-id: ${{ vars.SYNC_INTERNAL_APP_ID }}
private-key: ${{ secrets.SYNC_INTERNAL_PK }}
- name: 🔢 Get native build numbers
id: build-info
run: bash scripts/setGitHubOutput.sh
- name: 🚀 Publish OTA to denis (S3)
run: pnpm use-build-number bash scripts/denisPublish.sh
env:
RUNTIME_VERSION: ''
CHANNEL_NAME: pull-request-${{ github.event.pull_request.number }}
# Pin the publish to the same values exposed in the install link.
BSKY_IOS_BUILD_NUMBER: ${{ steps.build-info.outputs.BSKY_IOS_BUILD_NUMBER }}
BSKY_ANDROID_VERSION_CODE: ${{ steps.build-info.outputs.BSKY_ANDROID_VERSION_CODE }}
comment-pr-ota:
name: Comment PR OTA install link
needs: publish-pr-ota
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: 💬 Drop OTA install comment
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: pull-request-ota
message: |
The OTA deployment for this PR was successful! You may now apply it by either scanning the QR code or opening the deep link below in your browser:
<img src="https://bsky-qr.vercel.app?channel=pull-request-${{ github.event.pull_request.number }}&releaseVersion=${{ needs.publish-pr-ota.outputs.release-version }}&iosBuildNumber=${{ needs.publish-pr-ota.outputs.ios-build-number }}&androidBuildNumber=${{ needs.publish-pr-ota.outputs.android-build-number }}" width="300" height="300" alt="QR code for the PR OTA deployment">
`bluesky://intent/apply-ota?channel=pull-request-${{ github.event.pull_request.number }}&releaseVersion=${{ needs.publish-pr-ota.outputs.release-version }}&iosBuildNumber=${{ needs.publish-pr-ota.outputs.ios-build-number }}&androidBuildNumber=${{ needs.publish-pr-ota.outputs.android-build-number }}`
+18 -6
View File
@@ -4,28 +4,40 @@ 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
- name: ⬇️ Checkout public repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Generate GitHub App Token
# Don't persist the checkout auth header; the push below authenticates
# with the app token embedded in the remote URL instead
persist-credentials: false
- 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
- name: Push to internal repo
# Scope the token down from the app's full installation permissions;
# pushing is the only thing this token is used for. The workflows
# permission is required because the sync includes files under
# .github/workflows/, which GitHub refuses to push without it.
permission-contents: write
permission-workflows: write
- name: 🚀 Push to internal repo
env:
TOKEN: ${{ steps.app-token.outputs.token }}
run: |
git config user.name "github-actions"
git config user.email "test@users.noreply.github.com"
git config --unset-all http.https://github.com/.extraheader
git remote add internal https://x-access-token:${TOKEN}@github.com/bluesky-social/social-app-internal.git
git push internal main --force
+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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Install node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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
}
-46
View File
@@ -1,46 +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
run: git fetch origin ${{ github.base_ref }} --depth=1
- name: Install node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Reset yarn.lock to base
run: git show "origin/${{ github.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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: 🛡️ Run zizmor
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
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
+18 -3
View File
@@ -86,6 +86,9 @@ stats.json
# VSCode
.vscode
# Zed
.zed
# gitignore and github actions
!.gitignore
!.github
@@ -105,12 +108,17 @@ google-services.json
# Performance results (Flashlight)
.perf/
# ESLint
.eslintcache
# Oxlint
.oxlintcache
# i18n
src/locale/locales/_build/
src/locale/locales/**/*.js
src/locale/locales/**/messages.js
src/locale/locales/**/messages.mjs
src/locale/locales/**/messages.ts
# generated lexicon schemas (pnpm lexicons:generate)
src/lexicons/
# local builds
*.apk
@@ -125,3 +133,10 @@ 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
# pnpm
.pnpm-store/
+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()
}
+135
View File
@@ -0,0 +1,135 @@
/**
* Codemod to replace namespaced React calls with named imports
*
* Before:
* import React from 'react'
* React.useEffect(() => {}, [])
*
* After:
* import { useEffect } from 'react'
* useEffect(() => {}, [])
*
* Usage: jscodeshift -t .jscodeshift/repo/react-import.js <file-path>
* Example: jscodeshift -t .jscodeshift/repo/react-import.js src/App.native.tsx
*/
/* eslint-disable */
export const parser = 'tsx'
export default function transformer(file, api) {
const j = api.jscodeshift
const root = j(file.source)
// Find the React import
let reactImportPath = null
const reactMembers = new Set()
root.find(j.ImportDeclaration).forEach(path => {
const node = path.value
if (node.source.value === 'react') {
node.specifiers.forEach(spec => {
// Check if this is a default import of React
if (
spec.type === 'ImportDefaultSpecifier' &&
spec.local.name === 'React'
) {
reactImportPath = path
}
})
}
})
if (!reactImportPath) {
// No React import found, nothing to do
return file.source
}
// Find all React.* member expressions
root
.find(j.MemberExpression)
.filter(path => {
const node = path.value
return (
node.object.type === 'Identifier' &&
node.object.name === 'React' &&
node.property.type === 'Identifier'
)
})
.forEach(path => {
const propertyName = path.value.property.name
reactMembers.add(propertyName)
})
// Find all React.* JSX member expressions (e.g., <React.Fragment>)
root
.find(j.JSXMemberExpression)
.filter(path => {
const node = path.value
return node.object.name === 'React' && node.property.name
})
.forEach(path => {
const propertyName = path.value.property.name
reactMembers.add(propertyName)
})
// If no React members are used, remove the import
if (reactMembers.size === 0) {
reactImportPath.prune()
return root.toSource()
}
// Sort the members for consistent output
const sortedMembers = Array.from(reactMembers).sort()
// Create new import specifiers
const newSpecifiers = sortedMembers.map(name =>
j.importSpecifier(j.identifier(name), j.identifier(name)),
)
// Get the existing import specifiers
const sortedImports = Array.from(reactImportPath.value.specifiers).sort()
const existingSpecifiers = sortedImports.filter(
specifier => specifier.type !== 'ImportDefaultSpecifier',
)
const allSpecifiers = [
...new Map(
[...existingSpecifiers, ...newSpecifiers].map(item => [
item.imported.name,
item,
]),
).values(),
]
// Update the import declaration
reactImportPath.value.specifiers = allSpecifiers
// Replace all React.* member expressions with just the identifier
root
.find(j.MemberExpression)
.filter(path => {
const node = path.value
return (
node.object.type === 'Identifier' &&
node.object.name === 'React' &&
node.property.type === 'Identifier'
)
})
.replaceWith(path => {
return j.identifier(path.value.property.name)
})
// Replace all React.* JSX member expressions with just the identifier
root
.find(j.JSXMemberExpression)
.filter(path => {
const node = path.value
return node.object.name === 'React' && node.property.name
})
.replaceWith(path => {
return j.jsxIdentifier(path.value.property.name)
})
return root.toSource()
}
+106
View File
@@ -0,0 +1,106 @@
/**
* Codemod to replace namespaced React calls with named imports
*
* Before:
* import * as Toast from '#/view/com/util/Toast'
* Toast.show(message, 'xmark')
*
* After:
* import * as Toast from '#/components/Toast'
* Toast.show(message, {type: 'error'})
*
* Usage: jscodeshift -t .jscodeshift/repo/toast-v2.js <file-path>
* Example: jscodeshift -t .jscodeshift/repo/toast-v2.js src/App.native.tsx
*/
/* eslint-disable */
export const parser = 'tsx'
const OLD_IMPORT = '#/view/com/util/Toast'
const NEW_IMPORT = '#/components/Toast'
const convertLegacyToastType = type => {
switch (type) {
// these ones are fine
case 'default':
case 'success':
case 'error':
case 'warning':
case 'info':
return type
// legacy ones need conversion
case 'xmark':
return 'error'
case 'exclamation-circle':
return 'warning'
case 'check':
return 'success'
case 'clipboard-check':
return 'success'
case 'circle-exclamation':
case 'exclamation-circle':
return 'warning'
default:
return 'default'
}
}
export default function transformer(file, api) {
const j = api.jscodeshift
const root = j(file.source)
// Find Toast import declarations using the old path
const toastImports = root
.find(j.ImportDeclaration)
.filter(path => path.value.source.value === OLD_IMPORT)
if (toastImports.length === 0) {
return file.source
}
// Update import path
toastImports.forEach(path => {
path.value.source.value = NEW_IMPORT
})
// Collect all local names the Toast namespace is bound to
const toastLocalNames = new Set()
toastImports.forEach(path => {
path.value.specifiers.forEach(spec => {
if (spec.type === 'ImportNamespaceSpecifier') {
toastLocalNames.add(spec.local.name)
}
})
})
// Transform Toast.show(message, type) calls
root.find(j.CallExpression).forEach(path => {
const {callee, arguments: args} = path.value
// Match <ToastName>.show(...)
if (
callee.type !== 'MemberExpression' ||
callee.object.type !== 'Identifier' ||
!toastLocalNames.has(callee.object.name) ||
callee.property.name !== 'show'
) {
return
}
// Only transform 2-arg calls where the second arg is a string literal
if (args.length !== 2) return
const typeArg = args[1]
if (typeArg.type !== 'StringLiteral' && typeArg.type !== 'Literal') return
const legacyType = typeArg.value
const newType = convertLegacyToastType(legacyType)
// Replace the second argument with an options object: {type: 'newType'}
args[1] = j.objectExpression([
j.property('init', j.identifier('type'), j.stringLiteral(newType)),
])
})
return root.toSource()
}
+1 -1
View File
@@ -1 +1 @@
20
24.19.0
+406
View File
@@ -0,0 +1,406 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [
"typescript",
"react",
"import"
],
"categories": {
"correctness": "off"
},
"options": {
"typeAware": true
},
"env": {
"builtin": true
},
"settings": {
"import-x/extensions": [
".ts",
".tsx",
".cts",
".mts",
".js",
".jsx",
".cjs",
".mjs"
],
"import-x/external-module-folders": [
"node_modules",
"node_modules/@types"
],
"import-x/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx",
".cts",
".mts"
]
},
"import-x/resolver": {
"node": {
"extensions": [
".js",
".web.js",
".ios.js",
".android.js"
]
}
}
},
"ignorePatterns": [
"**/__mocks__/*.ts",
"ios/**",
"android/**",
"coverage/**",
"*.lock",
".husky/**",
"patches/**",
"*.html",
"bskyweb/**",
"bskyembed/**",
"bskyogcard/**",
"lint-rules/**",
"src/locale/locales/_build/**",
"src/locale/locales/**/*.js",
"src/lexicons/**",
"*.e2e.ts",
"*.e2e.tsx",
"eslint.config.mjs",
".jscodeshift/**"
],
"rules": {
"constructor-super": "error",
"for-direction": "error",
"getter-return": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-binary-expression": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-class-members": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-extra-boolean-cast": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-new-native-nonconstructor": "error",
"no-nonoctal-decimal-escape": "error",
"no-obj-calls": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-setter-return": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-unexpected-multiline": "error",
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-unused-vars": "error",
"no-useless-backreference": "error",
"no-useless-catch": "error",
"no-useless-escape": "error",
"no-with": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error",
"no-array-constructor": "error",
"no-unused-expressions": "error",
"import/namespace": "off",
"import/default": "error",
"import/no-named-as-default": "warn",
"import/no-named-as-default-member": "warn",
"import/no-duplicates": "warn",
"react/rules-of-hooks": "error",
"react/exhaustive-deps": "warn",
"typescript/await-thenable": "error",
"typescript/ban-ts-comment": "error",
"typescript/no-array-delete": "error",
"typescript/no-base-to-string": "error",
"typescript/no-duplicate-enum-values": "error",
"typescript/no-duplicate-type-constituents": "error",
"typescript/no-empty-object-type": "error",
"typescript/no-explicit-any": "error",
"typescript/no-extra-non-null-assertion": "error",
"typescript/no-floating-promises": "error",
"typescript/no-for-in-array": "error",
"typescript/no-implied-eval": "error",
"typescript/no-misused-new": "error",
"typescript/no-misused-promises": "error",
"typescript/no-namespace": "error",
"typescript/no-non-null-asserted-optional-chain": "error",
"typescript/no-redundant-type-constituents": "error",
"typescript/no-require-imports": "error",
"typescript/no-this-alias": "error",
"typescript/no-unnecessary-type-assertion": "error",
"typescript/no-unnecessary-type-constraint": "error",
"typescript/no-unsafe-argument": "error",
"typescript/no-unsafe-assignment": "error",
"typescript/no-unsafe-call": "error",
"typescript/no-unsafe-declaration-merging": "error",
"typescript/no-unsafe-enum-comparison": "error",
"typescript/no-unsafe-function-type": "error",
"typescript/no-unsafe-member-access": "error",
"typescript/no-unsafe-return": "error",
"typescript/no-unsafe-unary-minus": "error",
"typescript/no-wrapper-object-types": "error",
"typescript/only-throw-error": "error",
"typescript/prefer-as-const": "error",
"typescript/prefer-namespace-keyword": "error",
"typescript/prefer-promise-reject-errors": "error",
"typescript/require-await": "error",
"typescript/restrict-plus-operands": "error",
"typescript/restrict-template-expressions": "error",
"typescript/triple-slash-reference": "error",
"typescript/unbound-method": "error"
},
"overrides": [
{
"files": [
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.cts"
],
"rules": {
"constructor-super": "off",
"getter-return": "off",
"no-class-assign": "off",
"no-const-assign": "off",
"no-dupe-class-members": "off",
"no-dupe-keys": "off",
"no-func-assign": "off",
"no-import-assign": "off",
"no-new-native-nonconstructor": "off",
"no-obj-calls": "off",
"no-redeclare": "off",
"no-setter-return": "off",
"no-this-before-super": "off",
"no-unreachable": "off",
"no-unsafe-negation": "off",
"no-var": "error",
"no-with": "off",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error"
}
},
{
"files": [
"**/*.{js,jsx,ts,tsx}"
],
"rules": {
"bsky-internal/avoid-unwrapped-text": [
"error",
{
"impliedTextComponents": [
"H1",
"H2",
"H3",
"H4",
"H5",
"H6",
"P",
"Admonition",
"Admonition.Admonition",
"Toast.Action",
"AgeAssuranceAdmonition",
"Span",
"StackedButton"
],
"impliedTextProps": [],
"suggestedTextWrappers": {
"Button": "ButtonText",
"ToggleButton.Button": "ToggleButton.ButtonText",
"SegmentedControl.Item": "SegmentedControl.ItemText"
}
}
],
"bsky-internal/use-prefixed-imports": "error",
"bsky-internal/lingui-msg-rule": "error",
"react/display-name": "error",
"react/jsx-key": "error",
"react/jsx-no-comment-textnodes": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-target-blank": "error",
"react/jsx-no-undef": "error",
"react/no-children-prop": "error",
"react/no-danger-with-children": "error",
"react/no-direct-mutation-state": "error",
"react/no-find-dom-node": "error",
"react/no-is-mounted": "error",
"react/no-render-return-value": "error",
"react/no-string-refs": "error",
"react/no-unescaped-entities": "off",
"react/no-unknown-property": "error",
"react/no-unsafe": "off",
"react/react-in-jsx-scope": "off",
"react/hook-use-state": "warn",
"react-native-a11y/has-accessibility-hint": "error",
"react-native-a11y/has-accessibility-props": "error",
"react-native-a11y/has-valid-accessibility-actions": "error",
"react-native-a11y/has-valid-accessibility-component-type": "error",
"react-native-a11y/has-valid-accessibility-descriptors": "error",
"react-native-a11y/has-valid-accessibility-role": "error",
"react-native-a11y/has-valid-accessibility-state": "error",
"react-native-a11y/has-valid-accessibility-states": "error",
"react-native-a11y/has-valid-accessibility-traits": "error",
"react-native-a11y/has-valid-accessibility-value": "error",
"react-native-a11y/no-nested-touchables": "error",
"react-native-a11y/has-valid-accessibility-ignores-invert-colors": "error",
"react-native-a11y/has-valid-accessibility-live-region": "error",
"react-native-a11y/has-valid-important-for-accessibility": "error",
"react/react-compiler": "warn",
"simple-import-sort/imports": [
"error",
{
"groups": [
[
"^\\u0000"
],
[
"^node:"
],
[
"^(react\\/(.*)$)|^(react$)|^(react-native(.*)$)",
"^(expo(.*)$)|^(expo$)",
"^(?!(?:alf|components|lib|locale|logger|platform|screens|state|view)(?:$|\\/))@?\\w"
],
[
"^(?:#\\/)?(?:lib|state|logger|platform|locale)(?:$|\\/)",
"^(?:#\\/)?view(?:$|\\/)",
"^(?:#\\/)?screens(?:$|\\/)",
"^(?:#\\/)?alf(?:$|\\/)",
"^(?:#\\/)?components(?:$|\\/)",
"^#\\/",
"^\\."
],
[
"^"
]
]
}
],
"simple-import-sort/exports": "error",
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_.+",
"caughtErrors": "none",
"ignoreRestSiblings": true
}
],
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "react",
"importNames": [
"React",
"default"
],
"message": "React is already in the global type namespace. Use named imports for runtime modules."
}
]
}
],
"no-empty-pattern": "off",
"no-async-promise-executor": "off",
"no-constant-binary-expression": "warn",
"prefer-const": "off",
"no-empty": "off",
"no-unsafe-optional-chaining": "off",
"no-prototype-builtins": "off",
"no-var": "off",
"prefer-rest-params": "off",
"no-case-declarations": "off",
"no-irregular-whitespace": "off",
"no-useless-escape": "off",
"no-sparse-arrays": "off",
"no-fallthrough": "off",
"no-control-regex": "off",
"no-unused-expressions": [
"error",
{
"allowTernary": true
}
],
"import/consistent-type-specifier-style": [
"warn",
"prefer-inline"
],
"import/no-nodejs-modules": "error",
"typescript/consistent-type-imports": [
"warn",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/no-require-imports": "off",
"typescript/ban-ts-comment": "off",
"typescript/no-empty-object-type": "off",
"typescript/no-unsafe-function-type": "off",
"typescript/no-unsafe-assignment": "off",
"typescript/unbound-method": "off",
"typescript/no-unsafe-argument": "off",
"typescript/no-unsafe-return": "off"
},
"jsPlugins": [
"eslint-plugin-bsky-internal",
"eslint-plugin-react-native-a11y",
"eslint-plugin-simple-import-sort"
],
"env": {
"es2026": true,
"browser": true,
"node": true
}
},
{
"files": [
"bskylink/**/*.{js,jsx,ts,tsx}",
"bskyogcard/**/*.{js,jsx,ts,tsx}",
"dev-env/**/*.{js,jsx,ts,tsx}"
],
"rules": {
"import/no-nodejs-modules": "off"
}
},
{
"files": [
"**/__tests__/**/*.{js,jsx,ts,tsx}",
"**/*.test.{js,jsx,ts,tsx}"
],
"env": {
"jest": true
}
}
]
}
+5
View File
@@ -5,15 +5,20 @@
!**/*.jsx
!**/*.ts
!**/*.tsx
!**/*.mjs
!**/*.cjs
!*/
!**/*.css
!**/pnpm-workspace.yaml
# More specific ignores go below.
.expo
android
ios
src/locale/locales
src/lexicons
lib/react-compiler-runtime
bskyweb/static
coverage
+185
View File
@@ -0,0 +1,185 @@
# Asset licensing
The [MIT license](./LICENSE) in this repository covers our source code. It does not cover every file in the tree.
Some of the images, icons, fonts, and brand assets here are licensed to Bluesky Social PBC by third parties, or are our trademarks, or are third-party trademarks. We cannot pass those rights on to you. This document identifies them and names who holds them.
## This is not a license change
The MIT license on our source code is unchanged. This document records rights that Bluesky never held, and therefore could never have granted you.
We updated this file in August 2026 so the repository no longer carries a blanket MIT license with no asset carve-out and forking guidelines that ignored commissioned artwork.
For the assets Bluesky itself owns, we are not treating anyone's past use as bad faith. For the rest we are not the rights holder. The tables below name them. If you have shipped one of these in a fork, the [If you are forking](#if-you-are-forking) checklist is the shortest path to a clean position.
## Summary
| Where | Rights holder | Our MIT license covers it? | If you fork |
|---|---|---|---|
| [`assets/illustrations/`](#1-commissioned-artwork--licensed-to-bluesky-only) | Owen D. Pomery, via Brilliant Artists Ltd | No | Replace |
| [`assets/icons/`](#2-licensed-icon-system--not-ours-to-pass-on) (top level), Central icon glyphs in `bskyembed/assets/` except the Starter Pack mark | Iconists (David & Storm GbR) | No | Source your own |
| [Bluesky marks](#3-bluesky-trademarks-and-brand-assets) — app icons, logos, favicons | Bluesky Social PBC | No | Replace |
| [`assets/kawaii.png`, `assets/kawaii_smol.png`](#4-community-and-contest-artwork--credited-but-not-ours-to-license) | [@sawaratsuki.bsky.social](https://bsky.app/profile/sawaratsuki.bsky.social) | No | Replace or remove |
| [`assets/icons/custom_logo_japan.svg`](#4-community-and-contest-artwork--credited-but-not-ours-to-license) | A Bluesky Japan logo contest entrant | No | Replace or remove |
| [`assets/icons/apple_logo.svg`](#5-third-party-trademarks) | Apple Inc. | No | Rests on your own basis |
| [`assets/icons/android_logo.svg`](#5-third-party-trademarks) | Google LLC | No | Rests on your own basis |
| [`assets/icons/community/`](#5-third-party-trademarks) | Leaflet, Offprint, pckt, Standard.site, Germ Network | No | Rests on your own basis |
| [`assets/fonts/inter/`](#6-third-party-assets-you-may-redistribute), Inter files in `bskyogcard/src/assets/fonts/` | The Inter Project Authors | Separate — OFL 1.1 | **Keep, with the notice** |
| [Noto fonts downloaded by `bskyogcard/scripts/install-fonts.ts`](#6-third-party-assets-you-may-redistribute) | Adobe, Google LLC, and The Noto Project Authors | Separate — OFL 1.1 | **Keep, with the notice** |
| [`assets/icons/flags/`](#6-third-party-assets-you-may-redistribute) | @catamphetamine | Separate — MIT | **Keep, with the license** |
| [`bskyweb/static/media/MaterialIcons.*.ttf`](#6-third-party-assets-you-may-redistribute) | Google, Inc. | Separate — Apache 2.0 | **Keep, with the notice** |
| [`assets/images/`](#7-product-imagery--provenance-being-documented) | Mixed, and not yet fully documented — see Section 7 | No | Replace or ship without |
Everything in [Section 6](#6-third-party-assets-you-may-redistribute) is already permissively licensed. It is the largest group of files listed here and it needs no action from you beyond keeping the notices in place.
Assets are scoped by directory wherever possible, so that adding a file to a carved-out directory does not require an edit here. Individual paths are listed only where an asset does not sit in a dedicated directory.
---
## 1. Commissioned artwork — licensed to Bluesky only
**`assets/illustrations/`**
The landing-screen illustration, in light and dark variants (`assets/illustrations/illustration-mobile.png` and `assets/illustrations/illustration-mobile-dark.png`), used by `src/view/com/auth/SplashScreen.tsx`.
**Rights holder: Owen D. Pomery**, represented by Brilliant Artists Ltd. Bluesky Social PBC commissioned the work and holds a usage license. Copyright remains with the artist. Our license is limited to Bluesky's own products and channels, is exclusive to us, and does not permit us to sublicense the artwork or to distribute modified versions of it.
**If you are forking this repository, replace these files.** Because our license is exclusive, the artwork is not available for separate third-party licensing while that license runs. Please do not approach the artist or his agent for permission — the constraint is our agreement, not their willingness. If you have already shipped it, contact us and we will help you sort it out rather than leaving you to guess.
See [`assets/illustrations/README.md`](./assets/illustrations/README.md).
## 2. Licensed icon system — not ours to pass on
**`assets/icons/` (top level), and the Central icon glyphs in `bskyembed/assets/`, except `bskyembed/assets/starterPack.svg`**
**Rights holder: Iconists (David & Storm GbR).** The user-interface glyphs come from their [Central icon system](https://iconists.co/central). Bluesky Social PBC licenses them for use in our own products. **That license is for our own use. It does not include the right to pass any rights to the icons on to you.**
The fact that we have our own license does not mean that you cannot use these icons. It means that any right you have to use them has to come from Iconists, not us. Licenses are available from [iconists.co](https://iconists.co), and there are openly licensed alternatives if you prefer that.
This section covers every file at the top level of `assets/icons/` **except** those named elsewhere in this document — specifically `assets/icons/logomark.svg`, `assets/icons/newskie.svg`, `assets/icons/verifiedCheck.svg`, `assets/icons/verifierCheck.svg`, `assets/icons/starterPack.svg`, `assets/icons/starterPack_stroke2_corner0_rounded.svg`, `assets/icons/custom_logo_japan.svg`, `assets/icons/apple_logo.svg`, and `assets/icons/android_logo.svg`. The `assets/icons/flags/` and `assets/icons/community/` subdirectories are covered by [Section 6](#6-third-party-assets-you-may-redistribute) and [Section 5](#5-third-party-trademarks) respectively.
See [`assets/icons/README.md`](./assets/icons/README.md).
## 3. Bluesky trademarks and brand assets
**Rights holder: Bluesky Social PBC.** Our name, logo, butterfly mark, logotype, and app icons are our trademarks. They are not licensed to you under the MIT license or by this document. Use of them is governed by our [Trademark Policy](https://bsky.social/about/support/trademarks) and [Brand Guidelines](https://bsky.social/about/support/branding).
You may refer to Bluesky by name to describe interoperability or origin — for example, "a client for Bluesky," or "based on the Bluesky app." You may not use our marks as the identity of your own product or service, or in any way likely to suggest that Bluesky publishes, endorses, or supports it.
- `assets/app-icons/` — all iOS and Android app icon variants, including the `.icon` bundles
- `assets/favicon.png`
- `assets/logo.png`
- `assets/default-avatar.png`
- `assets/icon-android-foreground.png`
- `assets/icon-android-monochrome.png`
- `assets/icon-android-notification.png`
- `assets/splash/splash.png`
- `assets/splash/splash-dark.png`
- `assets/splash/android-splash-logo-white.png`
- `assets/icons/logomark.svg`
- `assets/icons/newskie.svg`
- `assets/icons/verifiedCheck.svg`
- `assets/icons/verifierCheck.svg`
- `assets/icons/starterPack.svg`
- `assets/icons/starterPack_stroke2_corner0_rounded.svg`
- `bskyembed/assets/logo.svg`
- `bskyembed/assets/logo_full_name.svg`
- `bskyembed/assets/starterPack.svg`
- `bskyweb/static/favicon.png`
- `bskyweb/static/favicon-16x16.png`
- `bskyweb/static/favicon-32x32.png`
- `bskyweb/static/apple-touch-icon.png`
- `bskyweb/static/safari-pinned-tab.svg`
- `bskyweb/static/social-card-default.png`
- `bskyweb/static/social-card-default-gradient.png`
- `bskyweb/embedr-static/favicon.png`
- `bskyweb/embedr-static/favicon-16x16.png`
- `bskyweb/embedr-static/favicon-32x32.png`
- `modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/`
- Inline vector path data in `src/view/icons/Logo.tsx`, `src/view/icons/Logomark.tsx`, `src/view/icons/LogomarkWithType.tsx`, and `src/view/icons/Logotype.tsx`
These files stay in this repository because the app needs them to build. **If you fork, replace them with your own** — that is the one thing this section asks of you. Shipping an app that looks like Bluesky is also a problem under the app stores' own rules on copycat apps, quite apart from trademark.
## 4. Community and contest artwork — credited, but not ours to license
These are third-party artworks that appear in the app with attribution. We hold no license that lets us pass rights to them on to you.
- `assets/kawaii.png` and `assets/kawaii_smol.png` — **rights holder:
[@sawaratsuki.bsky.social](https://bsky.app/profile/sawaratsuki.bsky.social)**. Shown as an opt-in variant and credited in `src/view/shell/Drawer.tsx` and `src/view/shell/desktop/RightNav.tsx`.
- `assets/icons/custom_logo_japan.svg`**rights holder: the entrant who won the Bluesky Japan logo contest.**
Replace or remove these if you fork. If you want to use them, contact the artist.
## 5. Third-party trademarks
These marks belong to other companies. We include them to identify their services in our UI — sign-in buttons, store badges, and links to third-party applications. We are neither granting nor withholding permission, because it is not ours to give. Your use of them rests on your own nominative-use basis or on permission from the mark owner.
- `assets/icons/apple_logo.svg`**Apple Inc.**
- `assets/icons/android_logo.svg`**Google LLC**
- `assets/icons/community/leaflet.svg`**Leaflet**
- `assets/icons/community/offprint.svg`**Offprint**
- `assets/icons/community/pckt.svg` and `assets/icons/community/pckt-full.svg`**pckt**
- `assets/icons/community/standard-site.svg`**Standard.site**
- `assets/icons/community/germ_logo.webp`**Germ Network**
Apple's and Google's marks in particular carry their own brand guidelines governing size, spacing, and permitted contexts. If you ship a sign-in button or a store badge, follow their guidelines.
## 6. Third-party assets you may redistribute
These are licensed on terms that permit redistribution. Nothing in this document restricts them. We list them so you know they are safe, and so you know to carry their notices. This is the largest group of assets in this document.
| Asset | Path | Rights holder | License | Notice |
|---|---|---|---|---|
| Inter typeface | `assets/fonts/inter/`, `bskyogcard/src/assets/fonts/Inter-*.ttf` | The Inter Project Authors | SIL Open Font License 1.1 | [`OFL.txt`](./assets/fonts/inter/OFL.txt) |
| Noto Sans families (OG card service) | Downloaded by `bskyogcard/scripts/install-fonts.ts` | Adobe, Google LLC, and The Noto Project Authors | SIL Open Font License 1.1 | [`README.md`](./bskyogcard/src/assets/fonts/README.md) |
| country-flag-icons | `assets/icons/flags/` | @catamphetamine | MIT | [`LICENSE`](./assets/icons/flags/LICENSE) |
| Material Icons | `bskyweb/static/media/MaterialIcons.*.ttf` | Google, Inc. | Apache License 2.0 | [`NOTICE.md`](./NOTICE.md) |
Build output under `bskyweb/static/media/` also contains compiled Inter files. They are the same OFL-licensed typeface, emitted by the web build. The bundled Inter license does not designate a Reserved Font Name.
The OG card build downloads Noto Sans fonts into `bskyogcard/src/assets/fonts/` and copies them into its build output. Their copyright notices and OFL text are in [`bskyogcard/src/assets/fonts/OFL-NOTO.txt`](./bskyogcard/src/assets/fonts/OFL-NOTO.txt). The CJK fonts reserve the name "Source."
See [`NOTICE.md`](./NOTICE.md) for the consolidated third-party notices.
## 7. Product imagery — provenance being documented
**`assets/images/`**
Product illustration and announcement imagery — onboarding art, chat backgrounds, feature announcement graphics, and similar.
**Rights holder: mixed, and we have not finished documenting it.** Some of this is Bluesky's own work. Some was commissioned from outside illustrators, on terms that do not let us pass rights on. We are working out which is which.
Until we have, **treat the whole directory as outside the MIT license and not licensed for your use.**
When this is resolved, one of two things will happen: this section will name the rights holder for each file, or the directory will be split so that the boundary itself carries the answer. If you need a specific file's status before then, ask us and we will find out.
If you are forking, replace these or ship without them. See [`assets/images/README.md`](./assets/images/README.md).
---
## If you are forking
You have our blessing to fork this application. These steps map one-to-one to the sections above.
1. **Replace `assets/illustrations/`** — commissioned artwork, licensed to Bluesky only. [Section 1](#1-commissioned-artwork--licensed-to-bluesky-only)
2. **Source your own UI icons** — the glyph set in `assets/icons/` is licensed to us for our own use. [Section 2](#2-licensed-icon-system--not-ours-to-pass-on)
3. **Replace the Bluesky marks** — app icons, favicons, logo files, and the inline logo paths in `src/view/icons/`. [Section 3](#3-bluesky-trademarks-and-brand-assets)
4. **Replace or remove the community and contest artwork.** [Section 4](#4-community-and-contest-artwork--credited-but-not-ours-to-license)
5. **Check your own position on the third-party marks.** [Section 5](#5-third-party-trademarks)
6. **Keep the assets you may redistribute, and keep their notices with them.** [Section 6](#6-third-party-assets-you-may-redistribute)
7. **Replace `assets/images/`, or ship without it.** [Section 7](#7-product-imagery--provenance-being-documented)
Then change your branding, support links, and analytics as described in the [Forking guidelines](./README.md#forking-guidelines). That part is not about licensing — it is what makes a fork clearly distinguishable from Bluesky, which matters both for your users and for app store review.
## Questions
If something in this repository looks like it should be on this list and is not, if a rights holder named here is wrong, or if you are unsure whether an asset is covered, open an issue or email [atmosphere@blueskyweb.xyz](mailto:atmosphere@blueskyweb.xyz).
## History
- **August 2026** — this document added, along with [`NOTICE.md`](./NOTICE.md), per-directory notices, and the required Apache 2.0 and OFL license texts. It documents pre-existing rights; it does not change the [MIT license](./LICENSE) or relicense any file.
- **Before that** — the repository carried a blanket MIT license with no asset carve-out, and the forking guidelines did not mention commissioned artwork, trademarks, or licensed icons.
---
*This document describes the licensing position of assets in this repository. It is not a grant of rights, and it does not modify the [MIT license](./LICENSE) as it applies to source code.*
+588
View File
@@ -0,0 +1,588 @@
# CLAUDE.md  Bluesky Social App Development Guide
This document provides guidance for working effectively in the Bluesky Social app codebase.
## Project Overview
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.2
- React Native 0.86 with Expo 57
- TypeScript 7
- React Navigation 7 for routing
- TanStack Query (React Query) for data fetching
- 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
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 pnpm scripts, never call the underlying tools directly
pnpm test # Run Jest tests
pnpm lint # Run Oxlint
pnpm typecheck # Run TypeScript type checking
pnpm prettier # Run Prettier for code formatting
# Internationalization
# DO NOT run these commands - extraction and compilation are handled by CI
pnpm intl:extract # Extract translation strings (nightly CI job)
pnpm intl:compile # Compile translations for runtime (nightly CI job)
# Build
pnpm build-web # Build web version
pnpm prebuild # Generate native projects
```
## Project Structure
```
src/
├── alf/ # Design system (ALF) - themes, atoms, tokens
├── components/ # Shared UI components (Button, Dialog, Menu, etc.)
├── screens/ # Full-page screen components (newer pattern)
├── features/ # Macro-features that bridge components/screens
├── view/
│ ├── screens/ # Full-page screens (legacy location)
│ ├── com/ # Reusable view components
│ └── shell/ # App shell (navigation bars, tabs)
├── state/
│ ├── queries/ # TanStack Query hooks
│ ├── preferences/ # User preferences (React Context)
│ ├── session/ # Authentication state
│ └── persisted/ # Persistent storage layer
├── lib/ # Utilities, constants, helpers
├── locale/ # i18n configuration and language files
└── Navigation.tsx # Main navigation configuration
```
### Project Structure in Depth
When building new things, follow these guidelines for where to put code.
#### Components vs Screens vs Features
**Components** are reusable UI elements that are not full screens. Should be
platform-agnostic when possible. Examples: Button, Dialog, Menu, TextField. Put
these in `/components` if they are shared across screens.
**Screens** are full-page components that represent a route in the app. They
often contain multiple components and handle layout for a page. New screens
should go in `/screens` (not `/view/screens`) to encourage better organization
and separation from legacy code.
For complex screens that have specific components or data needs that _are not
shared by other screens_, we encourage subdirectories within `/screens/<name>`
e.g. `/screens/ProfileScreen/ProfileScreen.tsx` and
`/screens/ProfileScreen/components/`.
**Features** are higher-level modules that may include context, data fetching,
components, and utilities related to a specific feature e.g.
`/features/liveNow`. They don't neatly fit into components or screens and often
span multiple screens. This is an optional pattern for organizing complex
features.
#### Legacy Directories
For the most part, avoid writing new files into the `/view` directory and
subdirectories. This is the older pattern for organizing screens and components,
and it has become a bit disorganized over time. New development should go into
`/screens`, `/components`, and `/features`.
#### State
The `/state` directory is where we've historically put all our data fetching and
state management logic. This is perfectly fine, but for new features, consider
organizing state logic closer to the components that use it, either within a
feature directory or co-located with a screen. The key is to keep related code
together and avoid having "god files" with too much unrelated logic.
#### Lib
The `/lib` directory is for utilities and helpers that don't fit into other
categories. This can include things like API clients, formatting functions,
constants, and other shared logic.
#### Top Level Directories
Avoid writing new top-level subdirectories within `/src`. We've done this for a
few things in the past that, but we have stronger patterns now. Examples:
`/logger` should probably have been written into `/lib`. And `ageAssurance` is
better classified within `/features`. We will probably migrate these things
eventually.
### File and Directory Naming Conventions
Typically JS style for variables, functions, etc. We use ProudCamelCase for
components, and camelCase directories and files.
For "macro" cases in `/features`, `/screens`, or `/components`, co-locate related
code in a directory with an `index.tsx` main component plus sibling
components/hooks/utils (e.g. `screens/ProfileScreen/index.tsx` +
`screens/ProfileScreen/components/`). Keep related code together so it lives where
someone would look for it. Don't overdo it: a component that fits in one file
should just be `Component.tsx`, not `Component/index.tsx`.
Platform-specific files are covered under "Platform-Specific Code" below.
### Comments
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 `—`.
Always use docblock (`/** */`) syntax for comments that document a type, type
member, method, function, or variable. These are the comments a reader expects
to find attached to a named declaration, and the docblock form makes that intent
clear and surfaces nicely in editor tooltips.
```tsx
type DateFieldProps = {
/**
* An empty string renders the placeholder and opens the picker at today (or
* maximumDate, if earlier).
*/
value: string | Date
}
/**
* Date-only input. Accepts a string in the format YYYY-MM-DD, or a Date object.
*/
export function DateField() {}
```
More generally, any multiline comment should use the `/* */` block syntax rather
than stacked `//` lines. Reserve `//` for short, single-line comments.
```tsx
/*
* The picker requires a valid date, so when value is empty we fall back to
* maximumDate (if set) or today.
*/
const fallbackDate = maximumDate ? toSimpleDateString(maximumDate) : today
```
### Documentation and Tests Within Features
For larger features or components, co-locate documentation and tests with the
code. A `README.md` in the directory (the `/Component/index.tsx` pattern lends
itself well to this) can document the whole feature, and feature-specific tests
belong alongside it as `Component.test.tsx` or in a `__tests__/` subdirectory.
Both are optional.
## Styling System (ALF)
ALF is the custom design system. Tailwind-inspired naming with underscores
instead of hyphens. Static atoms (`atoms as a`) are theme-independent; theme
atoms/palette come from `useTheme()` (`t.atoms.bg`, `t.palette.primary_500`).
Style props take an array of atoms + theme atoms + raw styles.
Order atoms by: flexbox (`a.flex_row`), spacing (`a.px_md`), text (`a.font_bold`),
themes (`t.atoms.text`), then raw styles (`{backgroundColor: t.palette.primary_500}`).
```tsx
import {atoms as a, useTheme} from '#/alf'
const t = useTheme()
<View style={[a.flex_row, a.gap_md, a.p_lg, t.atoms.bg]} />
```
### Key Concepts
Static atoms live in `a.*` (e.g. `a.flex_row`, `a.p_md`, `a.rounded_md`,
`a.text_lg`). Theme atoms/palette come from `useTheme()` (`t.atoms.bg`,
`t.atoms.text`, `t.atoms.border_contrast_low`, `t.palette.primary_500`).
**Platform utilities** (`import {web, native, ios, android, platform} from '#/alf'`)
return conditional styles inline in a style array: `web({cursor: 'pointer'})`,
`native({paddingBottom: 20})`, `platform({ios: {...}, android: {...}, web: {...}})`.
**Breakpoints:** `const {gtPhone, gtMobile, gtTablet} = useBreakpoints()` from `#/alf`.
### Naming Conventions
- Spacing: `2xs`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl` (t-shirt sizes)
- Text: `text_xs`, `text_sm`, `text_md`, `text_lg`, `text_xl`
- Gaps/Padding: `gap_sm`, `p_md`, `px_lg`, `py_xl`
- Flex: `flex_row`, `flex_1`, `align_center`, `justify_between`
- Borders: `border`, `border_t`, `rounded_md`, `rounded_full`
## 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.
- Prefer the implicit global `React` for types over `type` imports.
```tsx
import {Fragment} from 'react'
import {View} from 'react-native'
import {Trans} from '@lingui/react/macro'
import {Text} from '#/components/Typography'
function MyComponent({
items = [],
children,
}: {
items?: string[]
children: React.ReactNode
}) {
return (
<>
<View>
<Text>
<Trans>Example</Trans>
</Text>
</View>
<View>
{items.map((item, index) => (
<Fragment key={item}>
<Text>{index}</Text>
<Text>{item}</Text>
</Fragment>
))}
{children}
</View>
</>
)
}
```
### Dialog Component
Lives in `#/components/Dialog`. Bottom sheet on native, modal on web. Manage
state with `useDialogControl()`. `Dialog.Handle` renders native-only, `Dialog.Close`
web-only. CRITICAL: run any post-close action inside the `control.close(() => ...)`
callback (see Footguns). Compound-component usage; canonical example in any dialog
under `#/components`.
### Menu Component
Lives in `#/components/Menu`. Dropdown on web, bottom sheet dialog on native.
`Menu.Divider` is web-only, `Menu.ContainerItem` native-only. Compound API
(`Menu.Root` / `Menu.Trigger` / `Menu.Outer` / `Menu.Group` / `Menu.Item`); grep
existing usages across the app for a canonical example.
### Button Component
`import {Button, ButtonText, ButtonIcon} from '#/components/Button'`. Props:
- `color`: `'primary'` | `'secondary'` | `'negative'` | `'primary_subtle'` | `'negative_subtle'` | `'secondary_inverted'`
- `size`: `'tiny'` | `'small'` | `'large'`
- `shape`: `'default'` (pill) | `'round'` | `'square'` | `'rectangular'`
- `variant`: `'solid'` | `'outline'` | `'ghost'` (deprecated, prefer `color`)
### TextField
Compound component at `#/components/forms/TextField` (`TextField.LabelText`,
`TextField.Root`, `TextField.Icon`, `TextField.Input`). Prefer `defaultValue` over
`value` (see Footguns).
### Typography
`import {Text, H1, H2, P} from '#/components/Typography'`. The `Text` default style
is `[a.text_sm, a.leading_snug, t.atoms.text]`. Pass the `emoji` prop to any `Text`
that may contain emoji - user-generated text (display names etc.) almost always
does, so only omit it for static, emoji-free strings: `<Text emoji>Hello!</Text>`.
## Internationalization (i18n)
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 {plural} from '@lingui/core/macro'
import {Trans, useLingui} from '@lingui/react/macro'
function MyComponent() {
const {t: l} = useLingui()
// 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 = l`Hello, ${name}!`
// Pluralization
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>, {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
pnpm intl:extract # Extract new strings to locale files
pnpm intl:compile # Compile translations for runtime
```
## State Management
### TanStack Query (Data Fetching)
Follow the established pattern in `src/state/queries/`; `src/state/queries/feed.ts`
is a good canonical reference (it uses `createQueryKey`, matching key roots,
`useInfiniteQuery`, and `persistedVersion`).
- Build query keys with `createQueryKey(root, args)` (from `#/state/queries/util`)
using an object for `args`. The key root variable should match the hook name.
- Naming conventions: `use[Name]Query` for queries, `use[Name]Mutation` for
mutations, `use[Name]CacheMutation` for helpers that mutate cached data directly.
- Stale times come from `STALE` in `src/state/queries/index.ts`: `STALE.SECONDS.FIFTEEN`,
`STALE.MINUTES.ONE`, `STALE.MINUTES.FIVE`, `STALE.HOURS.ONE`, `STALE.INFINITY`.
- Paginated atproto APIs (those returning a `cursor`) use `useInfiniteQuery` with
`getNextPageParam: page => page.cursor`; flatten results with
`data?.pages.flatMap(page => page.items) ?? []`.
- Persist a query across restarts by passing options:
`createQueryKey(root, args, {persistedVersion: n})`. Bumping `n` clears the old
persisted data and refetches - do this whenever the data shape changes.
- Error handling in mutations: don't log network errors (just inform the user),
handle typed XRPC errors specifically (e.g. `err instanceof SomeNsid.SomeError`),
and send unexpected errors to `logger.error('...', {safeMessage: error})`.
### Preferences (React Context)
Boolean/simple UI preferences are exposed as paired hooks from `#/state/preferences`,
e.g. `useAutoplayDisabled()` / `useSetAutoplayDisabled()`.
### Session State
`import {useSession, useAgent} from '#/state/session'`. `useSession()` gives
`hasSession` and `currentAccount`; `useAgent()` gives the atproto agent for API calls.
## Navigation
React Navigation with type-safe route params. Type a screen with
`NativeStackScreenProps<CommonNavigatorParams, 'X'>` (`route`/`navigation` come
from props; params via `route.params`). Navigate programmatically with
`useNavigation()`, or the `navigate` helper from `#/Navigation`. Config lives in
`src/Navigation.tsx`, routes in `src/routes.ts`, types in `src/lib/routes/types.ts`.
## Platform-Specific Code
Use file extensions for platform-specific implementations. The bundler resolves
them automatically - just import the base path normally, never a conditional
`require()`.
```
Component.tsx # Shared/default
Component.web.tsx # Web-only
Component.native.tsx # iOS + Android
Component.ios.tsx # iOS-only
Component.android.tsx # Android-only
```
Prefer grouping variants into a `Component/` directory (`index.tsx`,
`index.web.tsx`, `index.native.tsx`) rather than sibling `Component.web.tsx` files,
so the shared surface reads as one "macro" module (e.g. `src/components/Dialog/index.tsx`
native vs `index.web.tsx` web). The app has both patterns; the directory form is
preferred for new code.
```tsx
// CORRECT - bundler picks storage.ts or storage.web.ts automatically
import * as storage from '#/state/drafts/storage'
// WRONG - don't use require() or conditional imports for platform files
const storage = IS_NATIVE
? require('#/state/drafts/storage')
: require('#/state/drafts/storage.web')
```
Runtime platform detection (not for imports): `import {IS_WEB, IS_NATIVE, IS_IOS, IS_ANDROID} from '#/env'`.
## Import Aliases
Always use the `#/` alias for absolute imports:
```tsx
// Good
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
// Avoid
import {useSession} from '../../../state/session'
```
## Footguns
Common pitfalls to avoid in this codebase:
### Dialog Close Callback (Critical)
**Always use `control.close(() => ...)` when performing actions after closing a dialog.** The callback ensures the action runs after the dialog's close animation completes. Failing to do this causes race conditions with React state updates.
```tsx
// WRONG - causes bugs with state updates, navigation, opening other dialogs
const onConfirm = () => {
control.close()
navigation.navigate('Home') // May race with dialog animation
}
// WRONG - same problem
const onConfirm = () => {
control.close()
otherDialogControl.open() // Will likely fail or cause visual glitches
}
// CORRECT - action runs after dialog fully closes
const onConfirm = () => {
control.close(() => {
navigation.navigate('Home')
})
}
// CORRECT - opening another dialog after close
const onConfirm = () => {
control.close(() => {
otherDialogControl.open()
})
}
// CORRECT - state updates after close
const onConfirm = () => {
control.close(() => {
setSomeState(newValue)
onCallback?.()
})
}
```
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`.
### Controlled vs Uncontrolled Inputs
Prefer `defaultValue` over `value` for TextInput on the old architecture:
```tsx
// Preferred - uncontrolled
<TextField.Input
defaultValue={initialEmail}
onChangeText={setEmail}
/>
// Avoid when possible - controlled (can cause performance issues)
<TextField.Input
value={email}
onChangeText={setEmail}
/>
```
### 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
Always test on multiple platforms when using these components.
### React Compiler is Enabled
This codebase uses React Compiler, so **don't proactively add `useMemo` or `useCallback`**. The compiler handles memoization automatically.
```tsx
// UNNECESSARY - React Compiler handles this
const handlePress = useCallback(() => {
doSomething()
}, [doSomething])
// JUST WRITE THIS
const handlePress = () => {
doSomething()
}
```
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
## Best Practices
1. **Accessibility**: Always provide `label` prop for interactive elements, use `accessibilityHint` where helpful
2. **Translations**: Wrap ALL user-facing strings with the `` l`` `` macro or the `<Trans>` component
3. **Styling**: Combine static atoms with theme atoms, use platform utilities for platform-specific styles
4. **State**: Use TanStack Query for server state, React Context for UI preferences
5. **Components**: Check if a component exists in `#/components/` before creating new ones
6. **Types**: Define explicit types for props, use `NativeStackScreenProps` for screens
7. **Testing**: Components should have `testID` props for E2E testing
## 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` |
+49 -41
View File
@@ -1,23 +1,21 @@
FROM golang:1.25-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"]
+4 -5
View File
@@ -1,4 +1,4 @@
FROM node:20.11-alpine3.18 as build
FROM node:24.19.0-alpine3.23 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.19.0-alpine3.23
RUN apk add --update dumb-init
@@ -26,11 +26,10 @@ COPY --from=build /app /app
RUN mkdir /app/data && chown node /app/data
VOLUME /app/data
EXPOSE 3000
EXPOSE 3000 9090
ENV LINK_PORT=3000
ENV LINK_METRICS_PORT=9090
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.19.0-alpine3.23 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.21.0
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.19.0-alpine3.23
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
+12 -8
View File
@@ -1,20 +1,24 @@
FROM golang:1.25-bookworm AS build-env
FROM golang:1.26-bookworm AS build-env
WORKDIR /usr/src/social-app
ENV DEBIAN_FRONTEND=noninteractive
# Node
ENV NODE_VERSION=20
ENV NODE_VERSION=24.19.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.21.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
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 20232025 Bluesky Social PBC
Copyright 20232026 Bluesky Social PBC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+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
+62
View File
@@ -0,0 +1,62 @@
# Third-party notices
This file collects the attribution and license notices that third-party components in this repository require us to carry. It is separate from [`ASSETS.md`](./ASSETS.md), which describes which assets our [MIT license](./LICENSE) does and does not cover.
If you distribute this software or a fork of it, these notices need to travel with it.
---
## Material Icons
**Path:** `bskyweb/static/media/MaterialIcons.*.ttf`
**License:** Apache License, Version 2.0 — full text at [`licenses/APACHE-2.0.txt`](./licenses/APACHE-2.0.txt)
```
Copyright 2018 Google, Inc. All Rights Reserved.
```
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*This font is emitted into the web build by `@expo/vector-icons`, which the Expo toolchain pulls in transitively. It is a distributed artifact rather than a source asset, which is why the notice lives here rather than beside the file.*
## Inter
**Paths:** `assets/fonts/inter/`, `bskyogcard/src/assets/fonts/Inter-*.ttf`, and compiled copies under `bskyweb/static/media/`
**License:** SIL Open Font License, Version 1.1 — full text at [`assets/fonts/inter/OFL.txt`](./assets/fonts/inter/OFL.txt)
```
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
```
Inter is licensed under the SIL Open Font License, Version 1.1. The bundled license does not designate a Reserved Font Name.
## Noto Sans
**Generated by:** `bskyogcard/scripts/install-fonts.ts`, into `bskyogcard/src/assets/fonts/` and the OG card build output
**License:** SIL Open Font License, Version 1.1 — full text at [`bskyogcard/src/assets/fonts/OFL-NOTO.txt`](./bskyogcard/src/assets/fonts/OFL-NOTO.txt)
```
(c) 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.
Copyright 2015-2020 Google LLC. All Rights Reserved.
Copyright 2024 The Noto Project Authors (https://github.com/notofonts/hebrew)
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/thai)
```
The OG card build downloads Noto Sans Arabic, Hebrew, HK, JP, KR, SC, TC, and Thai from Fontsource. All are licensed under OFL 1.1. The CJK families reserve the name "Source."
## country-flag-icons
**Path:** `assets/icons/flags/`
**License:** MIT — full text at [`assets/icons/flags/LICENSE`](./assets/icons/flags/LICENSE)
```
Copyright (c) 2020 @catamphetamine <purecatamphetamine@gmail.com>
```
---
For the licensing position of assets that are **not** covered by our MIT license — commissioned artwork, the licensed icon system, Bluesky trademarks, third-party marks, and the product imagery in `assets/images/` — see [`ASSETS.md`](./ASSETS.md), which names known rights holders and identifies the product imagery whose provenance is still being documented.
+14 -1
View File
@@ -57,6 +57,15 @@ Please be sure to:
- Change all branding in the repository and UI to clearly differentiate from Bluesky.
- Change any support links (feedback, email, terms of service, etc) to your own systems.
- Replace any analytics or error-collection systems with your own so we don't get super confused.
- Replace the landing-screen illustration in `assets/illustrations/`. It is commissioned artwork licensed to Bluesky alone, and our MIT license does not cover it.
- Source your own UI icons. The glyph set in `assets/icons/` is licensed to us by a third party for our own use, and that license does not extend to you.
- Replace the Bluesky logo, app icons, and other brand assets. Our trademarks are not licensed with the code.
Please read [./ASSETS.md](./ASSETS.md) before you ship. Not every file in this repository is
covered by our MIT license — some of the artwork, icons, fonts, and brand assets are licensed to
us by third parties or are trademarks, and `ASSETS.md` says which ones and what to do about them.
That file is new. Its absence is why some forks have shipped assets they did not have rights to,
and that was our omission rather than theirs.
## Security disclosures
@@ -68,7 +77,11 @@ Bluesky is an open social network built on the AT Protocol, a flexible technolog
## License (MIT)
See [./LICENSE](./LICENSE) for the full license.
See [./LICENSE](./LICENSE) for the full license, which covers the source code in this repository.
It does not cover every file. Certain images, icons, fonts, and brand assets are licensed to us
by third parties, or are trademarks, and are carved out — see [./ASSETS.md](./ASSETS.md). Required
third-party attribution notices are collected in [./NOTICE.md](./NOTICE.md).
Bluesky Social PBC has committed to a software patent non-aggression pledge. For details see [the original announcement](https://bsky.social/about/blog/10-01-2025-patent-pledge).
+16
View File
@@ -44,6 +44,12 @@ appId: xyz.blueskyweb.app
id: "e2eRefreshHome"
- tapOn:
id: "replyBtn"
# Wait for the composer to fully open before typing. Tapping replyBtn right
# after the previous publish can race the closing composer on Android.
- extendedWaitUntil:
visible:
id: "composerPublishBtn"
timeout: 10000
- inputText: "Reply text only"
- tapOn:
id: "composerPublishBtn"
@@ -51,6 +57,11 @@ appId: xyz.blueskyweb.app
id: "composeFAB"
- tapOn:
id: "replyBtn"
# Wait for the composer to fully open before typing.
- extendedWaitUntil:
visible:
id: "composerPublishBtn"
timeout: 10000
- inputText: "Reply with an image"
- tapOn:
id: "openMediaBtn"
@@ -63,6 +74,11 @@ appId: xyz.blueskyweb.app
id: "composeFAB"
- tapOn:
id: "replyBtn"
# Wait for the composer to fully open before typing.
- extendedWaitUntil:
visible:
id: "composerPublishBtn"
timeout: 10000
- inputText: "Reply with a https://example.com link card"
- tapOn:
id: "composerPublishBtn"
+10 -8
View File
@@ -39,7 +39,6 @@ appId: xyz.blueskyweb.app
id: "editListNameInput"
- eraseText
- inputText: "Bad Ppl"
- hideKeyboard
- tapOn:
id: "editListDescriptionInput"
- eraseText
@@ -92,7 +91,8 @@ appId: xyz.blueskyweb.app
- tapOn: "Add user to list"
- swipe:
direction: DOWN
- assertVisible: "View Bob's profile"
- assertVisible:
id: "profileCard-bob.test-link"
- tapOn: "Posts"
- assertVisible:
@@ -124,7 +124,8 @@ appId: xyz.blueskyweb.app
- tapOn: "Good Ppl"
- tapOn: "People"
- assertVisible: "View Bob's profile"
- assertVisible:
id: "profileCard-bob.test-link"
- tapOn:
point: "90%,43%"
- tapOn:
@@ -134,7 +135,8 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "bottomBarSearchBtn"
- tapOn: "Search for posts, users[,]? or feeds"
- tapOn:
id: "searchScreenInput"
- inputText: "bob"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
@@ -144,21 +146,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"
+81 -38
View File
@@ -29,45 +29,88 @@ appId: xyz.blueskyweb.app
id: "homeScreenFeedTabs-selector-1"
text: "alice-favs"
# Set alice-favs first
- tapOn: "Open drawer menu"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- tapOn:
label: "Tap on down arrow"
id: "feed-timeline-moveDown"
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
- assertVisible:
id: "homeScreenFeedTabs-selector-1"
text: "Following"
# Reordering feeds is driven by a drag on the feed-drag-handle. Maestro cannot
# activate the RNGH Pan gesture from a synthetic swipe on Android (proven
# twice - coordinate swipes never register the pan), so the reorder
# verification below runs on iOS only. If Android drag coverage is needed,
# revisit with the SavedFeedsA11y move buttons rather than a swipe.
- runFlow:
when:
platform: iOS
commands:
# Set alice-favs first
- tapOn: "Open drawer menu"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- swipe:
label: "Drag feed down"
from:
id: "feed-drag-handle"
direction: "DOWN"
duration: 1000
- assertVisible:
id: "saveChangesBtn"
enabled: true
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
- assertVisible:
id: "homeScreenFeedTabs-selector-1"
text: "Following"
# Set following first
- tapOn: "Open drawer menu"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- tapOn:
label: "Tap on down arrow"
id: "feed-feed-moveDown"
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "Following"
- assertVisible:
id: "homeScreenFeedTabs-selector-1"
text: "alice-favs"
# Set following first
- tapOn: "Open drawer menu"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- swipe:
label: "Drag feed down"
from:
id: "feed-drag-handle"
direction: "DOWN"
duration: 1000
- assertVisible:
id: "saveChangesBtn"
enabled: true
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "Following"
- assertVisible:
id: "homeScreenFeedTabs-selector-1"
text: "alice-favs"
# On Android, the reorder path above is skipped. Smoke-test that the feeds
# edit screen opens and the pinned feeds render, then return to a valid state.
- runFlow:
when:
platform: Android
commands:
- tapOn: "Open drawer menu"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- assertVisible: "Following"
- assertVisible: "alice-favs"
# Two back presses to reach Home: the first pops the saved-feeds editor
# back to the Feeds screen, the second pops Feeds back to Home. On iOS the
# equivalent path saves changes first (saveChangesBtn calls
# navigation.goBack), so a single "Go back" there already lands on Home.
# This Android smoke branch never saves, so it needs the extra pop to
# leave the screen state on Home, which the shared steps below expect.
- tapOn: "Go back"
- tapOn: "Go back"
# Remove following
- tapOn: "Open drawer menu"
+9 -2
View File
@@ -11,11 +11,18 @@ appId: xyz.blueskyweb.app
- tapOn: "Sign in"
- tapOn:
id: "selectServiceButton"
- tapOn: "Custom"
- tapOn:
id: "manualSelectBtn"
- tapOn:
id: "customServerTextInput"
- inputText: "http://localhost:3000"
- tapOn: "Done"
- runFlow:
when:
platform: Android
commands:
- hideKeyboard
- tapOn:
id: "doneBtn"
- tapOn:
id: "loginUsernameInput"
- inputText: "Alice"
@@ -15,6 +15,24 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "e2eStartOnboarding"
- tapOn: "Open avatar creator"
# The avatar-creator bottom sheet (Dialog.Inner, non-scrollable) opens only
# half-expanded on the short E2E emulator (720x1600), a ~220px sliver with the
# emoji grid below the fold. It is NOT a scroll view, so scrollUntilVisible's
# swipe grabs the sheet's own drag gesture and flings it closed. Instead, drag
# the sheet upward to expand it to full height, which brings the picker into
# view. iOS opens the sheet fully already, so this is Android-only.
- runFlow:
when:
platform: Android
commands:
- swipe:
label: "Drag the bottom sheet up to expand it"
start: "50%, 90%"
end: "50%, 20%"
duration: 600
- extendedWaitUntil:
visible: "Select an emoji"
timeout: 10000
- tapOn: "Select the zap emoji as your avatar"
- tapOn:
label: "Tap on yellow"
@@ -22,6 +40,20 @@ appId: xyz.blueskyweb.app
- tapOn: "Done"
- waitForAnimationToEnd
- tapOn: "Select an avatar"
# Reopening the creator sheet lands on the same half-expanded sliver on
# Android, so expand it again before reaching for the emoji grid. No-op on iOS.
- runFlow:
when:
platform: Android
commands:
- swipe:
label: "Drag the bottom sheet up to expand it"
start: "50%, 90%"
end: "50%, 20%"
duration: 600
- extendedWaitUntil:
visible: "Select an emoji"
timeout: 10000
- tapOn: "Select the atom emoji as your avatar"
- tapOn: "Done"
- waitForAnimationToEnd
+22 -7
View File
@@ -16,13 +16,28 @@ appId: xyz.blueskyweb.app
id: "e2eStartOnboarding"
- tapOn: "Select an avatar"
- waitForAnimationToEnd
- assertVisible: "Photos"
- assertVisible: "Collections"
- tapOn:
point: "50%,22%"
- waitForAnimationToEnd
- tapOn: "Done"
- waitForAnimationToEnd
- runFlow:
when:
platform: iOS
commands:
- assertVisible: "Photos"
- assertVisible: "Collections"
- tapOn:
point: "50%,22%"
- waitForAnimationToEnd
- tapOn: "Done"
- waitForAnimationToEnd
- runFlow:
when:
platform: Android
commands:
# The system photo picker opened here shows MediaStore photos, which
# the e2e run doesn't seed (media is seeded into app-scoped storage for
# the composer's mocked picker instead). With no photo to pick, dismiss
# the picker and continue - onContinue falls back to the generated
# placeholder avatar, and nothing later in the flow depends on the image.
- back
- waitForAnimationToEnd
- tapOn:
id: "onboardingContinue"
- assertVisible: "What are your interests?"
+5 -2
View File
@@ -45,7 +45,7 @@ appId: xyz.blueskyweb.app
id: "editProfileSaveBtn"
- assertNotVisible:
id: "editProfileModal"
- assertVisible: "Alicia"
- assertVisible: ".*Alicia.*"
- assertVisible: "One cool hacker"
# Remove display name and description via the edit profile modal
@@ -64,7 +64,10 @@ appId: xyz.blueskyweb.app
id: "editProfileSaveBtn"
- assertNotVisible:
id: "editProfileModal"
- assertVisible: "alice.test"
# The display-name node renders the handle as a Text with a nested badge View
# once the display name is cleared, so the a11y text is not the bare handle
# string on Android. Match it as a substring instead.
- assertVisible: ".*alice\\.test.*"
- assertNotVisible: "One cool hacker"
# Set avi and banner via the edit profile modal
+2 -1
View File
@@ -15,7 +15,8 @@ appId: xyz.blueskyweb.app
id: "bottomBarSearchBtn"
- tapOn:
id: "bottomBarSearchBtn"
- tapOn: "Search for posts, users[,]? or feeds"
- tapOn:
id: "searchScreenInput"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
@@ -22,5 +22,7 @@ appId: xyz.blueskyweb.app
text: "Send report to Dev-env Moderation"
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
- extendedWaitUntil:
notVisible:
id: "report:dialog"
timeout: 20000
+4 -2
View File
@@ -22,5 +22,7 @@ appId: xyz.blueskyweb.app
text: "Send report to Dev-env Moderation"
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
- extendedWaitUntil:
notVisible:
id: "report:dialog"
timeout: 20000
@@ -39,5 +39,7 @@ appId: xyz.blueskyweb.app
text: Your report will be sent to Dev-env Moderation.*
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
- extendedWaitUntil:
notVisible:
id: "report:dialog"
timeout: 20000
@@ -26,7 +26,10 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "report:details"
- inputText: "This is a test report"
- hideKeyboard
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
- extendedWaitUntil:
notVisible:
id: "report:dialog"
timeout: 20000
+4 -2
View File
@@ -15,8 +15,10 @@ appId: xyz.blueskyweb.app
id: "bottomBarSearchBtn"
- tapOn:
id: "bottomBarSearchBtn"
- assertVisible: "Search for posts, users[,]? or feeds"
- tapOn: "Search for posts, users[,]? or feeds"
- assertVisible:
id: "searchScreenInput"
- tapOn:
id: "searchScreenInput"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
+85 -12
View File
@@ -20,6 +20,12 @@ appId: xyz.blueskyweb.app
- inputText: "Test thread"
- tapOn:
id: "composerPublishBtn"
# Wait for the composer to close and the home feed to settle before signing
# out. Without a settle guard the next action can race the closing composer.
- extendedWaitUntil:
visible:
id: "composeFAB"
timeout: 10000
# Login, reply to the thread, and log out
- tapOn:
@@ -31,9 +37,19 @@ appId: xyz.blueskyweb.app
id: "viewHeaderHomeFeedPrefsBtn"
- tapOn:
id: "replyBtn"
# Wait for the composer to fully open before typing.
- extendedWaitUntil:
visible:
id: "composerPublishBtn"
timeout: 10000
- inputText: "Reply 1"
- tapOn:
id: "composerPublishBtn"
# Wait for the composer to close before signing out.
- extendedWaitUntil:
visible:
id: "composeFAB"
timeout: 10000
# Login, confirm notification exists, mute thread, and log out
- tapOn:
@@ -45,10 +61,8 @@ appId: xyz.blueskyweb.app
id: "viewHeaderHomeFeedPrefsBtn"
- tapOn:
id: "bottomBarNotificationsBtn"
- assertVisible:
id: "feedItem-by-bob.test"
- tapOn:
id: "feedItem-by-bob.test"
- assertVisible: ".*Reply 1.*"
- tapOn: ".*Reply 1.*"
- tapOn:
id: "postDropdownBtn"
childOf:
@@ -67,16 +81,78 @@ appId: xyz.blueskyweb.app
id: "bottomBarProfileBtn"
- tapOn:
id: "profilePager-selector-1"
- tapOn:
id: "replyBtn"
# Both replies target the thread root ("Test thread" by alice), which sits at
# the top of bob's Replies tab. That tab renders each post in the thread with
# its own replyBtn, so scope the tap to the root post's card
# (feedItem-by-alice.test) rather than relying on which replyBtn Maestro picks
# first. This keeps both reply taps deterministic regardless of list order or
# how many posts have rendered.
#
# Even with the close-gating below, the replyBtn tap can land on a recycled list
# row while the author feed re-renders after a publish, and be swallowed so the
# composer never opens. Wrapping the tap + open-wait in retry makes opening the
# composer idempotent: a swallowed tap just re-taps until the publish button
# appears. A first-try success does not retry.
- retry:
maxRetries: 3
commands:
- tapOn:
id: "replyBtn"
childOf:
id: "feedItem-by-alice.test"
# Wait for the composer to fully open before typing.
- extendedWaitUntil:
visible:
id: "composerPublishBtn"
timeout: 10000
- inputText: "Reply 2"
- tapOn:
id: "composerPublishBtn"
- tapOn:
id: "replyBtn"
# Wait for the composer to actually close before opening it again. replyBtn
# stays in the accessibility tree behind the open composer sheet, so waiting on
# its visibility returns immediately and does not gate on the close animation or
# the author-feed re-render that follows a post - the next replyBtn tap then
# fires mid-transition and is swallowed, so the composer never opens. Gate on
# the publish button disappearing (the composer is gone), then confirm the
# reply button underneath is back and let animations settle.
- extendedWaitUntil:
notVisible:
id: "composerPublishBtn"
timeout: 15000
- extendedWaitUntil:
visible:
id: "replyBtn"
timeout: 10000
- waitForAnimationToEnd
# As with Reply 2: even after gating on the composer close, this tap can hit a
# recycled row during the post-publish feed re-render and be swallowed, so wrap
# the open in retry to make it idempotent.
- retry:
maxRetries: 3
commands:
- tapOn:
id: "replyBtn"
childOf:
id: "feedItem-by-alice.test"
# Wait for the composer to fully open before typing.
- extendedWaitUntil:
visible:
id: "composerPublishBtn"
timeout: 10000
- inputText: "Reply 3"
- tapOn:
id: "composerPublishBtn"
# Wait for the composer to actually close before signing out. As above,
# replyBtn stays visible behind the sheet, so gate on the publish button
# disappearing first, then confirm the reply button underneath has returned.
- extendedWaitUntil:
notVisible:
id: "composerPublishBtn"
timeout: 15000
- extendedWaitUntil:
visible:
id: "replyBtn"
timeout: 10000
# Login, confirm notifications dont exist, unmute the thread, ~~confirm notifications exist~~
# Mute thread behaviour no longer change old notifications after muting/unmuting a thread -sfn
@@ -92,10 +168,7 @@ appId: xyz.blueskyweb.app
- assertVisible: ".*Reply 1.*"
- assertNotVisible: ".*Reply 2.*"
- assertNotVisible: ".*Reply 3.*"
- assertVisible:
id: "feedItem-by-bob.test"
- tapOn:
id: "feedItem-by-bob.test"
- tapOn: ".*Reply 1.*"
- tapOn:
id: "postDropdownBtn"
childOf:
-529
View File
@@ -1,529 +0,0 @@
import {createServer as createHTTPServer} from 'node:http'
import {parse} from 'node:url'
import {createServer, type TestPDS} from '../jest/test-pds'
async function main() {
let server: TestPDS
createHTTPServer(async (req, res) => {
const url = parse(req.url || '/', true)
if (req.method !== 'POST') {
return res.writeHead(200).end()
}
try {
console.log('Closing old server')
await server?.close()
console.log('Starting new server')
const inviteRequired = url?.query && 'invite' in url.query
server = await createServer({inviteRequired})
console.log('Listening at', server.pdsUrl)
if (url?.query) {
if ('users' in url.query) {
console.log('Generating mock users')
await server.mocker.createUser('alice')
await server.mocker.createUser('bob')
await server.mocker.createUser('carla')
await server.mocker.users.alice.agent.upsertProfile(() => ({
displayName: 'Alice',
description: 'Test user 1',
}))
await server.mocker.users.bob.agent.upsertProfile(() => ({
displayName: 'Bob',
description: 'Test user 2',
}))
await server.mocker.users.carla.agent.upsertProfile(() => ({
displayName: 'Carla',
description: 'Test user 3',
}))
if (inviteRequired) {
await server.mocker.createInvite(server.mocker.users.alice.did)
}
}
if ('follows' in url.query) {
console.log('Generating mock follows')
await server.mocker.follow('alice', 'bob')
await server.mocker.follow('alice', 'carla')
await server.mocker.follow('bob', 'alice')
await server.mocker.follow('bob', 'carla')
await server.mocker.follow('carla', 'alice')
await server.mocker.follow('carla', 'bob')
}
if ('posts' in url.query) {
console.log('Generating mock posts')
for (let user in server.mocker.users) {
await server.mocker.users[user].agent.post({text: 'Post'})
}
}
if ('feeds' in url.query) {
console.log('Generating mock feed')
await server.mocker.createFeed('alice', 'alice-favs', [])
}
if ('thread' in url.query) {
console.log('Generating mock posts')
const res = await server.mocker.users.bob.agent.post({
text: 'Thread root',
})
await server.mocker.users.carla.agent.post({
text: 'Thread reply',
reply: {
parent: {cid: res.cid, uri: res.uri},
root: {cid: res.cid, uri: res.uri},
},
})
}
if ('mergefeed' in url.query) {
console.log('Generating mock users')
await server.mocker.createUser('alice')
await server.mocker.createUser('bob')
await server.mocker.createUser('carla')
await server.mocker.createUser('dan')
await server.mocker.users.alice.agent.upsertProfile(() => ({
displayName: 'Alice',
description: 'Test user 1',
}))
await server.mocker.users.bob.agent.upsertProfile(() => ({
displayName: 'Bob',
description: 'Test user 2',
}))
await server.mocker.users.carla.agent.upsertProfile(() => ({
displayName: 'Carla',
description: 'Test user 3',
}))
await server.mocker.users.dan.agent.upsertProfile(() => ({
displayName: 'Dan',
description: 'Test user 4',
}))
console.log('Generating mock follows')
await server.mocker.follow('alice', 'bob')
await server.mocker.follow('alice', 'carla')
console.log('Generating mock posts')
let posts: Record<string, any[]> = {
alice: [],
bob: [],
carla: [],
dan: [],
}
for (let i = 0; i < 10; i++) {
for (let user in server.mocker.users) {
if (user === 'alice') continue
posts[user].push(
await server.mocker.createPost(user, `Post ${i}`),
)
}
}
for (let i = 0; i < 10; i++) {
for (let user in server.mocker.users) {
if (user === 'alice') continue
if (i % 5 === 0) {
await server.mocker.createReply(user, 'Self reply', {
cid: posts[user][i].cid,
uri: posts[user][i].uri,
})
}
if (i % 5 === 1) {
await server.mocker.createReply(user, 'Reply to bob', {
cid: posts.bob[i].cid,
uri: posts.bob[i].uri,
})
}
if (i % 5 === 2) {
await server.mocker.createReply(user, 'Reply to dan', {
cid: posts.dan[i].cid,
uri: posts.dan[i].uri,
})
}
await server.mocker.users[user].agent.post({text: `Post ${i}`})
}
}
console.log('Generating mock feeds')
await server.mocker.createFeed(
'alice',
'alice-favs',
posts.dan.map(p => p.uri),
)
await server.mocker.createFeed(
'alice',
'alice-favs2',
posts.dan.map(p => p.uri),
)
}
if ('labels' in url.query) {
console.log('Generating naughty users with labels')
const anchorPost = await server.mocker.createPost(
'alice',
'Anchor post',
)
for (const user of [
'dmca-account',
'dmca-profile',
'dmca-posts',
'porn-account',
'porn-profile',
'porn-posts',
'nudity-account',
'nudity-profile',
'nudity-posts',
'scam-account',
'scam-profile',
'scam-posts',
'unknown-account',
'unknown-profile',
'unknown-posts',
'hide-account',
'hide-profile',
'hide-posts',
'no-promote-account',
'no-promote-profile',
'no-promote-posts',
'warn-account',
'warn-profile',
'warn-posts',
'muted-account',
'muted-by-list-acc',
'blocking-account',
'blockedby-account',
'mutual-block-acc',
]) {
await server.mocker.createUser(user)
await server.mocker.follow('alice', user)
await server.mocker.follow(user, 'alice')
await server.mocker.createPost(user, `Unlabeled post from ${user}`)
await server.mocker.createReply(
user,
`Unlabeled reply from ${user}`,
anchorPost,
)
await server.mocker.like(user, anchorPost)
}
await server.mocker.labelAccount('dmca-violation', 'dmca-account')
await server.mocker.labelProfile('dmca-violation', 'dmca-profile')
await server.mocker.labelPost(
'dmca-violation',
await server.mocker.createPost('dmca-posts', 'dmca post'),
)
await server.mocker.labelPost(
'dmca-violation',
await server.mocker.createQuotePost(
'dmca-posts',
'dmca quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'dmca-violation',
await server.mocker.createReply(
'dmca-posts',
'dmca reply',
anchorPost,
),
)
await server.mocker.labelAccount('porn', 'porn-account')
await server.mocker.labelProfile('porn', 'porn-profile')
await server.mocker.labelPost(
'porn',
await server.mocker.createImagePost('porn-posts', 'porn post'),
)
await server.mocker.labelPost(
'porn',
await server.mocker.createQuotePost(
'porn-posts',
'porn quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'porn',
await server.mocker.createReply(
'porn-posts',
'porn reply',
anchorPost,
),
)
await server.mocker.labelAccount('nudity', 'nudity-account')
await server.mocker.labelProfile('nudity', 'nudity-profile')
await server.mocker.labelPost(
'nudity',
await server.mocker.createImagePost('nudity-posts', 'nudity post'),
)
await server.mocker.labelPost(
'nudity',
await server.mocker.createQuotePost(
'nudity-posts',
'nudity quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'nudity',
await server.mocker.createReply(
'nudity-posts',
'nudity reply',
anchorPost,
),
)
await server.mocker.labelAccount('scam', 'scam-account')
await server.mocker.labelProfile('scam', 'scam-profile')
await server.mocker.labelPost(
'scam',
await server.mocker.createPost('scam-posts', 'scam post'),
)
await server.mocker.labelPost(
'scam',
await server.mocker.createQuotePost(
'scam-posts',
'scam quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'scam',
await server.mocker.createReply(
'scam-posts',
'scam reply',
anchorPost,
),
)
await server.mocker.labelAccount(
'not-a-real-label',
'unknown-account',
)
await server.mocker.labelProfile(
'not-a-real-label',
'unknown-profile',
)
await server.mocker.labelPost(
'not-a-real-label',
await server.mocker.createPost('unknown-posts', 'unknown post'),
)
await server.mocker.labelPost(
'not-a-real-label',
await server.mocker.createQuotePost(
'unknown-posts',
'unknown quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'not-a-real-label',
await server.mocker.createReply(
'unknown-posts',
'unknown reply',
anchorPost,
),
)
await server.mocker.labelAccount('!hide', 'hide-account')
await server.mocker.labelProfile('!hide', 'hide-profile')
await server.mocker.labelPost(
'!hide',
await server.mocker.createPost('hide-posts', 'hide post'),
)
await server.mocker.labelPost(
'!hide',
await server.mocker.createQuotePost(
'hide-posts',
'hide quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'!hide',
await server.mocker.createReply(
'hide-posts',
'hide reply',
anchorPost,
),
)
await server.mocker.labelAccount('!no-promote', 'no-promote-account')
await server.mocker.labelProfile('!no-promote', 'no-promote-profile')
await server.mocker.labelPost(
'!no-promote',
await server.mocker.createPost(
'no-promote-posts',
'no-promote post',
),
)
await server.mocker.labelPost(
'!no-promote',
await server.mocker.createQuotePost(
'no-promote-posts',
'no-promote quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'!no-promote',
await server.mocker.createReply(
'no-promote-posts',
'no-promote reply',
anchorPost,
),
)
await server.mocker.labelAccount('!warn', 'warn-account')
await server.mocker.labelProfile('!warn', 'warn-profile')
await server.mocker.labelPost(
'!warn',
await server.mocker.createPost('warn-posts', 'warn post'),
)
await server.mocker.labelPost(
'!warn',
await server.mocker.createQuotePost(
'warn-posts',
'warn quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'!warn',
await server.mocker.createReply(
'warn-posts',
'warn reply',
anchorPost,
),
)
await server.mocker.users.alice.agent.mute('muted-account.test')
await server.mocker.createPost('muted-account', 'muted post')
await server.mocker.createQuotePost(
'muted-account',
'muted quote post',
anchorPost,
)
await server.mocker.createReply(
'muted-account',
'muted reply',
anchorPost,
)
const list = await server.mocker.createMuteList(
'alice',
'Muted Users',
)
await server.mocker.addToMuteList(
'alice',
list,
server.mocker.users['muted-by-list-acc'].did,
)
await server.mocker.createPost('muted-by-list-acc', 'muted post')
await server.mocker.createQuotePost(
'muted-by-list-acc',
'account quote post',
anchorPost,
)
await server.mocker.createReply(
'muted-by-list-acc',
'account reply',
anchorPost,
)
await server.mocker.createPost('blocking-account', 'blocking post')
await server.mocker.createQuotePost(
'blocking-account',
'blocking quote post',
anchorPost,
)
await server.mocker.createReply(
'blocking-account',
'blocking reply',
anchorPost,
)
await server.mocker.users.alice.agent.app.bsky.graph.block.create(
{
repo: server.mocker.users.alice.did,
},
{
subject: server.mocker.users['blocking-account'].did,
createdAt: new Date().toISOString(),
},
)
await server.mocker.createPost('blockedby-account', 'blockedby post')
await server.mocker.createQuotePost(
'blockedby-account',
'blockedby quote post',
anchorPost,
)
await server.mocker.createReply(
'blockedby-account',
'blockedby reply',
anchorPost,
)
await server.mocker.users[
'blockedby-account'
].agent.app.bsky.graph.block.create(
{
repo: server.mocker.users['blockedby-account'].did,
},
{
subject: server.mocker.users.alice.did,
createdAt: new Date().toISOString(),
},
)
await server.mocker.createPost(
'mutual-block-acc',
'mutual-block post',
)
await server.mocker.createQuotePost(
'mutual-block-acc',
'mutual-block quote post',
anchorPost,
)
await server.mocker.createReply(
'mutual-block-acc',
'mutual-block reply',
anchorPost,
)
await server.mocker.users.alice.agent.app.bsky.graph.block.create(
{
repo: server.mocker.users.alice.did,
},
{
subject: server.mocker.users['mutual-block-acc'].did,
createdAt: new Date().toISOString(),
},
)
await server.mocker.users[
'mutual-block-acc'
].agent.app.bsky.graph.block.create(
{
repo: server.mocker.users['mutual-block-acc'].did,
},
{
subject: server.mocker.users.alice.did,
createdAt: new Date().toISOString(),
},
)
// flush caches
await server.mocker.testNet.processAll()
}
}
console.log('Ready')
return res
.writeHead(200, {
'content-type': 'application/json',
})
.end(
JSON.stringify({
pdsUrl: server.pdsUrl,
appviewDid: server.appviewDid,
}),
)
} catch (e) {
console.error('Error!', e)
return res.writeHead(500).end()
}
}).listen(1986)
console.log('Mock server manager listening on 1986')
}
main()
+26 -8
View File
@@ -3,15 +3,33 @@ appId: xyz.blueskyweb.app
- launchApp:
appId: "xyz.blueskyweb.app"
clearState: true
- waitForAnimationToEnd
- tapOn: "http://localhost:8081"
- waitForAnimationToEnd
arguments:
"-EXDevMenuIsOnboardingFinished": true
- runFlow:
when:
platform: iOS
commands:
- extendedWaitUntil:
visible: "http://localhost:8081"
timeout: 60000
- tapOn: "http://localhost:8081"
- runFlow:
when:
platform: Android
commands:
- extendedWaitUntil:
visible: "http://10.0.2.2:8081"
timeout: 60000
- tapOn: "http://10.0.2.2:8081"
- extendedWaitUntil:
visible: "Continue"
timeout: 180000
- tapOn: "Continue"
- back
- extendedWaitUntil:
visible: "Continue"
- swipe:
from: "Bluesky"
direction: DOWN
duration: 100
visible:
id: e2eProxyHeaderInput
timeout: 180000
- tapOn:
id: e2eProxyHeaderInput
- inputText: ${output.result}
-6
View File
@@ -1,6 +0,0 @@
export default {
requestPermission: jest.fn(),
onForegroundEvent: jest.fn(),
setBadgeCount: jest.fn(),
displayNotification: jest.fn(),
}
@@ -1,9 +0,0 @@
export const CameraRoll = {
getPhotos: jest.fn().mockResolvedValue({
edges: [
{node: {image: {uri: 'path/to/image1.jpg'}}},
{node: {image: {uri: 'path/to/image2.jpg'}}},
{node: {image: {uri: 'path/to/image3.jpg'}}},
],
}),
}
@@ -1,4 +0,0 @@
export default {
configure: jest.fn().mockResolvedValue(0),
finish: jest.fn(),
}
-1
View File
@@ -1 +0,0 @@
export default {}
-10
View File
@@ -1,10 +0,0 @@
jest.mock('rn-fetch-blob', () => {
return {
__esModule: true,
default: {
fs: {
unlink: jest.fn(),
},
},
}
})
-2
View File
@@ -1,2 +0,0 @@
export const DropdownMenu = jest.fn().mockImplementation(() => {})
export const create = jest.fn().mockImplementation(() => {})
+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,
+107
View File
@@ -0,0 +1,107 @@
import {
buildStateObject,
getCurrentRoute,
isStateAtTabRoot,
} from '#/lib/routes/helpers'
import {type State} from '#/lib/routes/types'
describe('getCurrentRoute', () => {
it('returns Home when there is no state', () => {
expect(getCurrentRoute(undefined).name).toBe('Home')
})
it('descends into nested state using the index', () => {
const state = {
index: 0,
routes: [
{
name: 'HomeTab',
state: {
index: 1,
routes: [{name: 'Home'}, {name: 'PostThread'}],
},
},
],
} as unknown as State
expect(getCurrentRoute(state).name).toBe('PostThread')
})
/*
* Right after a cold start from a deep link, nested navigator states are
* still partial and have no `index`. React Navigation focuses the last
* route when rehydrating such a state, so getCurrentRoute must do the
* same (previously it stopped at the tab route, which re-enabled the
* drawer swipe gesture on top of the deep-linked screen).
*/
it('descends into partial nested state without an index', () => {
const state = {
routes: [
{
name: 'HomeTab',
state: {
routes: [{name: 'Home'}, {name: 'PostThread'}],
},
},
],
} as unknown as State
expect(getCurrentRoute(state).name).toBe('PostThread')
})
})
describe('isStateAtTabRoot', () => {
it('returns true for the initial deep link state of a tab root', () => {
const state = buildStateObject('HomeTab', 'Home', {}) as unknown as State
expect(isStateAtTabRoot(state)).toBe(true)
})
it('returns false for the initial deep link state of a nested screen', () => {
const state = buildStateObject(
'HomeTab',
'PostThread',
{name: 'alice.test', rkey: '123'},
[{name: 'Home', params: {}}],
) as unknown as State
expect(isStateAtTabRoot(state)).toBe(false)
})
})
describe('buildStateObject', () => {
it('focuses the deep-linked route in the nested state', () => {
const state = buildStateObject(
'HomeTab',
'PostThread',
{name: 'alice.test', rkey: '123'},
[{name: 'Home', params: {}}],
)
expect(state).toEqual({
index: 0,
routes: [
{
name: 'HomeTab',
state: {
index: 1,
routes: [
{name: 'Home', params: {}},
{name: 'PostThread', params: {name: 'alice.test', rkey: '123'}},
],
},
},
],
})
})
it('builds a single-route state for the Flat navigator', () => {
const state = buildStateObject('Flat', 'PostThread', {
name: 'alice.test',
rkey: '123',
})
expect(state).toEqual({
index: 0,
routes: [{name: 'PostThread', params: {name: 'alice.test', rkey: '123'}}],
})
})
})
+87 -8
View File
@@ -1,4 +1,5 @@
import {RichText} from '@atproto/api'
import {RichText} from '@bsky/sdk/richtext'
import {i18n} from '@lingui/core'
import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
import {
@@ -6,7 +7,8 @@ import {
createStarterPackLinkFromAndroidReferrer,
parseStarterPackUri,
} from '#/lib/strings/starter-pack'
import {tenorUrlToBskyGifUrl} from '#/state/queries/tenor'
import {messages} from '#/locale/locales/en/messages'
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'
@@ -202,6 +204,9 @@ describe('enforceLen', () => {
})
describe('cleanError', () => {
// cleanError uses lingui
i18n.loadAndActivate({locale: 'en', messages})
const inputs = [
'TypeError: Network request failed',
'Error: Aborted',
@@ -327,6 +332,7 @@ describe('shortenLinks', () => {
expect(outputRT.text).toEqual(outputs[i][0])
expect(outputRT.facets?.length).toEqual(outputs[i][1].length)
for (let j = 0; j < outputs[i][1].length; j++) {
// @ts-expect-error whatever
expect(outputRT.facets![j].features[0].uri).toEqual(outputs[i][1][j])
}
}
@@ -437,6 +443,20 @@ describe('parseEmbedPlayerFromUrl', () => {
'https://www.flickr.com/groups/898944@N23/',
'https://www.flickr.com/groups',
'https://maxblansjaar.bandcamp.com/album/false-comforts',
'https://grmnygrmny.bandcamp.com/track/fluid',
'https://sufjanstevens.bandcamp.com/',
'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 = [
@@ -815,6 +835,52 @@ describe('parseEmbedPlayerFromUrl', () => {
undefined,
undefined,
{
type: 'bandcamp_album',
source: 'bandcamp',
playerUri:
'https://bandcamp.com/EmbeddedPlayer/url=https%3A%2F%2Fmaxblansjaar.bandcamp.com%2Falbum%2Ffalse-comforts/size=large/bgcol=ffffff/linkcol=0687f5/minimal=true/transparent=true/',
},
{
type: 'bandcamp_track',
source: 'bandcamp',
playerUri:
'https://bandcamp.com/EmbeddedPlayer/url=https%3A%2F%2Fgrmnygrmny.bandcamp.com%2Ftrack%2Ffluid/size=large/bgcol=ffffff/linkcol=0687f5/minimal=true/transparent=true/',
},
undefined,
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', () => {
@@ -1005,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)
})
})
+85 -54
View File
@@ -1,5 +1,10 @@
// @ts-check
const pkg = require('./package.json')
/**
* @param {import('@expo/config-types').ExpoConfig} _config
* @returns {{ expo: import('@expo/config-types').ExpoConfig }}
*/
module.exports = function (_config) {
/**
* App version number. Should be incremented as part of a release cycle.
@@ -15,7 +20,7 @@ module.exports = function (_config) {
const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
const IS_PRODUCTION = process.env.EXPO_PUBLIC_ENV === 'production'
const IS_DEV = !IS_TESTFLIGHT || !IS_PRODUCTION
const IS_DEV = !IS_TESTFLIGHT && !IS_PRODUCTION
const ASSOCIATED_DOMAINS = [
'applinks:bsky.app',
@@ -30,6 +35,13 @@ module.exports = function (_config) {
const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN)
const IOS_ICON_FILE =
PLATFORM === 'web' // web build doesn't like .icon files
? './assets/app-icons/ios_icon_default_next.png'
: IS_TESTFLIGHT
? './assets/app-icons/ios_icon_testflight.icon'
: './assets/app-icons/ios_icon_default.icon'
return {
expo: {
version: VERSION,
@@ -42,20 +54,19 @@ module.exports = function (_config) {
},
icon: './assets/app-icons/ios_icon_default_next.png',
userInterfaceStyle: 'automatic',
primaryColor: '#1083fe',
newArchEnabled: false,
primaryColor: '#006AFF',
ios: {
supportsTablet: false,
bundleIdentifier: 'xyz.blueskyweb.app',
appleTeamId: process.env.EXPO_APPLE_TEAM_ID,
config: {
usesNonExemptEncryption: false,
},
icon:
PLATFORM === 'web' // web build doesn't like .icon files
? './assets/app-icons/ios_icon_default_next.png'
: './assets/app-icons/ios_icon_default.icon',
icon: IOS_ICON_FILE,
infoPlist: {
CADisableMinimumFrameDurationOnPhone: true,
UIBackgroundModes: ['remote-notification'],
NSUserActivityTypes: ['INSendMessageIntent'],
NSCameraUsageDescription:
'Used for profile pictures, posts, and other kinds of content.',
NSMicrophoneUsageDescription:
@@ -70,6 +81,7 @@ module.exports = function (_config) {
'an',
'ast',
'ca',
'cs',
'cy',
'da',
'de',
@@ -107,13 +119,15 @@ module.exports = function (_config) {
'zh-Hans',
'zh-Hant',
],
UIDesignRequiresCompatibility: true,
},
associatedDomains: ASSOCIATED_DOMAINS,
entitlements: {
'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,
'com.apple.developer.declared-age-range': true,
},
privacyManifests: {
NSPrivacyCollectedDataTypes: [
@@ -170,10 +184,6 @@ module.exports = function (_config) {
androidStatusBar: {
barStyle: 'light-content',
},
// Dark nav bar in light mode is better than light nav bar in dark mode
androidNavigationBar: {
barStyle: 'light-content',
},
android: {
icon: './assets/app-icons/android_icon_default_next.png',
adaptiveIcon: {
@@ -192,10 +202,14 @@ module.exports = function (_config) {
scheme: 'https',
host: 'bsky.app',
},
IS_DEV && {
scheme: 'http',
host: 'localhost:19006',
},
...(IS_DEV
? [
{
scheme: 'http',
host: 'localhost:19006',
},
]
: []),
],
category: ['BROWSABLE', 'DEFAULT'],
},
@@ -227,23 +241,36 @@ module.exports = function (_config) {
'react-native-edge-to-edge',
{android: {enforceNavigationBarContrast: false}},
],
USE_SENTRY && [
'@sentry/react-native/expo',
{
organization: 'blueskyweb',
project: 'app',
url: 'https://sentry.io',
},
],
...(USE_SENTRY
? [
/** @type {[string, any]} */ ([
'@sentry/react-native/expo',
{
organization: 'blueskyweb',
project: 'app',
url: 'https://sentry.io',
},
]),
]
: []),
[
'expo-build-properties',
{
ios: {
deploymentTarget: '15.1',
deploymentTarget: '16.4',
buildReactNativeFromSource: true,
ccacheEnabled: IS_DEV,
cxxLanguageStandard: 'c++23',
extraPods: [
{
name: 'MCEmojiPicker',
git: 'https://github.com/bluesky-social/MCEmojiPicker.git',
branch: 'main',
},
],
},
android: {
compileSdkVersion: 35,
compileSdkVersion: 36,
targetSdkVersion: 35,
buildToolsVersion: '35.0.0',
buildReactNativeFromSource: IS_PRODUCTION,
@@ -271,57 +298,61 @@ module.exports = function (_config) {
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidManifestIntentQueriesPlugin.js',
'./plugins/withAndroidStylesAccentColorPlugin.js',
'./plugins/withAndroidDayNightThemePlugin.js',
'./plugins/withAndroidNoJitpackPlugin.js',
'./plugins/shareExtension/withShareExtensions.js',
'./plugins/notificationsExtension/withNotificationsExtension.js',
[
'expo-font',
{
fonts: [
'./assets/fonts/inter/InterVariable.woff2',
'./assets/fonts/inter/InterVariable-Italic.woff2',
// Android only
'./assets/fonts/inter/Inter-Regular.otf',
'./assets/fonts/inter/Inter-Italic.otf',
'./assets/fonts/inter/Inter-Medium.otf',
'./assets/fonts/inter/Inter-MediumItalic.otf',
'./assets/fonts/inter/Inter-SemiBold.otf',
'./assets/fonts/inter/Inter-SemiBoldItalic.otf',
'./assets/fonts/inter/Inter-Bold.otf',
'./assets/fonts/inter/Inter-BoldItalic.otf',
],
ios: {
fonts: [
'./assets/fonts/inter/InterVariable.ttf',
'./assets/fonts/inter/InterVariable-Italic.ttf',
],
},
android: {
fonts: [
'./assets/fonts/inter/Inter-Regular.otf',
'./assets/fonts/inter/Inter-Italic.otf',
'./assets/fonts/inter/Inter-Medium.otf',
'./assets/fonts/inter/Inter-MediumItalic.otf',
'./assets/fonts/inter/Inter-SemiBold.otf',
'./assets/fonts/inter/Inter-SemiBoldItalic.otf',
'./assets/fonts/inter/Inter-Bold.otf',
'./assets/fonts/inter/Inter-BoldItalic.otf',
],
},
},
],
[
'expo-splash-screen',
{
ios: {
enableFullScreenImage_legacy: true,
backgroundColor: '#ffffff',
image: './assets/splash.png',
enableFullScreenImage_legacy: true, // iOS only
backgroundColor: '#006AFF', // primary_500
image: './assets/splash/splash.png',
resizeMode: 'cover',
dark: {
enableFullScreenImage_legacy: true,
backgroundColor: '#001429',
image: './assets/splash-dark.png',
enableFullScreenImage_legacy: true, // iOS only
backgroundColor: '#002861', // primary_900
image: './assets/splash/splash-dark.png',
resizeMode: 'cover',
},
},
android: {
backgroundColor: '#0c7cff',
image: './assets/splash-android-icon.png',
imageWidth: 150,
backgroundColor: '#006AFF', // primary_500
image: './assets/splash/android-splash-logo-white.png',
imageWidth: 102, // even division of 306px
dark: {
backgroundColor: '#0c2a49',
image: './assets/splash-android-icon-dark.png',
imageWidth: 150,
backgroundColor: '#002861', // primary_900
image: './assets/splash/android-splash-logo-white.png',
imageWidth: 102,
},
},
},
],
[
'@mozzius/expo-dynamic-app-icon',
'@bsky.app/expo-dynamic-app-icon',
{
/**
* Default set
@@ -396,7 +427,7 @@ module.exports = function (_config) {
'I agree to allow Bluesky to use my contacts for friend discovery until I opt out.',
},
],
].filter(Boolean),
],
extra: {
eas: {
build: {
+11
View File
@@ -0,0 +1,11 @@
**Rights holder: Bluesky Social PBC.**
The app icons in this directory are our trademarks — the butterfly mark and its variants, as shipped to the iOS App Store and Google Play.
**These are not covered by the [MIT license](../../LICENSE) that applies to the rest of this repository.** Use of Bluesky's marks is governed by our [Trademark Policy](https://bsky.social/about/support/trademarks) and [Brand Guidelines](https://bsky.social/about/support/branding), not by the license on our source code.
You may say that your app is a client for Bluesky, or that it is based on the Bluesky app. You may not use our marks as the identity of your own product, or in any way likely to suggest that Bluesky publishes, endorses, or supports it.
If you are forking this repository, replace these files with your own icons. Shipping an app that looks like Bluesky is also a problem under the app stores' own rules on copycat apps, quite apart from trademark.
See [`ASSETS.md`](../../ASSETS.md#3-bluesky-trademarks-and-brand-assets) for the full asset licensing picture.
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 KiB

@@ -0,0 +1,113 @@
{
"fill" : {
"automatic-gradient" : "srgb:1.00000,1.00000,1.00000,1.00000"
},
"groups" : [
{
"blend-mode-specializations" : [
{
"value" : "overlay"
},
{
"appearance" : "dark",
"value" : "screen"
},
{
"appearance" : "tinted",
"value" : "screen"
}
],
"blur-material-specializations" : [
{
"value" : 0.5
},
{
"appearance" : "dark",
"value" : 0.5
},
{
"appearance" : "tinted",
"value" : null
}
],
"hidden" : false,
"layers" : [
{
"image-name" : "TestFlight notice.png",
"name" : "TestFlight notice"
}
],
"lighting" : "individual",
"position" : {
"scale" : 0.4,
"translation-in-points" : [
0,
350
]
},
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"specular-specializations" : [
{
"value" : false
},
{
"appearance" : "dark",
"value" : false
},
{
"appearance" : "tinted",
"value" : false
}
],
"translucency-specializations" : [
{
"value" : {
"enabled" : true,
"value" : 0.5
}
},
{
"appearance" : "dark",
"value" : {
"enabled" : true,
"value" : 0.5
}
},
{
"appearance" : "tinted",
"value" : {
"enabled" : true,
"value" : 0.5
}
}
]
},
{
"layers" : [
{
"fill" : "none",
"glass" : false,
"image-name" : "iOS transparent.png",
"name" : "iOS transparent"
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}
Binary file not shown.
Binary file not shown.
+92
View File
@@ -0,0 +1,92 @@
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org/
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+13
View File
@@ -0,0 +1,13 @@
**Rights holder: The Inter Project Authors.**
[Inter](https://rsms.me/inter/) is a typeface by Rasmus Andersson, licensed under the SIL Open Font License, Version 1.1.
```
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
```
The full license text is in [`OFL.txt`](./OFL.txt) and must travel with these files.
**You may redistribute these files under the OFL.** They are separately licensed and are not covered by Bluesky's [MIT license](../../../LICENSE). Keep [`OFL.txt`](./OFL.txt) with redistributed copies.
The bundled OFL does not designate a Reserved Font Name.
+18
View File
@@ -0,0 +1,18 @@
**Rights holder: Iconists (David & Storm GbR).**
The user-interface glyphs in this directory come from their [Central icon system](https://iconists.co/central). Bluesky Social PBC licenses them for use in our own products. **That license is for our own use, and it does not extend to you.**
**These icons are not covered by the [MIT license](../../LICENSE) that applies to the rest of this repository.** The fact that we have our own license does not mean that you cannot use these icons. It means that any right you have to use them has to come from Iconists, not us. Licenses are available from [iconists.co](https://iconists.co), and there are openly licensed alternatives if you prefer that.
This notice covers the SVG files at the top level of this directory. It does not cover:
| Not covered here | Rights holder | See |
|---|---|---|
| `flags/` | @catamphetamine, MIT licensed | [`flags/README.md`](./flags/README.md) |
| `community/` | Third-party services | [`community/README.md`](./community/README.md) |
| `logomark.svg`, `newskie.svg`, `verifiedCheck.svg`, `verifierCheck.svg`, `starterPack.svg`, `starterPack_stroke2_corner0_rounded.svg` | Bluesky Social PBC — trademarks | [`ASSETS.md`](../../ASSETS.md#3-bluesky-trademarks-and-brand-assets) |
| `custom_logo_japan.svg` | A Bluesky Japan logo contest entrant | [`ASSETS.md`](../../ASSETS.md#4-community-and-contest-artwork--credited-but-not-ours-to-license) |
| `apple_logo.svg` | Apple Inc. | [`ASSETS.md`](../../ASSETS.md#5-third-party-trademarks) |
| `android_logo.svg` | Google LLC | [`ASSETS.md`](../../ASSETS.md#5-third-party-trademarks) |
Adding an icon here? If it came from Central, this notice covers it. If it came from anywhere else, add it to [`ASSETS.md`](../../ASSETS.md) so the notice does not go stale.
@@ -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

+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#006AFF" d="M16 2.5a4 4 0 0 1 4 4v12.995c0 1.62-1.825 2.567-3.15 1.636l-3.7-2.6a2 2 0 0 0-2.3 0l-3.7 2.6C5.825 22.062 4 21.115 4 19.495V6.5a4 4 0 0 1 4-4h8Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#006aff" d="M16 2.5a4 4 0 0 1 4 4v12.995c0 1.62-1.825 2.567-3.15 1.636l-3.7-2.6a2 2 0 0 0-2.3 0l-3.7 2.6C5.825 22.062 4 21.115 4 19.495V6.5a4 4 0 0 1 4-4h8Z"/></svg>

Before

Width:  |  Height:  |  Size: 250 B

After

Width:  |  Height:  |  Size: 250 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" fill-rule="evenodd" d="M12 0a2 2 0 0 1 1 3.73V5h4.2c1.68 0 2.52 0 3.162.327a3 3 0 0 1 1.31 1.31C22 7.28 22 8.12 22 9.8v.25a2.501 2.501 0 0 1 0 4.9V15c0 2.8 0 4.2-.545 5.27a5 5 0 0 1-2.185 2.185C18.2 23 16.8 23 14 23h-4c-2.8 0-4.2 0-5.27-.545a5 5 0 0 1-2.185-2.185C2 19.2 2 17.8 2 15v-.05a2.5 2.5 0 0 1 0-4.9V9.8c0-1.68 0-2.52.327-3.162a3 3 0 0 1 1.31-1.31C4.28 5 5.12 5 6.8 5H11V3.73A2 2 0 0 1 12 0M8 10a2 2 0 0 0-2 2v2a2 2 0 1 0 4 0v-2a2 2 0 0 0-2-2m8 0a2 2 0 0 0-2 2v2a2 2 0 1 0 4 0v-2a2 2 0 0 0-2-2" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 621 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="M12 2a1 1 0 0 1 1 1v2h3.2l1.113.005c.975.015 1.568.077 2.05.322a3 3 0 0 1 1.31 1.31C21 7.28 21 8.12 21 9.8v.287a1.498 1.498 0 0 1-.005 2.827c-.006 2.204-.058 3.41-.54 4.356l-.093.174a5 5 0 0 1-2.092 2.011l-.205.096c-.766.33-1.72.417-3.21.44L13 20h-2l-1.854-.009c-1.49-.023-2.445-.11-3.211-.44l-.205-.096a5 5 0 0 1-2.185-2.185c-.409-.803-.51-1.79-.536-3.415l-.005-.94A1.498 1.498 0 0 1 3 10.086V9.8c0-1.575 0-2.412.27-3.04l.057-.122a3 3 0 0 1 1.105-1.196l.206-.115C5.279 5 6.12 5 7.8 5H11V3a1 1 0 0 1 1-1M7.8 7c-.873 0-1.408.002-1.808.034a3 3 0 0 0-.367.051l-.063.018-.016.006a1 1 0 0 0-.437.437l-.006.016-.018.063a3 3 0 0 0-.05.367C5.001 8.392 5 8.927 5 9.8V12c0 1.433.002 2.388.062 3.121.058.71.16 1.036.265 1.241a3 3 0 0 0 1.31 1.31c.207.106.532.209 1.242.267.733.06 1.688.061 3.121.061h2c1.433 0 2.388-.002 3.121-.061.71-.058 1.036-.161 1.241-.266a3 3 0 0 0 1.31-1.31c.106-.206.209-.532.267-1.242.06-.733.061-1.688.061-3.121V9.8c0-.873-.002-1.408-.034-1.808a2.5 2.5 0 0 0-.051-.367l-.017-.063-.007-.016a1 1 0 0 0-.437-.437l-.015-.006-.064-.018a3 3 0 0 0-.367-.05C17.608 7.001 17.073 7 16.2 7zM9 10a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0v-2a1 1 0 0 1 1-1m6 0a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0v-2a1 1 0 0 1 1-1"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -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

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