Compare commits

...

166 Commits

Author SHA1 Message Date
Eric Bailey 2adce3ff8a Add notice to labeler card, show all labelers 2024-09-20 13:48:37 -05:00
Eric Bailey bd4514035d Add tiny notice 2024-09-20 11:01:41 -05:00
Eric Bailey a79c82e098 Use prod endpoint 2024-09-20 11:01:19 -05:00
Eric Bailey 4d4a092fb9 Merge remote-tracking branch 'origin/main' into mod-auth
* origin/main: (21 commits)
  Add border to menu on web (#5439)
  Add eslint rule to fix imports without the `#/` path alias (#5175)
  [Neue] Parallel font loading on web (#5411)
  Adjust toggle components sizing (#5408)
  [Neue] Copy and spacing tweaks (#5428)
  [Neue] Ligatures & composer (#5427)
  Remove expo/vector-icons from EditImage modal (#5422)
  Revert provider stacking changes (#5423)
  Fix ext embeds in DMs (#5419)
  Fix font loading on web (#5412)
  [Neue] Base (#5395)
  Update sentry sourcemaps upload (#5409)
  Fix for undefined ref on hot reload on web (#5407)
  remove expo-sentry (#5405)
  Revert unneeded changes to `expo-modules-core` patch (#5393)
  Temporary Sentry hack patch for iOS 18 (#5400)
  Make it work with Xcode 16 (#5386)
  Fix border radius on avatars (#5392)
  Make like animation on web same speed as mobile (#5391)
  Pre-fill alt text with 10-million card post (#5389)
  ...
2024-09-20 10:30:26 -05:00
futur cd88cbeab8 Add border to menu on web (#5439) 2024-09-20 10:09:55 -05:00
Samuel Newman f42d44112d Add eslint rule to fix imports without the #/ path alias (#5175) 2024-09-20 09:11:29 +01:00
Eric Bailey 22410a3cee [Neue] Parallel font loading on web (#5411)
* Parallel font loading

(cherry picked from commit 10e2b05b575bbbf8b0ca5b4a336817cd902d712b)

* Handle failures

* Rely on font-face and preload tags for font loading (#5431)

* Cache fonts for a year
2024-09-19 17:57:15 -05:00
Eric Bailey c8184e823d Adjust toggle components sizing (#5408)
(cherry picked from commit a9cbd23fcdcbacbdd2ca5f6bbe0bc7161e6591e3)
2024-09-19 16:02:47 -05:00
Eric Bailey c0cb5936f5 [Neue] Copy and spacing tweaks (#5428)
* Copy and spacing tweaks

* Copy tweaks
2024-09-19 16:02:29 -05:00
Eric Bailey 8a7fc9ad3f [Neue] Ligatures & composer (#5427)
* Disable contextual ligatures

* Ensure new type styles are applied to the composer

* Clean up using real devices

* Feedback
2024-09-19 14:41:31 -05:00
Eric Bailey 1523160467 Remove expo/vector-icons from EditImage modal (#5422) 2024-09-19 11:54:39 -05:00
Eric Bailey 24adcb48b7 Revert provider stacking changes (#5423) 2024-09-19 11:54:29 -05:00
Samuel Newman 8fde02fabc Fix ext embeds in DMs (#5419)
* fix ext embeds in DMs

* undo acciental rounding change
2024-09-19 15:32:12 +01:00
Eric Bailey bda355fd58 Fix font loading on web (#5412)
* Copy font files during build

* Fall back if error loading fonts
2024-09-18 20:22:03 -05:00
Eric Bailey cbc7cd0808 [Neue] Base (#5395)
* Add fontScale, gate it, fix some computes

* Add inter, integrate

* Clean up

* Apply to old Text component

* Use numeric weight

* Cleanup

* Clean up appearance settings

* Global tracking

* Fix regular italic variant

* Refactor settings and fontScale values

* Remove flags

* Get rid of lower weight font usage

* Remove gate from settings

* Refactor appearance settings for reuse

* Add neue type nux

* Update defaults

* Load fonts, add fallback families

* Load fonts via plugin in production

* Fixes

* Fix for web

* Nits

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-09-18 19:35:34 -05:00
Hailey fb3be79820 Update sentry sourcemaps upload (#5409) 2024-09-18 14:51:14 -07:00
Eric Bailey 6c8ef69654 Fix for undefined ref on hot reload on web (#5407) 2024-09-18 15:45:20 -05:00
Hailey 41d4b2c7ef remove expo-sentry (#5405) 2024-09-18 09:34:29 -07:00
Hailey f45f7148ee Revert unneeded changes to expo-modules-core patch (#5393) 2024-09-18 08:42:42 -07:00
Hailey 8d560de44b Temporary Sentry hack patch for iOS 18 (#5400) 2024-09-18 11:25:32 +01:00
Samuel Newman 67895f7f99 Make it work with Xcode 16 (#5386) 2024-09-17 12:27:22 -07:00
Eric Bailey b2b5be542a Fix border radius on avatars (#5392) 2024-09-17 12:07:19 -07:00
Roscoe Rubin-Rottenberg 751375ce3c Make like animation on web same speed as mobile (#5391) 2024-09-17 12:07:09 -07:00
Eric Bailey 2745cba3ea Pre-fill alt text with 10-million card post (#5389)
* Pre-fill alt text with 10-million card post (#5377)

* Clean up type

* Tweak alt copy

* Add pt translation, fix typo

---------

Co-authored-by: Calvin <clavin@users.noreply.github.com>
2024-09-17 13:55:19 -05:00
surfdude29 8f98d6b12f Tweak pt-BR string (#5372) 2024-09-17 11:34:48 -07:00
Eric Bailey 835a62e11b Remove space 2024-09-17 12:56:20 -05:00
Eric Bailey b4e544fd45 Merge remote-tracking branch 'origin/main' into mod-auth
* origin/main: (113 commits)
  Bump 1.91.2 (#5387)
  [Video] Additional android fixes (#5373)
  Milly tweaks (#5365)
  [Video] Fix safari showing spinner (#5364)
  [Video] Volume controls on web (#5363)
  Updates to Irish translation, back to 100% (#5345)
  Update catalan messages.po (#5223)
  Update Japanese translation (#5217)
  Update Korean localization (#5254)
  Update Chinese localization (#5291)
  Update French localization (#5227)
  Update Portuguese localization (#5256)
  [Video] Fix scrubber tap target (#5360)
  loosen checks on vtt file (#5359)
  Remove overflow hidden from external link outer el (#5356)
  Disallow some font scaling
  Nux after onboarding (#5357)
  Fix wrong empty state for liked by (#5343)
  [UITextView] Add background color support to iOS selectable text (#5335)
  Add context to `Unmute` and `Mute` strings (#5340)
  ...
2024-09-17 12:32:05 -05:00
Hailey 342919bc4e Bump 1.91.2 (#5387) 2024-09-17 10:00:31 -07:00
Hailey 0414b95c58 [Video] Additional android fixes (#5373)
* rm unused code

* bump lib

* invert bool
2024-09-17 14:04:59 +09:00
Eric Bailey b69fd23456 Milly tweaks (#5365)
Co-authored-by: Hailey <me@haileyok.com>
2024-09-16 14:52:28 -07:00
Samuel Newman 8daf6b7868 [Video] Fix safari showing spinner (#5364) 2024-09-16 14:22:22 -07:00
Samuel Newman 8241747fc2 [Video] Volume controls on web (#5363)
* split up VideoWebControls

* add basic slider

* logarithmic volume

* integrate mute state

* fix typo

* shared video volume

* rm log

* animate in/out

* disable for touch devices

* remove flicker on touch devices

* more detailed comment

* move into correct context provider

* add minHeight

* hack

* bettern umber

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-09-16 21:37:33 +01:00
Kevin Scannell 38c8f01594 Updates to Irish translation, back to 100% (#5345) 2024-09-16 12:08:26 -07:00
Ivan Beà b5d8ce114d Update catalan messages.po (#5223)
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: gsmt <samaritanojr006@gmail.com>
2024-09-16 12:00:26 -07:00
Takayuki KUSANO 4a0e2e3a05 Update Japanese translation (#5217) 2024-09-16 11:59:40 -07:00
Minseo Lee 7dfbd5d7d3 Update Korean localization (#5254) 2024-09-16 11:47:26 -07:00
Frudrax Cheng 08003a09ec Update Chinese localization (#5291)
Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>
Co-authored-by: Kuwa Lee <kuwalee1069@gmail.com>
2024-09-16 11:46:54 -07:00
surfdude29 c371b70722 Update French localization (#5227)
Co-authored-by: Stanislas Signoud <signez@stanisoft.net>
2024-09-16 11:44:42 -07:00
surfdude29 06f0785a8c Update Portuguese localization (#5256)
Co-authored-by: Henrique Marques <hfvmarques@gmail.com>
Co-authored-by: Arthur Tavares <arthurabreu00@gmail.com>
2024-09-16 11:36:39 -07:00
Samuel Newman 75e3f51c10 [Video] Fix scrubber tap target (#5360)
* put padding on correct element

* clear timeout on down
2024-09-16 18:15:07 +01:00
Samuel Newman d62e14eca1 loosen checks on vtt file (#5359) 2024-09-16 17:29:34 +01:00
Eric Bailey 0681727b64 Remove overflow hidden from external link outer el (#5356)
* Remove overflow hidden

* Borders when no thumb

* Fix overflow, add bg to no-thumb state

* Cleanup
2024-09-15 15:42:45 -05:00
Eric Bailey 42b28fec9d Disallow some font scaling 2024-09-15 15:11:04 -05:00
Eric Bailey 61deab7051 Nux after onboarding (#5357)
* Don't show nux dialogs until post-onboarding

* Don't show if over 10M
2024-09-15 14:58:10 -05:00
dan d6c11a7231 Fix wrong empty state for liked by (#5343) 2024-09-15 12:14:46 -07:00
Hailey 55da2704d8 [UITextView] Add background color support to iOS selectable text (#5335) 2024-09-15 12:11:55 -07:00
surfdude29 2a344d8f12 Add context to Unmute and Mute strings (#5340)
Co-authored-by: Hailey <me@haileyok.com>
2024-09-15 12:11:44 -07:00
Hailey f8658f0795 bump (#5348) 2024-09-14 19:38:51 +01:00
Paul Frazee 701ddfb171 Release 1.91.1 prep (#5339)
* Fixes to tests

* intl extract
2024-09-13 16:55:01 -07:00
Hailey d8b80309bd [Video] Fix regression on audio session at launch (#5338) 2024-09-13 16:48:17 -07:00
Eric Bailey e767c50f65 Don't open composer via hotkey if other dialog is already open (#5334)
* Don't open composer via hotkey if other dialog is already open

* Check for lightbox also

* Check for drawer
2024-09-13 18:14:46 -05:00
Eric Bailey d76f9abdd7 "N" keyboard shortcut to open a new post modal (#5197)
* feat: Add hook on web app to open composer with 'N' keyboard shortcut

* Extract, don't fire open composer if already open

* Ignore interactive elements

---------

Co-authored-by: João Gabriel <joaog@nocorp.io>
Co-authored-by: Hailey <me@haileyok.com>
2024-09-13 16:48:28 -05:00
Hailey cac43127f0 [Video] Bump video (#5333) 2024-09-13 14:46:02 -07:00
dan ce3893d816 Apply Following settings to Lists (#5313)
* Apply Following settings to Lists

* Remove dead code
2024-09-13 22:30:09 +01:00
Eric Bailey 88813f57c9 Always display next button on login page (#5326)
Co-authored-by: Vinícius Souza <39967235+vinifsouza@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
2024-09-13 14:21:45 -07:00
Hailey 533382173c [Video] Don't require email verification on self-host (#5332) 2024-09-13 14:08:45 -07:00
Hailey 843f9925f5 [Video] Remember mute state while scrolling (#5331) 2024-09-13 14:07:13 -07:00
dan 791bc7afbe Fix lexicon validation in PWI Discover (#5329) 2024-09-13 21:11:17 +01:00
Hailey 26508cfe6a [Video] Remove expo-video, use bluesky-video (#5282)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-09-13 12:44:42 -07:00
Eric Bailey 78a531f5ff Disable pointer events on media border (#5327) 2024-09-13 19:02:47 +01:00
Paul Frazee 9163f676f5 Merge branch 'events' into main 2024-09-13 10:30:55 -07:00
Eric Bailey 08ac3a27c2 Add events 2024-09-13 12:05:15 -05:00
Eric Bailey b3381da1c1 Image/video border + tweaks (#5324)
* Image/video border (#5253)

* Update AutoSizedImage.tsx

* Update AutoSizedImage.tsx

* Update Gallery.tsx

* Update ExternalLinkEmbed.tsx

* Update MediaPreview.tsx

* Update UserAvatar.tsx

* Update ExternalLinkEmbed.tsx

* Update ExternalPlayerEmbed.tsx

* Update ExternalGifEmbed.tsx

* Update GifEmbed.tsx

* Update ExternalGifEmbed.tsx

* Update GifEmbed.tsx

* Update UserAvatar.tsx

* Update ExternalPlayerEmbed.tsx

* Update ExternalPlayerEmbed.tsx

* video

* Update QuoteEmbed.tsx

* Tweaks, abstract components

---------

Co-authored-by: Minseo Lee <itoupluk427@gmail.com>
2024-09-13 18:02:58 +01:00
Paul Frazee c7231537f1 Merge branch 'ten-milly' into main 2024-09-13 08:57:41 -07:00
dan 1dc7ef137c Fix notification->post jump for real (#5314)
* Revert "Fix notification scroll jump (#5297)"

This reverts commit e0d9e75407.

* Query notifications first
2024-09-13 15:01:09 +01:00
Eric Bailey 0315814edd Separate alt/crop, use new icon (#5321) 2024-09-13 14:56:54 +01:00
Eric Bailey b47bac965f Merge remote-tracking branch 'origin/ten-milly-android-save' into ten-milly
* origin/ten-milly-android-save:
  On android, change ten milly nux secondary action to save instead of share
2024-09-12 21:24:57 -05:00
Eric Bailey 4637c66390 Let display name wrap 2024-09-12 21:24:32 -05:00
Eric Bailey 003f9e06d4 Fallback snoozing 2024-09-12 21:24:32 -05:00
dan fddb1255ec Enable video_upload gate (#5311) 2024-09-12 17:37:07 -07:00
Eric Bailey 47d99b8712 Adjust image sizing (#5302) 2024-09-12 22:35:59 +01:00
Eric Bailey d60a8f26c4 Suggested follows by actor (on profiles) updates (#5243)
* If fallback, return nothing

* Compress size a bit

* Hide on own profile

* Match load state

* Remove gcTime

* Filter out followed users

* Feedback
2024-09-12 22:34:10 +01:00
Paul Frazee 36ac551347 Add ja and pt-BR translations 2024-09-12 13:11:30 -07:00
Paul Frazee 7acf0e1284 On android, change ten milly nux secondary action to save instead of share 2024-09-12 12:57:08 -07:00
Eric Bailey cc8e7b5ae5 Handle display name 2024-09-12 13:56:25 -05:00
Eric Bailey 1f2e4b26c0 Handle overflow of bottom text 2024-09-12 13:54:20 -05:00
Eric Bailey 7bba213e1a Add retry 2024-09-12 13:45:58 -05:00
Eric Bailey bd79ce7ea0 Ensure dialog shows for all accounts without snoozing 2024-09-12 13:45:06 -05:00
Eric Bailey 46402fd010 Add gate 2024-09-12 13:14:51 -05:00
Paul Frazee 47bea32061 Add a hard-coded redirect for lulaoficial.bsky.social (#5303) 2024-09-12 11:09:58 -07:00
dan 897d8ba744 Revert "Show some known OP replies in Following (#5049)" (#5301)
This reverts commit bff6aedecf.
2024-09-12 18:45:19 +01:00
Wesley 86abeb80b9 Renaming the Follow button to "Follow back" when followed by user (#5281)
* Renaming the follow button to follow back when followed by user

* Fixing conditions and reusing existing translation
2024-09-12 15:59:12 +01:00
dan e0d9e75407 Fix notification scroll jump (#5297) 2024-09-12 15:39:04 +01:00
Minseo Lee 4d22adbcf6 Tabular numbers to video timestamp (#5293) 2024-09-12 15:30:57 +01:00
Samuel Newman 7da3ddbe24 [Video] speculative .vtt file selection fix (#5296)
* speculative vtt fix

* add logging
2024-09-12 15:16:38 +01:00
Eric Bailey 63f85f6493 Copy 2024-09-12 08:03:58 -05:00
Eric Bailey c99e43d6c6 Protect against other exit methods, protect against multiple fetches 2024-09-11 21:56:20 -05:00
Eric Bailey 45c8d89d92 Protect against 3p PDSs and bad responses 2024-09-11 21:52:26 -05:00
Eric Bailey 6e78ce53d7 Dev helpers, string cleanup 2024-09-11 21:47:25 -05:00
Eric Bailey c8b133863d Fix some nux types 2024-09-11 21:28:34 -05:00
Eric Bailey 9bb385a4dd Refactor, integrate nux, snoozing 2024-09-11 21:20:39 -05:00
Eric Bailey 63444052e8 Rename 2024-09-11 20:01:27 -05:00
Eric Bailey 77d60a5b80 Hook up data 2024-09-11 19:58:17 -05:00
Eric Bailey f8edd11bc5 Don't open for logged out users 2024-09-11 19:58:17 -05:00
Eric Bailey 2ee68e4f4d Copy 2024-09-11 19:58:16 -05:00
Eric Bailey 11ecea22d4 Add badges, clean up spacing 2024-09-11 19:58:16 -05:00
Eric Bailey f7db14f32f Disable avi 2024-09-11 19:58:16 -05:00
Eric Bailey eaf0081623 WIP, avi not working on web 2024-09-11 19:58:16 -05:00
Eric Bailey 3c8b3b4782 Progress on desktoip 2024-09-11 19:58:16 -05:00
Eric Bailey 76c584d981 WIP 2024-09-11 19:58:16 -05:00
Eric Bailey ae71f5ce84 NUX API (#5278)
* Set up nux API

* Bump SDK

* Naming

* Imports
2024-09-11 19:56:00 -05:00
Eduardo Tachotte cff7cbb4aa Add autoCapitalize to password field (#5216) 2024-09-12 00:28:23 +01:00
Gargaj 67f56af6d4 remove double closing tag (#5257) 2024-09-11 23:42:21 +01:00
Samuel Newman 8a6d83de3b make container relative (#5280) 2024-09-11 23:04:40 +01:00
Samuel Newman 991202966e [Video] Fix web autoplay (#5274) 2024-09-11 11:36:54 -07:00
Hailey dd2d0e6233 add (#5273) 2024-09-11 18:57:58 +01:00
Samuel Newman f943239894 fix min size for compression (#5272) 2024-09-11 18:50:36 +01:00
Samuel Newman 24b07c6cf4 [Video] Cap duration (#5270) 2024-09-11 10:33:57 -07:00
Samuel Newman a19c91d90e [Video] TEMP disable skip compression (#5271) 2024-09-11 10:33:10 -07:00
Samuel Newman 580b67ba37 disable autoplay within messages and trim feelers (#5260) 2024-09-11 16:20:32 +01:00
Samuel Newman b04ecbe54d hls buffering tweaks (#5266) 2024-09-11 16:20:20 +01:00
Samuel Newman db38438549 increase target area of scrubber (#5265) 2024-09-11 16:20:08 +01:00
Samuel Newman fc25992070 [Video] make hover state stick around if tapped (#5259) 2024-09-10 20:19:37 -07:00
Samuel Newman c22492147b remove scrollbar-gutter in fullscreen (#5258) 2024-09-10 23:18:08 +01:00
Hailey b37b64fb49 Verify Identical Domains Emit Origin (#5255) 2024-09-10 08:16:41 -07:00
Samuel Newman 08f5f37b34 [Video] Allow drag-and-drop & pasting video (#5252)
* allow DnD/pasting video

* rm await
2024-09-10 16:14:28 +01:00
Samuel Newman 6bc5a05f4b [Video] Much simpler fix to fullscreen bug (#5251)
* much simpler fix

* allow old behaviour on firefox

* rm logs
2024-09-10 16:10:13 +01:00
Frudrax Cheng db9cf92d87 Adjust the translated strings to the correct max size. (#5248)
* Update pt-BR localization

* Update ru localization
2024-09-10 11:21:28 +01:00
Samuel Newman 66239ba11d [Video] Fix fullscreen on Chrome (#5246) 2024-09-09 22:37:57 -07:00
Samuel Newman 0f6be244a6 max 1 subtitle file (#5244) 2024-09-10 02:02:53 +01:00
Samuel Newman 436e30fded [Video] use correct max size (#5245)
Co-authored-by: Hailey <me@haileyok.com>
2024-09-09 17:01:40 -07:00
Hailey 723a5e488e [Video] Add a string for localization (#5240) 2024-09-09 12:44:41 -07:00
Jaz 9e1eb1f9f1 Add CORS to bskyweb (#5221) 2024-09-09 18:39:28 +01:00
surfdude29 f240893980 Add context to Mute and Unmute labels on video control (#5234)
Co-authored-by: Marco Buono <thecoreh@gmail.com>
2024-09-09 09:29:14 -07:00
Samuel Newman 44f1cd9fb5 [Video] Media preview improvements (#5229)
* background color for images that haven't loaded

* fix recordwithmedia not appearing
2024-09-09 16:41:27 +01:00
Samuel Newman 6c6a76b193 [Video] Upload tweaks (#5228)
* use correct mime type

* fix wheel progress
2024-09-08 08:27:50 -07:00
surfdude29 95aee146b6 Update dates.ts (#5220) 2024-09-07 21:23:51 -07:00
Paul Frazee a6a3d203fd Release 1.91 prep (#5215)
* Run intl:extract

* Test fixes

* Update pt-BR translations for video
2024-09-07 13:52:00 -07:00
Kirill 63ab16a62d Add Russian translation (#3875)
* Add Russian translation

* Update messages.po

* Update messages.po (draft)

* Добавлены новые строки для перевода

В ручную объеденил исправленный RU с самым новым EN. Могут быть ошибки но быстрый тест проблем не выявил.

* Переведены не переведенные строки, некоторые исправления перевода.

* Еще небольшие правки

* Update messages.po (draft)

* Update messages.po (stage)

* Update messages.po

* Init lingui compiling ru language

* Update messages.po

* Update messages.po (clear)

* Update messages.po

* Update messages.po

* change await import to await Promise.all

* Update messages.po

* Update messages.po (clear)

---------

Co-authored-by: DearFox <59219907+DearFox@users.noreply.github.com>
2024-09-07 13:50:18 -07:00
Hailey 9b8d62ca25 [Video] Tweak order of elements in composer (#5213) 2024-09-07 13:29:27 -07:00
Hailey f1877e44f2 [Video] Fix type on web (#5211) 2024-09-07 13:03:53 -07:00
Takayuki KUSANO 0a61b06580 Update Japanese Translation (#5031)
* Update translation

* Update translation

* Fixed wording

* Change the translation of QRcode of starterpack

* Update translation

* Update translation

* Update translation

* Update translation
2024-09-07 12:29:37 -07:00
Minseo Lee 4037e7a50d Update Korean localization (#5035)
* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po
2024-09-07 12:29:08 -07:00
Frudrax Cheng 373735ac49 Update Chinese Localization (#5036)
* CN: Update Translates

* CN: Remove superseded strings

* CN: Update Translates#1

* TW: Update Translates & Remove superseded strings

* CN: Update Translates#2

* TW: Update and clean

* CN: Update Translates

* Both: Run intl:extract

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* CN: Fix typos

* Update Translates

* TW: Update Translates

* CN: Update Translates

* CN: Update Translates

* CN: Remove superseded strings

* TW: Update and clean

* CN: Update Translates

---------

Co-authored-by: Kuwa Lee <kuwalee1069@gmail.com>
Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>
2024-09-07 12:28:36 -07:00
miyun 665766b0d5 Overhaul IT locale (#5069)
* Overhaul IT locale

* Various corrections to the IT locale (thanks to surfdude29!)
2024-09-07 12:25:07 -07:00
Ivan Beà 2a7b333553 Update catalan messages.po (#5067)
* Update catalan messages.po

Another batch, take a look please @jordimas @darccio @surfdude29 @rortan134

* Update catalan messages.po

apply @jordimas corrections

* Update src/locale/locales/ca/messages.po

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/locale/locales/ca/messages.po

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/locale/locales/ca/messages.po

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/locale/locales/ca/messages.po

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/locale/locales/ca/messages.po

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/locale/locales/ca/messages.po

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update messages.po

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-09-07 12:23:50 -07:00
Samuel Newman 1b4fee3e43 [Video] Open the floodgates (partially) (#5208) 2024-09-07 12:15:15 -07:00
Hailey 51259e7c42 Revert "[Video] Ensure loop doesn't stop" (#5209) 2024-09-07 12:11:18 -07:00
Hailey 10cdc436b8 [Video] Ensure loop doesn't stop (#5207) 2024-09-07 11:54:51 -07:00
Hailey 2842f661db Add intent for verifying email (#5120) 2024-09-07 11:54:39 -07:00
Samuel Newman 45a719b256 [Video] Check upload limits before uploading (#5153)
* DRY up video service auth code

* throw error if over upload limits

* use token

* xmark on toast

* errors with nice translatable error messages

* Update src/state/queries/video/video.ts

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-09-07 19:27:32 +01:00
Samuel Newman b7d78fe59b [Video] Only compress if >25mb or unknown format (#5187)
Co-authored-by: Hailey <me@haileyok.com>
2024-09-07 11:22:44 -07:00
dan 42fb92064a Set onboarding_minimum_interests to false (#5204)
Co-authored-by: Hailey <me@haileyok.com>
2024-09-07 11:22:34 -07:00
Eric Bailey 5ec017282c Merge remote-tracking branch 'origin/main' into mod-auth
* origin/main:
  Set show_follow_suggestions_in_profile to true (#5205)
  Set fixed_bottom_bar to true (#5203)
  [Statsig] Add more events to downsample, increase downsample rate (#5198)
  fix: remove duplicate style `rounded_sm` (#5201)
  Add cursor pointer to 'New post' button (#5109)
  Add emoji picker to chat composer (#5196)
  [Video] Handle push/pop on Android for autoplay (#5194)
  nvm
  fix action
  Trigger a build maybe
  Fix starter packs scroll (#5190)
  Redesign play button (#5192)
  cleanup
  quick integration of ipcc service
2024-09-07 10:24:50 -05:00
Eric Bailey 8cd3c37919 Fall back in dev 2024-09-07 10:24:28 -05:00
dan 292117804f Set show_follow_suggestions_in_profile to true (#5205) 2024-09-07 17:08:19 +02:00
dan 7d7431d14e Set fixed_bottom_bar to true (#5203) 2024-09-07 17:07:30 +02:00
Hailey c8be9b78c6 [Statsig] Add more events to downsample, increase downsample rate (#5198)
* add some events for sampling

* include downsample rate in metadata

* fix metadata logic

* uncomment debug
2024-09-07 13:13:51 +02:00
jlca adef9cff10 fix: remove duplicate style rounded_sm (#5201) 2024-09-06 22:29:36 -07:00
nicofercavv 275f2bb004 Add cursor pointer to 'New post' button (#5109) 2024-09-06 18:18:31 -05:00
Eric Bailey 543be17674 Add emoji picker to chat composer (#5196)
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Adrov Igor <nucleartux@gmail.com>
2024-09-06 15:58:47 -07:00
Jaz 30d2ab8dd3 Merge pull request #5195 from brianolson/ipcc-handler
quick integration of ipcc service
2024-09-06 15:21:27 -07:00
Hailey 7e4f8cabd3 [Video] Handle push/pop on Android for autoplay (#5194) 2024-09-06 15:01:05 -07:00
Igor Adrov 00ce95893d Fix starter packs scroll (#5190) 2024-09-06 16:32:58 -05:00
Eric Bailey c5faa60344 Redesign play button (#5192) 2024-09-06 21:55:23 +01:00
Eric Bailey fa797719c3 Add api 2024-09-06 15:37:01 -05:00
Eric Bailey 310c121649 Filter hideable content from notifications 2024-09-06 15:09:27 -05:00
Eric Bailey f8772fc59a Filter out imperative labels from typeaheads 2024-09-06 15:09:27 -05:00
Eric Bailey 52ae6b603f Tweak logs 2024-09-06 15:09:27 -05:00
Eric Bailey e9588753f5 Add more debug logs 2024-09-06 15:09:27 -05:00
Eric Bailey 6ab64aca49 Cleanup 2024-09-06 15:09:27 -05:00
Eric Bailey ebc8581188 Cleanup, add mod 2024-09-06 15:09:27 -05:00
Eric Bailey 06878b0fcb Working 2024-09-06 15:09:27 -05:00
Eric Bailey ae3f2015f2 WIP 2024-09-06 15:09:27 -05:00
Eric Bailey 9ad2d11fa2 Filter out non-configurable mod authorities 2024-09-06 15:09:27 -05:00
Eric Bailey be1f1e49dd Set up additional mod authorities 2024-09-06 15:09:26 -05:00
243 changed files with 29377 additions and 14028 deletions
+1
View File
@@ -33,6 +33,7 @@ module.exports = {
],
'bsky-internal/use-exact-imports': 'error',
'bsky-internal/use-typed-gates': 'error',
'bsky-internal/use-prefixed-imports': 'warn',
'simple-import-sort/imports': [
'warn',
{
+1 -13
View File
@@ -74,8 +74,7 @@ appId: xyz.blueskyweb.app
- tapOn: "Delete List"
- tapOn:
id: "confirmBtn"
- assertVisible:
id: "listsEmpty"
- assertVisible: "This list is empty!"
- tapOn:
label: "Create a new curatelist"
@@ -161,17 +160,6 @@ appId: xyz.blueskyweb.app
- assertNotVisible:
id: "userAddRemoveListsModal"
- tapOn:
label: "Shows the curatelist on my profile"
id: "bottomBarProfileBtn"
- swipe:
from:
id: "profilePager-selector"
direction: LEFT
- tapOn:
id: "profilePager-selector-6"
- tapOn: "Good Ppl"
- tapOn:
label: "Adds and removes users on curatelists from the profile"
id: "bottomBarSearchBtn"
+6 -1
View File
@@ -10,8 +10,13 @@ appId: xyz.blueskyweb.app
id: "e2eSignInAlice"
# Pin alice's feed
- extendedWaitUntil:
visible:
id: "viewHeaderDrawerBtn"
- tapOn:
id: "bottomBarProfileBtn"
id: "viewHeaderDrawerBtn"
- tapOn:
id: "profileCardButton"
- swipe:
from:
id: "profilePager-selector"
+3 -10
View File
@@ -9,6 +9,9 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "e2eSignInAlice"
- extendedWaitUntil:
visible:
text: "Feeds ✨"
- tapOn:
label: "Can go to feeds page using feeds button in tab bar"
text: "Feeds ✨"
@@ -34,26 +37,16 @@ appId: xyz.blueskyweb.app
- tapOn:
label: "Can like posts"
id: "likeBtn"
- assertVisible:
id: "likeCount"
text: "1"
- tapOn:
id: "likeBtn"
- assertNotVisible:
id: "likeCount"
- tapOn:
label: "Can repost posts"
id: "repostBtn"
- tapOn: "Repost"
- assertVisible:
id: "repostCount"
text: "1"
- tapOn:
id: "repostBtn"
- tapOn: "Remove repost"
- assertNotVisible:
id: "repostCount"
- tapOn:
label: "Can delete posts"
+3
View File
@@ -11,6 +11,9 @@ appId: xyz.blueskyweb.app
# Navigate to my profile
- extendedWaitUntil:
visible:
id: "bottomBarSearchBtn"
- tapOn:
id: "bottomBarProfileBtn"
+3
View File
@@ -10,6 +10,9 @@ appId: xyz.blueskyweb.app
id: "e2eSignInAlice"
# Navigate to another user profile
- extendedWaitUntil:
visible:
id: "bottomBarSearchBtn"
- tapOn:
id: "bottomBarSearchBtn"
- tapOn:
+4 -12
View File
@@ -11,6 +11,8 @@ appId: xyz.blueskyweb.app
# Navigate to thread
- extendedWaitUntil:
visible: "Thread root"
- tapOn: "Thread root"
- assertVisible: "Thread reply"
@@ -19,32 +21,22 @@ appId: xyz.blueskyweb.app
id: "likeBtn"
childOf:
id: "postThreadItem-by-bob.test"
- assertVisible:
id: "likeCount-expanded"
- assertVisible: "1 like"
- tapOn:
id: "likeBtn"
childOf:
id: "postThreadItem-by-bob.test"
- assertNotVisible:
id: "likeCount-expanded"
- assertNotVisible: "1 like"
# Can like a reply post
- tapOn:
id: "likeBtn"
childOf:
id: "postThreadItem-by-carla.test"
- assertVisible:
id: "likeCount"
childOf:
id: "postThreadItem-by-carla.test"
- tapOn:
id: "likeBtn"
childOf:
id: "postThreadItem-by-carla.test"
- assertNotVisible:
id: "likeCount"
childOf:
id: "postThreadItem-by-carla.test"
# Can repost the root post
- tapOn:
+36 -3
View File
@@ -55,6 +55,7 @@ module.exports = function (config) {
: undefined
const UPDATES_ENABLED = !!UPDATES_CHANNEL
const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN)
const SENTRY_DIST = `${PLATFORM}.${VERSION}.${IS_TESTFLIGHT ? 'tf' : ''}${
IS_DEV ? 'dev' : ''
}`
@@ -186,7 +187,15 @@ module.exports = function (config) {
},
plugins: [
'expo-localization',
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
USE_SENTRY && [
'@sentry/react-native/expo',
{
organization: 'blueskyweb',
project: 'react-native',
release: VERSION,
dist: SENTRY_DIST,
},
],
[
'expo-build-properties',
{
@@ -211,7 +220,6 @@ module.exports = function (config) {
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
},
],
'expo-video',
'react-native-compressor',
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
'./plugins/withAndroidManifestPlugin.js',
@@ -222,6 +230,31 @@ module.exports = function (config) {
'./plugins/shareExtension/withShareExtensions.js',
'./plugins/notificationsExtension/withNotificationsExtension.js',
'./plugins/withAppDelegateReferrer.js',
[
'expo-font',
{
fonts: [
// './assets/fonts/inter/Inter-Thin.otf',
// './assets/fonts/inter/Inter-ThinItalic.otf',
// './assets/fonts/inter/Inter-ExtraLight.otf',
// './assets/fonts/inter/Inter-ExtraLightItalic.otf',
// './assets/fonts/inter/Inter-Light.otf',
// './assets/fonts/inter/Inter-LightItalic.otf',
'./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',
'./assets/fonts/inter/Inter-ExtraBold.otf',
'./assets/fonts/inter/Inter-ExtraBoldItalic.otf',
'./assets/fonts/inter/Inter-Black.otf',
'./assets/fonts/inter/Inter-BlackItalic.otf',
],
},
],
].filter(Boolean),
extra: {
eas: {
@@ -264,7 +297,7 @@ module.exports = function (config) {
* @see https://docs.expo.dev/guides/using-sentry/#app-configuration
*/
{
file: 'sentry-expo/upload-sourcemaps',
file: './postHooks/uploadSentrySourcemapsPostHook',
config: {
organization: 'blueskyweb',
project: 'react-native',
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v14h14V5H5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 229 B

@@ -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="M4 2c0-.552.41-1 .917-1h14.666c.507 0 .917.448.917 1v20c0 .552-.41 1-.917 1H4.917C4.41 23 4 22.552 4 22V2Zm1.833 1v18h12.834V3H5.833Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 276 B

@@ -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="M2 20.5c-.552 0-1-.41-1-.917V4.917C1 4.41 1.448 4 2 4h20c.552 0 1 .41 1 .917v14.666c0 .507-.448.917-1 .917H2Zm1-1.833h18V5.833H3v12.834Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 279 B

@@ -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 3a1 1 0 0 1 1 1v8.086l1.793-1.793a1 1 0 1 1 1.414 1.414l-3.5 3.5a1 1 0 0 1-1.414 0l-3.5-3.5a1 1 0 1 1 1.414-1.414L11 12.086V4a1 1 0 0 1 1-1ZM4 14a1 1 0 0 1 1 1v4h14v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 378 B

@@ -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="M4 21a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5v2H5v14h4v2H4Zm9-20v22h-2V1h2Zm7.707 2.293A1 1 0 0 1 21 4v1h-2V3h1a1 1 0 0 1 .707.293ZM19 7h2v2h-2V7Zm0 4h2v2h-2v-2Zm0 4h2v2h-2v-2Zm0 4h2v1a1 1 0 0 1-1 1h-1v-2Zm-2 0v2h-2v-2h2ZM15 5h2V3h-2v2Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 371 B

@@ -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="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v5h-2V5H5v4H3V4Zm20 9H1v-2h22v2Zm-2.293 7.707A1 1 0 0 1 20 21h-1v-2h2v1a1 1 0 0 1-.293.707ZM17 19v2h-2v-2h2Zm-4 0v2h-2v-2h2Zm-4 0v2H7v-2h2Zm-4 0v2H4a1 1 0 0 1-1-1v-1h2Zm0-2H3v-2h2v2Zm14-2v2h2v-2h-2Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 376 B

@@ -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="M9 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2h-5v14a1 1 0 1 1-2 0V6h-5a1 1 0 0 1-1-1Zm-3.073 7v8a1 1 0 1 0 2 0v-8H12a1 1 0 1 0 0-2H6.971a1.015 1.015 0 0 0-.089 0H2a1 1 0 1 0 0 2h3.927Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 317 B

@@ -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="M3.65 17.247c-.242.832-.632 1.178-1.325 1.178-.814 0-1.325-.476-1.325-1.23 0-.216.06-.51.173-.831L4.586 7.07c.364-1.014.979-1.482 1.966-1.482 1.022 0 1.629.45 2.001 1.473l3.43 9.303c.121.337.165.571.165.831 0 .72-.546 1.23-1.308 1.23-.736 0-1.126-.338-1.36-1.152l-.658-1.975H4.309l-.658 1.95ZM6.5 8.152l-1.62 5.12h3.335l-1.654-5.12H6.5Zm13.005 8.688c-.52.988-1.68 1.568-2.84 1.568-1.768 0-3.11-1.144-3.11-2.815 0-1.69 1.299-2.668 3.62-2.807l2.34-.138v-.615c0-.867-.607-1.369-1.56-1.369-.771 0-1.239.251-1.802.979-.277.312-.597.468-1.004.468-.615 0-1.057-.399-1.057-.97 0-.2.043-.382.13-.572.433-1.109 1.923-1.793 3.845-1.793 2.383 0 3.933 1.23 3.933 3.1v5.293c0 .84-.511 1.273-1.23 1.273-.684 0-1.16-.38-1.213-1.126v-.476h-.052Zm-3.43-1.386c0 .693.572 1.126 1.42 1.126 1.11 0 2.02-.719 2.02-1.723v-.676l-1.959.121c-.944.07-1.48.494-1.48 1.152Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 986 B

+1 -1
View File
@@ -372,7 +372,7 @@ function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) {
return (
<div
className="w-full overflow-hidden rounded-lg aspect-square"
className="w-full overflow-hidden rounded-lg aspect-square relative"
style={{aspectRatio: `${aspectRatio} / 1`}}>
<img
src={content.thumbnail}
+7
View File
@@ -80,6 +80,13 @@ func run(args []string) {
Value: "",
EnvVars: []string{"BASIC_AUTH_PASSWORD"},
},
&cli.StringSliceFlag{
Name: "cors-allowed-origins",
Usage: "list of allowed origins for CORS requests",
Required: false,
Value: cli.NewStringSlice("https://bsky.app", "https://main.bsky.dev", "https://app.staging.bsky.dev"),
EnvVars: []string{"CORS_ALLOWED_ORIGINS"},
},
},
},
}
+12
View File
@@ -57,6 +57,7 @@ func serve(cctx *cli.Context) error {
linkHost := cctx.String("link-host")
ipccHost := cctx.String("ipcc-host")
basicAuthPassword := cctx.String("basic-auth-password")
corsOrigins := cctx.StringSlice("cors-allowed-origins")
// Echo
e := echo.New()
@@ -168,6 +169,12 @@ func serve(cctx *cli.Context) error {
RedirectCode: http.StatusFound,
}))
// CORS middleware
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: corsOrigins,
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodOptions},
}))
//
// configure routes
//
@@ -203,6 +210,11 @@ func serve(cctx *cli.Context) error {
maxAge = 7 * (60 * 60 * 24) // 1 week
}
// fonts can be cached for a year
if strings.HasSuffix(path, ".otf") {
maxAge = 365 * (60 * 60 * 24) // 1 year
}
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
return next(c)
}
+143 -2
View File
@@ -13,7 +13,105 @@
<!-- Hello Humans! API docs at https://atproto.com -->
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Regular.1f5ed03b6dd9fd1f9982.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Italic.95778eb0c75dc956257e.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Medium.296aa2d65964269836b3.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-MediumItalic.0e57e17a6311368e2114.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-SemiBold.2277990330981b8409bb.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-SemiBoldItalic.f62fea3df3a521d6c8a7.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Bold.8d330503e1d034ad68de.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-BoldItalic.bb17e63f9baa0d861a20.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-ExtraBold.ff2581a193bf6b7e0b06.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-ExtraBoldItalic.0e50b40728d24d40fdf4.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Black.66e9a87f1c921e844ed4.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-BlackItalic.27b9f0ad06fd13a7b9da.otf">
<style>
@font-face {
font-family: "Inter-Regular";
src: local("Inter-Regular"), url(/static/media/Inter-Regular.1f5ed03b6dd9fd1f9982.otf) format("font/otf");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter-Italic";
src: local("Inter-Italic"), url(/static/media/Inter-Italic.95778eb0c75dc956257e.otf) format("font/otf");
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Inter-Medium";
src: local("Inter-Medium"), url(/static/media/Inter-Medium.296aa2d65964269836b3.otf) format("font/otf");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter-MediumItalic";
src: local("Inter-MediumItalic"), url(/static/media/Inter-MediumItalic.0e57e17a6311368e2114.otf) format("font/otf");
font-weight: 500;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Inter-SemiBold";
src: local("Inter-SemiBold"), url(/static/media/Inter-SemiBold.2277990330981b8409bb.otf) format("font/otf");
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter-SemiBoldItalic";
src: local("Inter-SemiBoldItalic"), url(/static/media/Inter-SemiBoldItalic.f62fea3df3a521d6c8a7.otf) format("font/otf");
font-weight: 600;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Inter-Bold";
src: local("Inter-Bold"), url(/static/media/Inter-Bold.8d330503e1d034ad68de.otf) format("font/otf");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter-BoldItalic";
src: local("Inter-BoldItalic"), url(/static/media/Inter-BoldItalic.bb17e63f9baa0d861a20.otf) format("font/otf");
font-weight: 700;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Inter-ExtraBold";
src: local("Inter-ExtraBold"), url(/static/media/Inter-ExtraBold.ff2581a193bf6b7e0b06.otf) format("font/otf");
font-weight: 800;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter-ExtraBoldItalic";
src: local("Inter-ExtraBoldItalic"), url(/static/media/Inter-ExtraBoldItalic.0e50b40728d24d40fdf4.otf) format("font/otf");
font-weight: 800;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Inter-Black";
src: local("Inter-Black"), url(/static/media/Inter-Black.66e9a87f1c921e844ed4.otf) format("font/otf");
font-weight: 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter-BlackItalic";
src: local("Inter-BlackItalic"), url(/static/media/Inter-BlackItalic.27b9f0ad06fd13a7b9da.otf) format("font/otf");
font-weight: 900;
font-style: italic;
font-display: swap;
}
/**
* Extend the react-native-web reset:
* https://github.com/necolas/react-native-web/blob/master/packages/react-native-web/src/exports/StyleSheet/initialRules.js
@@ -149,7 +247,6 @@
/* ProseMirror */
.ProseMirror {
font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
min-height: 140px;
}
.ProseMirror-dark {
@@ -258,7 +355,51 @@
.force-no-clicks * {
pointer-events: none !important;
}
</style>
input[type=range][orient=vertical] {
writing-mode: vertical-lr;
direction: rtl;
appearance: slider-vertical;
width: 16px;
vertical-align: bottom;
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
}
input[type="range"][orient=vertical]::-webkit-slider-runnable-track {
background: white;
height: 100%;
width: 4px;
border-radius: 4px;
}
input[type="range"][orient=vertical]::-moz-range-track {
background: white;
height: 100%;
width: 4px;
border-radius: 4px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
border-radius: 50%;
background-color: white;
height: 16px;
width: 16px;
margin-left: -6px;
}
input[type="range"][orient=vertical]::-moz-range-thumb {
border: none;
border-radius: 50%;
background-color: white;
height: 16px;
width: 16px;
margin-left: -6px;
}
</style>
{% include "scripts.html" %}
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
+1
View File
@@ -5,5 +5,6 @@ module.exports = {
'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
'use-exact-imports': require('./use-exact-imports'),
'use-typed-gates': require('./use-typed-gates'),
'use-prefixed-imports': require('./use-prefixed-imports'),
},
}
+4 -4
View File
@@ -1,4 +1,3 @@
/* eslint-disable bsky-internal/use-exact-imports */
const BANNED_IMPORTS = [
'@fortawesome/free-regular-svg-icons',
'@fortawesome/free-solid-svg-icons',
@@ -6,11 +5,12 @@ const BANNED_IMPORTS = [
exports.create = function create(context) {
return {
Literal(node) {
if (typeof node.value !== 'string') {
ImportDeclaration(node) {
const source = node.source
if (typeof source.value !== 'string') {
return
}
if (BANNED_IMPORTS.includes(node.value)) {
if (BANNED_IMPORTS.includes(source.value)) {
context.report({
node,
message:
+39
View File
@@ -0,0 +1,39 @@
const BANNED_IMPORT_PREFIXES = [
'alf/',
'components/',
'lib/',
'locale/',
'logger/',
'platform/',
'state/',
'storage/',
'view/',
]
module.exports = {
meta: {
type: 'suggestion',
fixable: 'code',
},
create(context) {
return {
ImportDeclaration(node) {
const source = node.source
if (typeof source.value !== 'string') {
return
}
if (
BANNED_IMPORT_PREFIXES.some(banned => source.value.startsWith(banned))
) {
context.report({
node: source,
message: `Use '#/${source.value}'`,
fix(fixer) {
return fixer.replaceText(source, `'#/${source.value}'`)
},
})
}
},
}
},
}
+1
View File
@@ -14,6 +14,7 @@ module.exports = {
'ja',
'ko',
'pt-BR',
'ru',
'tr',
'uk',
'zh-CN',
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.91.0",
"version": "1.91.2",
"private": true,
"engines": {
"node": ">=18"
@@ -15,7 +15,7 @@
"web": "expo start --web",
"use-build-number": "./scripts/useBuildNumberEnv.sh",
"use-build-number-with-bump": "./scripts/useBuildNumberEnvWithBump.sh",
"build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/ && cp -v ./web-build/static/media/*.png ./bskyweb/static/media/",
"build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/ && cp -v ./web-build/static/media/* ./bskyweb/static/media/",
"build-all": "yarn intl:build && yarn use-build-number-with-bump eas build --platform all",
"build-ios": "yarn use-build-number-with-bump eas build -p ios",
"build-android": "yarn use-build-number-with-bump eas build -p android",
@@ -52,7 +52,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
},
"dependencies": {
"@atproto/api": "0.13.5",
"@atproto/api": "^0.13.7",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
@@ -68,6 +68,7 @@
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-native-fontawesome": "^0.3.2",
"@haileyok/bluesky-video": "0.1.10",
"@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.7.1",
"@miblanchard/react-native-slider": "^2.3.1",
@@ -84,7 +85,7 @@
"@segment/analytics-react": "^1.0.0-rc1",
"@segment/analytics-react-native": "^2.10.1",
"@segment/sovran-react-native": "^0.4.5",
"@sentry/react-native": "5.5.0",
"@sentry/react-native": "5.32.0",
"@tamagui/focus-scope": "^1.84.1",
"@tanstack/query-async-storage-persister": "^5.25.0",
"@tanstack/react-query": "^5.8.1",
@@ -123,6 +124,7 @@
"expo-dev-client": "^4.0.14",
"expo-device": "~6.0.2",
"expo-file-system": "^17.0.1",
"expo-font": "~12.0.10",
"expo-haptics": "^13.0.1",
"expo-image": "~1.12.9",
"expo-image-manipulator": "^12.0.5",
@@ -139,7 +141,6 @@
"expo-system-ui": "~3.0.4",
"expo-task-manager": "~11.8.1",
"expo-updates": "~0.25.14",
"expo-video": "https://github.com/bluesky-social/expo/raw/expo-video-1.2.4-patch/packages/expo-video/expo-video-v1.2.4-1.tgz",
"expo-web-browser": "~13.0.3",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
@@ -177,7 +178,7 @@
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "~2.16.2",
"react-native-get-random-values": "~1.11.0",
"react-native-image-crop-picker": "0.40.3",
"react-native-image-crop-picker": "0.41.2",
"react-native-ios-context-menu": "^1.15.3",
"react-native-keyboard-controller": "^1.12.1",
"react-native-mmkv": "^2.12.2",
@@ -190,7 +191,7 @@
"react-native-safe-area-context": "4.10.1",
"react-native-screens": "~3.31.1",
"react-native-svg": "^15.3.0",
"react-native-uitextview": "^1.1.7",
"react-native-uitextview": "^1.3.0",
"react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.2",
"react-native-view-shot": "^3.8.0",
@@ -200,7 +201,6 @@
"react-responsive": "^9.0.2",
"react-textarea-autosize": "^8.5.3",
"rn-fetch-blob": "^0.12.0",
"sentry-expo": "~7.0.1",
"statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
+36
View File
@@ -0,0 +1,36 @@
diff --git a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
index 7e0b4cd..177454c 100644
--- a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
+++ b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
@@ -3,6 +3,8 @@ import { LogBox } from 'react-native';
* This is a workaround for using fetch on RN, this is a known issue in react-native and only generates a warning.
*/
export function ignoreRequireCycleLogs() {
- LogBox.ignoreLogs(['Require cycle:']);
+ try {
+ LogBox.ignoreLogs(['Require cycle:']);
+ } catch (e) {}
}
//# sourceMappingURL=ignorerequirecyclelogs.js.map
\ No newline at end of file
diff --git a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
index 0f244f2..ae7dfb3 100755
--- a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
+++ b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
@@ -174,6 +174,7 @@ if (!outputDir) {
process.exit(1);
}
+const otherArgs = process.argv.slice(3);
const files = getAssetPathsSync(outputDir);
const groupedAssets = groupAssets(files);
@@ -195,7 +196,7 @@ for (const [assetGroupName, assets] of Object.entries(groupedAssets)) {
const isHermes = assets.find(asset => asset.endsWith('.hbc'));
const windowsCallback = process.platform === "win32" ? 'node ' : '';
- execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')}`, {
+ execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')} ${otherArgs.join(' ')}`, {
env: {
...process.env,
[SENTRY_PROJECT]: sentryProject,
-15
View File
@@ -1,15 +0,0 @@
diff --git a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
index 7e0b4cd..177454c 100644
--- a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
+++ b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
@@ -3,6 +3,8 @@ import { LogBox } from 'react-native';
* This is a workaround for using fetch on RN, this is a known issue in react-native and only generates a warning.
*/
export function ignoreRequireCycleLogs() {
- LogBox.ignoreLogs(['Require cycle:']);
+ try {
+ LogBox.ignoreLogs(['Require cycle:']);
+ } catch (e) {}
}
//# sourceMappingURL=ignorerequirecyclelogs.js.map
\ No newline at end of file
-25
View File
@@ -12,28 +12,3 @@ index bb74e80..0aa0202 100644
Map<String, Object> constants = new HashMap<>(3);
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
diff --git a/node_modules/expo-modules-core/build/uuid/uuid.js b/node_modules/expo-modules-core/build/uuid/uuid.js
index 109d3fe..c421931 100644
--- a/node_modules/expo-modules-core/build/uuid/uuid.js
+++ b/node_modules/expo-modules-core/build/uuid/uuid.js
@@ -1,5 +1,7 @@
import bytesToUuid from './lib/bytesToUuid';
import { Uuidv5Namespace } from './uuid.types';
+import { ensureNativeModulesAreInstalled } from '../ensureNativeModulesAreInstalled';
+ensureNativeModulesAreInstalled();
const nativeUuidv4 = globalThis?.expo?.uuidv4;
const nativeUuidv5 = globalThis?.expo?.uuidv5;
function uuidv4() {
diff --git a/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift b/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift
index ee2268a..4851b67 100644
--- a/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift
+++ b/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift
@@ -173,7 +173,7 @@ public final class SharedObjectRegistry {
}
internal func clear() {
- Self.lockQueue.async {
+ Self.lockQueue.sync {
self.pairs.removeAll()
}
}
@@ -0,0 +1,34 @@
const exec = require('child_process').execSync
const SENTRY_AUTH_TOKEN = process.env.SENTRY_AUTH_TOKEN
module.exports = ({config}) => {
if (!SENTRY_AUTH_TOKEN) {
console.log(
'SENTRY_AUTH_TOKEN environment variable must be set to upload sourcemaps. Skipping.',
)
return
}
const org = config.organization
const project = config.project
const release = config.release
const dist = config.dist
if (!org || !project || !release || !dist) {
console.log(
'"organization", "project", "release", and "dist" must be set in the hook config to upload sourcemaps. Skipping.',
)
return
}
try {
console.log('Uploading sourcemaps to Sentry...')
exec(
`node node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps dist --url https://sentry.io/ -o ${org} -p ${project} -r ${release} -d ${dist}`,
)
console.log('Sourcemaps uploaded to Sentry.')
} catch (e) {
console.error('Error uploading sourcemaps to Sentry:', e)
}
}
+93 -76
View File
@@ -29,6 +29,11 @@ import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
ensureGeolocationResolved,
Provider as GeolocationProvider,
} from '#/state/geolocation'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -52,19 +57,25 @@ import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import {TestCtrls} from '#/view/com/testing/TestCtrls'
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoNativeContext'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast'
import {Shell} from '#/view/shell'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PortalProvider} from '#/components/Portal'
import {Splash} from '#/Splash'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import {AudioCategory, PlatformInfo} from '../modules/expo-bluesky-swiss-army'
SplashScreen.preventAutoHideAsync()
/**
* Begin geolocation ASAP
*/
beginResolveGeolocation()
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
const {currentAccount} = useSession()
@@ -105,52 +116,51 @@ function InnerApp() {
}, [_])
return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<Alf theme={theme}>
<ThemeProvider theme={theme}>
<Splash isReady={isReady && hasCheckedReferrer}>
<ActiveVideoProvider>
<RootSiblingParent>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<GestureHandlerRootView
style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</StatsigProvider>
</QueryProvider>
</React.Fragment>
</RootSiblingParent>
</ActiveVideoProvider>
</Splash>
</ThemeProvider>
</Alf>
</SafeAreaProvider>
<Alf theme={theme}>
<ThemeProvider theme={theme}>
<Splash isReady={isReady && hasCheckedReferrer}>
<RootSiblingParent>
<VideoVolumeProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<GestureHandlerRootView
style={s.h100pct}>
<TestCtrls />
<Shell />
<NuxDialogs />
</GestureHandlerRootView>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</StatsigProvider>
</QueryProvider>
</React.Fragment>
</VideoVolumeProvider>
</RootSiblingParent>
</Splash>
</ThemeProvider>
</Alf>
)
}
@@ -158,9 +168,9 @@ function App() {
const [isReady, setReady] = useState(false)
React.useEffect(() => {
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
PlatformInfo.setAudioActive(false)
initPersistedState().then(() => setReady(true))
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
setReady(true),
)
}, [])
if (!isReady) {
@@ -172,31 +182,38 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<A11yProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<InnerApp />
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</KeyboardProvider>
</A11yProvider>
<GeolocationProvider>
<A11yProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<IntentDialogProvider>
<InnerApp />
</IntentDialogProvider>
</SafeAreaProvider>
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</KeyboardProvider>
</A11yProvider>
</GeolocationProvider>
)
}
+84 -59
View File
@@ -18,6 +18,11 @@ import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
ensureGeolocationResolved,
Provider as GeolocationProvider,
} from '#/state/geolocation'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -35,21 +40,30 @@ import {
} from '#/state/session'
import {readLastActiveAccount} from '#/state/session/util'
import {Provider as ShellStateProvider} from '#/state/shell'
import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut'
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast'
import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PortalProvider} from '#/components/Portal'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
/**
* Begin geolocation ASAP
*/
beginResolveGeolocation()
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
const {currentAccount} = useSession()
@@ -59,6 +73,8 @@ function InnerApp() {
useIntentHandler()
const hasCheckedReferrer = useStarterPackEntry()
useComposerKeyboardShortcut()
// init
useEffect(() => {
async function onLaunch(account?: SessionAccount) {
@@ -93,41 +109,44 @@ function InnerApp() {
<Alf theme={theme}>
<ThemeProvider theme={theme}>
<RootSiblingParent>
<ActiveVideoProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<Shell />
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</StatsigProvider>
</QueryProvider>
</React.Fragment>
<ToastContainer />
</ActiveVideoProvider>
<VideoVolumeProvider>
<ActiveVideoProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<Shell />
<NuxDialogs />
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</StatsigProvider>
</QueryProvider>
<ToastContainer />
</React.Fragment>
</ActiveVideoProvider>
</VideoVolumeProvider>
</RootSiblingParent>
</ThemeProvider>
</Alf>
@@ -139,7 +158,9 @@ function App() {
const [isReady, setReady] = useState(false)
React.useEffect(() => {
initPersistedState().then(() => setReady(true))
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
setReady(true),
)
}, [])
if (!isReady) {
@@ -151,29 +172,33 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<A11yProvider>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<InnerApp />
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</A11yProvider>
<GeolocationProvider>
<A11yProvider>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<IntentDialogProvider>
<InnerApp />
</IntentDialogProvider>
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</A11yProvider>
</GeolocationProvider>
)
}
+4 -5
View File
@@ -661,16 +661,15 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
linking={LINKING}
theme={theme}
onStateChange={() => {
logEvent('router:navigate:sampled', {
from: prevLoggedRouteName.current,
})
prevLoggedRouteName.current = getCurrentRouteName()
const routeName = getCurrentRouteName()
if (routeName === 'Notifications') {
logEvent('router:navigate:notifications:sampled', {})
}
}}
onReady={() => {
attachRouteToLogEvents(getCurrentRouteName)
logModuleInitTime()
onReady()
logEvent('router:navigate:sampled', {})
}}>
{children}
</NavigationContainer>
+11 -14
View File
@@ -225,43 +225,43 @@ export const atoms = {
},
text_2xs: {
fontSize: tokens.fontSize._2xs,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_xs: {
fontSize: tokens.fontSize.xs,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_sm: {
fontSize: tokens.fontSize.sm,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_md: {
fontSize: tokens.fontSize.md,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_lg: {
fontSize: tokens.fontSize.lg,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_xl: {
fontSize: tokens.fontSize.xl,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_2xl: {
fontSize: tokens.fontSize._2xl,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_3xl: {
fontSize: tokens.fontSize._3xl,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_4xl: {
fontSize: tokens.fontSize._4xl,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_5xl: {
fontSize: tokens.fontSize._5xl,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
leading_tight: {
lineHeight: 1.15,
@@ -273,10 +273,7 @@ export const atoms = {
lineHeight: 1.5,
},
tracking_normal: {
letterSpacing: 0,
},
tracking_wide: {
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
font_normal: {
fontWeight: tokens.fontWeight.normal,
+118
View File
@@ -0,0 +1,118 @@
import {useFonts as defaultUseFonts} from 'expo-font'
import {isWeb} from '#/platform/detection'
import {Device, device} from '#/storage'
const FAMILIES = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif`
const factor = 0.0625 // 1 - (15/16)
const fontScaleMultipliers: Record<Device['fontScale'], number> = {
'-2': 1 - factor * 3,
'-1': 1 - factor * 2,
'0': 1 - factor * 1, // default
'1': 1,
'2': 1 + factor * 1,
}
export function computeFontScaleMultiplier(scale: Device['fontScale']) {
return fontScaleMultipliers[scale]
}
export function getFontScale() {
return device.get(['fontScale']) ?? '0'
}
export function setFontScale(fontScale: Device['fontScale']) {
device.set(['fontScale'], fontScale)
}
export function getFontFamily() {
return device.get(['fontFamily']) || 'theme'
}
export function setFontFamily(fontFamily: Device['fontFamily']) {
device.set(['fontFamily'], fontFamily)
}
/*
* IMPORTANT: This is unused. Expo statically extracts these fonts, but we load
* them manually so that we can parallelize the loading along with the JS
* bundle.
*
* See `#/alf/util/useFonts` for the actually used hooks.
*
* All used fonts MUST be configured here. Unused fonts are commented out, but
* the files are there if we need them.
*/
export function DO_NOT_USE() {
return defaultUseFonts({
// 'Inter-Thin': require('../../assets/fonts/inter/Inter-Thin.otf'),
// 'Inter-ThinItalic': require('../../assets/fonts/inter/Inter-ThinItalic.otf'),
// 'Inter-ExtraLight': require('../../assets/fonts/inter/Inter-ExtraLight.otf'),
// 'Inter-ExtraLightItalic': require('../../assets/fonts/inter/Inter-ExtraLightItalic.otf'),
// 'Inter-Light': require('../../assets/fonts/inter/Inter-Light.otf'),
// 'Inter-LightItalic': require('../../assets/fonts/inter/Inter-LightItalic.otf'),
'Inter-Regular': require('../../assets/fonts/inter/Inter-Regular.otf'),
'Inter-Italic': require('../../assets/fonts/inter/Inter-Italic.otf'),
'Inter-Medium': require('../../assets/fonts/inter/Inter-Medium.otf'),
'Inter-MediumItalic': require('../../assets/fonts/inter/Inter-MediumItalic.otf'),
'Inter-SemiBold': require('../../assets/fonts/inter/Inter-SemiBold.otf'),
'Inter-SemiBoldItalic': require('../../assets/fonts/inter/Inter-SemiBoldItalic.otf'),
'Inter-Bold': require('../../assets/fonts/inter/Inter-Bold.otf'),
'Inter-BoldItalic': require('../../assets/fonts/inter/Inter-BoldItalic.otf'),
'Inter-ExtraBold': require('../../assets/fonts/inter/Inter-ExtraBold.otf'),
'Inter-ExtraBoldItalic': require('../../assets/fonts/inter/Inter-ExtraBoldItalic.otf'),
'Inter-Black': require('../../assets/fonts/inter/Inter-Black.otf'),
'Inter-BlackItalic': require('../../assets/fonts/inter/Inter-BlackItalic.otf'),
})
}
/*
* Unused fonts are commented out, but the files are there if we need them.
*/
export function applyFonts(
style: Record<string, any>,
fontFamily: 'system' | 'theme',
) {
if (fontFamily === 'theme') {
style.fontFamily =
{
// '100': 'Inter-Thin',
// '200': 'Inter-ExtraLight',
// '300': 'Inter-Light',
'100': 'Inter-Regular',
'200': 'Inter-Regular',
'300': 'Inter-Regular',
'400': 'Inter-Regular',
'500': 'Inter-Medium',
'600': 'Inter-SemiBold',
'700': 'Inter-Bold',
'800': 'Inter-ExtraBold',
'900': 'Inter-Black',
}[style.fontWeight as string] || 'Inter-Regular'
if (style.fontStyle === 'italic') {
if (style.fontFamily === 'Inter-Regular') {
style.fontFamily = 'Inter-Italic'
} else {
style.fontFamily += 'Italic'
}
}
// fallback families only supported on web
if (isWeb) {
style.fontFamily += `, ${FAMILIES}`
}
} else {
// fallback families only supported on web
if (isWeb) {
style.fontFamily = style.fontFamily || FAMILIES
}
}
/**
* Disable contextual ligatures
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant}
*/
style.fontVariant = ['no-contextual']
}
+99 -10
View File
@@ -1,32 +1,98 @@
import React from 'react'
import {useMediaQuery} from 'react-responsive'
import {
computeFontScaleMultiplier,
getFontFamily,
getFontScale,
setFontFamily as persistFontFamily,
setFontScale as persistFontScale,
} from '#/alf/fonts'
import {createThemes, defaultTheme} from '#/alf/themes'
import {Theme, ThemeName} from '#/alf/types'
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
import {Device} from '#/storage'
export {atoms} from '#/alf/atoms'
export * from '#/alf/fonts'
export * as tokens from '#/alf/tokens'
export * from '#/alf/types'
export * from '#/alf/util/flatten'
export * from '#/alf/util/platform'
export * from '#/alf/util/themeSelector'
export type Alf = {
themeName: ThemeName
theme: Theme
themes: ReturnType<typeof createThemes>
fonts: {
scale: Exclude<Device['fontScale'], undefined>
scaleMultiplier: number
family: Device['fontFamily']
setFontScale: (fontScale: Exclude<Device['fontScale'], undefined>) => void
setFontFamily: (fontFamily: Device['fontFamily']) => void
}
/**
* Feature flags or other gated options
*/
flags: {}
}
/*
* Context
*/
export const Context = React.createContext<{
themeName: ThemeName
theme: Theme
}>({
export const Context = React.createContext<Alf>({
themeName: 'light',
theme: defaultTheme,
themes: createThemes({
hues: {
primary: BLUE_HUE,
negative: RED_HUE,
positive: GREEN_HUE,
},
}),
fonts: {
scale: getFontScale(),
scaleMultiplier: computeFontScaleMultiplier(getFontScale()),
family: getFontFamily(),
setFontScale: () => {},
setFontFamily: () => {},
},
flags: {},
})
export function ThemeProvider({
children,
theme: themeName,
}: React.PropsWithChildren<{theme: ThemeName}>) {
const [fontScale, setFontScale] = React.useState<Alf['fonts']['scale']>(() =>
getFontScale(),
)
const [fontScaleMultiplier, setFontScaleMultiplier] = React.useState(() =>
computeFontScaleMultiplier(fontScale),
)
const setFontScaleAndPersist = React.useCallback<
Alf['fonts']['setFontScale']
>(
fontScale => {
setFontScale(fontScale)
persistFontScale(fontScale)
setFontScaleMultiplier(computeFontScaleMultiplier(fontScale))
},
[setFontScale],
)
const [fontFamily, setFontFamily] = React.useState<Alf['fonts']['family']>(
() => getFontFamily(),
)
const setFontFamilyAndPersist = React.useCallback<
Alf['fonts']['setFontFamily']
>(
fontFamily => {
setFontFamily(fontFamily)
persistFontFamily(fontFamily)
},
[setFontFamily],
)
const themes = React.useMemo(() => {
return createThemes({
hues: {
@@ -36,24 +102,47 @@ export function ThemeProvider({
},
})
}, [])
const theme = themes[themeName]
return (
<Context.Provider
value={React.useMemo(
value={React.useMemo<Alf>(
() => ({
themes,
themeName: themeName,
theme: theme,
theme: themes[themeName],
fonts: {
scale: fontScale,
scaleMultiplier: fontScaleMultiplier,
family: fontFamily,
setFontScale: setFontScaleAndPersist,
setFontFamily: setFontFamilyAndPersist,
},
flags: {},
}),
[theme, themeName],
[
themeName,
themes,
fontScale,
setFontScaleAndPersist,
fontFamily,
setFontFamilyAndPersist,
fontScaleMultiplier,
],
)}>
{children}
</Context.Provider>
)
}
export function useTheme() {
return React.useContext(Context).theme
export function useAlf() {
return React.useContext(Context)
}
export function useTheme(theme?: ThemeName) {
const alf = useAlf()
return React.useMemo(() => {
return theme ? alf.themes[theme] : alf.theme
}, [theme, alf])
}
export function useBreakpoints() {
+4
View File
@@ -1,3 +1,7 @@
import {Platform} from 'react-native'
export const TRACKING = Platform.OS === 'android' ? 0.1 : 0
export const color = {
temp_purple: 'rgb(105 0 255)',
temp_purple_dark: 'rgb(83 0 202)',
+2 -10
View File
@@ -7,7 +7,6 @@ import {
PressableProps,
StyleProp,
StyleSheet,
Text,
TextProps,
TextStyle,
View,
@@ -17,7 +16,7 @@ import {LinearGradient} from 'expo-linear-gradient'
import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf'
import {Props as SVGIconProps} from '#/components/icons/common'
import {normalizeTextStyles} from '#/components/Typography'
import {Text} from '#/components/Typography'
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
export type ButtonColor =
@@ -635,14 +634,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
const textStyles = useSharedButtonTextStyles()
return (
<Text
{...rest}
style={normalizeTextStyles([
a.font_bold,
a.text_center,
textStyles,
style,
])}>
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
{children}
</Text>
)
+2 -1
View File
@@ -37,6 +37,7 @@ import {Portal} from '#/components/Portal'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/types'
export * from '#/components/Dialog/utils'
// @ts-ignore
export const Input = createInput(BottomSheetTextInput)
@@ -256,7 +257,7 @@ export const ScrollableInner = React.forwardRef<
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
},
flatten(style),
style,
]}
contentContainerStyle={a.pb_4xl}
ref={ref}>
+1
View File
@@ -27,6 +27,7 @@ import {Portal} from '#/components/Portal'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/types'
export * from '#/components/Dialog/utils'
export {Input} from '#/components/forms/TextField'
const stopPropagation = (e: any) => e.stopPropagation()
+18
View File
@@ -0,0 +1,18 @@
import React from 'react'
import {DialogControlProps} from '#/components/Dialog/types'
export function useAutoOpen(control: DialogControlProps, showTimeout?: number) {
React.useEffect(() => {
if (showTimeout) {
const timeout = setTimeout(() => {
control.open()
}, showTimeout)
return () => {
clearTimeout(timeout)
}
} else {
control.open()
}
}, [control, showTimeout])
}
+32 -34
View File
@@ -8,7 +8,6 @@ import {useNavigation} from '@react-navigation/native'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {NavigationProp} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -61,16 +60,13 @@ function CardOuter({
export function SuggestedFollowPlaceholder() {
const t = useTheme()
return (
<CardOuter style={[a.gap_sm, t.atoms.border_contrast_low]}>
<CardOuter style={[a.gap_md, t.atoms.border_contrast_low]}>
<ProfileCard.Header>
<ProfileCard.AvatarPlaceholder />
<ProfileCard.NameAndHandlePlaceholder />
</ProfileCard.Header>
<View style={[a.py_xs]}>
<ProfileCard.NameAndHandlePlaceholder />
</View>
<ProfileCard.DescriptionPlaceholder />
<ProfileCard.DescriptionPlaceholder numberOfLines={2} />
</CardOuter>
)
}
@@ -177,13 +173,13 @@ function useExperimentalSuggestedUsersQuery() {
}
export function SuggestedFollows({feed}: {feed: FeedDescriptor}) {
const gate = useGate()
const [feedType, feedUri] = feed.split('|')
const {currentAccount} = useSession()
const [feedType, feedUriOrDid] = feed.split('|')
if (feedType === 'author') {
if (gate('show_follow_suggestions_in_profile')) {
return <SuggestedFollowsProfile did={feedUri} />
} else {
if (currentAccount?.did === feedUriOrDid) {
return null
} else {
return <SuggestedFollowsProfile did={feedUriOrDid} />
}
} else {
return <SuggestedFollowsHome />
@@ -203,6 +199,7 @@ export function SuggestedFollowsProfile({did}: {did: string}) {
isSuggestionsLoading={isSuggestionsLoading}
profiles={data?.suggestions ?? []}
error={error}
viewContext="profile"
/>
)
}
@@ -218,6 +215,7 @@ export function SuggestedFollowsHome() {
isSuggestionsLoading={isSuggestionsLoading}
profiles={profiles}
error={error}
viewContext="feed"
/>
)
}
@@ -226,10 +224,12 @@ export function ProfileGrid({
isSuggestionsLoading,
error,
profiles,
viewContext = 'feed',
}: {
isSuggestionsLoading: boolean
profiles: AppBskyActorDefs.ProfileViewDetailed[]
error: Error | null
viewContext: 'profile' | 'feed'
}) {
const t = useTheme()
const {_} = useLingui()
@@ -286,7 +286,7 @@ export function ProfileGrid({
shape="round"
/>
</ProfileCard.Header>
<ProfileCard.Description profile={profile} />
<ProfileCard.Description profile={profile} numberOfLines={2} />
</ProfileCard.Outer>
</CardOuter>
)}
@@ -303,33 +303,31 @@ export function ProfileGrid({
return (
<View
style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}>
<View style={[a.pt_2xl, a.px_lg, a.flex_row, a.pb_xs]}>
<Text
style={[
a.flex_1,
a.text_lg,
a.font_bold,
t.atoms.text_contrast_medium,
]}>
<Trans>Suggested for you</Trans>
<View
style={[
a.p_lg,
a.pb_xs,
a.flex_row,
a.align_center,
a.justify_between,
]}>
<Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}>
{viewContext === 'profile' ? (
<Trans>Similar accounts</Trans>
) : (
<Trans>Suggested for you</Trans>
)}
</Text>
<Person fill={t.atoms.text_contrast_low.color} />
<Person fill={t.atoms.text_contrast_low.color} size="sm" />
</View>
{gtMobile ? (
<View style={[a.flex_1, a.px_lg, a.pt_md, a.pb_xl, a.gap_md]}>
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}>
<View style={[a.flex_1, a.px_lg, a.pt_sm, a.pb_lg, a.gap_md]}>
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_sm]}>
{content}
</View>
<View
style={[
a.flex_row,
a.justify_end,
a.align_center,
a.pt_xs,
a.gap_md,
]}>
<View style={[a.flex_row, a.justify_end, a.align_center, a.gap_md]}>
<InlineLinkText
label={_(msg`Browse more suggestions`)}
to="/search"
@@ -345,7 +343,7 @@ export function ProfileGrid({
showsHorizontalScrollIndicator={false}
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
decelerationRate="fast">
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
{content}
<Button
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, ViewStyleProp} from '#/alf'
export function Fill({
children,
style,
}: {children?: React.ReactNode} & ViewStyleProp) {
return <View style={[a.absolute, a.inset_0, style]}>{children}</View>
}
+24 -4
View File
@@ -9,6 +9,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {useLabelerInfoQuery} from '#/state/queries/labeler'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
import {Link as InternalLink, LinkProps} from '#/components/Link'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
@@ -43,21 +44,40 @@ export function Avatar({avatar}: {avatar?: string}) {
}
export function Title({value}: {value: string}) {
return <Text style={[a.text_md, a.font_bold]}>{value}</Text>
return <Text style={[a.text_md, a.font_bold, a.leading_tight]}>{value}</Text>
}
export function Description({value, handle}: {value?: string; handle: string}) {
return value ? (
<Text numberOfLines={2}>
<RichText value={value} style={[]} />
<RichText value={value} style={[a.leading_snug]} />
</Text>
) : (
<Text>
<Text style={[a.leading_snug]}>
<Trans>By {sanitizeHandle(handle, '@')}</Trans>
</Text>
)
}
export function RegionalNotice() {
const t = useTheme()
return (
<View
style={[
a.flex_row,
a.align_center,
a.gap_xs,
a.pt_2xs,
{marginLeft: -2},
]}>
<Flag fill={t.atoms.text_contrast_low.color} size="sm" />
<Text style={[a.italic, a.leading_snug]}>
<Trans>Required in your region</Trans>
</Text>
</View>
)
}
export function LikeCount({count}: {count: number}) {
const t = useTheme()
return (
@@ -85,7 +105,7 @@ export function Content({children}: React.PropsWithChildren<{}>) {
a.align_center,
a.justify_between,
]}>
<View style={[a.gap_xs, a.flex_1]}>{children}</View>
<View style={[a.gap_2xs, a.flex_1]}>{children}</View>
<ChevronRight size="md" style={[a.z_10, t.atoms.text_contrast_low]} />
</View>
+1
View File
@@ -75,6 +75,7 @@ export function LikedByList({uri}: {uri: string}) {
isLoading={isUriLoading || isLikedByLoading}
isError={isError}
emptyType="results"
emptyTitle={_(msg`No likes yet`)}
emptyMessage={_(
msg`Nobody has liked this yet. Maybe you should be the first!`,
)}
+45
View File
@@ -0,0 +1,45 @@
import React from 'react'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
import {Fill} from '#/components/Fill'
/**
* Applies and thin border within a bounding box. Used to contrast media from
* bg of the container.
*/
export function MediaInsetBorder({
children,
style,
opaque,
}: {
children?: React.ReactNode
/**
* Used where this border needs to match adjacent borders, such as in
* external link previews
*/
opaque?: boolean
} & ViewStyleProp) {
const t = useTheme()
const isLight = t.name === 'light'
return (
<Fill
style={[
a.rounded_sm,
a.border,
opaque
? [t.atoms.border_contrast_low]
: [
isLight
? t.atoms.border_contrast_low
: t.atoms.border_contrast_high,
{opacity: 0.6},
],
{
pointerEvents: 'none',
},
style,
]}>
{children}
</Fill>
)
}
+11 -8
View File
@@ -10,7 +10,8 @@ import {
import {Trans} from '@lingui/macro'
import {parseTenorGif} from '#/lib/strings/embed-player'
import {atoms as a} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {Text} from '#/components/Typography'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
@@ -43,10 +44,10 @@ export function Embed({
))}
</Outer>
)
} else if (AppBskyEmbedExternal.isView(embed) && embed.external.thumb) {
} else if (AppBskyEmbedExternal.isView(media) && media.external.thumb) {
let url: URL | undefined
try {
url = new URL(embed.external.uri)
url = new URL(media.external.uri)
} catch {}
if (url) {
const {success} = parseTenorGif(url)
@@ -54,17 +55,17 @@ export function Embed({
return (
<Outer style={style}>
<GifItem
thumbnail={embed.external.thumb}
alt={embed.external.title}
thumbnail={media.external.thumb}
alt={media.external.title}
/>
</Outer>
)
}
}
} else if (AppBskyEmbedVideo.isView(embed)) {
} else if (AppBskyEmbedVideo.isView(media)) {
return (
<Outer style={style}>
<VideoItem thumbnail={embed.thumbnail} alt={embed.alt} />
<VideoItem thumbnail={media.thumbnail} alt={media.alt} />
</Outer>
)
}
@@ -91,18 +92,20 @@ export function ImageItem({
alt?: string
children?: React.ReactNode
}) {
const t = useTheme()
return (
<View style={[a.relative, a.flex_1, {aspectRatio: 1, maxWidth: 100}]}>
<Image
key={thumbnail}
source={{uri: thumbnail}}
style={[a.flex_1, a.rounded_xs]}
style={[a.flex_1, a.rounded_xs, t.atoms.bg_contrast_25]}
contentFit="cover"
accessible={true}
accessibilityIgnoresInvertColors
accessibilityHint={alt}
accessibilityLabel=""
/>
<MediaInsetBorder style={[a.rounded_xs]} />
{children}
</View>
)
+2
View File
@@ -179,8 +179,10 @@ export function Outer({
style={[
a.rounded_sm,
a.p_xs,
a.border,
t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
t.atoms.shadow_md,
t.atoms.border_contrast_low,
style,
]}>
{children}
+24 -19
View File
@@ -220,8 +220,10 @@ export function NameAndHandlePlaceholder() {
export function Description({
profile: profileUnshadowed,
numberOfLines = 3,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
numberOfLines?: number
}) {
const profile = useProfileShadow(profileUnshadowed)
const {description} = profile
@@ -244,31 +246,34 @@ export function Description({
<RichText
value={rt}
style={[a.leading_snug]}
numberOfLines={3}
numberOfLines={numberOfLines}
disableLinks
/>
</View>
)
}
export function DescriptionPlaceholder() {
export function DescriptionPlaceholder({
numberOfLines = 3,
}: {
numberOfLines?: number
}) {
const t = useTheme()
return (
<View style={[a.gap_xs]}>
<View
style={[a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, {height: 12}]}
/>
<View
style={[a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, {height: 12}]}
/>
<View
style={[
a.rounded_xs,
a.w_full,
t.atoms.bg_contrast_50,
{height: 12, width: 100},
]}
/>
<View style={[{gap: 8}]}>
{Array(numberOfLines)
.fill(0)
.map((_, i) => (
<View
key={i}
style={[
a.rounded_xs,
a.w_full,
t.atoms.bg_contrast_50,
{height: 12, width: i + 1 === numberOfLines ? '60%' : '100%'},
]}
/>
))}
</View>
)
}
@@ -276,8 +281,8 @@ export function DescriptionPlaceholder() {
export type FollowButtonProps = {
profile: AppBskyActorDefs.ProfileViewBasic
moderationOpts: ModerationOpts
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext']
logContext: LogEvents['profile:follow:sampled']['logContext'] &
LogEvents['profile:unfollow:sampled']['logContext']
} & Partial<ButtonProps>
export function FollowButton(props: FollowButtonProps) {
+3 -1
View File
@@ -59,7 +59,9 @@ export function Outer({
export function TitleText({children}: React.PropsWithChildren<{}>) {
const {titleId} = React.useContext(Context)
return (
<Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}>
<Text
nativeID={titleId}
style={[a.text_2xl, a.font_bold, a.pb_sm, a.leading_snug]}>
{children}
</Text>
)
@@ -18,7 +18,7 @@ interface ProfilesListProps {
export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>(
function PostsListImpl({listUri, headerHeight, scrollElRef}, ref) {
const feed: FeedDescriptor = `list|${listUri}|as_following`
const feed: FeedDescriptor = `list|${listUri}`
const {_} = useLingui()
const onScrollToTop = useCallback(() => {
@@ -40,7 +40,7 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
ref,
) {
const t = useTheme()
const bottomBarOffset = useBottomBarOffset(200)
const bottomBarOffset = useBottomBarOffset(300)
const initialNumToRender = useInitialNumToRender()
const {currentAccount} = useSession()
const {data, refetch, isError} = useAllListMembersQuery(listUri)
+21 -5
View File
@@ -3,7 +3,7 @@ import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
import {UITextView} from 'react-native-uitextview'
import {isNative} from '#/platform/detection'
import {atoms, flatten, useTheme, web} from '#/alf'
import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
export type TextProps = RNTextProps & {
/**
@@ -34,19 +34,30 @@ export function leading<
* If the `lineHeight` value is > 2, we assume it's an absolute value and
* returns it as-is.
*/
export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
export function normalizeTextStyles(
styles: StyleProp<TextStyle>,
{
fontScale,
fontFamily,
}: {
fontScale: number
fontFamily: Alf['fonts']['family']
} & Pick<Alf, 'flags'>,
) {
const s = flatten(styles)
// should always be defined on these components
const fontSize = s.fontSize || atoms.text_md.fontSize
s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
if (s?.lineHeight) {
if (s.lineHeight !== 0 && s.lineHeight <= 2) {
s.lineHeight = Math.round(fontSize * s.lineHeight)
s.lineHeight = Math.round(s.fontSize * s.lineHeight)
}
} else if (!isNative) {
s.lineHeight = s.fontSize
}
applyFonts(s, fontFamily)
return s
}
@@ -54,8 +65,13 @@ export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
* Our main text component. Use this most of the time.
*/
export function Text({style, selectable, ...rest}: TextProps) {
const {fonts, flags} = useAlf()
const t = useTheme()
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)])
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], {
fontScale: fonts.scaleMultiplier,
fontFamily: fonts.family,
flags,
})
return <UITextView selectable={selectable} uiTextView style={s} {...rest} />
}
@@ -0,0 +1,118 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppearanceToggleButtonGroup} from '#/screens/Settings/AppearanceSettings'
import {atoms as a, useAlf, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
import {Divider} from '#/components/Divider'
import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
import {Text} from '#/components/Typography'
export function NeueTypography() {
const t = useTheme()
const {_} = useLingui()
const nuxDialogs = useNuxDialogContext()
const control = Dialog.useDialogControl()
const {fonts} = useAlf()
Dialog.useAutoOpen(control, 3e3)
const onClose = React.useCallback(() => {
nuxDialogs.dismissActiveNux()
}, [nuxDialogs])
const onChangeFontFamily = React.useCallback(
(values: string[]) => {
const next = values[0] === 'system' ? 'system' : 'theme'
fonts.setFontFamily(next)
},
[fonts],
)
const onChangeFontScale = React.useCallback(
(values: string[]) => {
const next = values[0] || ('0' as any)
fonts.setFontScale(next)
},
[fonts],
)
return (
<Dialog.Outer control={control} onClose={onClose}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
<View style={[a.gap_xl]}>
<View style={[a.gap_md]}>
<Text style={[a.text_3xl, {fontWeight: '900'}]}>
<Trans>New font settings </Trans>
</Text>
<Text style={[a.text_lg, a.leading_snug, {maxWidth: 400}]}>
<Trans>
We're introducing a new theme font, along with adjustable font
sizing.
</Trans>
</Text>
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
You can adjust these in your Appearance Settings later.
</Trans>
</Text>
</View>
<Divider />
<View style={[a.gap_lg]}>
<AppearanceToggleButtonGroup
title={_(msg`Font`)}
description={_(
msg`For the best experience, we recommend using the theme font.`,
)}
icon={Aa}
items={[
{
label: _(msg`System`),
name: 'system',
},
{
label: _(msg`Theme`),
name: 'theme',
},
]}
values={[fonts.family]}
onChange={onChangeFontFamily}
/>
<AppearanceToggleButtonGroup
title={_(msg`Font size`)}
icon={TextSize}
items={[
{
label: _(msg`Smaller`),
name: '-1',
},
{
label: _(msg`Default`),
name: '0',
},
{
label: _(msg`Larger`),
name: '1',
},
]}
values={[fonts.scale]}
onChange={onChangeFontScale}
/>
</View>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
@@ -0,0 +1,129 @@
import React from 'react'
import {View} from 'react-native'
import Svg, {Circle, Path} from 'react-native-svg'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Nux, useUpsertNuxMutation} from '#/state/queries/nuxs'
import {atoms as a, ViewStyleProp} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {InlineLinkText} from '#/components/Link'
import * as Prompt from '#/components/Prompt'
import {TenMillion} from './'
export function Trigger({children}: {children: ButtonProps['children']}) {
const {_} = useLingui()
const {mutate: upsertNux} = useUpsertNuxMutation()
const [show, setShow] = React.useState(false)
const [fallback, setFallback] = React.useState(false)
const control = Prompt.usePromptControl()
const handleOnPress = () => {
if (!fallback) {
setShow(true)
upsertNux({
id: Nux.TenMillionDialog,
completed: true,
data: undefined,
})
} else {
control.open()
}
}
const onHandleFallback = () => {
setFallback(true)
control.open()
}
return (
<>
<Button
label={_(msg`Bluesky is celebrating 10 million users!`)}
onPress={handleOnPress}>
{children}
</Button>
{show && !fallback && (
<TenMillion
showTimeout={0}
onClose={() => setShow(false)}
onFallback={onHandleFallback}
/>
)}
<Prompt.Outer control={control}>
<View style={{maxWidth: 300}}>
<Prompt.TitleText>
<Trans>Bluesky is celebrating 10 million users!</Trans>
</Prompt.TitleText>
</View>
<Prompt.DescriptionText>
<Trans>
Together, we're rebuilding the social internet. We're glad you're
here.
</Trans>
</Prompt.DescriptionText>
<Prompt.DescriptionText>
<Trans>
To learn more,{' '}
<InlineLinkText
label={_(msg`View our post`)}
to="/profile/bsky.app/post/3l47prg3wgy23"
onPress={() => {
control.close()
}}
style={[a.text_md, a.leading_snug]}>
<Trans>check out our post.</Trans>
</InlineLinkText>
</Trans>
</Prompt.DescriptionText>
<Dialog.Close />
</Prompt.Outer>
</>
)
}
export function Icon({width, style}: {width: number} & ViewStyleProp) {
return (
<Svg width={width} height={width} viewBox="0 0 36 36" style={style}>
<Path
fill="#dd2e44"
d="M11.626 7.488a1.4 1.4 0 0 0-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937c.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269c1.562-1.562-.971-6.627-5.656-11.313c-4.687-4.686-9.752-7.218-11.315-5.656"
/>
<Path
fill="#ea596e"
d="M13 12L.416 32.506l-.282.635l.011.011c-.208.403.14 1.223.853 1.937c.232.232.473.408.709.557L17 17z"
/>
<Path
fill="#a0041e"
d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124c-1.473 1.474-6.453-1.118-11.126-5.788c-4.671-4.672-7.263-9.654-5.79-11.127c1.474-1.473 6.454 1.119 11.127 5.791"
/>
<Path
fill="#aa8dd8"
d="M18.59 13.609a1 1 0 0 1-.734.215c-.868-.094-1.598-.396-2.109-.873c-.541-.505-.808-1.183-.735-1.862c.128-1.192 1.324-2.286 3.363-2.066c.793.085 1.147-.17 1.159-.292c.014-.121-.277-.446-1.07-.532c-.868-.094-1.598-.396-2.11-.873c-.541-.505-.809-1.183-.735-1.862c.13-1.192 1.325-2.286 3.362-2.065c.578.062.883-.057 1.012-.134c.103-.063.144-.123.148-.158c.012-.121-.275-.446-1.07-.532a1 1 0 0 1-.886-1.102a.997.997 0 0 1 1.101-.886c2.037.219 2.973 1.542 2.844 2.735c-.13 1.194-1.325 2.286-3.364 2.067c-.578-.063-.88.057-1.01.134c-.103.062-.145.123-.149.157c-.013.122.276.446 1.071.532c2.037.22 2.973 1.542 2.844 2.735s-1.324 2.286-3.362 2.065c-.578-.062-.882.058-1.012.134c-.104.064-.144.124-.148.158c-.013.121.276.446 1.07.532a1 1 0 0 1 .52 1.773"
/>
<Path
fill="#77b255"
d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478c.324 1.154-.378 2.615-2.35 3.17c-.77.216-1.001.584-.97.701c.034.118.425.312 1.193.095c1.972-.555 3.333.325 3.657 1.479c.326 1.155-.378 2.614-2.351 3.17c-.769.216-1.001.585-.967.702s.423.311 1.192.095a1 1 0 1 1 .54 1.925c-1.971.555-3.333-.323-3.659-1.479c-.324-1.154.379-2.613 2.353-3.169c.77-.217 1.001-.584.967-.702c-.032-.117-.422-.312-1.19-.096c-1.974.556-3.334-.322-3.659-1.479c-.325-1.154.378-2.613 2.351-3.17c.768-.215.999-.585.967-.701c-.034-.118-.423-.312-1.192-.096a1 1 0 1 1-.54-1.923"
/>
<Path
fill="#aa8dd8"
d="M23.001 20.16a1.001 1.001 0 0 1-.626-1.781c.218-.175 5.418-4.259 12.767-3.208a1 1 0 1 1-.283 1.979c-6.493-.922-11.187 2.754-11.233 2.791a1 1 0 0 1-.625.219"
/>
<Path
fill="#77b255"
d="M5.754 16a1 1 0 0 1-.958-1.287c1.133-3.773 2.16-9.794.898-11.364c-.141-.178-.354-.353-.842-.316c-.938.072-.849 2.051-.848 2.071a1 1 0 1 1-1.994.149c-.103-1.379.326-4.035 2.692-4.214c1.056-.08 1.933.287 2.552 1.057c2.371 2.951-.036 11.506-.542 13.192a1 1 0 0 1-.958.712"
/>
<Circle cx="25.5" cy="9.5" r="1.5" fill="#5c913b" />
<Circle cx="2" cy="18" r="2" fill="#9266cc" />
<Circle cx="32.5" cy="19.5" r="1.5" fill="#5c913b" />
<Circle cx="23.5" cy="31.5" r="1.5" fill="#5c913b" />
<Circle cx="28" cy="4" r="2" fill="#ffcc4d" />
<Circle cx="32.5" cy="8.5" r="1.5" fill="#ffcc4d" />
<Circle cx="29.5" cy="12.5" r="1.5" fill="#ffcc4d" />
<Circle cx="7.5" cy="23.5" r="1.5" fill="#ffcc4d" />
</Svg>
)
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,719 @@
import React from 'react'
import {View} from 'react-native'
import Animated, {FadeIn} from 'react-native-reanimated'
import ViewShot from 'react-native-view-shot'
import {Image} from 'expo-image'
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
import * as MediaLibrary from 'expo-media-library'
import {moderateProfile} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {networkRetry} from '#/lib/async/retry'
import {getCanvas} from '#/lib/canvas'
import {shareUrl} from '#/lib/sharing'
import {logEvent} from '#/lib/statsig/statsig'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {isIOS, isNative} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfileQuery} from '#/state/queries/profile'
import {useAgent, useSession} from '#/state/session'
import {useComposerControls} from 'state/shell'
import {formatCount} from '#/view/com/util/numeric/format'
import {Logomark} from '#/view/icons/Logomark'
import * as Toast from 'view/com/util/Toast'
import {
atoms as a,
ThemeProvider,
tokens,
useBreakpoints,
useTheme,
} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
import {OnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/OnePercent'
import {PointOnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/PointOnePercent'
import {TenPercent} from '#/components/dialogs/nuxs/TenMillion/icons/TenPercent'
import {Divider} from '#/components/Divider'
import {GradientFill} from '#/components/GradientFill'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/Download'
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
const DEBUG = false
const RATIO = 8 / 10
const WIDTH = 2000
const HEIGHT = WIDTH * RATIO
function getFontSize(count: number) {
const length = count.toString().length
if (length < 7) {
return 80
} else if (length < 5) {
return 100
} else {
return 70
}
}
function getPercentBadge(percent: number) {
if (percent <= 0.001) {
return PointOnePercent
} else if (percent <= 0.01) {
return OnePercent
} else if (percent <= 0.1) {
return TenPercent
}
return null
}
function Frame({children}: {children: React.ReactNode}) {
return (
<View
style={[
a.relative,
a.w_full,
a.overflow_hidden,
{
paddingTop: '80%',
},
]}>
{children}
</View>
)
}
export function TenMillion({
showTimeout,
onClose,
onFallback,
}: {
showTimeout?: number
onClose?: () => void
onFallback?: () => void
}) {
const agent = useAgent()
const nuxDialogs = useNuxDialogContext()
const [userNumber, setUserNumber] = React.useState<number>(0)
const fetching = React.useRef(false)
React.useEffect(() => {
async function fetchUserNumber() {
const isBlueskyHosted = agent.sessionManager.pdsUrl
?.toString()
.includes('bsky.network')
if (isBlueskyHosted && agent.session?.accessJwt) {
const res = await fetch(
`https://bsky.social/xrpc/com.atproto.temp.getSignupNumber`,
{
headers: {
Authorization: `Bearer ${agent.session.accessJwt}`,
},
},
)
if (!res.ok) {
throw new Error('Network request failed')
}
const data = await res.json()
if (data.number && data.number <= 10_000_000) {
setUserNumber(data.number)
} else {
// should be rare
nuxDialogs.dismissActiveNux()
onFallback?.()
}
} else {
nuxDialogs.dismissActiveNux()
onFallback?.()
}
}
if (!fetching.current) {
fetching.current = true
networkRetry(3, fetchUserNumber).catch(() => {
nuxDialogs.dismissActiveNux()
onFallback?.()
})
}
}, [
agent.sessionManager.pdsUrl,
agent.session?.accessJwt,
setUserNumber,
nuxDialogs.dismissActiveNux,
nuxDialogs,
onFallback,
])
return userNumber ? (
<TenMillionInner
userNumber={userNumber}
showTimeout={showTimeout ?? 3e3}
onClose={onClose}
/>
) : null
}
export function TenMillionInner({
userNumber,
showTimeout,
onClose: onCloseOuter,
}: {
userNumber: number
showTimeout: number
onClose?: () => void
}) {
const t = useTheme()
const lightTheme = useTheme('light')
const {_, i18n} = useLingui()
const control = Dialog.useDialogControl()
const {gtMobile} = useBreakpoints()
const {openComposer} = useComposerControls()
const {currentAccount} = useSession()
const {
isLoading: isProfileLoading,
data: profile,
error: profileError,
} = useProfileQuery({
did: currentAccount!.did,
})
const moderationOpts = useModerationOpts()
const nuxDialogs = useNuxDialogContext()
const moderation = React.useMemo(() => {
return profile && moderationOpts
? moderateProfile(profile, moderationOpts)
: undefined
}, [profile, moderationOpts])
const [uri, setUri] = React.useState<string | null>(null)
const percent = userNumber / 10_000_000
const Badge = getPercentBadge(percent)
const isLoadingData = isProfileLoading || !moderation || !profile
const isLoadingImage = !uri
const displayName = React.useMemo(() => {
if (!profile || !moderation) return ''
return sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)
}, [profile, moderation])
const handle = React.useMemo(() => {
if (!profile) return ''
return sanitizeHandle(profile.handle, '@')
}, [profile])
const joinedDate = React.useMemo(() => {
if (!profile || !profile.createdAt) return ''
const date = i18n.date(profile.createdAt, {
month: 'short',
day: 'numeric',
year: 'numeric',
})
return date
}, [i18n, profile])
const error: string = React.useMemo(() => {
if (profileError) {
return _(
msg`Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🦋`,
)
}
return ''
}, [_, profileError])
/*
* Opening and closing
*/
React.useEffect(() => {
const timeout = setTimeout(() => {
control.open()
}, showTimeout)
return () => {
clearTimeout(timeout)
}
}, [control, showTimeout])
const onClose = React.useCallback(() => {
nuxDialogs.dismissActiveNux()
onCloseOuter?.()
}, [nuxDialogs, onCloseOuter])
/*
* Actions
*/
const sharePost = React.useCallback(() => {
if (uri) {
control.close(() => {
setTimeout(() => {
logEvent('tmd:post', {})
openComposer({
text: _(
msg`Bluesky now has over 10 million users, and I was #${i18n.number(
userNumber,
)}!`,
),
imageUris: [
{
uri,
width: WIDTH,
height: HEIGHT,
altText: _(
msg`A virtual certificate with text "Celebrating 10M users on Bluesky, #${i18n.number(
userNumber,
)}, ${displayName} ${handle}, joined on ${joinedDate}"`,
),
},
],
})
}, 1e3)
})
}
}, [
_,
i18n,
control,
openComposer,
uri,
userNumber,
displayName,
handle,
joinedDate,
])
const onNativeShare = React.useCallback(() => {
if (uri) {
control.close(() => {
logEvent('tmd:share', {})
shareUrl(uri)
})
}
}, [uri, control])
const onNativeDownload = React.useCallback(async () => {
if (uri) {
const res = await requestMediaLibraryPermissionsAsync()
if (!res) {
Toast.show(
_(
msg`You must grant access to your photo library to save the image.`,
),
'xmark',
)
return
}
try {
await MediaLibrary.createAssetAsync(uri)
logEvent('tmd:download', {})
Toast.show(_(msg`Image saved to your camera roll!`))
} catch (e: unknown) {
console.log(e)
Toast.show(_(msg`An error occurred while saving the image!`), 'xmark')
return
}
}
}, [_, uri])
const onWebDownload = React.useCallback(async () => {
if (uri) {
const canvas = await getCanvas(uri)
const imgHref = canvas
.toDataURL('image/png')
.replace('image/png', 'image/octet-stream')
const link = document.createElement('a')
link.setAttribute('download', `Bluesky 10M Users.png`)
link.setAttribute('href', imgHref)
link.click()
logEvent('tmd:download', {})
}
}, [uri])
/*
* Canvas stuff
*/
const imageRef = React.useRef<ViewShot>(null)
const captureInProgress = React.useRef(false)
const onCanvasReady = React.useCallback(async () => {
if (
imageRef.current &&
imageRef.current.capture &&
!captureInProgress.current
) {
captureInProgress.current = true
const uri = await imageRef.current.capture()
setUri(uri)
}
}, [setUri])
const canvas = isLoadingData ? null : (
<View
style={[
a.absolute,
a.overflow_hidden,
DEBUG
? {
width: 600,
height: 600 * RATIO,
}
: {
width: 1,
height: 1,
},
]}>
<View style={{width: 600}}>
<ThemeProvider theme="light">
<Frame>
<ViewShot
ref={imageRef}
options={{width: WIDTH, height: HEIGHT}}
style={[a.absolute, a.inset_0]}>
<View
onLayout={onCanvasReady}
style={[
a.absolute,
a.inset_0,
a.align_center,
a.justify_center,
{
top: -1,
bottom: -1,
left: -1,
right: -1,
paddingVertical: 48,
paddingHorizontal: 48,
},
]}>
<GradientFill gradient={tokens.gradients.bonfire} />
<View
style={[
a.flex_1,
a.w_full,
a.align_center,
a.justify_center,
a.rounded_md,
{
backgroundColor: 'white',
shadowRadius: 32,
shadowOpacity: 0.1,
elevation: 24,
shadowColor: tokens.gradients.bonfire.values[0][1],
},
]}>
<View
style={[
a.absolute,
a.px_xl,
a.py_xl,
{
top: 0,
left: 0,
},
]}>
<Logomark fill={t.palette.primary_500} width={36} />
</View>
{/* Centered content */}
<View
style={[
{
paddingBottom: isNative ? 0 : 24,
},
]}>
<Text
allowFontScaling={false}
style={[
a.text_md,
a.font_bold,
a.text_center,
a.pb_sm,
lightTheme.atoms.text_contrast_medium,
]}>
<Trans>
Celebrating {formatCount(i18n, 10000000)} users
</Trans>{' '}
🎉
</Text>
<View style={[a.flex_row, a.align_start]}>
<Text
allowFontScaling={false}
style={[
a.absolute,
{
color: t.palette.primary_500,
fontSize: 32,
fontWeight: '900',
width: 32,
top: isNative ? -10 : 0,
left: 0,
transform: [
{
translateX: -16,
},
],
},
]}>
#
</Text>
<Text
allowFontScaling={false}
style={[
a.relative,
a.text_center,
{
fontStyle: 'italic',
fontSize: getFontSize(userNumber),
lineHeight: getFontSize(userNumber),
fontWeight: '900',
letterSpacing: -2,
},
]}>
{i18n.number(userNumber)}
</Text>
</View>
{Badge && (
<View
style={[
a.absolute,
{
width: 64,
height: 64,
top: isNative ? 75 : 85,
right: '5%',
transform: [
{
rotate: '8deg',
},
],
},
]}>
<Badge fill={t.palette.primary_500} />
</View>
)}
</View>
{/* End centered content */}
<View
style={[
a.absolute,
a.px_xl,
a.py_xl,
{
bottom: 0,
left: 0,
right: 0,
},
]}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
{/*
<UserAvatar
size={36}
avatar={profile.avatar}
moderation={moderation.ui('avatar')}
onLoad={onCanvasReady}
/>
*/}
<View style={[a.gap_2xs, a.flex_1]}>
<Text
allowFontScaling={false}
style={[
a.flex_1,
a.text_sm,
a.font_bold,
a.leading_tight,
{maxWidth: '60%'},
]}>
{displayName}
</Text>
<View
style={[a.flex_row, a.justify_between, a.gap_4xl]}>
<Text
allowFontScaling={false}
numberOfLines={1}
style={[
a.flex_1,
a.text_sm,
a.font_semibold,
a.leading_snug,
lightTheme.atoms.text_contrast_medium,
]}>
{handle}
</Text>
{profile.createdAt && (
<Text
allowFontScaling={false}
numberOfLines={1}
ellipsizeMode="head"
style={[
a.flex_1,
a.text_sm,
a.font_semibold,
a.leading_snug,
a.text_right,
lightTheme.atoms.text_contrast_low,
]}>
<Trans>Joined on {joinedDate}</Trans>
</Text>
)}
</View>
</View>
</View>
</View>
</View>
</View>
</ViewShot>
</Frame>
</ThemeProvider>
</View>
</View>
)
return (
<Dialog.Outer control={control} onClose={onClose}>
<Dialog.ScrollableInner
label={_(msg`Ten Million`)}
style={[
{
padding: 0,
paddingTop: 0,
},
]}>
<View
style={[
a.rounded_md,
a.overflow_hidden,
isNative && {
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
},
]}>
<Frame>
<View
style={[a.absolute, a.inset_0, a.align_center, a.justify_center]}>
<GradientFill gradient={tokens.gradients.bonfire} />
{error ? (
<View>
<Text
style={[
a.text_md,
a.leading_snug,
a.text_center,
a.pb_md,
{
maxWidth: 300,
},
]}>
(°°)
</Text>
<Text
style={[
a.text_xl,
a.font_bold,
a.leading_snug,
a.text_center,
{
maxWidth: 300,
},
]}>
{error}
</Text>
</View>
) : isLoadingData || isLoadingImage ? (
<Loader size="xl" fill="white" />
) : (
<Animated.View
entering={FadeIn.duration(150)}
style={[a.w_full, a.h_full]}>
<Image
accessibilityIgnoresInvertColors
source={{uri}}
style={[a.w_full, a.h_full]}
/>
</Animated.View>
)}
</View>
</Frame>
{canvas}
<View style={[gtMobile ? a.p_2xl : a.p_xl]}>
<Text
allowFontScaling={false}
style={[
a.text_5xl,
a.leading_tight,
a.pb_lg,
{
fontWeight: '900',
},
]}>
<Trans>Thanks for being one of our first 10 million users.</Trans>
</Text>
<Text style={[a.leading_snug, a.text_lg, a.pb_xl]}>
<Trans>
Together, we're rebuilding the social internet. We're glad
you're here.
</Trans>
</Text>
<Divider />
<View
style={[
a.flex_row,
a.align_center,
a.justify_end,
a.gap_md,
a.pt_xl,
]}>
{gtMobile && (
<Text
style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
<Trans>Brag a little!</Trans>
</Text>
)}
<Button
disabled={isLoadingImage}
label={
isNative && isIOS
? _(msg`Share image externally`)
: _(msg`Download image`)
}
size="large"
variant="solid"
color="secondary"
shape="square"
onPress={
isNative
? isIOS
? onNativeShare
: onNativeDownload
: onWebDownload
}>
<ButtonIcon icon={isNative && isIOS ? Share : Download} />
</Button>
<Button
disabled={isLoadingImage}
label={_(msg`Share image in post`)}
size="large"
variant="solid"
color="primary"
onPress={sharePost}>
<ButtonText>{_(msg`Share`)}</ButtonText>
<ButtonIcon position="right" icon={ImageIcon} />
</Button>
</View>
</View>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
+183
View File
@@ -0,0 +1,183 @@
import React from 'react'
import {AppBskyActorDefs} from '@atproto/api'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {
Nux,
useNuxs,
useRemoveNuxsMutation,
useUpsertNuxMutation,
} from '#/state/queries/nuxs'
import {
usePreferencesQuery,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences'
import {useProfileQuery} from '#/state/queries/profile'
import {SessionAccount, useSession} from '#/state/session'
import {useOnboardingState} from '#/state/shell'
import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography'
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
// NUXs
import {TenMillion} from '#/components/dialogs/nuxs/TenMillion'
import {IS_DEV} from '#/env'
type Context = {
activeNux: Nux | undefined
dismissActiveNux: () => void
}
const queuedNuxs: {
id: Nux
enabled?: (props: {
gate: ReturnType<typeof useGate>
currentAccount: SessionAccount
currentProfile: AppBskyActorDefs.ProfileViewDetailed
preferences: UsePreferencesQueryResponse
}) => boolean
}[] = [
{
id: Nux.TenMillionDialog,
},
{
id: Nux.NeueTypography,
enabled(props) {
if (props.currentProfile.createdAt) {
if (new Date(props.currentProfile.createdAt) < new Date('2024-09-25')) {
return true
}
}
return false
},
},
]
const Context = React.createContext<Context>({
activeNux: undefined,
dismissActiveNux: () => {},
})
export function useNuxDialogContext() {
return React.useContext(Context)
}
export function NuxDialogs() {
const {currentAccount} = useSession()
const {data: preferences} = usePreferencesQuery()
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const onboardingActive = useOnboardingState().isActive
const isLoading =
!currentAccount || !preferences || !profile || onboardingActive
return !isLoading ? (
<Inner
currentAccount={currentAccount}
currentProfile={profile}
preferences={preferences}
/>
) : null
}
function Inner({
currentAccount,
currentProfile,
preferences,
}: {
currentAccount: SessionAccount
currentProfile: AppBskyActorDefs.ProfileViewDetailed
preferences: UsePreferencesQueryResponse
}) {
const gate = useGate()
const {nuxs} = useNuxs()
const [snoozed, setSnoozed] = React.useState(() => {
return isSnoozed()
})
const [activeNux, setActiveNux] = React.useState<Nux | undefined>()
const {mutateAsync: upsertNux} = useUpsertNuxMutation()
const {mutate: removeNuxs} = useRemoveNuxsMutation()
const snoozeNuxDialog = React.useCallback(() => {
snooze()
setSnoozed(true)
}, [setSnoozed])
const dismissActiveNux = React.useCallback(() => {
if (!activeNux) return
setActiveNux(undefined)
}, [activeNux, setActiveNux])
if (IS_DEV && typeof window !== 'undefined') {
// @ts-ignore
window.clearNuxDialog = (id: Nux) => {
if (!IS_DEV || !id) return
removeNuxs([id])
unsnooze()
}
}
React.useEffect(() => {
if (snoozed) return
if (!nuxs) return
for (const {id, enabled} of queuedNuxs) {
const nux = nuxs.find(nux => nux.id === id)
// check if completed first
if (nux && nux.completed) {
continue
}
// then check gate (track exposure)
if (
enabled &&
!enabled({gate, currentAccount, currentProfile, preferences})
) {
continue
}
logger.debug(`NUX dialogs: activating '${id}' NUX`)
// we have a winner
setActiveNux(id)
// immediately snooze for a day
snoozeNuxDialog()
// immediately update remote data (affects next reload)
upsertNux({
id,
completed: true,
data: undefined,
}).catch(e => {
logger.error(`NUX dialogs: failed to upsert '${id}' NUX`, {
safeMessage: e.message,
})
})
break
}
}, [
nuxs,
snoozed,
snoozeNuxDialog,
upsertNux,
gate,
currentAccount,
currentProfile,
preferences,
])
const ctx = React.useMemo(() => {
return {
activeNux,
dismissActiveNux,
}
}, [activeNux, dismissActiveNux])
return (
<Context.Provider value={ctx}>
{activeNux === Nux.TenMillionDialog && <TenMillion />}
{activeNux === Nux.NeueTypography && <NeueTypography />}
</Context.Provider>
)
}
+22
View File
@@ -0,0 +1,22 @@
import {simpleAreDatesEqual} from '#/lib/strings/time'
import {device} from '#/storage'
export function snooze() {
device.set(['lastNuxDialog'], new Date().toISOString())
}
export function unsnooze() {
device.set(['lastNuxDialog'], undefined)
}
export function isSnoozed() {
const lastNuxDialog = device.get(['lastNuxDialog'])
if (!lastNuxDialog) return false
const last = new Date(lastNuxDialog)
const now = new Date()
// already snoozed today
if (simpleAreDatesEqual(last, now)) {
return true
}
return false
}
+17
View File
@@ -0,0 +1,17 @@
import React from 'react'
const MessageContext = React.createContext(false)
export function MessageContextProvider({
children,
}: {
children: React.ReactNode
}) {
return (
<MessageContext.Provider value={true}>{children}</MessageContext.Provider>
)
}
export function useIsWithinMessage() {
return React.useContext(MessageContext)
}
+10 -7
View File
@@ -4,6 +4,7 @@ import {AppBskyEmbedRecord} from '@atproto/api'
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
import {atoms as a, native, useTheme} from '#/alf'
import {MessageContextProvider} from './MessageContext'
let MessageItemEmbed = ({
embed,
@@ -13,13 +14,15 @@ let MessageItemEmbed = ({
const t = useTheme()
return (
<View style={[a.my_xs, t.atoms.bg, native({flexBasis: 0})]}>
<PostEmbeds
embed={embed}
allowNestedQuotes
viewContext={PostEmbedViewContext.Feed}
/>
</View>
<MessageContextProvider>
<View style={[a.my_xs, t.atoms.bg, native({flexBasis: 0})]}>
<PostEmbeds
embed={embed}
allowNestedQuotes
viewContext={PostEmbedViewContext.Feed}
/>
</View>
</MessageContextProvider>
)
}
MessageItemEmbed = React.memo(MessageItemEmbed)
+10 -10
View File
@@ -351,8 +351,8 @@ export function Checkbox() {
t.atoms.border_contrast_high,
{
borderWidth: 1,
height: 20,
width: 20,
height: 24,
width: 24,
},
baseStyles,
hovered ? baseHoverStyles : {},
@@ -383,9 +383,9 @@ export function Switch() {
t.atoms.border_contrast_high,
{
borderWidth: 1,
height: 20,
width: 32,
padding: 2,
height: 24,
width: 36,
padding: 3,
},
baseStyles,
hovered ? baseHoverStyles : {},
@@ -395,8 +395,8 @@ export function Switch() {
style={[
a.rounded_full,
{
height: 14,
width: 14,
height: 16,
width: 16,
},
selected
? {
@@ -436,8 +436,8 @@ export function Radio() {
t.atoms.border_contrast_high,
{
borderWidth: 1,
height: 20,
width: 20,
height: 24,
width: 24,
},
baseStyles,
hovered ? baseHoverStyles : {},
@@ -447,7 +447,7 @@ export function Radio() {
style={[
a.absolute,
a.rounded_full,
{height: 12, width: 12},
{height: 16, width: 16},
selected
? {
backgroundColor: t.palette.primary_500,
+2
View File
@@ -21,6 +21,7 @@ import {
ja,
ko,
ptBR,
ru,
tr,
uk,
zhCN,
@@ -47,6 +48,7 @@ const locales: Record<AppLanguage, Locale | undefined> = {
ja,
ko,
['pt-BR']: ptBR,
ru,
tr,
uk,
['zh-CN']: zhCN,
+2 -2
View File
@@ -15,8 +15,8 @@ export function useFollowMethods({
logContext,
}: {
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext']
logContext: LogEvents['profile:follow:sampled']['logContext'] &
LogEvents['profile:unfollow:sampled']['logContext']
}) {
const {_} = useLingui()
const requireAuth = useRequireAuth()
+53
View File
@@ -0,0 +1,53 @@
import {
useCallback,
useEffect,
useRef,
useState,
useSyncExternalStore,
} from 'react'
import {isFirefox, isSafari} from '#/lib/browser'
import {isWeb} from '#/platform/detection'
function fullscreenSubscribe(onChange: () => void) {
document.addEventListener('fullscreenchange', onChange)
return () => document.removeEventListener('fullscreenchange', onChange)
}
export function useFullscreen(ref?: React.RefObject<HTMLElement>) {
if (!isWeb) throw new Error("'useFullscreen' is a web-only hook")
const isFullscreen = useSyncExternalStore(fullscreenSubscribe, () =>
Boolean(document.fullscreenElement),
)
const scrollYRef = useRef<null | number>(null)
const [prevIsFullscreen, setPrevIsFullscreen] = useState(isFullscreen)
const toggleFullscreen = useCallback(() => {
if (isFullscreen) {
document.exitFullscreen()
} else {
if (!ref) throw new Error('No ref provided')
if (!ref.current) return
scrollYRef.current = window.scrollY
ref.current.requestFullscreen()
}
}, [isFullscreen, ref])
useEffect(() => {
if (prevIsFullscreen === isFullscreen) return
setPrevIsFullscreen(isFullscreen)
// Chrome has an issue where it doesn't scroll back to the top after exiting fullscreen
// Let's play it safe and do it if not FF or Safari, since anything else will probably be chromium
if (prevIsFullscreen && !isFirefox && !isSafari) {
setTimeout(() => {
if (scrollYRef.current !== null) {
window.scrollTo(0, scrollYRef.current)
scrollYRef.current = null
}
}, 100)
}
}, [isFullscreen, prevIsFullscreen])
return [isFullscreen, toggleFullscreen] as const
}
+13
View File
@@ -0,0 +1,13 @@
import {createSinglePathSVG} from './TEMPLATE'
export const AspectRatio11_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v14h14V5H5Z',
})
export const AspectRatio43_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M2 20.5c-.552 0-1-.41-1-.917V4.917C1 4.41 1.448 4 2 4h20c.552 0 1 .41 1 .917v14.666c0 .507-.448.917-1 .917H2Zm1-1.833h18V5.833H3v12.834Z',
})
export const AspectRatio34_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4 2c0-.552.41-1 .917-1h14.666c.507 0 .917.448.917 1v20c0 .552-.41 1-.917 1H4.917C4.41 23 4 22.552 4 22V2Zm1.833 1v18h12.834V3H5.833Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Download_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 3a1 1 0 0 1 1 1v8.086l1.793-1.793a1 1 0 1 1 1.414 1.414l-3.5 3.5a1 1 0 0 1-1.414 0l-3.5-3.5a1 1 0 1 1 1.414-1.414L11 12.086V4a1 1 0 0 1 1-1ZM4 14a1 1 0 0 1 1 1v4h14v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1Z',
})

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