Compare commits

...

146 Commits

Author SHA1 Message Date
Eric Bailey d74d5e9d2c Fix isPartOfLastBranchFromDepth assignment 2025-08-15 16:06:17 -05:00
Samuel Newman 314c380fc4 don't override backspace if alt or meta key (#8855) 2025-08-15 21:41:44 +03:00
Samuel Newman f2892779da Use medium font weight in a few places (#8848)
* medium font toasts

* medium font tabs

* medium font trending pill
2025-08-15 08:33:23 -07:00
Elijah Seed-Arita 5ca665467e feat: use OP's language as recommendation when replying (#8832)
* feat: use OP's language as recommendation when replying

* fix: address nits
2025-08-15 06:16:13 -07:00
pfrazee 4e0be91cff Nightly source-language update 2025-08-15 02:43:46 +00:00
Samuel Newman a761a2e260 Update Xcode in runner to 16.4 (#8842) 2025-08-14 23:21:42 +03:00
Bartosz Kaszubowski 9440068b69 Profile back button tweaks for web (#8795)
* Profile back button tweaks

* apply review feedback - remove StyleSheet

* Update src/screens/Profile/Header/Shell.tsx

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2025-08-14 10:54:27 -07:00
Eric Bailey 7b2e61bf4d Integrate Sonner for toasts (#8839)
* Integrate Sonner for toasts

* Fix animation on iOS

* Refactor API

* Update e2e file
2025-08-14 09:51:40 -05:00
Samuel Newman 221623f55a Improve "replied to a post" component (#8602)
* unify component

* change bottom padding from 2px to 4px
2025-08-14 03:14:18 -07:00
pfrazee f4dca5d230 Nightly source-language update 2025-08-14 02:42:45 +00:00
Eric Bailey 80ce71283b Bump version to 1.107.0 (#8840) 2025-08-13 17:42:11 -05:00
Samuel Newman 7a334362ae [Perf - part 1] Hoist service config query (#8812) 2025-08-14 01:13:51 +03:00
Samuel Newman b2c56cbd6d Add displayName to contexts (#8814) 2025-08-14 01:12:31 +03:00
Samuel Newman 275fece3e3 [Perf - part 3] Stop every dialog control in the entire app rerendering when opening a dialog (#8815) 2025-08-14 01:11:44 +03:00
Samuel Newman eef639ea54 Preconnect to go.bsky.app (#8825)
* preconnect to go.bsky.app

* other location why not
2025-08-13 17:00:27 -05:00
Samuel Newman 36b2a87a06 Skip intl:compile if not needed (#8837)
* skip intl compile if not needed

* add intl:compile to pull request ci

* compile -> build just to match others
2025-08-13 16:58:42 -05:00
Samuel Newman f001af2fe3 stop incorrectly using suffixtext (#8352) 2025-08-14 00:39:22 +03:00
Samuel Newman d5cc19f282 run lint-native:fix (#8836) 2025-08-13 22:15:08 +03:00
Samuel Newman edd205483d Fix chat button placeholder size on profile (#8827) 2025-08-13 17:20:23 +03:00
Elijah Seed-Arita 485f7ee95b fix: don't open user profile when clicking edit button in user list (#8831) 2025-08-13 01:52:21 -07:00
Arturo Fonseca bfcdb83536 Fix deletion of composed emojis in web composer (#8829) 2025-08-13 01:20:00 -07:00
pfrazee 6cd7dc0f9a Nightly source-language update 2025-08-13 02:42:56 +00:00
Samuel Newman 338fa4dfbe remove imports from @atproto/api internals (#8403) 2025-08-12 08:52:34 -07:00
Samuel Newman bf869d9185 remove unicode ltr chars and replace with css (#8826) 2025-08-12 05:59:03 -07:00
Eric Bailey 683d7bf5a7 Translations (#8823)
* Extract en

* Pull and extract

* Pull and extract
2025-08-11 20:54:31 -05:00
Eric Bailey 44cdf32c1f Update dates (#8822) 2025-08-11 19:01:01 -05:00
Samuel Newman 86b53e7bcc set maxwidth on feed context string (#8817) 2025-08-11 18:57:32 -05:00
Samuel Newman 3ce14b0c6f filter out muted/blocked convos from share menu (#8802) 2025-08-11 18:56:46 -05:00
Samuel Newman 9dfddd3c42 add bottom border to feed headers in explore on web (#8801) 2025-08-11 18:54:28 -05:00
Samuel Newman ef171e2892 Tweak small buttons (#8818)
* set size=small buttons to `text_sm`

* change to `leading_snug`

* add gap to repost buttons

* Add medium-weight font, use for buttons (#8819)

* add medium weight inter

* add medium font to buttons

* Adjust bold weights

* Fix ref

---------

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

* Align with designs

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2025-08-11 18:04:45 -05:00
Samuel Newman 17c5cec356 make sure admonition text doesn't touch button (#8816) 2025-08-11 18:01:32 -05:00
surfdude29 f873f3b301 Add context and comments to button text, label and title in email verification dialog (#8810)
* add context and comment for translators to button text in email verification dialog

* shorten comment

* add comment for other string

* tweak comment

* tweak again
2025-08-11 18:01:10 -05:00
Eric Bailey a3bb8cca6e Fix policy state (#8821)
* Mark completedForDevice prior to creating an account

* Mark policies as completed after initial create account step
2025-08-11 15:51:08 -05:00
Samuel Newman bb949e4f44 Fix translations on Android using PROCESS_TEXT intent (#8486)
* use intents to translate text on android

* clean up config plugins

* restore day night plugin

just to be safe

* leave a comment for why we can't open translate directly

* add todo

* fix lockfile lint
2025-08-11 08:37:39 -07:00
pfrazee 2a6172cbaf Nightly source-language update 2025-08-10 02:54:10 +00:00
jim 4d1a03b99e Merge pull request #8766 from internet-development/binaryfiddler/starter-pack
Update starter pack wizard screen
2025-08-09 00:31:09 -07:00
Chenyu Huang 52637472e6 add missing import 2025-08-08 10:15:29 -07:00
Eric Bailey 23a797cfab Captcha fixes (#8803)
* Add env to scripts

* Update var name
2025-08-08 09:57:17 -05:00
Eric Bailey 22a8b63dcf Update links (#8800) 2025-08-07 15:40:12 -07:00
Eric Bailey 2ba2d9e5b5 Pull latest translations (#8799) 2025-08-07 15:24:55 -07:00
Samuel Newman 93c4719a21 Check handle as you type (#8601)
* check handle as you type

* metrics

* add metric types

* fix overflow

* only check reserved handles for bsky.social, fix test

* change validation check name

* tweak input

* move ghosttext component to textfield

* tweak styles to try and match latest

* add suggestions

* improvements, metrics

* share logic between typeahead and next button

* Apply suggestions from code review

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

* update checks, disable button if unavailable

* convert to lowercase

* fix bug with checkHandleAvailability

* add gate

* move files around to make clearer

* fix bad import

* Fix flashing next button

* Enable for TF

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2025-08-07 17:01:01 -05:00
Bartosz Kaszubowski f708e88410 Add cursor pointer to web select trigger (#8787) 2025-08-07 16:46:35 -05:00
Eric Bailey 88856b0ff7 Fix disabled primary button text color (#8796) 2025-08-07 16:44:19 -05:00
Eric Bailey 08a8324290 Fix e2e (#8794)
* Only enable policy update overlay once the actual Overlay mounts (after onboarding and all that)

* Disable policy overlay in e2e

* Add comments

* Add extra insurance

* Rm log

* Fix shared prefs test

* Fix login flows by optionally closing 'Remember password' system dialog

* Return missing testID

* Bump dev-env
2025-08-07 13:11:16 -07:00
Eric Bailey 11c9931fd2 Fix policy overlay logic (#8793)
* Only enable policy update overlay once the actual Overlay mounts (after onboarding and all that)

* Disable policy overlay in e2e

* Add comments

* Add extra insurance

* Rm log
2025-08-07 14:55:19 -05:00
hailey c0593e4979 Add device attestation to signup flow (#8757) 2025-08-07 12:33:38 -07:00
Samuel Newman 39e775a376 add border curve in neater way (#8792) 2025-08-07 11:30:49 -05:00
Bartosz Kaszubowski d84d54bae3 Fix web scrollable select unwanted overflow (#8786) 2025-08-07 09:23:01 -07:00
Samuel Newman 0a2b1fcfb8 add profile hover card to notif items (#8666) 2025-08-07 09:15:49 -07:00
Eric Bailey cb54082eda [APP-1310] Button refresh (#8758)
* Rm gradient buttons from Storybook

* TEMP move storybook button section

* Remove gradient_sky

* Remove actual defs for gradient_sky and gradient_primary

* Remove other gradient defs

* Remove gradient support entirely

* Deprecate 'variant' in favor of 'color'

* Fork base styles codepath to make variant deprecation more obvious

* Remove text styles for when no color is set, never been used

* Fork text styles codepath to make variant deprecation more obvious

* Revert temp storybook commit, remove deprecated values

* Replace remaining gradient button usage

* Update Buttons storybook section

* Update tiny styles

* Update small styles

* Update large sizes

* Ensure proper alignment of buttons in storybook

* Update button colors

* Rename negative_secondary to negative_subtle

* Remove unnecessary select()

* Update icon size and gap

* Update negative_subtle styles

* Custom button colors

* Add borderCurve
2025-08-07 11:08:31 -05:00
Samuel Newman 20ac2e2733 Only parse notifs with a reason (#8785)
* only parse notifs with a reason

* move error log to a warn, make it actually fire
2025-08-07 09:07:43 -07:00
Eric Bailey c103687d66 Toast tweaks (#8791)
* Tweak colors

* Fix e2e

* Color tweaks

* Fix alignment
2025-08-07 08:45:35 -05:00
pfrazee dbb9ff6918 Nightly source-language update 2025-08-07 02:52:42 +00:00
Anastasiya Uraleva 909ddc86e1 [APP-1365]: Ensure toast text meets a11y guidelines (#8779)
* update: toast colors for accessibility

* update: toast color codes from figma

* condense the colors to remove select calls that are unecessary

* remove unused select
2025-08-06 16:27:44 -05:00
Eric Bailey 75094f4860 [LEG-203] Add onboarding text for policy updates (#8783)
* Add onboarding text for policy updates

* Add gate, add to other case

* Update other UI cases to use Admonitions

* Th

* Update date
2025-08-06 15:16:08 -05:00
Eric Bailey 328aa2be94 [APP-1356] Policy update dialog (#8782)
* Add blocking announcement dialog feature

* WIP custom dialog

* Rework dialog and add native FocusScope

* Lock scroll on web, fix backdrop

* Add web FocusScope

* Create custom Outlet for these announcements

* Clean up FocusScope native impl

* Comments

* Some styling fixes

* Handle screen reader specifically

* Clean up state, remove Portal edits

* Reorg, rename

* Add syncing, tests

* Revert dialog updates

* Revert formatting

* Delete unused file

* Format

* Add FullWindowOverlay

* remove mmkv storage in debug btn

* Add debug code

* fix taps passing through on iOS

* Reorg

* Reorg, rename everything

* Complete policy update after signup

* Add logger

* Move context around, unmount portals on native

* Move a11y prop into FocusScope

* Remove useMemo

* Update dates

* Move debug to dev settings

* Unmount web portals until policy update completed

* UPdate dates

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2025-08-06 15:15:52 -05:00
Chenyu 9153474e59 Update src/screens/StarterPack/Wizard/index.tsx
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2025-08-06 11:41:28 -07:00
Chenyu Huang 4c8b30df19 left align header content 2025-08-06 11:24:04 -07:00
Arturo Fonseca fd37d92f85 Fix quality of first seconds of videos on web (#8761) (#8778) 2025-08-05 02:04:32 -07:00
pfrazee 53cf0b3b46 Nightly source-language update 2025-08-05 02:53:53 +00:00
Samuel Newman a5437ebbeb Fix web video ViewportObserver component (#8776)
* Revert "[APP-1083] bug fix: videos not accurately autoplaying on web (#8692)"

This reverts commit 9aa35e9fbb.

* fix overflow hidden breaking the video viewport observer
2025-08-04 10:24:53 -07:00
Samuel Newman 15bc77321b Fix android starting at notif screen (#8775)
* fix android launching at notif screen

* rm heuristic
2025-08-04 09:41:09 -07:00
Samuel Newman 7b5d11eb80 Fix video thumbnails on native (#8774) 2025-08-04 18:42:55 +03:00
kindgracekind b32568260f Add feed tuner to remove muted threads (#8773) 2025-08-04 10:27:27 -05:00
Anastasiya Uraleva 9aa35e9fbb [APP-1083] bug fix: videos not accurately autoplaying on web (#8692)
* update: auto play video on web with intersection position

* place back the threshold: 0.5

* update: optimize the intersection observer with a throttled scroll listener

---------

Co-authored-by: Anastasiya <anastasiya@Mac.localdomain>
Co-authored-by: Anastasiya <anastasiya@Anastasiyas-MacBook-Pro.local>
2025-08-04 06:17:50 -07:00
surfdude29 14e39c4aec Tweak accessibility label (#8767)
* tweak accessibility label

* change `alert` to `message` and add comment instead of context

* tweak comment
2025-08-04 03:49:35 -07:00
Chenyu Huang 73ce358052 update starter pack wizard layout 2025-08-01 11:32:14 -07:00
pfrazee 598641742e Nightly source-language update 2025-08-01 02:58:13 +00:00
jim f21478735c Merge pull request #8762 from JimmyCushnie/updates-readme-like-a-boss
Minor README improvements
2025-07-31 19:46:17 -07:00
jim 9eba567135 Merge pull request #8763 from internet-development/caidan/app-1364-fix-suggested-follows-small-web-view 2025-07-31 14:28:00 -07:00
Caidan Williams 79f365b21c fix(ui): small bug where on small web views the final card was not full height 2025-07-31 11:51:11 -07:00
Caidan Williams 8b344ca9fd fix(ui): adjust padding for ProfileGrid layout consistency
Fixes the issue of the shadow being cutoff by the overflow, now will
give proper spacing to the cards to display the shadow.
2025-07-31 11:51:11 -07:00
Jimmy Cushnie 648dda7541 README: fix verb form of CC
While both forms are used, CC'd is much more common and generally considered the correct form.
2025-07-31 13:59:29 -04:00
Jimmy Cushnie bf455f3e19 README: fix brand capitalization ("Github" -> "GitHub") 2025-07-31 13:55:13 -04:00
Jimmy Cushnie 91574f1f56 README: improve phrasing of packages mention 2025-07-31 13:52:49 -04:00
Jimmy Cushnie 3949747197 README: Use alert for the contributions note 2025-07-31 13:48:40 -04:00
Samuel Newman f4724e378d Don't animate images that will be posted as JPGs (#8760)
* don't play gifs that'll be png'd

* add media inset border

* fix unnecessary typecast

* Revert "fix unnecessary typecast"

This reverts commit 3a8bcc9277.
2025-07-31 09:47:22 -07:00
Eric Bailey e87555c481 [APP-1310] Button cleanup (#8754)
* Rm gradient buttons from Storybook

* TEMP move storybook button section

* Remove gradient_sky

* Remove actual defs for gradient_sky and gradient_primary

* Remove other gradient defs

* Remove gradient support entirely

* Deprecate 'variant' in favor of 'color'

* Fork base styles codepath to make variant deprecation more obvious

* Remove text styles for when no color is set, never been used

* Fork text styles codepath to make variant deprecation more obvious

* Revert temp storybook commit, remove deprecated values

* Replace remaining gradient button usage
2025-07-31 10:32:16 -05:00
Eric Bailey 3bcfcba6d8 Some toasts cleanup and reorg (#8748)
* Reorg

* Move animation into css file

* Update style comment

* Extract core component, use platform-specific wrappers

* Pull out platform specific styles

* Just move styles into Toast component itself

* Rename cleanup

* Update API

* Add duration optional prop

* Add some type docs

* add exp eased slide aniamtions

* Make toasts full width on mobile web

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2025-07-31 10:15:35 -05:00
surfdude29 33e0714948 tweak case of labels in SearchResults.tsx (#8759) 2025-07-31 06:40:39 -07:00
Eric Bailey b3d207c7b9 Make proper extension of Button more clearly defined (#8753)
* Remove unecessary color prop from SettingsList LinkItem

* Add UninheritableButtonProps to avoid misuse
2025-07-31 05:41:10 -07:00
pfrazee 85981deb31 Nightly source-language update 2025-07-31 02:51:14 +00:00
Jaz 045101286f Merge pull request #8755 from bluesky-social/echoprom_fix
fix echoprom config
2025-07-30 15:15:01 -07:00
Jaz Volpert b25677eb7f build 2025-07-30 21:42:56 +00:00
Jaz Volpert 4393a275ae fix echoprom config 2025-07-30 21:42:26 +00:00
Eric Bailey 3ff42f1595 Fix bad substitution (#8752) 2025-07-30 13:56:27 -05:00
Jaz 3dc6dcd9b9 Merge pull request #8749 from bluesky-social/go-124
Update golang toolchain to 1.24 and clean imports
2025-07-30 11:07:35 -07:00
Samuel Newman 25e60548c4 Mark old components as deprecated (#8747)
* mark a bunch of stuff as deprecated

* mark s as deprecated (serverence reference????)

* rm type import
2025-07-30 11:05:45 -07:00
hailey fade51e9eb Rework variable naming, logic for rendering chat requests (#8737) 2025-07-30 11:00:14 -07:00
Jaz Volpert 324ebdf2cb Cleanup workflows 2025-07-30 17:57:56 +00:00
Jaz Volpert ef167eaca0 build 2025-07-30 17:41:18 +00:00
Jaz Volpert 13499de325 Merge branch 'main' into go-124 2025-07-30 17:40:42 +00:00
Eric Bailey d4b23d3ab4 [APP-1173] Clean up env (#8701)
* Clean up env files

* Use new env in Sentry setup file

* Use new env in Bitdrift setup file

* Use new env in chat proxy header

* Prefix Bitdrift key with EXPO_PUBLIC

* Deprecate SENTRY_RELEASE since we use package.json now

* Use existing EXPO_PUBLIC_BUNDLE_IDENTIFIER short commit has as Sentry dist value

* Fix missing bundle identifier for Render deploys

* Deprecate SENTRY_DIST in favor of EXPO_PUBLIC_BUNDLE_IDENTIFIER

* Prefix SENTRY_DSN with EXPO_PUBLIC to match others

* Remove debugging field

* Replace NODE_ENV in places where its safe

* Self review

* Properly patch Sentry package

* Echo variables to .env in Dockerfile instead of passing to shell script

* Make sure EXPO_PUBLIC_ENV is set for web container builds

* Update IS_TESTFLIGHT to include testflight-android

* Slice bundle hash to match other platforms, needed for render.com deployments

* [APP-1331] Migrate `app-info` to new env (#8703)

* Move env files into directory with platform specific files

* Migrate usages of app-info to new env

* Fix bad import

* Update BUNDLE_DATE format comment

* Trim RENDER_GIT_COMMIT to first 7 to match --short sha

* Clarify build process env vars and ensure they are explicitly passed in

* Revert Sentry patch as a result of prev commit

* Update webpack Sentry dist value based on prev commits

* Add PACKAGE_VERSION and replace in statsig to fix conflict

* Fix render substitution syntax

* Remove invalid syntax

* Remove unnecessary testflight check

* Just use long commit hash

* Slice full hash for display in app

* Fix missing space in ios workflow

* Pass in sentry CLI env vars, align matching values

* Align on RELEASE_VERSION

* Add new env setup to missed OTA spot

* Update webpack to use same SENTRY_RELEASE var

* Just fallback to package version for Render deploys

* Remove TF check for BUNDLE_DATE

* Set EXPO_PUBLIC_ENV for bundle update

* Consistent naming "Env"

* Add comment

* Use RELEASE_VERSION instead of package.json

* Update PR comment CI
2025-07-30 12:33:40 -05:00
Austin McKinley 2d23d821d3 Update golang toolchain to 1.24 and clean imports 2025-07-30 10:28:03 -07:00
Samuel Newman db7bdae51a Convert old toast types to new ones, mark as deprecated (#8746)
* convert old types to new types

* add depreciation warning for old warnings

* rm as consts
2025-07-30 08:18:36 -07:00
Samuel Newman ee7f508717 Fix dataSet for noFeedback links (#8745)
* fix dataSet for noFeedback links

* rm memo

* rm useless spread
2025-07-30 08:17:57 -07:00
jim 7caf2922dd Merge pull request #8744 from internet-development/@APiligrim/update-toast
hot fix: imports for toast and alignment
2025-07-30 01:17:59 -07:00
Ana 18add1cff9 update type errors 2025-07-30 01:08:14 -07:00
Ana d141921abf add: prettier formating 2025-07-30 00:48:56 -07:00
Ana 315e7341bf update: lint fixes for toast.tsx 2025-07-30 00:40:34 -07:00
Ana f8781270e5 update: toast style after lint 2025-07-30 00:36:14 -07:00
Ana f472c7f540 update sorting 2025-07-30 00:25:17 -07:00
Ana 67ba92fa62 update: imports for toast 2025-07-30 00:02:03 -07:00
jim c2b8bdb33c Merge pull request #8743 from internet-development/@APiligrim/update-toast
[APP-1345] Hot fix: add toast.style file
2025-07-29 23:35:38 -07:00
Ana 61969e8dfa add: Toast.style 2025-07-29 23:28:10 -07:00
jim 658dd448d5 Merge pull request #8734 from internet-development/@APiligrim/update-toast
[APP-1345] Update Toast UI on the web and add to storybook
2025-07-29 22:51:16 -07:00
Ana 34ea6e8f34 update: toast styles that reuse consistent style 2025-07-29 22:12:36 -07:00
pfrazee 0a00e02c70 Nightly source-language update 2025-07-30 02:51:39 +00:00
Jaz 15c9835f1d Merge pull request #8740 from bluesky-social/embedr-metrics
Add metrics to embedr service
2025-07-29 18:05:15 -07:00
Austin McKinley 27c51401d2 git tidy 2025-07-29 17:54:11 -07:00
Austin McKinley 2b92697e3e switch to using DefaultServeMux, include go.mod and go.sum changes 2025-07-29 17:51:55 -07:00
jim c31b0338a1 Merge pull request #8733 from internet-development/caidan/app-1330-new-design-for-suggested-for-you-interstitial
feat: new design for "Suggested for you" interstitial
2025-07-29 17:42:30 -07:00
Caidan Williams 940e00c8cb feat(ui): add "See More Suggested Profiles" card 2025-07-29 17:37:01 -07:00
Caidan Williams 7b4ec21e3e refactor(ui): adjust border styles in ProfileGrid card 2025-07-29 17:37:01 -07:00
Caidan Williams e5457c946a feat(ui): improve suggested follow placeholder card
Now matches the same size and shape as the rendered card, which prevents
content layout shift.
2025-07-29 17:37:01 -07:00
Caidan Williams d83480cc64 refactor(ui): remove "Browse more accounts" card on mobile view 2025-07-29 17:37:01 -07:00
Caidan Williams 8dc074c477 fix(ui): profile name and description overflowing on web 2025-07-29 17:37:01 -07:00
Caidan Williams da47cd0d1b fix(ui): placeholder web size and flex grow 2025-07-29 17:37:01 -07:00
Caidan Williams ab5be5cb3f refactor(feed): wrap placeholder description placeholder in full-width view 2025-07-29 17:37:01 -07:00
Caidan Williams 22db8ed639 feat(feed): update suggested profiles link text 2025-07-29 17:37:01 -07:00
Caidan Williams 4c8d5db6a6 refactor(ui): web profile cards maxLength 4 -> 3 with flex grow 2025-07-29 17:37:01 -07:00
Caidan Williams b2fe27afe7 refactor(ui): remove "Browse more suggestions" on web in favor of "See all" 2025-07-29 17:37:01 -07:00
Caidan Williams f35dc19a48 feat(ui): update SuggestedFollowPlaceholder to match new design 2025-07-29 17:37:01 -07:00
Caidan Williams 4e507caa7c feat: update AvatarPlaceholder to take size prop 2025-07-29 17:37:01 -07:00
Caidan Williams 87f2bc5be3 feat: create NamePlaceholder component 2025-07-29 17:37:01 -07:00
Caidan Williams f58ad8e419 refactor: replace magic 6px with dynamic a.gap_md/2 2025-07-29 17:37:01 -07:00
Caidan Williams 2bf4ee5690 fix(ui): suggested profile card grid on web sm -> md 2025-07-29 17:37:01 -07:00
Caidan Williams 48f670f26a fix(ui): follow button should always be pushed to the bottom on the profile card 2025-07-29 17:37:01 -07:00
Caidan Williams 955a864245 refactor: update MOBILE_CARD_WIDTH instead of using inline width 2025-07-29 17:37:01 -07:00
Caidan Williams 0598d73617 refactor(ui): follow button and description to match design 2025-07-29 17:37:01 -07:00
Caidan Williams f478ebdeb5 refactor(ui): swap ProfileCard.NameAndHandle for ProfileCard.Name 2025-07-29 17:37:01 -07:00
Caidan Williams 205e7f0190 feat(ui): update ProfileCard.Avatar size 40 -> 88 2025-07-29 17:37:01 -07:00
Caidan Williams 3c89552e92 refactor(ui): replace ProfileCard.Header with custom View 2025-07-29 17:37:01 -07:00
Caidan Williams c93c36f6e5 feat(ui): force width on suggested profile card 2025-07-29 17:37:01 -07:00
Caidan Williams c6e7816fa7 feat(ui): update padding, border radius, and border on suggested profile card 2025-07-29 17:37:01 -07:00
Caidan Williams ff4eecc2e4 feat: update ProfileCard.Description to take style prop 2025-07-29 17:37:01 -07:00
Caidan Williams 2a6d04e6e2 feat: update ProfileCard.Avatar to take size prop 2025-07-29 17:37:01 -07:00
Caidan Williams e465c9e461 feat(ui): add dropshadow to suggested profile card 2025-07-29 17:37:01 -07:00
Caidan Williams b8a257a1c7 feat(ui): replace suggested profiles interstital icon with link "See all" 2025-07-29 17:37:01 -07:00
Caidan Williams 92e0119734 feat(ui): updated suggested profiles interstitial "Suggested for you" text color 2025-07-29 17:37:01 -07:00
Caidan Williams 287f5c0d42 feat(ui): update suggested profiles interstitial top padding 16 -> 12 2025-07-29 17:37:00 -07:00
Austin McKinley ea226da275 add metrics to embedr service 2025-07-29 17:36:58 -07:00
Elijah Seed-Arita 332dbc1bb4 fix: small profile followup fixes (#8735)
* fix: small profile followup fixes

* fix: change platform-specific compensation for profileHeaderDisplayName marginBottom to target all native

* fix: apply suggestion for consistent cross-platform profile username margin
2025-07-29 16:20:33 -05:00
Eric Bailey bc78fb224e Add comment (#8739) 2025-07-29 15:14:12 -05:00
Ana 890dee3eef update: web toast 2025-07-28 22:07:28 -07:00
Ana c13b1374d7 add: toasts to storybook 2025-07-28 20:24:57 -07:00
330 changed files with 36797 additions and 28241 deletions
+29 -5
View File
@@ -1,8 +1,32 @@
# Copy this to `.env` and `.env.test` files
# The env the app is running in e.g. development, testflight, production
EXPO_PUBLIC_ENV=development
BITDRIFT_API_KEY=
SENTRY_AUTH_TOKEN=
EXPO_PUBLIC_LOG_LEVEL=debug
EXPO_PUBLIC_LOG_DEBUG=
# This is the semver release version of the app, pulled from package.json
EXPO_PUBLIC_RELEASE_VERSION=
# This is the commit hash that the current bundle was made from.
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
# Should be formatted YYMMDDHH so that it increases for each build.
EXPO_PUBLIC_BUNDLE_DATE=0
# The log level for the app's logger transports
EXPO_PUBLIC_LOG_LEVEL=debug
# Enable debug logs for specific logger instances
EXPO_PUBLIC_LOG_DEBUG=session
# Chat service DID
EXPO_PUBLIC_CHAT_PROXY_DID=
#
#
# Bluesky specific values
#
#
# Sentry DSN for telemetry
EXPO_PUBLIC_SENTRY_DSN=
# Bitdrift API key. If undefined, Bitdrift will be disabled.
EXPO_PUBLIC_BITDRIFT_API_KEY=
@@ -43,12 +43,11 @@ jobs:
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Set outputs
id: vars
- name: Env
id: env
run: |
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Build and push Docker image
id: build-and-push
@@ -62,8 +61,8 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.vars.outputs.sha_short }}
SENTRY_DIST=${{ steps.vars.outputs.SENTRY_DIST }}
SENTRY_RELEASE=${{ steps.vars.outputs.SENTRY_RELEASE }}
EXPO_PUBLIC_ENV=production
EXPO_PUBLIC_RELEASE_VERSION=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
@@ -3,6 +3,7 @@ on:
push:
branches:
- main
- echoprom_fix
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
+26 -11
View File
@@ -62,23 +62,31 @@ jobs:
- name: Check for i18n compilation errors
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
- name: ✏️ Write environment variables
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
id: env
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "$json" > google-services.json
- name: Setup Sentry vars for build-time injection
id: sentry
run: |
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
- name: 🏗️ EAS Build
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p android --profile ${{ inputs.profile || 'testflight-android' }} --local --output build.aab --non-interactive
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p android
--profile ${{ inputs.profile || 'testflight-android' }}
--local --output build.aab --non-interactive
- name: ✍️ Rename Testflight bundle
if: ${{ inputs.profile != 'production' }}
@@ -140,7 +148,14 @@ jobs:
- name: 🏗️ Build Production APK
if: ${{ inputs.profile == 'production' }}
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p android --profile production-apk --local --output build.apk --non-interactive
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p android
--profile production-apk
--local --output build.apk --non-interactive
- name: 🚀 Upload Production APK Artifact
id: upload-artifact-production-apk
+18 -10
View File
@@ -53,7 +53,7 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.2'
xcode-version: '16.4'
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
@@ -75,22 +75,30 @@ jobs:
- name: Check for i18n compilation errors
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
# EXPO_PUBLIC_ENV is handled in eas.json
- name: ✏️ Write environment variables
id: env
run: |
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: Setup Sentry vars for build-time injection
id: sentry
run: |
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
- name: 🏗️ EAS Build
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p ios --profile ${{ inputs.profile || 'testflight' }} --local --output build.ipa --non-interactive
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p ios
--profile ${{ inputs.profile || 'testflight' }}
--local --output build.ipa --non-interactive
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
+52 -18
View File
@@ -101,25 +101,31 @@ jobs:
if: ${{ !steps.fingerprint.outputs.includes-changes }}
uses: dcarbone/install-jq-action@v2
- name: ✏️ Write environment variables
# eas.json not used here, set EXPO_PUBLIC_ENV
- name: Env
id: env
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_ENV=${{ inputs.channel || 'testflight' }}" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "$json" > google-services.json
- name: Setup Sentry vars for build-time injection
id: sentry
run: |
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
- name: 🏗️ Create Bundle
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn export
- name: 📦 Package Bundle and 🚀 Deploy
if: ${{ !steps.fingerprint.outputs.includes-changes }}
@@ -205,16 +211,30 @@ jobs:
- name: 🔤 Compile translations
run: yarn intl:build
- name: ✏️ Write environment variables
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
id: env
run: |
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: 🏗️ EAS Build
run: yarn use-build-number-with-bump eas build -p ios --profile testflight --local --output build.ipa --non-interactive
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p ios
--profile testflight
--local --output build.ipa --non-interactive
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
@@ -282,17 +302,31 @@ jobs:
- name: 🔤 Compile translations
run: yarn intl:build
- name: ✏️ Write environment variables
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
id: env
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "$json" > google-services.json
- name: 🏗️ EAS Build
run: yarn use-build-number-with-bump eas build -p android --profile testflight-android --local --output build.apk --non-interactive
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p android
--profile testflight-android
--local --output build.apk --non-interactive
- name: ⏰ Get a timestamp
id: timestamp
+15 -10
View File
@@ -152,23 +152,28 @@ jobs:
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
- name: ✏️ Write environment variables
- name: Env
id: env
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_ENV=testflight" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "$json" > google-services.json
- name: Setup Sentry vars for build-time injection
id: sentry
run: |
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
- name: 🏗️ Create Bundle
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="testflight" yarn export
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn export
- name: 📦 Package Bundle and 🚀 Deploy
run: yarn use-build-number bash scripts/bundleUpdate.sh
+2 -1
View File
@@ -50,6 +50,7 @@ jobs:
git config --global user.name "github-actions[bot]"
git merge --no-edit ${{ github.head_ref }}
yarn install
yarn intl:build
- name: 🔦 Generate stats file for PR
run: |
@@ -73,6 +74,7 @@ jobs:
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
run: |
yarn install
yarn intl:build
yarn generate-webpack-stats-file
mv stats.json stats-base.json
@@ -143,4 +145,3 @@ jobs:
with:
header: fingerprint-diff
delete: true
+17 -14
View File
@@ -1,4 +1,4 @@
FROM golang:1.23-bullseye AS build-env
FROM golang:1.24.5-bullseye AS build-env
WORKDIR /usr/src/social-app
@@ -19,28 +19,28 @@ ENV GOARCH="amd64"
ENV CGO_ENABLED=1
ENV GOEXPERIMENT="loopvar"
# The latest git hash of the preview branch on render.com
# https://render.com/docs/docker-secrets#environment-variables-in-docker-builds
ARG RENDER_GIT_COMMIT
#
# Expo
#
ARG EXPO_PUBLIC_ENV
ENV EXPO_PUBLIC_ENV=${EXPO_PUBLIC_ENV:-development}
ARG EXPO_PUBLIC_RELEASE_VERSION
ENV EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION
ARG EXPO_PUBLIC_BUNDLE_IDENTIFIER
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-dev}
# The latest git hash of the preview branch on render.com
ARG RENDER_GIT_COMMIT
# If not set by GitHub workflows, we're probably in Render
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-$RENDER_GIT_COMMIT}
#
# Sentry
#
ARG SENTRY_AUTH_TOKEN
ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN:-unknown}
# Will fall back to package.json#version, but this is handled elsewhere
ARG SENTRY_RELEASE
ENV SENTRY_RELEASE=$SENTRY_RELEASE
ARG SENTRY_DIST
# Default to RENDER_GIT_COMMIT if not set by GitHub workflows
ENV SENTRY_DIST=${SENTRY_DIST:-$RENDER_GIT_COMMIT}
ARG SENTRY_DSN
ENV SENTRY_DSN=$SENTRY_DSN
ARG EXPO_PUBLIC_SENTRY_DSN
ENV EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN
#
# Copy everything into the container
@@ -60,13 +60,16 @@ RUN \. "$NVM_DIR/nvm.sh" && \
nvm install $NODE_VERSION && \
nvm use $NODE_VERSION && \
echo "Using bundle identifier: $EXPO_PUBLIC_BUNDLE_IDENTIFIER" && \
echo "EXPO_PUBLIC_ENV=$EXPO_PUBLIC_ENV" >> .env && \
echo "EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION" >> .env && \
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER" >> .env && \
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env && \
echo "EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN" >> .env && \
npm install --global yarn && \
yarn && \
yarn intl:build 2>&1 | tee i18n.log && \
if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compile errors!\n\n"; fi && \
EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER EXPO_PUBLIC_BUNDLE_DATE=$() SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$SENTRY_RELEASE SENTRY_DIST=$SENTRY_DIST SENTRY_DSN=$SENTRY_DSN yarn build-web
SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$EXPO_PUBLIC_RELEASE_VERSION SENTRY_DIST=$EXPO_PUBLIC_BUNDLE_IDENTIFIER yarn build-web
# DEBUG
RUN find ./bskyweb/static && find ./web-build/static
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.23-bullseye AS build-env
FROM golang:1.24.5-bullseye AS build-env
WORKDIR /usr/src/social-app
+4 -3
View File
@@ -10,7 +10,7 @@ Get the app itself:
## Development Resources
This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also open source, but in [a different git repository](https://github.com/bluesky-social/atproto).
This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), which are also open source, but in [a different git repository](https://github.com/bluesky-social/atproto).
There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application.
@@ -19,7 +19,7 @@ The [Build Instructions](./docs/build.md) are a good place to get started with t
The Authenticated Transfer Protocol ("AT Protocol" or "atproto") is a decentralized social media protocol. You don't *need* to understand AT Protocol to work with this application, but it can help. Learn more at:
- [Overview and Guides](https://atproto.com/guides/overview)
- [Github Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions
- [GitHub Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions
- [Protocol Specifications](https://atproto.com/specs/atp)
- [Blogpost on self-authenticating data structures](https://bsky.social/about/blog/3-6-2022-a-self-authenticating-social-protocol)
@@ -27,6 +27,7 @@ The Bluesky Social application encompasses a set of schemas and APIs built in th
## Contributions
> [!NOTE]
> While we do accept contributions, we prioritize high quality issues and pull requests. Adhering to the below guidelines will ensure a more timely review.
**Rules:**
@@ -59,7 +60,7 @@ Please be sure to:
## Security disclosures
If you discover any security issues, please send an email to security@bsky.app. The email is automatically CCed to the entire team and we'll respond promptly.
If you discover any security issues, please send an email to security@bsky.app. The email is automatically CC'd to the entire team and we'll respond promptly.
## Are you a developer interested in building on atproto?
+3
View File
@@ -28,6 +28,9 @@ appId: xyz.blueskyweb.app
- hideKeyboard
- tapOn:
id: "nextBtn"
- tapOn:
text: "Not Now"
optional: true
- inputText: "e2e-test"
- tapOn:
id: "nextBtn"
+3
View File
@@ -23,4 +23,7 @@ appId: xyz.blueskyweb.app
id: "loginPasswordInput"
- inputText: "hunter2"
- pressKey: Enter
- tapOn:
text: "Not Now"
optional: true
- assertVisible: "Following"
+6 -5
View File
@@ -1,7 +1,8 @@
import React, {ReactNode} from 'react'
import {View, ScrollView, Modal, FlatList, TextInput} from 'react-native'
import React, {type ReactNode} from 'react'
import {FlatList, Modal, ScrollView, TextInput, View} from 'react-native'
const BottomSheetModalContext = React.createContext(null)
BottomSheetModalContext.displayName = 'BottomSheetModalContext'
const BottomSheetModalProvider = (props: any) => {
return <BottomSheetModalContext.Provider {...props} value={{}} />
@@ -47,13 +48,13 @@ export {useBottomSheetInternal}
export {useBottomSheetDynamicSnapPoints}
export {
BottomSheetModalProvider,
BottomSheetBackdrop,
BottomSheetFlatList,
BottomSheetFooter,
BottomSheetHandle,
BottomSheetModal,
BottomSheetFooter,
BottomSheetModalProvider,
BottomSheetScrollView,
BottomSheetFlatList,
BottomSheetTextInput,
}
+5 -5
View File
@@ -1,4 +1,4 @@
import {IsValidHandle, validateServiceHandle} from '#/lib/strings/handles'
import {type IsValidHandle, validateServiceHandle} from '#/lib/strings/handles'
describe('handle validation', () => {
const valid = [
@@ -18,17 +18,17 @@ describe('handle validation', () => {
})
const invalid = [
['al', 'bsky.social', 'frontLength'],
['al', 'bsky.social', 'frontLengthNotTooShort'],
['-alice', 'bsky.social', 'hyphenStartOrEnd'],
['alice-', 'bsky.social', 'hyphenStartOrEnd'],
['%%%', 'bsky.social', 'handleChars'],
['1234567890123456789', 'bsky.social', 'frontLength'],
['1234567890123456789', 'bsky.social', 'frontLengthNotTooLong'],
[
'1234567890123456789',
'my-custom-pds-with-long-name.social',
'frontLength',
'frontLengthNotTooLong',
],
['al', 'my-custom-pds-with-long-name.social', 'frontLength'],
['al', 'my-custom-pds-with-long-name.social', 'frontLengthNotTooShort'],
['a'.repeat(300), 'toolong.com', 'totalLength'],
] satisfies [string, string, keyof IsValidHandle][]
it.each(invalid)(
+6 -3
View File
@@ -234,8 +234,9 @@ module.exports = function (_config) {
],
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
'./plugins/withGradleJVMHeapSizeIncrease.js',
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestLargeHeapPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidManifestIntentQueriesPlugin.js',
'./plugins/withAndroidStylesAccentColorPlugin.js',
'./plugins/withAndroidDayNightThemePlugin.js',
'./plugins/withAndroidNoJitpackPlugin.js',
@@ -250,10 +251,12 @@ module.exports = function (_config) {
// Android only
'./assets/fonts/inter/Inter-Regular.otf',
'./assets/fonts/inter/Inter-Italic.otf',
'./assets/fonts/inter/Inter-Medium.otf',
'./assets/fonts/inter/Inter-MediumItalic.otf',
'./assets/fonts/inter/Inter-SemiBold.otf',
'./assets/fonts/inter/Inter-SemiBoldItalic.otf',
'./assets/fonts/inter/Inter-ExtraBold.otf',
'./assets/fonts/inter/Inter-ExtraBoldItalic.otf',
'./assets/fonts/inter/Inter-Bold.otf',
'./assets/fonts/inter/Inter-BoldItalic.otf',
],
},
],
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M15.793 10.293a1 1 0 0 1 1.338-.068l.076.068 3.293 3.293a2 2 0 0 1 .138 2.677l-.138.151-3.293 3.293a1 1 0 1 1-1.414-1.414L18.086 16H8a5 5 0 0 1-5-5V5a1 1 0 0 1 2 0v6a3 3 0 0 0 3 3h10.086l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z"/></svg>

After

Width:  |  Height:  |  Size: 334 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 250 B

+2
View File
@@ -608,6 +608,8 @@ type IPCCResponse struct {
AgeRestrictedGeo bool `json:"isAgeRestrictedGeo,omitempty"`
}
// IP address data is powered by IPinfo
// https://ipinfo.io
func (srv *Server) WebIpCC(c echo.Context) error {
realIP := c.RealIP()
addr, err := netip.ParseAddr(realIP)
+7
View File
@@ -46,6 +46,13 @@ func run(args []string) {
Value: ":8100",
EnvVars: []string{"HTTP_ADDRESS"},
},
&cli.StringFlag{
Name: "metrics-address",
Usage: "Specify the local IP/port to bind the metrics server to",
Required: false,
Value: ":9090",
EnvVars: []string{"METRICS_HTTP_ADDRESS"},
},
&cli.BoolFlag{
Name: "debug",
Usage: "Enable debug mode",
+44 -9
View File
@@ -13,29 +13,36 @@ import (
"syscall"
"time"
_ "net/http/pprof"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/util/cliutil"
"github.com/bluesky-social/indigo/xrpc"
"github.com/bluesky-social/social-app/bskyweb"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/labstack/echo-contrib/echoprometheus"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/klauspost/compress/gzhttp"
"github.com/klauspost/compress/gzip"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/urfave/cli/v2"
)
type Server struct {
echo *echo.Echo
httpd *http.Server
xrpcc *xrpc.Client
dir identity.Directory
echo *echo.Echo
httpd *http.Server
metricsHttpd *http.Server
xrpcc *xrpc.Client
dir identity.Directory
}
func serve(cctx *cli.Context) error {
debug := cctx.Bool("debug")
httpAddress := cctx.String("http-address")
appviewHost := cctx.String("appview-host")
metricsAddress := cctx.String("metrics-address")
// Echo
e := echo.New()
@@ -65,13 +72,28 @@ func serve(cctx *cli.Context) error {
return err
}
metricsMux := http.DefaultServeMux
metricsMux.Handle("/metrics", promhttp.Handler())
metricsHttpd := &http.Server{
Addr: metricsAddress,
Handler: metricsMux,
}
go func() {
if err := metricsHttpd.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Error("failed to start metrics server", "error", err)
}
}()
//
// server
//
server := &Server{
echo: e,
xrpcc: xrpcc,
dir: identity.DefaultDirectory(),
echo: e,
xrpcc: xrpcc,
dir: identity.DefaultDirectory(),
metricsHttpd: metricsHttpd,
}
// Create the HTTP server.
@@ -132,6 +154,14 @@ func serve(cctx *cli.Context) error {
RedirectCode: http.StatusFound,
}))
echoprom := echoprometheus.NewMiddlewareWithConfig(
echoprometheus.MiddlewareConfig{
DoNotUseRequestPathFor404: true,
},
)
e.Use(echoprom)
//
// configure routes
//
@@ -220,6 +250,11 @@ func (srv *Server) Shutdown() error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Shutdown metrics server too
if srv.metricsHttpd != nil {
srv.metricsHttpd.Shutdown(ctx)
}
return srv.httpd.Shutdown(ctx)
}
+21 -22
View File
@@ -1,24 +1,26 @@
module github.com/bluesky-social/social-app/bskyweb
go 1.23
toolchain go1.23.4
go 1.24.5
require (
github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc
github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a
github.com/flosch/pongo2/v6 v6.0.0
github.com/ipfs/go-log v1.0.5
github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.17.3
github.com/labstack/echo/v4 v4.11.3
github.com/klauspost/compress v1.18.0
github.com/labstack/echo-contrib v0.17.4
github.com/labstack/echo/v4 v4.13.4
github.com/prometheus/client_golang v1.22.0
github.com/urfave/cli/v2 v2.25.7
)
require github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -26,7 +28,6 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
@@ -52,17 +53,16 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/labstack/gommon v0.4.1 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx/v2 v2.0.12 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
@@ -73,10 +73,9 @@ require (
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/orandin/slog-gorm v1.3.2 // indirect
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
@@ -94,14 +93,14 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gorm.io/driver/postgres v1.5.7 // indirect
gorm.io/driver/sqlite v1.5.5 // indirect
gorm.io/gorm v1.25.9 // indirect
+44 -44
View File
@@ -2,14 +2,14 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc h1:chbGD59Cn1mw07kbq9Uvb8WUFIr1dcoL5TOhT+I9bV4=
github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng=
github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a h1:S12KN45uIkRglMHC8PqD/Vsz0+u3KbIaBF/6rit8/Pg=
github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a/go.mod h1:0XUyOCRtL4/OiyeqMTmr6RlVHQMDgw3LS7CfibuZR5Q=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -33,10 +33,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -102,8 +100,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA=
github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -113,10 +111,14 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM=
github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk=
github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
@@ -130,16 +132,13 @@ github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
@@ -154,6 +153,8 @@ github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7B
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/orandin/slog-gorm v1.3.2 h1:C0lKDQPAx/pF+8K2HL7bdShPwOEJpPM0Bn80zTzxU1g=
@@ -163,14 +164,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
@@ -197,8 +198,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
@@ -252,8 +253,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -271,16 +272,16 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -292,13 +293,12 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -310,10 +310,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@@ -332,8 +332,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+1
View File
@@ -8,6 +8,7 @@
Preconnect to essential domains
-->
<link rel="preconnect" href="https://bsky.social">
<link rel="preconnect" href="https://go.bsky.app">
<title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
<!-- Hello Humans! API docs at https://atproto.com -->
+2 -2
View File
@@ -89,9 +89,9 @@ If you change `SENTRY_AUTH_TOKEN`, you need to do `yarn prebuild` before running
### Adding bitdrift
Adding bitdirft is NOT required. You can keep `BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup.
Adding bitdrift is NOT required. You can keep `EXPO_PUBLIC_BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup.
However, if you're a part of the Bluesky team and want to enable bitdrift, fill in `BITDRIFT_API_KEY` in your `.env` to enable bitdrift.
However, if you're a part of the Bluesky team and want to enable bitdrift, fill in `EXPO_PUBLIC_BITDRIFT_API_KEY` in your `.env` to enable bitdrift.
### Adding and Updating Locales
+7 -7
View File
@@ -43,7 +43,7 @@ class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDele
let payload = try? JSONDecoder().decode(WebViewActionPayload.self, from: data) else {
return
}
switch payload.action {
case .present:
self.presentAppStoreOverlay()
@@ -65,18 +65,18 @@ class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDele
guard let url = navigationAction.request.url else {
return .allow
}
// Store the previous one to compare later, but only set starterPackUrl when we find the right one
prevUrl = url
// pathComponents starts with "/" as the first component, then each path name. so...
// ["/", "start", "name", "rkey"]
if isStarterPackUrl(url){
if isStarterPackUrl(url) {
self.starterPackUrl = url
}
return .allow
}
func isStarterPackUrl(_ url: URL) -> Bool {
var host: String?
if #available(iOS 16.0, *) {
@@ -84,11 +84,11 @@ class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDele
} else {
host = url.host
}
switch host {
case "bsky.app":
if url.pathComponents.count == 4,
(url.pathComponents[1] == "start" || url.pathComponents[1] == "starter-pack") {
url.pathComponents[1] == "start" || url.pathComponents[1] == "starter-pack" {
return true
}
return false
@@ -19,7 +19,6 @@ import expo.modules.kotlin.AppContext
import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
class BottomSheetView(
context: Context,
appContext: AppContext,
@@ -31,12 +30,14 @@ class BottomSheetView(
private lateinit var dialogRootViewGroup: DialogRootViewGroup
private var eventDispatcher: EventDispatcher? = null
private val rawScreenHeight = context.resources.displayMetrics.heightPixels.toFloat()
private val rawScreenHeight =
context.resources.displayMetrics.heightPixels
.toFloat()
private val safeScreenHeight = (rawScreenHeight - getNavigationBarHeight()).toFloat()
private fun getNavigationBarHeight(): Int {
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
}
private val onAttemptDismiss by EventDispatcher()
@@ -45,7 +46,7 @@ class BottomSheetView(
// Props
var disableDrag = false
set (value) {
set(value) {
field = value
this.setDraggable(!value)
}
@@ -157,8 +158,8 @@ class BottomSheetView(
// Presentation
private fun getHalfExpandedRatio(contentHeight: Float): Float {
return when {
private fun getHalfExpandedRatio(contentHeight: Float): Float =
when {
// Full height sheets
contentHeight >= safeScreenHeight -> 0.99f
// Medium height sheets (>50% but <100%)
@@ -168,7 +169,6 @@ class BottomSheetView(
else ->
this.clampRatio(this.getTargetHeight() / rawScreenHeight)
}
}
private fun present() {
if (this.isOpen || this.isOpening || this.isClosing) return
@@ -139,7 +139,10 @@ class DialogRootViewGroup(
return super.onHoverEvent(event)
}
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
override fun onChildStartedNativeGesture(
childView: View?,
ev: MotionEvent,
) {
eventDispatcher?.let { jSTouchDispatcher.onChildStartedNativeGesture(ev, it) }
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher)
}
@@ -5,6 +5,7 @@ import {createPortalGroup_INTERNAL} from './lib/Portal'
type PortalContext = React.ElementType<{children: React.ReactNode}>
export const Context = React.createContext({} as PortalContext)
Context.displayName = 'BottomSheetPortalContext'
export const useBottomSheetPortal_INTERNAL = () => React.useContext(Context)
+1
View File
@@ -18,6 +18,7 @@ export function createPortalGroup_INTERNAL() {
append: () => {},
remove: () => {},
})
Context.displayName = 'BottomSheetPortalContext'
function Provider(props: React.PropsWithChildren<{}>) {
const map = React.useRef<ComponentMap>({})
@@ -16,8 +16,8 @@ import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
class GifView(
context: Context,
appContext: AppContext,
context: Context,
appContext: AppContext,
) : ExpoView(context, appContext) {
// Events
private val onPlayerStateChange by EventDispatcher()
@@ -82,65 +82,65 @@ class GifView(
}
this.webpRequest =
glide.load(source)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.skipMemoryCache(false)
.listener(
object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>?,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
placeholderRequest?.let { glide.clear(it) }
return false
}
glide
.load(source)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.skipMemoryCache(false)
.listener(
object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>?,
dataSource: DataSource,
isFirstResource: Boolean,
): Boolean {
placeholderRequest?.let { glide.clear(it) }
return false
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean
): Boolean = true
}
)
.into(this.imageView)
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean,
): Boolean = true
},
).into(this.imageView)
if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) {
this.placeholderRequest =
glide.load(placeholderSource)
.diskCacheStrategy(DiskCacheStrategy.DATA)
// Let's not bloat the memory cache with placeholders
.skipMemoryCache(true)
.listener(
object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>?,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
// Incase this request finishes after the webp, let's just not set
// the drawable. This shouldn't happen because the request should
// get cancelled
if (imageView.drawable == null) {
imageView.setImageDrawable(resource)
}
return true
}
glide
.load(placeholderSource)
.diskCacheStrategy(DiskCacheStrategy.DATA)
// Let's not bloat the memory cache with placeholders
.skipMemoryCache(true)
.listener(
object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>?,
dataSource: DataSource,
isFirstResource: Boolean,
): Boolean {
// Incase this request finishes after the webp, let's just not set
// the drawable. This shouldn't happen because the request should
// get cancelled
if (imageView.drawable == null) {
imageView.setImageDrawable(resource)
}
return true
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean
): Boolean = true
},
)
.submit()
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean,
): Boolean = true
},
).submit()
}
}
@@ -174,10 +174,10 @@ class GifView(
fun firePlayerStateChange() {
onPlayerStateChange(
mapOf(
"isPlaying" to this.isPlaying,
"isLoaded" to this.isLoaded,
),
mapOf(
"isPlaying" to this.isPlaying,
"isLoaded" to this.isLoaded,
),
)
}
@@ -5,11 +5,12 @@ import expo.modules.kotlin.modules.ModuleDefinition
import java.net.URL
class EmojiPickerModule : Module() {
override fun definition() = ModuleDefinition {
Name("EmojiPicker")
override fun definition() =
ModuleDefinition {
Name("EmojiPicker")
View(EmojiPickerModuleView::class) {
Events("onEmojiSelected")
}
View(EmojiPickerModuleView::class) {
Events("onEmojiSelected")
}
}
}
@@ -8,33 +8,35 @@ import expo.modules.kotlin.AppContext
import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
@SuppressLint("ViewConstructor")
class EmojiPickerModuleView(context: Context, appContext: AppContext) :
ExpoView(context, appContext) {
private var emojiView: EmojiPickerView = EmojiPickerView(context)
private val onEmojiSelected by EventDispatcher()
class EmojiPickerModuleView(
context: Context,
appContext: AppContext,
) : ExpoView(context, appContext) {
private var emojiView: EmojiPickerView = EmojiPickerView(context)
private val onEmojiSelected by EventDispatcher()
init {
setupView()
init {
setupView()
}
private fun setupView() {
addView(
emojiView,
LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT,
),
)
emojiView.setOnEmojiPickedListener { emoji ->
onEmojiSelected(mapOf("emoji" to emoji.emoji))
}
}
private fun setupView() {
addView(
emojiView, LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT
)
)
emojiView.setOnEmojiPickedListener { emoji ->
onEmojiSelected(mapOf("emoji" to emoji.emoji))
}
}
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
removeView(emojiView)
setupView()
}
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
removeView(emojiView)
setupView()
}
}
+10 -5
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.106.0",
"version": "1.107.0",
"private": true,
"engines": {
"node": ">=20"
@@ -20,7 +20,7 @@
},
"scripts": {
"prepare": "is-ci || husky install",
"postinstall": "patch-package && yarn intl:compile",
"postinstall": "patch-package && yarn intl:compile-if-needed",
"prebuild": "expo prebuild --clean",
"android": "expo run:android",
"android:prod": "expo run:android --variant release",
@@ -58,6 +58,7 @@
"intl:extract": "lingui extract --clean --locale en",
"intl:extract:all": "lingui extract --clean",
"intl:compile": "lingui compile",
"intl:compile-if-needed": "is-ci || [ -f src/locale/locales/en/messages.js ] || yarn intl:compile",
"intl:pull": "crowdin download translations --verbose -b main",
"intl:push": "crowdin push translations --verbose -b main",
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
@@ -70,7 +71,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.15.26",
"@atproto/api": "^0.16.2",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
@@ -142,10 +143,11 @@
"expo-file-system": "~18.1.10",
"expo-font": "~13.3.1",
"expo-haptics": "~14.1.4",
"expo-image": "~2.2.1",
"expo-image": "^2.4.0",
"expo-image-crop-tool": "^0.1.8",
"expo-image-manipulator": "~13.1.7",
"expo-image-picker": "~16.1.4",
"expo-intent-launcher": "^12.1.5",
"expo-linear-gradient": "~14.1.5",
"expo-linking": "~7.1.5",
"expo-localization": "~16.1.5",
@@ -186,6 +188,7 @@
"react-native": "^0.79.3",
"react-native-compressor": "^1.11.0",
"react-native-date-picker": "^5.0.12",
"react-native-device-attest": "^0.1.6",
"react-native-drawer-layout": "^4.1.8",
"react-native-edge-to-edge": "^1.6.0",
"react-native-gesture-handler": "2.25.0",
@@ -211,6 +214,8 @@
"react-remove-scroll-bar": "^2.3.8",
"react-responsive": "^9.0.2",
"react-textarea-autosize": "^8.5.3",
"sonner": "^2.0.7",
"sonner-native": "^0.21.0",
"statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
@@ -219,7 +224,7 @@
"zod": "^3.20.2"
},
"devDependencies": {
"@atproto/dev-env": "^0.3.155",
"@atproto/dev-env": "^0.3.160",
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0",
@@ -0,0 +1,30 @@
const {withAndroidManifest} = require('@expo/config-plugins')
const withProcessTextQuery = config =>
// eslint-disable-next-line no-shadow
withAndroidManifest(config, config => {
const manifest = config.modResults.manifest
// Ensure <queries> stub exists
if (!manifest.queries) manifest.queries = [{}]
const queries = manifest.queries[0]
queries.intent = queries.intent || []
const exists = queries.intent.some(
i =>
i.action?.[0]?.$?.['android:name'] ===
'android.intent.action.PROCESS_TEXT',
)
if (!exists) {
queries.intent.push({
action: [{$: {'android:name': 'android.intent.action.PROCESS_TEXT'}}],
data: [{$: {'android:mimeType': 'text/plain'}}],
})
}
return config
})
module.exports = withProcessTextQuery
+51 -43
View File
@@ -29,6 +29,7 @@ import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
@@ -70,8 +71,10 @@ import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import {ToastOutlet} from '#/components/Toast'
import {Splash} from '#/Splash'
import {BottomSheetProvider} from '../modules/bottom-sheet'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
@@ -137,49 +140,54 @@ function InnerApp() {
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<AgeAssuranceProvider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<ServiceAccountManager>
<HideBottomBarBorderProvider>
<GestureHandlerRootView
style={s.h100pct}>
<GlobalGestureEventsProvider>
<IntentDialogProvider>
<TestCtrls />
<Shell />
<NuxDialogs />
</IntentDialogProvider>
</GlobalGestureEventsProvider>
</GestureHandlerRootView>
</HideBottomBarBorderProvider>
</ServiceAccountManager>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceProvider>
</StatsigProvider>
<PolicyUpdateOverlayProvider>
<StatsigProvider>
<AgeAssuranceProvider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<ServiceAccountManager>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<GestureHandlerRootView
style={s.h100pct}>
<GlobalGestureEventsProvider>
<IntentDialogProvider>
<TestCtrls />
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</GlobalGestureEventsProvider>
</GestureHandlerRootView>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceAccountManager>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceProvider>
</StatsigProvider>
</PolicyUpdateOverlayProvider>
</QueryProvider>
</React.Fragment>
</VideoVolumeProvider>
+47 -41
View File
@@ -18,6 +18,7 @@ import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
@@ -50,7 +51,6 @@ 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 * 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'
@@ -58,9 +58,11 @@ import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import {ToastOutlet} from '#/components/Toast'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
@@ -117,47 +119,51 @@ function InnerApp() {
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<AgeAssuranceProvider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<ServiceConfigProvider>
<HideBottomBarBorderProvider>
<IntentDialogProvider>
<Shell />
<NuxDialogs />
</IntentDialogProvider>
</HideBottomBarBorderProvider>
</ServiceConfigProvider>
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceProvider>
</StatsigProvider>
<PolicyUpdateOverlayProvider>
<StatsigProvider>
<AgeAssuranceProvider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<ServiceConfigProvider>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<IntentDialogProvider>
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceConfigProvider>
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceProvider>
</StatsigProvider>
</PolicyUpdateOverlayProvider>
</QueryProvider>
<ToastContainer />
</React.Fragment>
</ActiveVideoProvider>
</VideoVolumeProvider>
+9
View File
@@ -332,6 +332,9 @@ export const atoms = {
font_normal: {
fontWeight: tokens.fontWeight.normal,
},
font_medium: {
fontWeight: tokens.fontWeight.medium,
},
font_bold: {
fontWeight: tokens.fontWeight.bold,
},
@@ -979,6 +982,12 @@ export const atoms = {
hidden: {
display: 'none',
},
inline: web({
display: 'inline',
}),
block: web({
display: 'block',
}),
/*
* Transition
+6 -6
View File
@@ -1,7 +1,7 @@
import {TextStyle} from 'react-native'
import {type TextStyle} from 'react-native'
import {isAndroid, isWeb} from '#/platform/detection'
import {Device, device} from '#/storage'
import {type Device, device} from '#/storage'
const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
@@ -43,11 +43,11 @@ export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') {
style.fontFamily =
{
400: 'Inter-Regular',
500: 'Inter-Regular',
500: 'Inter-Medium',
600: 'Inter-SemiBold',
700: 'Inter-SemiBold',
800: 'Inter-ExtraBold',
900: 'Inter-ExtraBold',
700: 'Inter-Bold',
800: 'Inter-Bold',
900: 'Inter-Bold',
}[String(style.fontWeight || '400')] || 'Inter-Regular'
if (style.fontStyle === 'italic') {
+3 -2
View File
@@ -8,9 +8,9 @@ import {
setFontScale as persistFontScale,
} from '#/alf/fonts'
import {createThemes, defaultTheme} from '#/alf/themes'
import {Theme, ThemeName} from '#/alf/types'
import {type Theme, type ThemeName} from '#/alf/types'
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
import {Device} from '#/storage'
import {type Device} from '#/storage'
export {atoms} from '#/alf/atoms'
export * from '#/alf/breakpoints'
@@ -61,6 +61,7 @@ export const Context = React.createContext<Alf>({
},
flags: {},
})
Context.displayName = 'AlfContext'
export function ThemeProvider({
children,
+1
View File
@@ -53,6 +53,7 @@ export const borderRadius = {
*/
export const fontWeight = {
normal: '400',
medium: '500',
bold: '600',
heavy: '800',
} as const
+6 -4
View File
@@ -11,9 +11,11 @@ export function DO_NOT_USE() {
return useFonts({
'Inter-Regular': require('../../../assets/fonts/inter/Inter-Regular.otf'),
'Inter-Italic': require('../../../assets/fonts/inter/Inter-Italic.otf'),
'Inter-Bold': require('../../../assets/fonts/inter/Inter-SemiBold.otf'),
'Inter-BoldItalic': require('../../../assets/fonts/inter/Inter-SemiBoldItalic.otf'),
'Inter-Black': require('../../../assets/fonts/inter/Inter-ExtraBold.otf'),
'Inter-BlackItalic': require('../../../assets/fonts/inter/Inter-ExtraBoldItalic.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'),
})
}
+1
View File
@@ -23,6 +23,7 @@ type Context = {
const Context = createContext<Context>({
type: 'info',
})
Context.displayName = 'AdmonitionContext'
export function Icon() {
const t = useTheme()
+369 -351
View File
@@ -14,31 +14,37 @@ import {
View,
type ViewStyle,
} from 'react-native'
import {LinearGradient} from 'expo-linear-gradient'
import {atoms as a, flatten, select, tokens, useTheme} from '#/alf'
import {atoms as a, flatten, select, useTheme} from '#/alf'
import {type Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography'
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
/**
* The `Button` component, and some extensions of it like `Link` are intended
* to be generic and therefore apply no styles by default. These `VariantProps`
* are what control the `Button`'s presentation, and are intended only use cases where the buttons appear as, well, buttons.
*
* If `Button` or an extension of it are used for other compound components, use this property to avoid misuse of these variant props further down the line.
*
* @example
* type MyComponentProps = Omit<ButtonProps, UninheritableButtonProps> & {...}
*/
export type UninheritableButtonProps = 'variant' | 'color' | 'size' | 'shape'
export type ButtonVariant = 'solid' | 'outline' | 'ghost'
export type ButtonColor =
| 'primary'
| 'secondary'
| 'secondary_inverted'
| 'negative'
| 'negative_secondary'
| 'gradient_primary'
| 'gradient_sky'
| 'gradient_midnight'
| 'gradient_sunrise'
| 'gradient_sunset'
| 'gradient_nordic'
| 'gradient_bonfire'
| 'primary_subtle'
| 'negative_subtle'
export type ButtonSize = 'tiny' | 'small' | 'large'
export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = {
/**
* The style variation of the button
* @deprecated Use `color` instead.
*/
variant?: ButtonVariant
/**
@@ -103,6 +109,7 @@ const Context = React.createContext<VariantProps & ButtonState>({
pressed: false,
disabled: false,
})
Context.displayName = 'ButtonContext'
export function useButtonContext() {
return React.useContext(Context)
@@ -131,6 +138,15 @@ export const Button = React.forwardRef<View, ButtonProps>(
},
ref,
) => {
/**
* The `variant` prop is deprecated in favor of simply specifying `color`.
* If a `color` is set, then we want to use the existing codepaths for
* "solid" buttons. This is to maintain backwards compatibility.
*/
if (!variant && color) {
variant = 'solid'
}
const t = useTheme()
const [state, setState] = React.useState({
pressed: false,
@@ -203,8 +219,13 @@ export const Button = React.forwardRef<View, ButtonProps>(
const baseStyles: ViewStyle[] = []
const hoverStyles: ViewStyle[] = []
if (color === 'primary') {
if (variant === 'solid') {
/*
* This is the happy path for new button styles, following the
* deprecation of `variant` prop. This redundant `variant` check is here
* just to make this handling easier to understand.
*/
if (variant === 'solid') {
if (color === 'primary') {
if (!disabled) {
baseStyles.push({
backgroundColor: t.palette.primary_500,
@@ -214,108 +235,30 @@ export const Button = React.forwardRef<View, ButtonProps>(
})
} else {
baseStyles.push({
backgroundColor: select(t.name, {
light: t.palette.primary_700,
dim: t.palette.primary_300,
dark: t.palette.primary_300,
}),
backgroundColor: t.palette.primary_200,
})
}
} else if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.primary_500,
})
hoverStyles.push(a.border, {
backgroundColor: t.palette.primary_50,
})
} else {
baseStyles.push(a.border, {
borderColor: t.palette.primary_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.primary_100,
})
}
}
} else if (color === 'secondary') {
if (variant === 'solid') {
} else if (color === 'secondary') {
if (!disabled) {
baseStyles.push(t.atoms.bg_contrast_25)
hoverStyles.push(t.atoms.bg_contrast_50)
hoverStyles.push(t.atoms.bg_contrast_100)
} else {
baseStyles.push(t.atoms.bg_contrast_100)
baseStyles.push(t.atoms.bg_contrast_50)
}
} else if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_300,
})
hoverStyles.push(t.atoms.bg_contrast_50)
} else {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.contrast_25,
})
}
}
} else if (color === 'secondary_inverted') {
if (variant === 'solid') {
} else if (color === 'secondary_inverted') {
if (!disabled) {
baseStyles.push({
backgroundColor: t.palette.contrast_900,
})
hoverStyles.push({
backgroundColor: t.palette.contrast_950,
backgroundColor: t.palette.contrast_975,
})
} else {
baseStyles.push({
backgroundColor: t.palette.contrast_600,
})
}
} else if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_300,
})
hoverStyles.push(t.atoms.bg_contrast_50)
} else {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.contrast_25,
})
}
}
} else if (color === 'negative') {
if (variant === 'solid') {
} else if (color === 'negative') {
if (!disabled) {
baseStyles.push({
backgroundColor: t.palette.negative_500,
@@ -324,41 +267,36 @@ export const Button = React.forwardRef<View, ButtonProps>(
backgroundColor: t.palette.negative_600,
})
} else {
baseStyles.push({
backgroundColor: t.palette.negative_700,
})
}
} else if (color === 'primary_subtle') {
if (!disabled) {
baseStyles.push({
backgroundColor: select(t.name, {
light: t.palette.negative_700,
dim: t.palette.negative_300,
dark: t.palette.negative_300,
light: t.palette.primary_50,
dim: t.palette.primary_100,
dark: t.palette.primary_100,
}),
})
hoverStyles.push({
backgroundColor: select(t.name, {
light: t.palette.primary_100,
dim: t.palette.primary_200,
dark: t.palette.primary_200,
}),
})
} else {
baseStyles.push({
backgroundColor: select(t.name, {
light: t.palette.primary_25,
dim: t.palette.primary_50,
dark: t.palette.primary_50,
}),
})
}
} else if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.negative_500,
})
hoverStyles.push(a.border, {
backgroundColor: t.palette.negative_50,
})
} else {
baseStyles.push(a.border, {
borderColor: t.palette.negative_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.negative_100,
})
}
}
} else if (color === 'negative_secondary') {
if (variant === 'solid') {
} else if (color === 'negative_subtle') {
if (!disabled) {
baseStyles.push({
backgroundColor: select(t.name, {
@@ -377,80 +315,194 @@ export const Button = React.forwardRef<View, ButtonProps>(
} else {
baseStyles.push({
backgroundColor: select(t.name, {
light: t.palette.negative_100,
light: t.palette.negative_25,
dim: t.palette.negative_50,
dark: t.palette.negative_50,
}),
})
}
} else if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
}
} else {
/*
* BEGIN DEPRECATED STYLES
*/
if (color === 'primary') {
if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.negative_500,
})
hoverStyles.push(a.border, {
backgroundColor: t.palette.negative_50,
})
} else {
baseStyles.push(a.border, {
borderColor: t.palette.negative_200,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.primary_500,
})
hoverStyles.push(a.border, {
backgroundColor: t.palette.primary_50,
})
} else {
baseStyles.push(a.border, {
borderColor: t.palette.primary_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.primary_100,
})
}
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.negative_100,
} else if (color === 'secondary') {
if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_300,
})
hoverStyles.push(t.atoms.bg_contrast_50)
} else {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.contrast_25,
})
}
}
} else if (color === 'secondary_inverted') {
if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_300,
})
hoverStyles.push(t.atoms.bg_contrast_50)
} else {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.contrast_25,
})
}
}
} else if (color === 'negative') {
if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.negative_500,
})
hoverStyles.push(a.border, {
backgroundColor: t.palette.negative_50,
})
} else {
baseStyles.push(a.border, {
borderColor: t.palette.negative_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.negative_100,
})
}
}
} else if (color === 'negative_subtle') {
if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.negative_500,
})
hoverStyles.push(a.border, {
backgroundColor: t.palette.negative_50,
})
} else {
baseStyles.push(a.border, {
borderColor: t.palette.negative_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.negative_100,
})
}
}
}
/*
* END DEPRECATED STYLES
*/
}
if (shape === 'default') {
if (size === 'large') {
baseStyles.push({
paddingVertical: 13,
paddingHorizontal: 20,
borderRadius: 8,
gap: 8,
paddingVertical: 12,
paddingHorizontal: 25,
borderRadius: 10,
gap: 3,
})
} else if (size === 'small') {
baseStyles.push({
paddingVertical: 9,
paddingHorizontal: 12,
borderRadius: 6,
gap: 6,
paddingVertical: 8,
paddingHorizontal: 13,
borderRadius: 8,
gap: 3,
})
} else if (size === 'tiny') {
baseStyles.push({
paddingVertical: 4,
paddingHorizontal: 8,
borderRadius: 4,
gap: 4,
paddingVertical: 5,
paddingHorizontal: 9,
borderRadius: 6,
gap: 2,
})
}
} else if (shape === 'round' || shape === 'square') {
/*
* These sizes match the actual rendered size on screen, based on
* Chrome's web inspector
*/
if (size === 'large') {
if (shape === 'round') {
baseStyles.push({height: 46, width: 46})
baseStyles.push({height: 44, width: 44})
} else {
baseStyles.push({height: 44, width: 44})
}
} else if (size === 'small') {
if (shape === 'round') {
baseStyles.push({height: 34, width: 34})
baseStyles.push({height: 33, width: 33})
} else {
baseStyles.push({height: 34, width: 34})
baseStyles.push({height: 33, width: 33})
}
} else if (size === 'tiny') {
if (shape === 'round') {
baseStyles.push({height: 22, width: 22})
baseStyles.push({height: 25, width: 25})
} else {
baseStyles.push({height: 21, width: 21})
baseStyles.push({height: 25, width: 25})
}
}
@@ -458,7 +510,9 @@ export const Button = React.forwardRef<View, ButtonProps>(
baseStyles.push(a.rounded_full)
} else if (shape === 'square') {
if (size === 'tiny') {
baseStyles.push(a.rounded_xs)
baseStyles.push({
borderRadius: 6,
})
} else {
baseStyles.push(a.rounded_sm)
}
@@ -471,49 +525,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
}
}, [t, variant, color, size, shape, disabled])
const gradientValues = React.useMemo(() => {
const gradient = {
primary: tokens.gradients.sky,
secondary: tokens.gradients.sky,
secondary_inverted: tokens.gradients.sky,
negative: tokens.gradients.sky,
negative_secondary: tokens.gradients.sky,
gradient_primary: tokens.gradients.primary,
gradient_sky: tokens.gradients.sky,
gradient_midnight: tokens.gradients.midnight,
gradient_sunrise: tokens.gradients.sunrise,
gradient_sunset: tokens.gradients.sunset,
gradient_nordic: tokens.gradients.nordic,
gradient_bonfire: tokens.gradients.bonfire,
}[color || 'primary']
if (variant === 'gradient') {
if (gradient.values.length < 2) {
throw new Error(
'Gradient buttons must have at least two colors in the gradient',
)
}
return {
colors: gradient.values.map(([_, color]) => color) as [
string,
string,
...string[],
],
hoverColors: gradient.values.map(_ => gradient.hover_value) as [
string,
string,
...string[],
],
locations: gradient.values.map(([location, _]) => location) as [
number,
number,
...number[],
],
}
}
}, [variant, color])
const context = React.useMemo<ButtonContext>(
() => ({
...state,
@@ -545,6 +556,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
a.flex_row,
a.align_center,
a.justify_center,
a.curve_continuous,
flattenedBaseStyles,
...(state.hovered || state.pressed
? [hoverStyles, flatten(hoverStyleProp)]
@@ -556,27 +568,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
onHoverOut={onHoverOut}
onFocus={onFocus}
onBlur={onBlur}>
{variant === 'gradient' && gradientValues && (
<View
style={[
a.absolute,
a.inset_0,
a.overflow_hidden,
{borderRadius: flattenedBaseStyles.borderRadius},
]}>
<LinearGradient
colors={
state.hovered || state.pressed
? gradientValues.hoverColors
: gradientValues.colors
}
locations={gradientValues.locations}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[a.absolute, a.inset_0]}
/>
</View>
)}
<Context.Provider value={context}>
{typeof children === 'function' ? children(context) : children}
</Context.Provider>
@@ -592,159 +583,187 @@ export function useSharedButtonTextStyles() {
return React.useMemo(() => {
const baseStyles: TextStyle[] = []
if (color === 'primary') {
if (variant === 'solid') {
/*
* This is the happy path for new button styles, following the
* deprecation of `variant` prop. This redundant `variant` check is here
* just to make this handling easier to understand.
*/
if (variant === 'solid') {
if (color === 'primary') {
if (!disabled) {
baseStyles.push({color: t.palette.white})
} else {
baseStyles.push({color: t.palette.white, opacity: 0.5})
}
} else if (variant === 'outline') {
if (!disabled) {
baseStyles.push({
color: t.palette.primary_600,
})
} else {
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.primary_600})
} else {
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
}
}
} else if (color === 'secondary') {
if (variant === 'solid' || variant === 'gradient') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_700,
})
} else {
baseStyles.push({
color: t.palette.contrast_400,
color: select(t.name, {
light: t.palette.white,
dim: t.atoms.text_inverted.color,
dark: t.atoms.text_inverted.color,
}),
})
}
} else if (variant === 'outline') {
} else if (color === 'secondary') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
baseStyles.push(t.atoms.text_contrast_medium)
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
} else if (variant === 'ghost') {
} else if (color === 'secondary_inverted') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
baseStyles.push(t.atoms.text_inverted)
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
}
} else if (color === 'secondary_inverted') {
if (variant === 'solid' || variant === 'gradient') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_50,
})
} else {
baseStyles.push({
color: t.palette.contrast_400,
})
}
} else if (variant === 'outline') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
}
} else if (color === 'negative') {
if (variant === 'solid' || variant === 'gradient') {
} else if (color === 'negative') {
if (!disabled) {
baseStyles.push({color: t.palette.white})
} else {
baseStyles.push({color: t.palette.white, opacity: 0.5})
baseStyles.push({color: t.palette.negative_300})
}
} else if (variant === 'outline') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
}
} else if (color === 'negative_secondary') {
if (variant === 'solid' || variant === 'gradient') {
} else if (color === 'primary_subtle') {
if (!disabled) {
baseStyles.push({
color: select(t.name, {
light: t.palette.negative_500,
dim: t.palette.negative_950,
dark: t.palette.negative_900,
light: t.palette.primary_600,
dim: t.palette.primary_800,
dark: t.palette.primary_800,
}),
})
} else {
baseStyles.push({
color: select(t.name, {
light: t.palette.negative_500,
dim: t.palette.negative_700,
dark: t.palette.negative_700,
light: t.palette.primary_200,
dim: t.palette.primary_200,
dark: t.palette.primary_200,
}),
opacity: 0.5,
})
}
} else if (variant === 'outline') {
} else if (color === 'negative_subtle') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
baseStyles.push({
color: select(t.name, {
light: t.palette.negative_600,
dim: t.palette.negative_800,
dark: t.palette.negative_800,
}),
})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
baseStyles.push({
color: select(t.name, {
light: t.palette.negative_200,
dim: t.palette.negative_200,
dark: t.palette.negative_200,
}),
})
}
}
} else {
if (!disabled) {
baseStyles.push({color: t.palette.white})
} else {
baseStyles.push({color: t.palette.white, opacity: 0.5})
/*
* BEGIN DEPRECATED STYLES
*/
if (color === 'primary') {
if (variant === 'outline') {
if (!disabled) {
baseStyles.push({
color: t.palette.primary_600,
})
} else {
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.primary_600})
} else {
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
}
}
} else if (color === 'secondary') {
if (variant === 'outline') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
}
} else if (color === 'secondary_inverted') {
if (variant === 'outline') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
}
} else if (color === 'negative') {
if (variant === 'outline') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
}
} else if (color === 'negative_subtle') {
if (variant === 'outline') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
}
}
/*
* END DEPRECATED STYLES
*/
}
if (size === 'large') {
baseStyles.push(a.text_md, a.leading_tight)
baseStyles.push(a.text_md, a.leading_snug, a.font_medium)
} else if (size === 'small') {
baseStyles.push(a.text_sm, a.leading_tight)
baseStyles.push(a.text_sm, a.leading_snug, a.font_medium)
} else if (size === 'tiny') {
baseStyles.push(a.text_xs, a.leading_tight)
baseStyles.push(a.text_xs, a.leading_snug, a.font_medium)
}
return StyleSheet.flatten(baseStyles)
@@ -755,7 +774,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
const textStyles = useSharedButtonTextStyles()
return (
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
<Text {...rest} style={[a.text_center, textStyles, style]}>
{children}
</Text>
)
@@ -763,14 +782,16 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
export function ButtonIcon({
icon: Comp,
position,
size,
}: {
icon: React.ComponentType<SVGIconProps>
/**
* @deprecated no longer needed
*/
position?: 'left' | 'right'
size?: SVGIconProps['size']
}) {
const {size: buttonSize, disabled} = useButtonContext()
const {size: buttonSize} = useButtonContext()
const textStyles = useSharedButtonTextStyles()
const {iconSize, iconContainerSize} = React.useMemo(() => {
/**
@@ -779,7 +800,7 @@ export function ButtonIcon({
const iconSizeShorthand =
size ??
(({
large: 'sm',
large: 'md',
small: 'sm',
tiny: 'xs',
}[buttonSize || 'small'] || 'sm') as Exclude<
@@ -794,7 +815,7 @@ export function ButtonIcon({
const iconSize = {
xs: 12,
sm: 16,
md: 20,
md: 18,
lg: 24,
xl: 28,
'2xl': 32,
@@ -805,9 +826,9 @@ export function ButtonIcon({
* don't increase button size
*/
const iconContainerSize = {
large: 18,
small: 16,
tiny: 13,
large: 20,
small: 17,
tiny: 15,
}[buttonSize || 'small']
return {
@@ -823,9 +844,6 @@ export function ButtonIcon({
{
width: iconContainerSize,
height: iconContainerSize,
opacity: disabled ? 0.7 : 1,
marginLeft: position === 'left' ? -2 : 0,
marginRight: position === 'right' ? -2 : 0,
},
]}>
<View
+3
View File
@@ -7,10 +7,13 @@ import {
} from '#/components/ContextMenu/types'
export const Context = React.createContext<ContextType | null>(null)
Context.displayName = 'ContextMenuContext'
export const MenuContext = React.createContext<MenuContextType | null>(null)
MenuContext.displayName = 'ContextMenuMenuContext'
export const ItemContext = React.createContext<ItemContextType | null>(null)
ItemContext.displayName = 'ContextMenuItemContext'
export function useContextMenuContext() {
const context = React.useContext(Context)
+1
View File
@@ -23,6 +23,7 @@ export const Context = createContext<DialogContextProps>({
setDisableDrag: () => {},
isWithinDialog: false,
})
Context.displayName = 'DialogContext'
export function useDialogContext() {
return useContext(Context)
+126 -77
View File
@@ -25,18 +25,17 @@ import {
type ViewStyleProp,
web,
} from '#/alf'
import {Button} from '#/components/Button'
import {Button, ButtonText} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard'
import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import {InlineLinkText} from '#/components/Link'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
import {ProgressGuideList} from './ProgressGuide/List'
const MOBILE_CARD_WIDTH = 300
const MOBILE_CARD_WIDTH = 165
function CardOuter({
children,
@@ -48,8 +47,8 @@ function CardOuter({
<View
style={[
a.w_full,
a.p_lg,
a.rounded_md,
a.p_md,
a.rounded_lg,
a.border,
t.atoms.bg,
t.atoms.border_contrast_low,
@@ -65,14 +64,30 @@ function CardOuter({
export function SuggestedFollowPlaceholder() {
const t = useTheme()
return (
<CardOuter style={[a.gap_md, t.atoms.border_contrast_low]}>
<ProfileCard.Header>
<ProfileCard.AvatarPlaceholder />
<ProfileCard.NameAndHandlePlaceholder />
</ProfileCard.Header>
<ProfileCard.DescriptionPlaceholder numberOfLines={2} />
return (
<CardOuter
style={[a.gap_md, t.atoms.border_contrast_low, t.atoms.shadow_sm]}>
<ProfileCard.Outer>
<View
style={[a.flex_col, a.align_center, a.gap_sm, a.pb_sm, a.mb_auto]}>
<ProfileCard.AvatarPlaceholder size={88} />
<ProfileCard.NamePlaceholder />
<View style={[a.w_full]}>
<ProfileCard.DescriptionPlaceholder numberOfLines={2} />
</View>
</View>
<Button
label=""
size="small"
variant="solid"
color="secondary"
disabled
style={[a.w_full, a.rounded_sm]}>
<ButtonText>Follow</ButtonText>
</Button>
</ProfileCard.Outer>
</CardOuter>
)
}
@@ -243,10 +258,9 @@ export function ProfileGrid({
const t = useTheme()
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const navigation = useNavigation<NavigationProp>()
const {gtMobile} = useBreakpoints()
const isLoading = isSuggestionsLoading || !moderationOpts
const maxLength = gtMobile ? 4 : 6
const maxLength = gtMobile ? 3 : 6
const content = isLoading ? (
Array(maxLength)
@@ -254,7 +268,14 @@ export function ProfileGrid({
.map((_, i) => (
<View
key={i}
style={[gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}])]}>
style={[
gtMobile &&
web([
a.flex_0,
a.flex_grow,
{width: `calc(30% - ${a.gap_md.gap / 2}px)`},
]),
]}>
<SuggestedFollowPlaceholder />
</View>
))
@@ -276,44 +297,69 @@ export function ProfileGrid({
}}
style={[
a.flex_1,
gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}]),
gtMobile &&
web([
a.flex_0,
a.flex_grow,
{width: `calc(30% - ${a.gap_md.gap / 2}px)`},
]),
]}>
{({hovered, pressed}) => (
<CardOuter
style={[
a.flex_1,
t.atoms.shadow_sm,
(hovered || pressed) && t.atoms.border_contrast_high,
]}>
<ProfileCard.Outer>
<ProfileCard.Header>
<View
style={[
a.flex_col,
a.align_center,
a.gap_sm,
a.pb_sm,
a.mb_auto,
]}>
<ProfileCard.Avatar
profile={profile}
moderationOpts={moderationOpts}
size={88}
/>
<ProfileCard.NameAndHandle
profile={profile}
moderationOpts={moderationOpts}
/>
<ProfileCard.FollowButton
profile={profile}
moderationOpts={moderationOpts}
logContext="FeedInterstitial"
shape="round"
colorInverted
onFollow={() => {
logEvent('suggestedUser:follow', {
logContext:
viewContext === 'feed'
? 'InterstitialDiscover'
: 'InterstitialProfile',
location: 'Card',
recId,
position: index,
})
}}
/>
</ProfileCard.Header>
<ProfileCard.Description profile={profile} numberOfLines={2} />
<View style={[a.flex_col, a.align_center, a.max_w_full]}>
<ProfileCard.Name
profile={profile}
moderationOpts={moderationOpts}
/>
<ProfileCard.Description
profile={profile}
numberOfLines={2}
style={[
t.atoms.text_contrast_medium,
a.text_center,
a.text_xs,
]}
/>
</View>
</View>
<ProfileCard.FollowButton
profile={profile}
moderationOpts={moderationOpts}
logContext="FeedInterstitial"
withIcon={false}
style={[a.rounded_sm]}
onFollow={() => {
logEvent('suggestedUser:follow', {
logContext:
viewContext === 'feed'
? 'InterstitialDiscover'
: 'InterstitialProfile',
location: 'Card',
recId,
position: index,
})
}}
/>
</ProfileCard.Outer>
</CardOuter>
)}
@@ -332,37 +378,31 @@ export function ProfileGrid({
style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}>
<View
style={[
a.p_lg,
a.pb_xs,
a.px_lg,
a.pt_md,
a.flex_row,
a.align_center,
a.justify_between,
]}>
<Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}>
<Text style={[a.text_sm, a.font_bold, t.atoms.text]}>
{viewContext === 'profile' ? (
<Trans>Similar accounts</Trans>
) : (
<Trans>Suggested for you</Trans>
)}
</Text>
<Person fill={t.atoms.text_contrast_low.color} size="sm" />
<InlineLinkText
label={_(msg`See more suggested profiles on the Explore page`)}
to="/search">
<Trans>See more</Trans>
</InlineLinkText>
</View>
{gtMobile ? (
<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]}>
<View style={[a.p_lg, a.pt_md]}>
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}>
{content}
</View>
<View style={[a.flex_row, a.justify_end, a.align_center, a.gap_md]}>
<InlineLinkText
label={_(msg`Browse more suggestions`)}
to="/search"
style={[t.atoms.text_contrast_medium]}>
<Trans>Browse more suggestions</Trans>
</InlineLinkText>
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
</View>
</View>
) : (
<BlockDrawerGesture>
@@ -372,28 +412,10 @@ export function ProfileGrid({
showsHorizontalScrollIndicator={false}
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
decelerationRate="fast">
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
<View style={[a.p_lg, a.pt_md, a.flex_row, a.gap_md]}>
{content}
<Button
label={_(msg`Browse more accounts on the Explore page`)}
onPress={() => {
navigation.navigate('SearchTab')
}}>
<CardOuter style={[a.flex_1, {borderWidth: 0}]}>
<View style={[a.flex_1, a.justify_center]}>
<View style={[a.flex_row, a.px_lg]}>
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
<Trans>
Browse more suggestions on the Explore page
</Trans>
</Text>
<Arrow size="xl" />
</View>
</View>
</CardOuter>
</Button>
<SeeMoreSuggestedProfilesCard />
</View>
</ScrollView>
</View>
@@ -403,6 +425,33 @@ export function ProfileGrid({
)
}
function SeeMoreSuggestedProfilesCard() {
const navigation = useNavigation<NavigationProp>()
const t = useTheme()
const {_} = useLingui()
return (
<Button
label={_(msg`Browse more accounts on the Explore page`)}
style={[a.flex_col]}
onPress={() => {
navigation.navigate('SearchTab')
}}>
<CardOuter style={[a.flex_1, t.atoms.shadow_sm]}>
<View style={[a.flex_1, a.justify_center]}>
<View style={[a.flex_col, a.align_center, a.gap_md]}>
<Text style={[a.leading_snug, a.text_center]}>
<Trans>See more accounts you might like</Trans>
</Text>
<Arrow size="xl" />
</View>
</View>
</CardOuter>
</Button>
)
}
export function SuggestedFeeds() {
const numFeedsToDisplay = 3
const t = useTheme()
+144
View File
@@ -0,0 +1,144 @@
import {
Children,
cloneElement,
isValidElement,
type ReactElement,
type ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react'
import {
AccessibilityInfo,
findNodeHandle,
Pressable,
Text,
View,
} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useA11y} from '#/state/a11y'
/**
* Conditionally wraps children in a `FocusTrap` component based on whether
* screen reader support is enabled. THIS SHOULD BE USED SPARINGLY, only when
* no better option is available.
*/
export function FocusScope({children}: {children: ReactNode}) {
const {screenReaderEnabled} = useA11y()
return screenReaderEnabled ? <FocusTrap>{children}</FocusTrap> : children
}
/**
* `FocusTrap` is intended as a last-ditch effort to ensure that users keep
* focus within a certain section of the app, like an overlay.
*
* It works by placing "guards" at the start and end of the active content.
* Then when the user reaches either of those guards, it will announce that
* they have reached the start or end of the content and tell them how to
* remain within the active content section.
*/
function FocusTrap({children}: {children: ReactNode}) {
const {_} = useLingui()
const child = useRef<View>(null)
/*
* Here we add a ref to the first child of this component. This currently
* overrides any ref already on that first child, so we throw an error here
* to prevent us from ever accidentally doing this.
*/
const decoratedChildren = useMemo(() => {
return Children.toArray(children).map((node, i) => {
if (i === 0 && isValidElement(node)) {
const n = node as ReactElement<any>
if (n.props.ref !== undefined) {
throw new Error(
'FocusScope needs to override the ref on its first child.',
)
}
return cloneElement(n, {
...n.props,
ref: child,
})
}
return node
})
}, [children])
const focusNode = useCallback((ref: View | null) => {
if (!ref) return
const node = findNodeHandle(ref)
if (node) {
AccessibilityInfo.setAccessibilityFocus(node)
}
}, [])
useEffect(() => {
setTimeout(() => {
focusNode(child.current)
}, 1e3)
}, [focusNode])
return (
<>
<Pressable
accessible
accessibilityLabel={_(
msg`You've reached the start of the active content.`,
)}
accessibilityHint={_(
msg`Please go back, or activate this element to return to the start of the active content.`,
)}
accessibilityActions={[{name: 'activate', label: 'activate'}]}
onAccessibilityAction={event => {
switch (event.nativeEvent.actionName) {
case 'activate': {
focusNode(child.current)
}
}
}}>
<Noop />
</Pressable>
<View
/**
* This property traps focus effectively on iOS, but not on Android.
*/
accessibilityViewIsModal>
{decoratedChildren}
</View>
<Pressable
accessibilityLabel={_(
msg`You've reached the end of the active content.`,
)}
accessibilityHint={_(
msg`Please go back, or activate this element to return to the start of the active content.`,
)}
accessibilityActions={[{name: 'activate', label: 'activate'}]}
onAccessibilityAction={event => {
switch (event.nativeEvent.actionName) {
case 'activate': {
focusNode(child.current)
}
}
}}>
<Noop />
</Pressable>
</>
)
}
function Noop() {
return (
<Text
accessible={false}
style={{
height: 1,
opacity: 0,
}}>
{' '}
</Text>
)
}
+15
View File
@@ -0,0 +1,15 @@
import {type ReactNode} from 'react'
import {FocusScope as RadixFocusScope} from 'radix-ui/internal'
/*
* The web version of the FocusScope component is a proper implementation, we
* use this in Dialogs and such already. It's here as a convenient counterpart
* to the hacky native solution.
*/
export function FocusScope({children}: {children: ReactNode}) {
return (
<RadixFocusScope.FocusScope loop asChild trapped>
{children}
</RadixFocusScope.FocusScope>
)
}
+2 -1
View File
@@ -1,11 +1,12 @@
import {createContext, useContext, useMemo} from 'react'
import {View} from 'react-native'
import {atoms as a, ViewStyleProp} from '#/alf'
import {atoms as a, type ViewStyleProp} from '#/alf'
const Context = createContext({
gap: 0,
})
Context.displayName = 'GridContext'
export function Row({
children,
+1
View File
@@ -77,6 +77,7 @@ export function Outer({
}
const AlignmentContext = createContext<'platform' | 'left'>('platform')
AlignmentContext.displayName = 'AlignmentContext'
export function Content({
children,
+1
View File
@@ -3,3 +3,4 @@ import React from 'react'
export const ScrollbarOffsetContext = React.createContext({
isWithinOffsetView: false,
})
ScrollbarOffsetContext.displayName = 'ScrollbarOffsetContext'
+4 -3
View File
@@ -1,7 +1,8 @@
import React, {memo} from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {memo} from 'react'
import {type StyleProp, View, type ViewStyle} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import type React from 'react'
import {cleanError} from '#/lib/strings/errors'
import {CenteredView} from '#/view/com/util/Views'
@@ -95,7 +96,7 @@ function ListFooterMaybeError({
)}
</Text>
<Button
variant="gradient"
variant="solid"
label={_(msg`Press to retry`)}
style={[
a.align_center,
+3
View File
@@ -0,0 +1,3 @@
export function LockScroll() {
return null
}
+3
View File
@@ -0,0 +1,3 @@
import {RemoveScrollBar} from 'react-remove-scroll-bar'
export const LockScroll = RemoveScrollBar
+2
View File
@@ -3,8 +3,10 @@ import React from 'react'
import {type ContextType, type ItemContextType} from '#/components/Menu/types'
export const Context = React.createContext<ContextType | null>(null)
Context.displayName = 'MenuContext'
export const ItemContext = React.createContext<ItemContextType | null>(null)
ItemContext.displayName = 'MenuItemContext'
export function useMenuContext() {
const context = React.useContext(Context)
@@ -0,0 +1,38 @@
import {View} from 'react-native'
import {Trans} from '@lingui/macro'
import {Logo} from '#/view/icons/Logo'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
export function Badge() {
const t = useTheme()
return (
<View style={[a.align_start]}>
<View
style={[
a.pl_md,
a.pr_lg,
a.py_sm,
a.rounded_full,
a.flex_row,
a.align_center,
a.gap_xs,
{
backgroundColor: t.palette.primary_25,
},
]}>
<Logo fill={t.palette.primary_600} width={14} />
<Text
style={[
a.font_bold,
{
color: t.palette.primary_600,
},
]}>
<Trans>Announcement</Trans>
</Text>
</View>
</View>
)
}
@@ -0,0 +1,139 @@
import {type ReactNode} from 'react'
import {ScrollView, View} from 'react-native'
import {
useSafeAreaFrame,
useSafeAreaInsets,
} from 'react-native-safe-area-context'
import {LinearGradient} from 'expo-linear-gradient'
import {isAndroid, isNative} from '#/platform/detection'
import {useA11y} from '#/state/a11y'
import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
import {transparentifyColor} from '#/alf/util/colorGeneration'
import {FocusScope} from '#/components/FocusScope'
import {LockScroll} from '#/components/LockScroll'
const GUTTER = 24
export function Overlay({
children,
label,
}: {
children: ReactNode
label: string
}) {
const t = useTheme()
const {gtPhone} = useBreakpoints()
const {reduceMotionEnabled} = useA11y()
const insets = useSafeAreaInsets()
const frame = useSafeAreaFrame()
return (
<>
<LockScroll />
<View style={[a.fixed, a.inset_0, !reduceMotionEnabled && a.fade_in]}>
{gtPhone ? (
<View style={[a.absolute, a.inset_0, {opacity: 0.8}]}>
<View
style={[
a.fixed,
a.inset_0,
{backgroundColor: t.palette.black},
!reduceMotionEnabled && a.fade_in,
]}
/>
</View>
) : (
<LinearGradient
colors={[
transparentifyColor(t.atoms.bg.backgroundColor, 0),
t.atoms.bg.backgroundColor,
t.atoms.bg.backgroundColor,
]}
start={[0.5, 0]}
end={[0.5, 1]}
style={[a.absolute, a.inset_0]}
/>
)}
</View>
<ScrollView
showsVerticalScrollIndicator={false}
style={[
a.z_10,
gtPhone &&
web({
paddingHorizontal: GUTTER,
paddingVertical: '10vh',
}),
]}
contentContainerStyle={[a.align_center]}>
{/**
* This is needed to prevent centered dialogs from overflowing
* above the screen, and provides a "natural" centering so that
* stacked dialogs appear relatively aligned.
*/}
<View
style={[
a.w_full,
a.z_20,
a.align_center,
!gtPhone && [a.justify_end, {minHeight: frame.height}],
isNative && [
{
paddingBottom: Math.max(insets.bottom, a.p_2xl.padding),
},
],
]}>
{!gtPhone && (
<View
style={[
a.flex_1,
a.w_full,
{
minHeight: Math.max(insets.top, a.p_2xl.padding),
},
]}>
<LinearGradient
colors={[
transparentifyColor(t.atoms.bg.backgroundColor, 0),
t.atoms.bg.backgroundColor,
]}
start={[0.5, 0]}
end={[0.5, 1]}
style={[a.absolute, a.inset_0]}
/>
</View>
)}
<FocusScope>
<View
accessible={isAndroid}
role="dialog"
aria-role="dialog"
aria-label={label}
style={flatten([
a.relative,
a.w_full,
a.p_2xl,
t.atoms.bg,
!reduceMotionEnabled && a.zoom_fade_in,
gtPhone && [
a.rounded_md,
a.border,
t.atoms.shadow_lg,
t.atoms.border_contrast_low,
web({
maxWidth: 420,
}),
],
])}>
{children}
</View>
</FocusScope>
</View>
</ScrollView>
</>
)
}
@@ -0,0 +1,7 @@
import {createPortalGroup} from '#/components/Portal'
const portalGroup = createPortalGroup()
export const Provider = portalGroup.Provider
export const Portal = portalGroup.Portal
export const Outlet = portalGroup.Outlet
@@ -0,0 +1,195 @@
import {describe, test} from '@jest/globals'
import {
computeCompletedState,
syncCompletedState,
} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
jest.mock('../../../state/queries/nuxs')
describe('computeCompletedState', () => {
test(`initial state`, () => {
const completed = computeCompletedState({
nuxIsReady: false,
nuxIsCompleted: false,
nuxIsOptimisticallyCompleted: false,
completedForDevice: undefined,
})
expect(completed).toBe(true)
})
test(`nux loaded state`, () => {
const completed = computeCompletedState({
nuxIsReady: true,
nuxIsCompleted: false,
nuxIsOptimisticallyCompleted: false,
completedForDevice: undefined,
})
expect(completed).toBe(false)
})
test(`nux saving state`, () => {
const completed = computeCompletedState({
nuxIsReady: true,
nuxIsCompleted: false,
nuxIsOptimisticallyCompleted: true,
completedForDevice: undefined,
})
expect(completed).toBe(true)
})
test(`nux is completed`, () => {
const completed = computeCompletedState({
nuxIsReady: true,
nuxIsCompleted: true,
nuxIsOptimisticallyCompleted: false,
completedForDevice: undefined,
})
expect(completed).toBe(true)
})
test(`initial state, but already completed for device`, () => {
const completed = computeCompletedState({
nuxIsReady: false,
nuxIsCompleted: false,
nuxIsOptimisticallyCompleted: false,
completedForDevice: true,
})
expect(completed).toBe(true)
})
})
describe('syncCompletedState', () => {
describe('!nuxIsReady', () => {
test(`!completedForDevice, no-op`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
syncCompletedState({
nuxIsReady: false,
nuxIsCompleted: false,
nuxIsOptimisticallyCompleted: false,
completedForDevice: false,
save,
setCompletedForDevice,
})
expect(save).not.toHaveBeenCalled()
expect(setCompletedForDevice).not.toHaveBeenCalled()
})
test(`completedForDevice, no-op`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
syncCompletedState({
nuxIsReady: false,
nuxIsCompleted: false,
nuxIsOptimisticallyCompleted: false,
completedForDevice: true,
save,
setCompletedForDevice,
})
expect(save).not.toHaveBeenCalled()
expect(setCompletedForDevice).not.toHaveBeenCalled()
})
})
describe('nuxIsReady', () => {
describe(`!nuxIsCompleted`, () => {
describe(`!nuxIsOptimisticallyCompleted`, () => {
test(`!completedForDevice, no-op`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
syncCompletedState({
nuxIsReady: true,
nuxIsCompleted: false,
nuxIsOptimisticallyCompleted: false,
completedForDevice: false,
save,
setCompletedForDevice,
})
expect(save).not.toHaveBeenCalled()
expect(setCompletedForDevice).not.toHaveBeenCalled()
})
test(`completedForDevice, syncs to server`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
syncCompletedState({
nuxIsReady: true,
nuxIsCompleted: false,
nuxIsOptimisticallyCompleted: false,
completedForDevice: true,
save,
setCompletedForDevice,
})
expect(save).toHaveBeenCalled()
expect(setCompletedForDevice).not.toHaveBeenCalled()
})
})
/**
* Catches the case where we already called `save` to sync device state
* to server, thus `nuxIsOptimisticallyCompleted` is true.
*/
describe(`nuxIsOptimisticallyCompleted`, () => {
test(`completedForDevice, no-op`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
syncCompletedState({
nuxIsReady: true,
nuxIsCompleted: false,
nuxIsOptimisticallyCompleted: true,
completedForDevice: true,
save,
setCompletedForDevice,
})
expect(save).not.toHaveBeenCalled()
expect(setCompletedForDevice).not.toHaveBeenCalled()
})
})
})
describe(`nuxIsCompleted`, () => {
test(`!completedForDevice, syncs to device`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
syncCompletedState({
nuxIsReady: true,
nuxIsCompleted: true,
nuxIsOptimisticallyCompleted: false,
completedForDevice: false,
save,
setCompletedForDevice,
})
expect(save).not.toHaveBeenCalled()
expect(setCompletedForDevice).toHaveBeenCalled()
})
test(`completedForDevice, no-op`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
syncCompletedState({
nuxIsReady: true,
nuxIsCompleted: true,
nuxIsOptimisticallyCompleted: false,
completedForDevice: true,
save,
setCompletedForDevice,
})
expect(save).not.toHaveBeenCalled()
expect(setCompletedForDevice).not.toHaveBeenCalled()
})
})
})
})
@@ -0,0 +1,7 @@
import {ID} from '#/components/PolicyUpdateOverlay/updates/202508/config'
/**
* The singulary active update ID. This is configured here to ensure that
* the relationship is clear.
*/
export const ACTIVE_UPDATE_ID = ID
@@ -0,0 +1,68 @@
import {
createContext,
type ReactNode,
useContext,
useMemo,
useState,
} from 'react'
import {useSession} from '#/state/session'
import {Provider as PortalProvider} from '#/components/PolicyUpdateOverlay/Portal'
import {
type PolicyUpdateState,
usePolicyUpdateState,
} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
const Context = createContext<{
state: PolicyUpdateState
setIsReadyToShowOverlay: () => void
}>({
state: {
completed: true,
complete: () => {},
},
/**
* Although our data will be ready to go when the app shell mounts, we don't
* want to show the overlay until we actually render it, which happens after
* sigin/signup/onboarding in `createNativeStackNavigatorWithAuth`.
*/
setIsReadyToShowOverlay: () => {},
})
Context.displayName = 'PolicyUpdateOverlayContext'
export function usePolicyUpdateContext() {
const context = useContext(Context)
if (!context) {
throw new Error(
'usePolicyUpdateContext must be used within a PolicyUpdateProvider',
)
}
return context
}
export function Provider({children}: {children?: ReactNode}) {
const {hasSession} = useSession()
const [isReadyToShowOverlay, setIsReadyToShowOverlay] = useState(false)
const state = usePolicyUpdateState({
// only enable the policy update overlay in non-test environments
enabled:
isReadyToShowOverlay && hasSession && process.env.NODE_ENV !== 'test',
})
const ctx = useMemo(
() => ({
state,
setIsReadyToShowOverlay() {
if (isReadyToShowOverlay) return
setIsReadyToShowOverlay(true)
},
}),
[state, isReadyToShowOverlay, setIsReadyToShowOverlay],
)
return (
<PortalProvider>
<Context.Provider value={ctx}>{children}</Context.Provider>
</PortalProvider>
)
}
@@ -0,0 +1,49 @@
import {useEffect} from 'react'
import {View} from 'react-native'
import {isIOS} from '#/platform/detection'
import {atoms as a} from '#/alf'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
import {Portal} from '#/components/PolicyUpdateOverlay/Portal'
import {Content} from '#/components/PolicyUpdateOverlay/updates/202508'
export {Provider} from '#/components/PolicyUpdateOverlay/context'
export {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
export {Outlet} from '#/components/PolicyUpdateOverlay/Portal'
export function PolicyUpdateOverlay() {
const {state, setIsReadyToShowOverlay} = usePolicyUpdateContext()
useEffect(() => {
/**
* Tell the context that we are ready to show the overlay.
*/
setIsReadyToShowOverlay()
}, [setIsReadyToShowOverlay])
/*
* See `window.clearNux` example in `/state/queries/nuxs` for a way to clear
* NUX state for local testing and debugging.
*/
if (state.completed) return null
return (
<Portal>
<FullWindowOverlay>
<View
style={[
a.fixed,
a.inset_0,
// setting a zIndex when using FullWindowOverlay on iOS
// means the taps pass straight through to the underlying content (???)
// so don't set it on iOS. FullWindowOverlay already does the job.
!isIOS && {zIndex: 9999},
]}>
<Content state={state} />
</View>
</FullWindowOverlay>
</Portal>
)
}
@@ -0,0 +1,3 @@
import {Logger} from '#/logger'
export const logger = Logger.create(Logger.Context.PolicyUpdate)
@@ -0,0 +1,7 @@
/*
* Keep this file separate to avoid import issues.
*/
import {Nux} from '#/state/queries/nuxs'
export const ID = Nux.PolicyUpdate202508
@@ -0,0 +1,190 @@
import {useCallback} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isAndroid} from '#/platform/detection'
import {useA11y} from '#/state/a11y'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {InlineLinkText, Link} from '#/components/Link'
import {Badge} from '#/components/PolicyUpdateOverlay/Badge'
import {Overlay} from '#/components/PolicyUpdateOverlay/Overlay'
import {type PolicyUpdateState} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
import {Text} from '#/components/Typography'
export function Content({state}: {state: PolicyUpdateState}) {
const t = useTheme()
const {_} = useLingui()
const {screenReaderEnabled} = useA11y()
const handleClose = useCallback(() => {
state.complete()
}, [state])
const linkStyle = [a.text_md]
const links = {
terms: {
overridePresentation: false,
to: `https://bsky.social/about/support/tos`,
label: _(msg`Terms of Service`),
},
privacy: {
overridePresentation: false,
to: `https://bsky.social/about/support/privacy-policy`,
label: _(msg`Privacy Policy`),
},
copyright: {
overridePresentation: false,
to: `https://bsky.social/about/support/copyright`,
label: _(msg`Copyright Policy`),
},
guidelines: {
overridePresentation: false,
to: `https://bsky.social/about/support/community-guidelines`,
label: _(msg`Community Guidelines`),
},
blog: {
overridePresentation: false,
to: `https://bsky.social/about/blog/08-14-2025-updated-terms-and-policies`,
label: _(msg`Our blog post`),
},
}
const linkButtonStyles = {
overridePresentation: false,
color: 'secondary',
size: 'small',
} as const
const label = isAndroid
? _(
msg`Were updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 15th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 15th, 2025. Learn more about these changes and how to share your thoughts with us by reading our blog post.`,
)
: _(msg`We're updating our policies`)
return (
<Overlay label={label}>
<View style={[a.align_start, a.gap_xl]}>
<Badge />
{screenReaderEnabled ? (
<View style={[a.gap_sm]}>
<Text emoji style={[a.text_2xl, a.font_bold, a.leading_snug]}>
<Trans>Hey there 👋</Trans>
</Text>
<Text style={[a.leading_snug, a.text_md]}>
<Trans>
Were updating our Terms of Service, Privacy Policy, and
Copyright Policy, effective September 15th, 2025.
</Trans>
</Text>
<Text style={[a.leading_snug, a.text_md]}>
<Trans>
We're also updating our Community Guidelines, and we want your
input! These new guidelines will take effect on October 15th,
2025.
</Trans>
</Text>
<Text style={[a.leading_snug, a.text_md]}>
<Trans>
Learn more about these changes and how to share your thoughts
with us by reading our blog post.
</Trans>
</Text>
<Link {...links.terms} {...linkButtonStyles}>
<ButtonText>
<Trans>Terms of Service</Trans>
</ButtonText>
</Link>
<Link {...links.privacy} {...linkButtonStyles}>
<ButtonText>
<Trans>Privacy Policy</Trans>
</ButtonText>
</Link>
<Link {...links.copyright} {...linkButtonStyles}>
<ButtonText>
<Trans>Copyright Policy</Trans>
</ButtonText>
</Link>
<Link {...links.blog} {...linkButtonStyles}>
<ButtonText>
<Trans>Read our blog post</Trans>
</ButtonText>
</Link>
</View>
) : (
<View style={[a.gap_sm]}>
<Text emoji style={[a.text_2xl, a.font_bold, a.leading_snug]}>
<Trans>Hey there 👋</Trans>
</Text>
<Text style={[a.leading_snug, a.text_md]}>
<Trans>
Were updating our{' '}
<InlineLinkText {...links.terms} style={linkStyle}>
Terms of Service
</InlineLinkText>
,{' '}
<InlineLinkText {...links.privacy} style={linkStyle}>
Privacy Policy
</InlineLinkText>
, and{' '}
<InlineLinkText {...links.copyright} style={linkStyle}>
Copyright Policy
</InlineLinkText>
, effective September 15th, 2025.
</Trans>
</Text>
<Text style={[a.leading_snug, a.text_md]}>
<Trans>
We're also updating our{' '}
<InlineLinkText {...links.guidelines} style={linkStyle}>
Community Guidelines
</InlineLinkText>
, and we want your input! These new guidelines will take effect
on October 15th, 2025.
</Trans>
</Text>
<Text style={[a.leading_snug, a.text_md]}>
<Trans>
Learn more about these changes and how to share your thoughts
with us by{' '}
<InlineLinkText {...links.blog} style={linkStyle}>
reading our blog post.
</InlineLinkText>
</Trans>
</Text>
</View>
)}
<View style={[a.w_full, a.gap_md]}>
<Button
label={_(msg`Continue`)}
accessibilityHint={_(
msg`Tap to acknowledge that you understand and agree to these updates and continue using Bluesky`,
)}
color="primary"
size="large"
onPress={handleClose}>
<ButtonText>
<Trans>Continue</Trans>
</ButtonText>
</Button>
<Text
style={[
a.leading_snug,
a.text_sm,
a.italic,
t.atoms.text_contrast_medium,
]}>
<Trans>
By clicking "Continue" you acknowledge that you understand and
agree to these updates.
</Trans>
</Text>
</View>
</View>
</Overlay>
)
}
@@ -0,0 +1,155 @@
import {useMemo} from 'react'
import {useNux, useSaveNux} from '#/state/queries/nuxs'
import {ACTIVE_UPDATE_ID} from '#/components/PolicyUpdateOverlay/config'
import {logger} from '#/components/PolicyUpdateOverlay/logger'
import {IS_DEV} from '#/env'
import {device, useStorage} from '#/storage'
export type PolicyUpdateState = {
completed: boolean
complete: () => void
}
export function usePolicyUpdateState({
enabled,
}: {
/**
* Used to skip the policy update overlay until we're actually ready to
* show it.
*/
enabled: boolean
}) {
const nux = useNux(ACTIVE_UPDATE_ID)
const {mutate: save, variables} = useSaveNux()
const deviceStorage = useStorage(device, [ACTIVE_UPDATE_ID])
const debugOverride =
!!useStorage(device, ['policyUpdateDebugOverride'])[0] && IS_DEV
return useMemo(() => {
/**
* If not enabled, then just return a completed state so the app functions
* as normal.
*/
if (!enabled) {
return {
completed: true,
complete() {},
}
}
const nuxIsReady = nux.status === 'ready'
const nuxIsCompleted = nux.nux?.completed === true
const nuxIsOptimisticallyCompleted = !!variables?.completed
const [completedForDevice, setCompletedForDevice] = deviceStorage
const completed = computeCompletedState({
nuxIsReady,
nuxIsCompleted,
nuxIsOptimisticallyCompleted,
completedForDevice,
})
logger.debug(`state`, {
completed,
nux,
completedForDevice,
})
if (!debugOverride) {
syncCompletedState({
nuxIsReady,
nuxIsCompleted,
nuxIsOptimisticallyCompleted,
completedForDevice,
save,
setCompletedForDevice,
})
}
return {
completed,
complete() {
logger.debug(`user completed`)
save({
id: ACTIVE_UPDATE_ID,
completed: true,
data: undefined,
})
setCompletedForDevice(true)
},
}
}, [enabled, nux, save, variables, deviceStorage, debugOverride])
}
export function computeCompletedState({
nuxIsReady,
nuxIsCompleted,
nuxIsOptimisticallyCompleted,
completedForDevice,
}: {
nuxIsReady: boolean
nuxIsCompleted: boolean
nuxIsOptimisticallyCompleted: boolean
completedForDevice: boolean | undefined
}): boolean {
/**
* Assume completed to prevent flash
*/
let completed = true
/**
* Prefer server state, if available
*/
if (nuxIsReady) {
completed = nuxIsCompleted
}
/**
* Override with optimistic state or device state
*/
if (nuxIsOptimisticallyCompleted || !!completedForDevice) {
completed = true
}
return completed
}
export function syncCompletedState({
nuxIsReady,
nuxIsCompleted,
nuxIsOptimisticallyCompleted,
completedForDevice,
save,
setCompletedForDevice,
}: {
nuxIsReady: boolean
nuxIsCompleted: boolean
nuxIsOptimisticallyCompleted: boolean
completedForDevice: boolean | undefined
save: ReturnType<typeof useSaveNux>['mutate']
setCompletedForDevice: (value: boolean) => void
}) {
/*
* Sync device state to server state for this account
*/
if (
nuxIsReady &&
!nuxIsCompleted &&
!nuxIsOptimisticallyCompleted &&
!!completedForDevice
) {
logger.debug(`syncing device state to server state`)
save({
id: ACTIVE_UPDATE_ID,
completed: true,
data: undefined,
})
} else if (nuxIsReady && nuxIsCompleted && !completedForDevice) {
logger.debug(`syncing server state to device state`)
/*
* Sync server state to device state
*/
setCompletedForDevice(true)
}
}
@@ -0,0 +1,21 @@
import {useCallback} from 'react'
import {ACTIVE_UPDATE_ID} from '#/components/PolicyUpdateOverlay/config'
import {logger} from '#/components/PolicyUpdateOverlay/logger'
import {device, useStorage} from '#/storage'
/*
* Marks the active policy update as completed in device storage.
* `usePolicyUpdateState` will react to this and replicate this status in the
* server NUX state for this account.
*/
export function usePreemptivelyCompleteActivePolicyUpdate() {
const [_completedForDevice, setCompletedForDevice] = useStorage(device, [
ACTIVE_UPDATE_ID,
])
return useCallback(() => {
logger.debug(`preemptively completing active policy update`)
setCompletedForDevice(true)
}, [setCompletedForDevice])
}
+1
View File
@@ -28,6 +28,7 @@ export function createPortalGroup() {
append: () => {},
remove: () => {},
})
Context.displayName = 'PortalContext'
function Provider(props: React.PropsWithChildren<{}>) {
const map = useRef<ComponentMap>({})
@@ -15,6 +15,7 @@ const Context = React.createContext<{
setActiveView: (viewId: string) => void
sendViewPosition: (viewId: string, y: number) => void
} | null>(null)
Context.displayName = 'ActiveVideoWebContext'
export function Provider({children}: {children: React.ReactNode}) {
if (!isWeb) {
@@ -1,4 +1,4 @@
import React, {useRef} from 'react'
import {useImperativeHandle, useRef, useState} from 'react'
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
import {type AppBskyEmbedVideo} from '@atproto/api'
import {BlueskyVideoView} from '@haileyok/bluesky-video'
@@ -17,91 +17,88 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import {TimeIndicator} from './TimeIndicator'
export const VideoEmbedInnerNative = React.forwardRef(
function VideoEmbedInnerNative(
{
embed,
setStatus,
setIsLoading,
setIsActive,
}: {
embed: AppBskyEmbedVideo.View
setStatus: (status: 'playing' | 'paused') => void
setIsLoading: (isLoading: boolean) => void
setIsActive: (isActive: boolean) => void
export function VideoEmbedInnerNative({
ref,
embed,
setStatus,
setIsLoading,
setIsActive,
}: {
ref: React.Ref<{togglePlayback: () => void}>
embed: AppBskyEmbedVideo.View
setStatus: (status: 'playing' | 'paused') => void
setIsLoading: (isLoading: boolean) => void
setIsActive: (isActive: boolean) => void
}) {
const {_} = useLingui()
const videoRef = useRef<BlueskyVideoView>(null)
const autoplayDisabled = useAutoplayDisabled()
const isWithinMessage = useIsWithinMessage()
const [muted, setMuted] = useVideoMuteState()
const [isPlaying, setIsPlaying] = useState(false)
const [timeRemaining, setTimeRemaining] = useState(0)
const [error, setError] = useState<string>()
useImperativeHandle(ref, () => ({
togglePlayback: () => {
videoRef.current?.togglePlayback()
},
ref: React.Ref<{togglePlayback: () => void}>,
) {
const {_} = useLingui()
const videoRef = useRef<BlueskyVideoView>(null)
const autoplayDisabled = useAutoplayDisabled()
const isWithinMessage = useIsWithinMessage()
const [muted, setMuted] = useVideoMuteState()
}))
const [isPlaying, setIsPlaying] = React.useState(false)
const [timeRemaining, setTimeRemaining] = React.useState(0)
const [error, setError] = React.useState<string>()
if (error) {
throw new Error(error)
}
React.useImperativeHandle(ref, () => ({
togglePlayback: () => {
videoRef.current?.togglePlayback()
},
}))
if (error) {
throw new Error(error)
}
return (
<View style={[a.flex_1, a.relative]}>
<BlueskyVideoView
url={embed.playlist}
autoplay={!autoplayDisabled && !isWithinMessage}
beginMuted={autoplayDisabled ? false : muted}
style={[a.rounded_sm]}
onActiveChange={e => {
setIsActive(e.nativeEvent.isActive)
}}
onLoadingChange={e => {
setIsLoading(e.nativeEvent.isLoading)
}}
onMutedChange={e => {
setMuted(e.nativeEvent.isMuted)
}}
onStatusChange={e => {
setStatus(e.nativeEvent.status)
setIsPlaying(e.nativeEvent.status === 'playing')
}}
onTimeRemainingChange={e => {
setTimeRemaining(e.nativeEvent.timeRemaining)
}}
onError={e => {
setError(e.nativeEvent.error)
}}
ref={videoRef}
accessibilityLabel={
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
}
accessibilityHint=""
/>
<VideoControls
enterFullscreen={() => {
videoRef.current?.enterFullscreen(true)
}}
toggleMuted={() => {
videoRef.current?.toggleMuted()
}}
togglePlayback={() => {
videoRef.current?.togglePlayback()
}}
isPlaying={isPlaying}
timeRemaining={timeRemaining}
/>
<MediaInsetBorder />
</View>
)
},
)
return (
<View style={[a.flex_1, a.relative]}>
<BlueskyVideoView
url={embed.playlist}
autoplay={!autoplayDisabled && !isWithinMessage}
beginMuted={autoplayDisabled ? false : muted}
style={[a.rounded_sm]}
onActiveChange={e => {
setIsActive(e.nativeEvent.isActive)
}}
onLoadingChange={e => {
setIsLoading(e.nativeEvent.isLoading)
}}
onMutedChange={e => {
setMuted(e.nativeEvent.isMuted)
}}
onStatusChange={e => {
setStatus(e.nativeEvent.status)
setIsPlaying(e.nativeEvent.status === 'playing')
}}
onTimeRemainingChange={e => {
setTimeRemaining(e.nativeEvent.timeRemaining)
}}
onError={e => {
setError(e.nativeEvent.error)
}}
ref={videoRef}
accessibilityLabel={
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
}
accessibilityHint=""
/>
<VideoControls
enterFullscreen={() => {
videoRef.current?.enterFullscreen(true)
}}
toggleMuted={() => {
videoRef.current?.toggleMuted()
}}
togglePlayback={() => {
videoRef.current?.togglePlayback()
}}
isPlaying={isPlaying}
timeRemaining={timeRemaining}
/>
<MediaInsetBorder />
</View>
)
}
function VideoControls({
enterFullscreen,
@@ -224,15 +224,19 @@ function useHLS({
throw new HLSUnsupportedError()
}
const latestEstimate = BandwidthEstimate.get()
const hls = new Hls({
maxMaxBufferLength: 10, // only load 10s ahead
// note: the amount buffered is affected by both maxBufferLength and maxBufferSize
// it will buffer until it is greater than *both* of those values
// so we use maxMaxBufferLength to set the actual maximum amount of buffering instead
startLevel:
latestEstimate === undefined ? -1 : Hls.DefaultConfig.startLevel,
// the '-1' value makes a test request to estimate bandwidth and quality level
// before showing the first fragment
})
hlsRef.current = hls
const latestEstimate = BandwidthEstimate.get()
if (latestEstimate !== undefined) {
hls.bandwidthEstimate = latestEstimate
}
@@ -8,6 +8,7 @@ const Context = React.createContext<{
volume: number
setVolume: React.Dispatch<React.SetStateAction<number>>
} | null>(null)
Context.displayName = 'VideoVolumeContext'
export function Provider({children}: {children: React.ReactNode}) {
const [muted, setMuted] = React.useState(true)
+9 -12
View File
@@ -1,4 +1,4 @@
import React, {useCallback, useState} from 'react'
import {useCallback, useRef, useState} from 'react'
import {ActivityIndicator, View} from 'react-native'
import {ImageBackground} from 'expo-image'
import {type AppBskyEmbedVideo} from '@atproto/api'
@@ -81,13 +81,13 @@ export function VideoEmbed({embed, crop}: Props) {
function InnerWrapper({embed}: Props) {
const {_} = useLingui()
const ref = React.useRef<{togglePlayback: () => void}>(null)
const ref = useRef<{togglePlayback: () => void}>(null)
const [status, setStatus] = React.useState<'playing' | 'paused' | 'pending'>(
const [status, setStatus] = useState<'playing' | 'paused' | 'pending'>(
'pending',
)
const [isLoading, setIsLoading] = React.useState(false)
const [isActive, setIsActive] = React.useState(false)
const [isLoading, setIsLoading] = useState(false)
const [isActive, setIsActive] = useState(false)
const showSpinner = useThrottledValue(isActive && isLoading, 100)
const showOverlay =
@@ -96,11 +96,9 @@ function InnerWrapper({embed}: Props) {
(status === 'paused' && !isActive) ||
status === 'pending'
React.useEffect(() => {
if (!isActive && status !== 'pending') {
setStatus('pending')
}
}, [isActive, status])
if (!isActive && status !== 'pending') {
setStatus('pending')
}
return (
<>
@@ -131,8 +129,7 @@ function InnerWrapper({embed}: Props) {
onPress={() => {
ref.current?.togglePlayback()
}}
label={_(msg`Play video`)}
color="secondary">
label={_(msg`Play video`)}>
{showSpinner ? (
<View
style={[
@@ -1,4 +1,11 @@
import {useCallback, useEffect, useRef, useState} from 'react'
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react'
import {View} from 'react-native'
import {type AppBskyEmbedVideo} from '@atproto/api'
import {msg} from '@lingui/macro'
@@ -83,9 +90,7 @@ export function VideoEmbed({
style={{display: 'flex', flex: 1, cursor: 'default'}}
onClick={evt => evt.stopPropagation()}>
<ErrorBoundary renderError={renderError} key={key}>
<ViewportObserver
sendPosition={sendPosition}
isAnyViewActive={currentActiveView !== null}>
<OnlyNearScreen>
<VideoEmbedInnerWeb
embed={embed}
active={active}
@@ -93,31 +98,40 @@ export function VideoEmbed({
onScreen={onScreen}
lastKnownTime={lastKnownTime}
/>
</ViewportObserver>
</OnlyNearScreen>
</ErrorBoundary>
</div>
)
return (
<View style={[a.pt_xs]}>
{cropDisabled ? (
<View style={[a.w_full, a.overflow_hidden, {aspectRatio: max ?? 1}]}>
{contents}
</View>
) : (
<ConstrainedImage
fullBleed={crop === 'square'}
aspectRatio={constrained || 1}>
{contents}
</ConstrainedImage>
)}
<ViewportObserver
sendPosition={sendPosition}
isAnyViewActive={currentActiveView !== null}>
{cropDisabled ? (
<View style={[a.w_full, a.overflow_hidden, {aspectRatio: max ?? 1}]}>
{contents}
</View>
) : (
<ConstrainedImage
fullBleed={crop === 'square'}
aspectRatio={constrained || 1}>
{contents}
</ConstrainedImage>
)}
</ViewportObserver>
</View>
)
}
const NearScreenContext = createContext(false)
NearScreenContext.displayName = 'VideoNearScreenContext'
/**
* Renders a 100vh tall div and watches it with an IntersectionObserver to
* send the position of the div when it's near the screen.
*
* IMPORTANT: ViewportObserver _must_ not be within a `overflow: hidden` container.
*/
function ViewportObserver({
children,
@@ -164,7 +178,9 @@ function ViewportObserver({
return (
<View style={[a.flex_1, a.flex_row]}>
{nearScreen && children}
<NearScreenContext.Provider value={nearScreen}>
{children}
</NearScreenContext.Provider>
<div
ref={ref}
style={{
@@ -182,6 +198,18 @@ function ViewportObserver({
)
}
/**
* Awkward data flow here, but we need to hide the video when it's not near the screen.
* But also, ViewportObserver _must_ not be within a `overflow: hidden` container.
* So we put it at the top level of the component tree here, then hide the children of
* the auto-resizing container.
*/
export const OnlyNearScreen = ({children}: {children: React.ReactNode}) => {
const nearScreen = useContext(NearScreenContext)
return nearScreen ? children : null
}
function VideoError({error, retry}: {error: unknown; retry: () => void}) {
const {_} = useLingui()
+63
View File
@@ -0,0 +1,63 @@
import {View} from 'react-native'
import {Trans} from '@lingui/macro'
import {useSession} from '#/state/session'
import {UserInfoText} from '#/view/com/util/UserInfoText'
import {atoms as a, useTheme} from '#/alf'
import {ArrowCornerDownRight_Stroke2_Corner2_Rounded as ArrowCornerDownRightIcon} from '#/components/icons/ArrowCornerDownRight'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
export function PostRepliedTo({
parentAuthor,
isParentBlocked,
isParentNotFound,
}: {
parentAuthor: string | bsky.profile.AnyProfileView | undefined
isParentBlocked?: boolean
isParentNotFound?: boolean
}) {
const t = useTheme()
const {currentAccount} = useSession()
const textStyle = [a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]
let label
if (isParentBlocked) {
label = <Trans context="description">Replied to a blocked post</Trans>
} else if (isParentNotFound) {
label = <Trans context="description">Replied to a post</Trans>
} else if (parentAuthor) {
const did =
typeof parentAuthor === 'string' ? parentAuthor : parentAuthor.did
const isMe = currentAccount?.did === did
if (isMe) {
label = <Trans context="description">Replied to you</Trans>
} else {
label = (
<Trans context="description">
Replied to{' '}
<ProfileHoverCard did={did}>
<UserInfoText did={did} attr="displayName" style={textStyle} />
</ProfileHoverCard>
</Trans>
)
}
}
if (!label) {
// Should not happen.
return null
}
return (
<View style={[a.flex_row, a.align_center, a.pb_xs, a.gap_xs]}>
<ArrowCornerDownRightIcon
size="xs"
style={[t.atoms.text_contrast_medium, {top: -1}]}
/>
<Text style={textStyle}>{label}</Text>
</View>
)
}
@@ -2,13 +2,13 @@ import {Pressable} from 'react-native'
import * as Clipboard from 'expo-clipboard'
import {t} from '@lingui/macro'
import {IS_INTERNAL} from '#/lib/app-info'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {useGate} from '#/lib/statsig/statsig'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {IS_INTERNAL} from '#/env'
export function DiscoverDebug({
feedContext,
@@ -32,8 +32,7 @@ export function DiscoverDebug({
hitSlop={10}
style={[
a.absolute,
a.bottom_0,
{zIndex: 1000},
{zIndex: 1000, maxWidth: 65, bottom: -4},
gtMobile ? a.right_0 : a.left_0,
]}
onPress={e => {
@@ -42,6 +41,7 @@ export function DiscoverDebug({
Toast.show(t`Copied to clipboard`, 'clipboard-check')
}}>
<Text
numberOfLines={1}
style={{
color: theme.palette.contrast_400,
fontSize: 7,
@@ -13,6 +13,7 @@ const PostControlContext = createContext<{
active?: boolean
color?: {color: string}
}>({})
PostControlContext.displayName = 'PostControlContext'
// Base button style, which the the other ones extend
export function PostControlButton({
@@ -17,9 +17,9 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {IS_INTERNAL} from '#/lib/app-info'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {useOpenLink} from '#/lib/hooks/useOpenLink'
import {useTranslate} from '#/lib/hooks/useTranslate'
import {getCurrentRoute} from '#/lib/routes/helpers'
import {makeProfileLink} from '#/lib/routes/links'
import {
@@ -29,7 +29,6 @@ import {
import {logEvent, useGate} from '#/lib/statsig/statsig'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {toShareUrl} from '#/lib/strings/url-helpers'
import {getTranslatorLink} from '#/locale/helpers'
import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/post-shadow'
import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -83,6 +82,7 @@ import {
useReportDialogControl,
} from '#/components/moderation/ReportDialog'
import * as Prompt from '#/components/Prompt'
import {IS_INTERNAL} from '#/env'
import * as bsky from '#/types/bsky'
let PostMenuItems = ({
@@ -118,6 +118,7 @@ let PostMenuItems = ({
const {hidePost} = useHiddenPostsApi()
const feedFeedback = useFeedFeedbackContext()
const openLink = useOpenLink()
const translate = useTranslate()
const navigation = useNavigation<NavigationProp>()
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
const blockPromptControl = useDialogControl()
@@ -172,11 +173,6 @@ let PostMenuItems = ({
return makeProfileLink(postAuthor, 'post', urip.rkey)
}, [postUri, postAuthor])
const translatorUrl = getTranslatorLink(
record.text,
langPrefs.primaryLanguage,
)
const onDeletePost = () => {
deletePostMutate({uri: postUri}).then(
() => {
@@ -234,8 +230,8 @@ let PostMenuItems = ({
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}
const onPressTranslate = async () => {
await openLink(translatorUrl, true)
const onPressTranslate = () => {
translate(record.text, langPrefs.primaryLanguage)
if (
bsky.dangerousIsType<AppBskyFeedPost.Record>(
+2 -2
View File
@@ -145,7 +145,7 @@ let RepostButtonDialogInner = ({
<View style={a.gap_xl}>
<View style={a.gap_xs}>
<Button
style={[a.justify_start, a.px_md]}
style={[a.justify_start, a.px_md, a.gap_sm]}
label={
isReposted
? _(msg`Remove repost`)
@@ -167,7 +167,7 @@ let RepostButtonDialogInner = ({
<Button
disabled={embeddingDisabled}
testID="quoteBtn"
style={[a.justify_start, a.px_md]}
style={[a.justify_start, a.px_md, a.gap_sm]}
label={
embeddingDisabled
? _(msg`Quote posts disabled`)
@@ -4,10 +4,12 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
import {type NavigationProp} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
import {useSession} from '#/state/session'
@@ -57,7 +59,11 @@ export function RecentChats({postUri}: {postUri: string}) {
member => member.did !== currentAccount?.did,
)
if (!otherMember || otherMember.handle === 'missing.invalid')
if (
!otherMember ||
otherMember.handle === 'missing.invalid' ||
convo.muted
)
return null
return (
@@ -87,7 +93,7 @@ export function RecentChats({postUri}: {postUri: string}) {
const WIDTH = 80
function RecentChatItem({
profile,
profile: profileUnshadowed,
onPress,
moderationOpts,
}: {
@@ -98,6 +104,8 @@ function RecentChatItem({
const {_} = useLingui()
const t = useTheme()
const profile = useProfileShadow(profileUnshadowed)
const moderation = moderateProfile(profile, moderationOpts)
const name = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
@@ -105,6 +113,10 @@ function RecentChatItem({
)
const verification = useSimpleVerificationState({profile})
if (isBlockedOrBlocking(profile) || isMuted(profile)) {
return null
}
return (
<Button
onPress={onPress}
+36 -9
View File
@@ -20,7 +20,13 @@ import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, platform, useTheme} from '#/alf'
import {
atoms as a,
platform,
type TextStyleProp,
useTheme,
type ViewStyleProp,
} from '#/alf'
import {
Button,
ButtonIcon,
@@ -136,12 +142,14 @@ export function Avatar({
onPress,
disabledPreview,
liveOverride,
size = 40,
}: {
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
onPress?: () => void
disabledPreview?: boolean
liveOverride?: boolean
size?: number
}) {
const moderation = moderateProfile(profile, moderationOpts)
@@ -149,7 +157,7 @@ export function Avatar({
return disabledPreview ? (
<UserAvatar
size={40}
size={size}
avatar={profile.avatar}
type={profile.associated?.labeler ? 'labeler' : 'user'}
moderation={moderation.ui('avatar')}
@@ -157,7 +165,7 @@ export function Avatar({
/>
) : (
<PreviewableUserAvatar
size={40}
size={size}
profile={profile}
moderation={moderation.ui('avatar')}
onBeforePress={onPress}
@@ -166,7 +174,7 @@ export function Avatar({
)
}
export function AvatarPlaceholder() {
export function AvatarPlaceholder({size = 40}: {size?: number}) {
const t = useTheme()
return (
<View
@@ -174,8 +182,8 @@ export function AvatarPlaceholder() {
a.rounded_full,
t.atoms.bg_contrast_25,
{
width: 40,
height: 40,
width: size,
height: size,
},
]}
/>
@@ -274,7 +282,7 @@ export function Name({
)
const verification = useSimpleVerificationState({profile})
return (
<View style={[a.flex_row, a.align_center]}>
<View style={[a.flex_row, a.align_center, a.max_w_full]}>
<Text
emoji
style={[
@@ -343,13 +351,32 @@ export function NameAndHandlePlaceholder() {
)
}
export function NamePlaceholder({style}: ViewStyleProp) {
const t = useTheme()
return (
<View
style={[
a.rounded_xs,
t.atoms.bg_contrast_25,
{
width: '60%',
height: 14,
},
style,
]}
/>
)
}
export function Description({
profile: profileUnshadowed,
numberOfLines = 3,
style,
}: {
profile: bsky.profile.AnyProfileView
numberOfLines?: number
}) {
} & TextStyleProp) {
const profile = useProfileShadow(profileUnshadowed)
const rt = useMemo(() => {
if (!('description' in profile)) return
@@ -369,7 +396,7 @@ export function Description({
<View style={[a.pt_xs]}>
<RichText
value={rt}
style={[a.leading_snug]}
style={[a.leading_snug, style]}
numberOfLines={numberOfLines}
disableLinks
/>
+1 -1
View File
@@ -1,4 +1,4 @@
import {ProfileHoverCardProps} from './types'
import {type ProfileHoverCardProps} from './types'
export function ProfileHoverCard({children}: ProfileHoverCardProps) {
return children
@@ -74,7 +74,9 @@ export function ProfileHoverCard(props: ProfileHoverCardProps) {
return props.children
} else {
return (
<View onPointerMove={onPointerMove} style={[a.flex_shrink, props.style]}>
<View
onPointerMove={onPointerMove}
style={[a.flex_shrink, props.inline && a.inline, props.style]}>
<ProfileHoverCardInner {...props} />
</View>
)
@@ -326,7 +328,7 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
onPointerLeave={onPointerLeaveTarget}
// @ts-ignore web only prop
onMouseUp={onPress}
style={{flexShrink: 1}}>
style={[a.flex_shrink, props.inline && a.inline]}>
{props.children}
{isVisible && (
<Portal>
+1
View File
@@ -6,4 +6,5 @@ export type ProfileHoverCardProps = ViewStyleProp & {
children: React.ReactNode
did: string
disable?: boolean
inline?: boolean
}
@@ -539,7 +539,7 @@ let Tab = ({
]}>
<Text
style={[
/* TODO: medium weight */
a.font_medium,
active || hovered || pressed || focused
? t.atoms.text
: t.atoms.text_contrast_medium,
+1
View File
@@ -27,6 +27,7 @@ const Context = React.createContext<{
titleId: '',
descriptionId: '',
})
Context.displayName = 'PromptContext'
export function Outer({
children,
+3
View File
@@ -34,10 +34,12 @@ type ContextType = {
} & Pick<RootProps, 'value' | 'onValueChange' | 'disabled'>
const Context = createContext<ContextType | null>(null)
Context.displayName = 'SelectContext'
const ValueTextContext = createContext<
[any, React.Dispatch<React.SetStateAction<any>>]
>([undefined, () => {}])
ValueTextContext.displayName = 'ValueTextContext'
function useSelectContext() {
const ctx = useContext(Context)
@@ -229,6 +231,7 @@ const ItemContext = createContext<{
focused: false,
pressed: false,
})
ItemContext.displayName = 'SelectItemContext'
export function useItemContext() {
return useContext(ItemContext)
+5 -1
View File
@@ -23,6 +23,7 @@ import {
} from './types'
const SelectedValueContext = createContext<string | undefined | null>(null)
SelectedValueContext.displayName = 'SelectSelectedValueContext'
export function Root(props: RootProps) {
return (
@@ -98,13 +99,14 @@ export function Trigger({children, label}: TriggerProps) {
t.atoms.bg_contrast_25,
a.rounded_sm,
a.w_full,
{maxWidth: 400},
a.align_center,
a.gap_sm,
a.justify_between,
a.py_sm,
a.px_md,
a.pointer,
{
maxWidth: 400,
outline: 0,
borderWidth: 2,
borderStyle: 'solid',
@@ -190,6 +192,7 @@ export function Content<T>({items, renderItem}: ContentProps<T>) {
a.border,
t.atoms.border_contrast_low,
a.rounded_sm,
a.overflow_hidden,
]}>
<RadixSelect.ScrollUpButton style={flatten(up)}>
<ChevronUpIcon style={[t.atoms.text]} size="xs" />
@@ -217,6 +220,7 @@ const ItemContext = createContext<{
pressed: false,
selected: false,
})
ItemContext.displayName = 'SelectItemContext'
export function useItemContext() {
return useContext(ItemContext)
@@ -1,12 +1,11 @@
import React, {useCallback} from 'react'
import {ListRenderItemInfo, View} from 'react-native'
import {AppBskyFeedDefs} from '@atproto/api'
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {type ListRenderItemInfo, View} from 'react-native'
import {type AppBskyFeedDefs} from '@atproto/api'
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
import {isNative, isWeb} from '#/platform/detection'
import {List, ListRef} from '#/view/com/util/List'
import {SectionRef} from '#/screens/Profile/Sections/types'
import {List, type ListRef} from '#/view/com/util/List'
import {type SectionRef} from '#/screens/Profile/Sections/types'
import {atoms as a, useTheme} from '#/alf'
import * as FeedCard from '#/components/FeedCard'
@@ -37,7 +36,10 @@ export const FeedsList = React.forwardRef<SectionRef, ProfilesListProps>(
scrollToTop: onScrollToTop,
}))
const renderItem = ({item, index}: ListRenderItemInfo<GeneratorView>) => {
const renderItem = ({
item,
index,
}: ListRenderItemInfo<AppBskyFeedDefs.GeneratorView>) => {
return (
<View
style={[
@@ -1,16 +1,22 @@
import {useRef} from 'react'
import type {ListRenderItemInfo} from 'react-native'
import {type ListRenderItemInfo} from 'react-native'
import {View} from 'react-native'
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {
type AppBskyActorDefs,
type AppBskyFeedDefs,
type ModerationOpts,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {isWeb} from '#/platform/detection'
import {useSession} from '#/state/session'
import {ListMethods} from '#/view/com/util/List'
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
import {type ListMethods} from '#/view/com/util/List'
import {
type WizardAction,
type WizardState,
} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, native, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -21,7 +27,7 @@ import {
import {Text} from '#/components/Typography'
function keyExtractor(
item: AppBskyActorDefs.ProfileViewBasic | GeneratorView,
item: AppBskyActorDefs.ProfileViewBasic | AppBskyFeedDefs.GeneratorView,
index: number,
) {
return `${item.did}-${index}`
@@ -1,13 +1,12 @@
import {Keyboard, View} from 'react-native'
import {
AppBskyActorDefs,
AppBskyFeedDefs,
type AppBskyActorDefs,
type AppBskyFeedDefs,
moderateFeedGenerator,
moderateProfile,
ModerationOpts,
ModerationUI,
type ModerationOpts,
type ModerationUI,
} from '@atproto/api'
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -16,13 +15,16 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
import {
type WizardAction,
type WizardState,
} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Toggle from '#/components/forms/Toggle'
import {Checkbox} from '#/components/forms/Toggle'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
import type * as bsky from '#/types/bsky'
function WizardListCard({
type,
@@ -174,7 +176,7 @@ export function WizardFeedCard({
moderationOpts,
}: {
btnType: 'checkbox' | 'remove'
generator: GeneratorView
generator: AppBskyFeedDefs.GeneratorView
state: WizardState
dispatch: (action: WizardAction) => void
moderationOpts: ModerationOpts
+167
View File
@@ -0,0 +1,167 @@
import {createContext, useContext, useMemo} from 'react'
import {View} from 'react-native'
import {atoms as a, select, useAlf, useTheme} from '#/alf'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import {type ToastType} from '#/components/Toast/types'
import {Text} from '#/components/Typography'
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '../icons/CircleCheck'
type ContextType = {
type: ToastType
}
export type ToastComponentProps = {
type?: ToastType
content: React.ReactNode
}
export const ICONS = {
default: CircleCheck,
success: CircleCheck,
error: ErrorIcon,
warning: WarningIcon,
info: CircleInfo,
}
const Context = createContext<ContextType>({
type: 'default',
})
Context.displayName = 'ToastContext'
export function Toast({type = 'default', content}: ToastComponentProps) {
const {fonts} = useAlf()
const t = useTheme()
const styles = useToastStyles({type})
const Icon = ICONS[type]
/**
* Vibes-based number, adjusts `top` of `View` that wraps the text to
* compensate for different type sizes and keep the first line of text
* aligned with the icon. - esb
*/
const fontScaleCompensation = useMemo(
() => parseInt(fonts.scale) * -1 * 0.65,
[fonts.scale],
)
return (
<Context.Provider value={useMemo(() => ({type}), [type])}>
<View
style={[
a.flex_1,
a.py_lg,
a.pl_xl,
a.pr_2xl,
a.rounded_md,
a.border,
a.flex_row,
a.gap_sm,
t.atoms.shadow_sm,
{
backgroundColor: styles.backgroundColor,
borderColor: styles.borderColor,
},
]}>
<Icon size="md" fill={styles.iconColor} />
<View
style={[
a.flex_1,
{
top: fontScaleCompensation,
},
]}>
{typeof content === 'string' ? (
<ToastText>{content}</ToastText>
) : (
content
)}
</View>
</View>
</Context.Provider>
)
}
export function ToastText({children}: {children: React.ReactNode}) {
const {type} = useContext(Context)
const {textColor} = useToastStyles({type})
return (
<Text
selectable={false}
style={[
a.text_md,
a.font_medium,
a.leading_snug,
a.pointer_events_none,
{
color: textColor,
},
]}>
{children}
</Text>
)
}
function useToastStyles({type}: {type: ToastType}) {
const t = useTheme()
return useMemo(() => {
return {
default: {
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
borderColor: t.atoms.border_contrast_low.borderColor,
iconColor: t.atoms.text.color,
textColor: t.atoms.text.color,
},
success: {
backgroundColor: t.palette.primary_25,
borderColor: select(t.name, {
light: t.palette.primary_300,
dim: t.palette.primary_200,
dark: t.palette.primary_100,
}),
iconColor: select(t.name, {
light: t.palette.primary_600,
dim: t.palette.primary_700,
dark: t.palette.primary_700,
}),
textColor: select(t.name, {
light: t.palette.primary_600,
dim: t.palette.primary_700,
dark: t.palette.primary_700,
}),
},
error: {
backgroundColor: t.palette.negative_25,
borderColor: select(t.name, {
light: t.palette.negative_200,
dim: t.palette.negative_200,
dark: t.palette.negative_100,
}),
iconColor: select(t.name, {
light: t.palette.negative_700,
dim: t.palette.negative_900,
dark: t.palette.negative_900,
}),
textColor: select(t.name, {
light: t.palette.negative_700,
dim: t.palette.negative_900,
dark: t.palette.negative_900,
}),
},
warning: {
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
borderColor: t.atoms.border_contrast_low.borderColor,
iconColor: t.atoms.text.color,
textColor: t.atoms.text.color,
},
info: {
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
borderColor: t.atoms.border_contrast_low.borderColor,
iconColor: t.atoms.text.color,
textColor: t.atoms.text.color,
},
}[type]
}, [t, type])
}
+1
View File
@@ -0,0 +1 @@
export const DURATION = 3e3
+16
View File
@@ -0,0 +1,16 @@
export function ToastOutlet() {
return null
}
export const api = () => {}
api.success = () => {}
api.wiggle = () => {}
api.error = () => {}
api.warning = () => {}
api.info = () => {}
api.promise = () => {}
api.custom = () => {}
api.loading = () => {}
api.dismiss = () => {}
export function show() {}
+49
View File
@@ -0,0 +1,49 @@
import {View} from 'react-native'
import {toast as sonner, Toaster} from 'sonner-native'
import {atoms as a} from '#/alf'
import {DURATION} from '#/components/Toast/const'
import {
Toast as BaseToast,
type ToastComponentProps,
} from '#/components/Toast/Toast'
import {type BaseToastOptions} from '#/components/Toast/types'
export {DURATION} from '#/components/Toast/const'
/**
* Toasts are rendered in a global outlet, which is placed at the top of the
* component tree.
*/
export function ToastOutlet() {
return <Toaster pauseWhenPageIsHidden gap={a.gap_sm.gap} />
}
/**
* The toast UI component
*/
export function Toast({type, content}: ToastComponentProps) {
return (
<View style={[a.px_xl, a.w_full]}>
<BaseToast content={content} type={type} />
</View>
)
}
/**
* Access the full Sonner API
*/
export const api = sonner
/**
* Our base toast API, using the `Toast` export of this file.
*/
export function show(
content: React.ReactNode,
{type, ...options}: BaseToastOptions = {},
) {
sonner.custom(<Toast content={content} type={type} />, {
...options,
duration: options?.duration ?? DURATION,
})
}
+40
View File
@@ -0,0 +1,40 @@
import {toast as sonner, Toaster} from 'sonner'
import {atoms as a} from '#/alf'
import {DURATION} from '#/components/Toast/const'
import {Toast} from '#/components/Toast/Toast'
import {type BaseToastOptions} from '#/components/Toast/types'
/**
* Toasts are rendered in a global outlet, which is placed at the top of the
* component tree.
*/
export function ToastOutlet() {
return (
<Toaster
position="bottom-left"
gap={a.gap_sm.gap}
offset={a.p_xl.padding}
mobileOffset={a.p_xl.padding}
/>
)
}
/**
* Access the full Sonner API
*/
export const api = sonner
/**
* Our base toast API, using the `Toast` export of this file.
*/
export function show(
content: React.ReactNode,
{type, ...options}: BaseToastOptions = {},
) {
sonner(<Toast content={content} type={type} />, {
unstyled: true, // required on web
...options,
duration: options?.duration ?? DURATION,
})
}
+29
View File
@@ -0,0 +1,29 @@
import {type toast as sonner} from 'sonner-native'
/**
* This is not exported from `sonner-native` so just hacking it in here.
*/
export type ExternalToast = Exclude<
Parameters<typeof sonner.custom>[1],
undefined
>
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info'
/**
* Not all properties are available on all platforms, so we pick out only those
* we support. Add more here as needed.
*/
export type BaseToastOptions = Pick<
ExternalToast,
'duration' | 'dismissible' | 'promiseOptions'
> & {
type?: ToastType
/**
* These methods differ between web/native implementations
*/
onDismiss?: () => void
onPress?: () => void
onAutoClose?: () => void
}
+2
View File
@@ -53,12 +53,14 @@ const TooltipContext = createContext<TooltipContextType>({
visible: false,
onVisibleChange: () => {},
})
TooltipContext.displayName = 'TooltipContext'
const TargetContext = createContext<TargetContextType>({
targetMeasurements: undefined,
setTargetMeasurements: () => {},
shouldMeasure: false,
})
TargetContext.displayName = 'TargetContext'
export function Outer({
children,

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