Compare commits

..

224 Commits

Author SHA1 Message Date
Eric Bailey f2443a7193 Resume session before anything else 2024-03-15 14:32:25 -05:00
Eric Bailey b4bb838d04 Move labeler fetch below auth 2024-03-15 12:09:10 -05:00
Eric Bailey 3e12132d4f Submit view tweaks 2024-03-15 09:09:56 -05:00
Paul Frazee 7c675b4a2f Move label defs into a context to reduce recomputes 2024-03-14 20:41:51 -07:00
Eric Bailey 8e12918699 Drop staleTime to 60s 2024-03-14 18:40:20 -05:00
Eric Bailey 6a7f904457 Memoize the memoizers 2024-03-14 18:24:04 -05:00
Eric Bailey 7f178ffb88 Set staleTime to 6hrs for labeler details 2024-03-14 18:08:46 -05:00
Eric Bailey c5d5261257 Tighten space on LabelerCard 2024-03-14 16:35:55 -05:00
Paul Frazee d410aeef39 Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators 2024-03-14 13:14:25 -07:00
Paul Frazee fc7eb58491 Use purple theming on labelers 2024-03-14 13:14:16 -07:00
Eric Bailey cc472565f6 Fix bad merge 2024-03-14 15:06:55 -05:00
Eric Bailey 429a5abb9a Merge remote-tracking branch 'origin/main' into 3p-moderators
* origin/main:
  add warning to profile as well
  use prompt.basic instead of full prompt
  [Statsig] Track posting, end reached (#3206)
  [Statsig] Track likes, reposts, follows (#3195)
  1.73 Version Bump (#3200)
2024-03-14 14:55:49 -05:00
Paul Frazee 35c4eac249 Show the labels tab on logged out view 2024-03-14 12:53:41 -07:00
Eric Bailey 4e525cd44f Show metrics if self 2024-03-14 14:51:45 -05:00
Eric Bailey 67edca4fbb Mute 'account' not 'user' 2024-03-14 14:48:52 -05:00
Eric Bailey b6993d9d0a Remove metrics from header 2024-03-14 14:40:41 -05:00
Eric Bailey dbb34eb57e Handle card overflow 2024-03-14 12:55:23 -05:00
Eric Bailey 2b0db1fa31 Fix profile menu 2024-03-14 12:49:46 -05:00
Eric Bailey 84f4c423fd Fix underline color in header 2024-03-14 11:59:17 -05:00
Paul Frazee b8162aa3a7 Tweak copy 2024-03-14 08:19:48 -07:00
Paul Frazee a94f99a8d2 Add some explainers on how blocking affects labelers 2024-03-14 08:16:28 -07:00
Paul Frazee 646c318777 revert dipendencies 2024-03-13 21:42:49 -07:00
Paul Frazee aa1dc76bde update dipendencies 2024-03-13 21:27:31 -07:00
Paul Frazee 02c270c102 Fix: persist labels to local storage to reduce coverage gaps 2024-03-13 21:19:06 -07:00
Paul Frazee 429981708a Remove extra @ 2024-03-13 20:46:59 -07:00
Paul Frazee b35183b323 Bump @atproto/api@0.11.1 2024-03-13 20:46:10 -07:00
Paul Frazee e8adf03cd5 Remove the like count from the subscribed labeler listing 2024-03-13 17:20:07 -07:00
Eric Bailey ef794018f4 Remove remnant 2024-03-13 17:16:30 -05:00
Eric Bailey 1c26471541 Remove focus handling 2024-03-13 17:14:01 -05:00
Eric Bailey 45fcf149c4 Fix references 2024-03-13 17:13:51 -05:00
Eric Bailey 8b7385ee23 Give mod page links interactive states 2024-03-13 16:33:43 -05:00
Paul Frazee 1f8acd92a2 Fix: send appeals to the right labeler 2024-03-13 14:27:03 -07:00
Paul Frazee 7d8f373d2b :erge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators 2024-03-13 14:26:46 -07:00
Eric Bailey 2d0a65e835 Fix spacing below text-only posts 2024-03-13 15:25:47 -05:00
Eric Bailey 65b2b451a7 Handle self labels 2024-03-13 15:15:54 -05:00
Paul Frazee 2cbf77b55a Tune content hider sizing on web desktop 2024-03-13 13:05:19 -07:00
Paul Frazee 6d0ac69a23 Hackfix to translate gore labels to graphic-media 2024-03-13 12:49:26 -07:00
Eric Bailey 3679852e17 Clean up ModServiceCard, rename to LabelingServiceCard 2024-03-13 14:31:12 -05:00
Eric Bailey 444dbe928a Fix ToggleButton overflow 2024-03-13 13:45:25 -05:00
Eric Bailey 14951ab413 Add to AWS build CI 2024-03-13 13:42:23 -05:00
Eric Bailey 85a0085754 Remove silly type 2024-03-13 13:40:00 -05:00
Eric Bailey 274e7cf91f Merge remote-tracking branch 'origin' into 3p-moderators
* origin:
  filter out files with non-image mime types
  Send route name with Statsig events (#3194)
  Tweak prompt styles on mobile web (#3193)
2024-03-13 13:22:06 -05:00
Eric Bailey 5040146e83 Bump SDK package 2024-03-13 13:11:40 -05:00
Eric Bailey f9fbe9d27d Add @ to mod details dialog handle 2024-03-13 13:07:53 -05:00
Paul Frazee 2f54dcc97e Merge branch 'main' into 3p-moderators 2024-03-12 18:44:05 -07:00
Paul Frazee 80bdbe6c60 Replace the report modal entirely with the report dialog 2024-03-12 17:39:35 -07:00
Paul Frazee 36478a3eea Implement report submission 2024-03-12 12:20:54 -07:00
Paul Frazee cad66269d3 Update to use new SDK appLabelers semantics 2024-03-12 11:43:15 -07:00
Eric Bailey 02f949781a Move labelers query on Mod screen 2024-03-11 10:33:02 -05:00
Paul Frazee 2e60680296 Moderation UI tweaks for mobile 2024-03-10 18:44:58 -07:00
Paul Frazee 3706719033 Tune moderation UIs 2024-03-10 18:31:49 -07:00
Eric Bailey 2d5080d34d Better spacing solution 2024-03-10 18:39:15 -05:00
Eric Bailey 3f14b62b20 Remove bad focus styles from post dropdown 2024-03-10 18:26:06 -05:00
Eric Bailey 60d043ecef Labels on me and dialog 2024-03-10 18:26:06 -05:00
Eric Bailey c0ed0775ed Style tweaks to post/profile alerts 2024-03-10 18:26:06 -05:00
Eric Bailey ebd60ce38e Adjust spacing of embeds in posts 2024-03-10 18:26:06 -05:00
Eric Bailey 918e1e6b53 Adjust spacing of inform badges 2024-03-10 18:26:06 -05:00
Eric Bailey a69699a883 Design tweaks to ContentHider and ModDetailsDialog 2024-03-10 18:26:06 -05:00
Eric Bailey e1c128c77c Fix link bug 2024-03-10 18:26:06 -05:00
Eric Bailey efc3e3f450 Adjust source text in ContentHider 2024-03-10 18:26:06 -05:00
Eric Bailey 8b760378ff Fix text string breakage 2024-03-10 18:26:06 -05:00
Eric Bailey cb4974961a Conten hider design tweaks 2024-03-10 18:26:06 -05:00
Eric Bailey 7edb6829dc Fix bad merge 2024-03-10 18:26:06 -05:00
Eric Bailey 1d5068d584 WIP 2024-03-10 18:26:06 -05:00
Paul Frazee 5b5c3d8b50 Readd the local hiddenPosts handling 2024-03-10 14:26:11 -07:00
Paul Frazee 5ea406b6bc Correctly show the label behavior when adult content is disabled 2024-03-10 14:22:08 -07:00
Paul Frazee a180788038 Fix: dont try to detect link/label mismatches on post meta 2024-03-10 13:16:48 -07:00
Paul Frazee 6cc61bf8e2 Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators 2024-03-10 13:16:30 -07:00
Paul Frazee 816a48e975 Fix attrs 2024-03-10 13:14:15 -07:00
Paul Frazee 74f1495417 Avoid dups due to bad config 2024-03-10 13:14:08 -07:00
Eric Bailey a71ee87dfa Add route to go server 2024-03-10 15:09:54 -05:00
Paul Frazee a40ebfa795 Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators 2024-03-10 13:08:21 -07:00
Paul Frazee c1b554a619 Update the default labeler avatar to a shield 2024-03-10 13:08:12 -07:00
Eric Bailey 0a1fad8332 Tweaks 2024-03-10 15:07:29 -05:00
Paul Frazee 8c4b9aebe2 Add special handling of the mod authorities 2024-03-10 12:47:20 -07:00
Paul Frazee f0144e7439 Change how onboarding handles adult content enabled/disabled 2024-03-10 11:09:43 -07:00
Paul Frazee d21f4d9c7f Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators 2024-03-10 10:45:51 -07:00
Eric Bailey 5566f586b5 Merge remote-tracking branch 'origin/main' into 3p-moderators
* origin/main:
  Fix font styles on web (#3162)
  Fix `aria-label` on the Share button (#3159)
  Fix RSS URLs treated as internal (#3156)
  Poll Statsig for config changes (#3158)
  Fix missing check for invite code (#3157)
  Adjustments to ALF prompt buttons (Dialogs Pt. 2) (#3144)
  Make ALF prompt scrollable for accessibility (#3150)
  Fix reactivity of dialogs (Dialogs Pt. 1) (#3146)
  Refactor `PostDropdownBtn` to use new `Menu` (#3112)
2024-03-10 12:43:29 -05:00
Eric Bailey 6f4556d479 Cleanup 2024-03-10 12:35:16 -05:00
Eric Bailey db5749124f Fix optimistic like 2024-03-10 12:31:21 -05:00
Paul Frazee c7e20a4b3d Rework the moderationlabelpref 2024-03-09 17:50:29 -08:00
Eric Bailey 7a02e9dcb5 Liked by screen 2024-03-09 19:29:46 -06:00
Eric Bailey ee08469b4d Remove focused overrides from toggles 2024-03-09 17:54:53 -06:00
Paul Frazee af124516b3 Fix some colors 2024-03-09 15:39:59 -08:00
Paul Frazee 86de3d630a Ditch tertiary button color, lighten secondary button 2024-03-09 15:32:44 -08:00
Paul Frazee e0f93cb9e6 Fixes to the adult content age-gating in moderaiton 2024-03-09 15:27:09 -08:00
Paul Frazee 0bd52704da Remove focus styles from buttons - let the browser behavior handle it 2024-03-09 15:11:14 -08:00
Paul Frazee 3030825a69 Move nudity out of the adult content control 2024-03-09 14:33:17 -08:00
Paul Frazee fb05cffdd6 Switch 'gore' to 'graphic-media' 2024-03-09 13:35:00 -08:00
Paul Frazee 185c3d6ee1 Add test-environment mod authority 2024-03-09 13:25:22 -08:00
Paul Frazee 8862c1f540 Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators 2024-03-09 12:47:36 -08:00
Paul Frazee 68d4d4142c Update mutewords and hiddenposts to use the new sdk 2024-03-09 12:45:26 -08:00
Paul Frazee 2f8573b339 Switch moderation UIs to tertiary color 2024-03-09 11:08:40 -08:00
Paul Frazee 944d4c6a82 Add tertiary button color 2024-03-09 11:08:18 -08:00
Eric Bailey ed91041448 Design tweaks for mod pref dialog 2024-03-09 11:25:21 -06:00
Eric Bailey ca561de2d0 Tweaks to Labeler profile 2024-03-08 11:20:42 -06:00
Eric Bailey 1a9f6fcf73 Like button improvements 2024-03-08 11:01:15 -06:00
Eric Bailey 040e9bf63c Swap in RichText 2024-03-08 10:47:16 -06:00
Eric Bailey 0b621d4f9c Adjust font sizes 2024-03-08 10:35:06 -06:00
Eric Bailey e88eaf2d16 Handle underage users on mod screen 2024-03-08 10:33:40 -06:00
Eric Bailey 7a7c234909 Remove deprecated global labels 2024-03-08 10:15:17 -06:00
Eric Bailey c94999e3ce Tweak adult setting styles 2024-03-08 10:13:35 -06:00
Eric Bailey cb6674a69e Add mutewords control back to Mod screen 2024-03-08 10:10:26 -06:00
Eric Bailey db14378cff Fix empty moderation page 2024-03-08 10:05:01 -06:00
Paul Frazee 6ac4b6be5c Fix missing postAuthor 2024-03-07 23:05:04 -08:00
Paul Frazee 05f3be04b3 Merge branch 'main' into 3p-moderators 2024-03-07 23:02:14 -08:00
Paul Frazee b37c3adac3 Handle global moderation label prefs with the global settings 2024-03-07 17:58:09 -08:00
Paul Frazee 249eb46343 Use the default setting of custom labels 2024-03-07 17:49:15 -08:00
Paul Frazee 42a6b80399 Add adult-content-enabled behavior to label config 2024-03-07 17:47:09 -08:00
Paul Frazee 59d5c8ecc0 Update to use new sdk updates 2024-03-07 16:16:53 -08:00
Paul Frazee b951bacdff Update nudity descriptions 2024-03-07 16:01:02 -08:00
Eric Bailey e21b72ee8b Replace non-single-path SVG 2024-03-07 14:35:11 -06:00
Eric Bailey 94312ac8ed Bit of ReportDialog cleanup 2024-03-07 14:29:44 -06:00
Eric Bailey b6bf57fca7 Some work on Likes dialog, needs discussion 2024-03-07 14:05:53 -06:00
Eric Bailey 2826483fb9 Improve types of RQ 2024-03-07 13:15:46 -06:00
Eric Bailey 7d80e5db54 Fix lint errors 2024-03-07 13:00:03 -06:00
Eric Bailey a2db4d6098 Fix type errors 2024-03-07 11:40:53 -06:00
Paul Frazee 38b217458a Enforce limit of ten labeler subscriptions 2024-03-06 12:43:01 -08:00
Paul Frazee aa70d0e2ef Update onboarding 2024-03-06 11:50:55 -08:00
Paul Frazee c5872f6644 Get report flow working again with temporarily fixed report options 2024-03-06 10:43:08 -08:00
Paul Frazee a1fe034c2e Implement the label appeal flow 2024-03-06 00:17:09 -08:00
Paul Frazee 26a338f586 Fix likes dialog on mobile 2024-03-05 21:27:47 -08:00
Paul Frazee 5100c6b93b Cleanup 2024-03-05 21:18:07 -08:00
Paul Frazee 019cc9dd7e Fix: remove text outside of text element 2024-03-05 21:18:01 -08:00
Paul Frazee a610ce875f Implement Likes dialog 2024-03-05 20:46:39 -08:00
Paul Frazee 2a7af0b8c0 Small cleanup 2024-03-05 19:15:46 -08:00
Paul Frazee a84e264dae Implement label handling 2024-03-05 16:18:49 -08:00
Paul Frazee e794e84b48 Add asyncstorage query persistence 2024-03-05 11:34:28 -08:00
Paul Frazee 14a99cf3fc Get moderation screen working 2024-03-04 23:03:19 -08:00
Paul Frazee a4ad364781 Rework label pref ui 2024-03-04 22:02:18 -08:00
Paul Frazee 19c0ae80eb Implement preference controls on labelers 2024-03-04 18:14:02 -08:00
Paul Frazee b26e280373 Tweak ui 2024-03-04 16:33:07 -08:00
Paul Frazee f99307033c Implement new label config controls (wip) 2024-03-04 16:00:15 -08:00
Paul Frazee 4d0dc7438b Update profile to pull from labeler definition 2024-03-04 14:02:54 -08:00
Paul Frazee 473ca0deff Delete some dead code and do some refactoring 2024-03-04 13:24:22 -08:00
Paul Frazee f30f31f123 Update to use new labeler semantics 2024-03-04 13:04:33 -08:00
Paul Frazee aa2f94e2fb More debugmod UI tuning 2024-03-02 12:18:11 -08:00
Paul Frazee 8cda28e9fd Fix profile-card warning rendering 2024-03-02 12:08:32 -08:00
Paul Frazee d1ffe94b00 Improve consistency of display name blurring 2024-03-02 12:03:21 -08:00
Paul Frazee bde59a6de5 Minor ui tweaks 2024-03-01 16:35:38 -08:00
Paul Frazee 984fe481c3 Update ModDebug to be a bit more intuitive and support custom labels 2024-03-01 16:35:25 -08:00
Paul Frazee 775a11f557 Remove a bunch of now-dead label strings 2024-02-29 16:56:37 -08:00
Paul Frazee ad135fa705 Remove dead code and start moving toward latest modsdk 2024-02-29 16:51:37 -08:00
Paul Frazee 05863426c5 Moderator variation of profile 2024-02-27 16:55:31 -08:00
Paul Frazee 697b262ba4 Experiment to put labeling under a tab of a normal profile 2024-02-27 14:33:57 -08:00
Eric Bailey 6b9e3764b3 Rename LabelsOnMeDialog, fix close 2024-02-26 18:52:28 -06:00
Eric Bailey 763fd5bc93 Handle ReportDialog load better 2024-02-26 18:39:12 -06:00
Eric Bailey 0dc1cdfdcf Update LabelsOnMe dialog 2024-02-26 18:23:49 -06:00
Eric Bailey 304b6f4f00 Update ReportDialog 2024-02-26 18:10:46 -06:00
Eric Bailey 67fea6d317 Update ModerationDetailsDialog 2024-02-26 17:41:51 -06:00
Eric Bailey dbd42ae0c5 Merge remote-tracking branch 'origin/main' into 3p-moderators
* origin/main: (69 commits)
  Update .po files
  use `showControls` to show/hide live text icon on ios (#2982)
  Fix dim mode unread notif color
  Make dim theme dim (#2966)
  Add handle validation to create account UI (#2959)
  Normalize relative day (#2874)
  increase timeout to 15s (#2958)
  use `useOpenLink` hook for links in ALF (#2975)
  Rename Home Feed Prefs to Following Feed Prefs (#2965)
  Refactor feed header components (#2964)
  patch react-navigation to fix history bug (#2955)
  Use EAS managed build number, run build/submit on GH Actions (#2841)
  Fix `numberOfLines` not updating on iOS 15 (#2956)
  Fix UITextView line height adjustment for DynamicType, always use the max width for the view (#2916)
  Navigate back from a deleted post's route (#2948)
  Add optional close callback to Dialog (#2947)
  Fix flash when pressing into just-created post (#2945)
  Last usage (#2944)
  Update blogpost URL in ExportCarDialog.tsx (#2939)
  Prefer full posts for post thread placeholder (#2943)
  ...
2024-02-26 14:50:43 -06:00
Paul Frazee e3a4d800b4 Cleanup: delete old code 2024-02-22 19:42:57 -08:00
Paul Frazee 047df5939f A bunch of speculative non-working UI 2024-02-22 16:32:22 -08:00
Paul Frazee acd9532331 Wire up more of the modservice data to profiles 2024-02-21 11:28:49 -08:00
Paul Frazee 67d0dde5b7 Add moderator-service profile view 2024-02-20 22:54:10 -08:00
Paul Frazee 7d9890d465 Add notifs to debugmod and dont filter notifs from followed users 2024-02-20 15:09:01 -08:00
Eric Bailey e28e21fcc1 Enable toggle on mod page 2024-02-16 10:05:38 -06:00
Paul Frazee b6bce411e7 Small tweaks to the moderation screen 2024-02-15 20:07:39 -08:00
Paul Frazee 58af8e0f99 Move moderation card from the profile header to a tab 2024-02-15 20:04:07 -08:00
Eric Bailey bfe7c2ff38 Mod screen design 2024-02-15 21:13:50 -06:00
Paul Frazee abaf6e6bae Simplify the ProfileModerationService ui 2024-02-15 19:03:20 -08:00
Eric Bailey 014b6222e4 Fix button overflow 2024-02-15 19:41:17 -06:00
Eric Bailey 700693b87b ReportDialog updates 2024-02-15 19:30:14 -06:00
Paul Frazee 283bb44d73 Use moderateNotification() 2024-02-15 16:40:28 -08:00
Paul Frazee 4dd2fd03c7 Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators 2024-02-15 16:20:33 -08:00
Paul Frazee b8d62a8af8 Rework list of labels on own content 2024-02-15 16:20:20 -08:00
Eric Bailey ddef1d438a Styling, handle disabled labelers 2024-02-15 17:29:47 -06:00
Eric Bailey 8870c5a183 Improve Moderation screen and dialog 2024-02-15 17:01:00 -06:00
Eric Bailey c5d31cd8b0 Update enablement on ProfileMod screen 2024-02-15 16:21:41 -06:00
Paul Frazee 6d9e1f4dd9 Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators 2024-02-15 13:13:32 -08:00
Paul Frazee ef1a5ca2fd Handle muting correctly 2024-02-15 13:13:26 -08:00
Eric Bailey 91bc703964 Clean up ProfileMod screen 2024-02-15 13:24:32 -06:00
Eric Bailey 90662d6cc3 Clean up Mod screen 2024-02-15 13:24:32 -06:00
Eric Bailey 6e4ae28a2b Better loading flow on Mod screen 2024-02-15 13:24:32 -06:00
Paul Frazee 1b33b88ae2 Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators 2024-02-15 11:13:27 -08:00
Paul Frazee e72113ec85 Get post hiding back in gear 2024-02-15 11:13:18 -08:00
Eric Bailey f255d7a483 Little cleanup 2024-02-15 11:14:38 -06:00
Eric Bailey b73f9710f9 Handle reports on profile vs content 2024-02-15 10:37:34 -06:00
Eric Bailey b52beefa27 Add copyright and other option to report 2024-02-15 10:21:48 -06:00
Eric Bailey 16db93d8f9 Fix toggles 2024-02-15 09:53:29 -06:00
Paul Frazee 4d8b4cf6f7 Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators 2024-02-14 21:51:02 -08:00
Paul Frazee b009a34a7a Refactor/rewrite the entire moderation-application system 2024-02-14 21:48:27 -08:00
Eric Bailey 332c990404 Fix native error 2024-02-14 20:46:17 -06:00
Eric Bailey 46d95350fb Hook up data to ReportDialog 2024-02-14 20:35:26 -06:00
Eric Bailey 5587fe73cb Update invalidation 2024-02-14 19:08:23 -06:00
Eric Bailey 7313e70ca7 Hook up data 2024-02-14 19:05:42 -06:00
Paul Frazee cc635ae949 More work on the mod debug screen 2024-02-13 20:13:18 -08:00
Eric Bailey 46076c4afb Some validation, fixes 2024-02-13 18:00:41 -06:00
Eric Bailey da735f4894 Rough enablement 2024-02-13 15:40:52 -06:00
Eric Bailey 78acf4c86e Rough enablement 2024-02-13 14:40:13 -06:00
Eric Bailey 9f028512e6 Handle subscription 2024-02-13 14:02:32 -06:00
Eric Bailey 02bbc5e0d5 Hook up data to settings page 2024-02-13 14:02:32 -06:00
Eric Bailey 6f5058d474 Abstract ModerationServiceCard 2024-02-13 14:02:32 -06:00
Eric Bailey 06ede19066 Update atoms values 2024-02-13 14:02:32 -06:00
Paul Frazee 4e9eed5389 Fix types 2024-02-13 11:21:25 -08:00
Eric Bailey 95707a1511 Rough sketch 2024-02-12 18:48:11 -06:00
Eric Bailey 53da8d7714 Migrate everything 2024-02-12 17:45:07 -06:00
Eric Bailey 65582457b7 Migrate buttons 2024-02-12 16:27:24 -06:00
Eric Bailey a289b616b2 Move moderation screen 2024-02-12 15:48:17 -06:00
Eric Bailey 9075c501ec Merge remote-tracking branch 'origin/main' into 3p-moderators
* origin/main: (33 commits)
  Improved server selector during account creation and signin (#2840)
  remove line height from text input for composer on ios (#2844)
  Add follow button to highlighted post (#2828)
  Cleaning up old codes pt_BR (#2809)
  Fix wrong translations in zh-CN localization  (#2812)
  Adjust Japanese translation (#2814)
  Update ja messages.po (#2821)
  change follow to follow all when !== 20 (#2831)
  ios adult content link fix 🤦 (#2845)
  improves build.md completeness (#2835)
  increase stroke width for active hashtag icon (#2829)
  Update `blueskyweb.xyz` links to `bsky.social` (#2830)
  eas nightlies (#2826)
  Always show post dropdown button at the bottom of the post, add share button to highlighted post (#2646)
  Measure header layout reliably (#2817)
  Round line height (#2824)
  Design system tweaks (#2822)
  Fix layout calculations (#2816)
  Fix flashes and jumps when opening profile (#2815)
  Show text cursor on web bio (#2813)
  ...
2024-02-12 15:43:33 -06:00
Eric Bailey 937b66e3b5 Integrate updates 2024-02-08 18:53:49 -06:00
Eric Bailey 9b5f80be7e Submit 2024-02-08 18:52:11 -06:00
Eric Bailey e45cc6f774 Report dialog step 2 2024-02-08 18:52:11 -06:00
Eric Bailey f71cccd620 Add first ReportDialog screen 2024-02-08 18:52:11 -06:00
Eric Bailey b6552e9a84 Add example 2024-02-08 18:52:11 -06:00
Eric Bailey 97572fd5a2 Memoize 2024-02-08 18:52:11 -06:00
Paul Frazee 667729f7db Clean up and reorganize localized moderation strings 2024-02-08 16:40:50 -08:00
Eric Bailey fe20b4d575 Add note 2024-02-08 13:29:08 -06:00
Eric Bailey 4edbcd2b94 Dialog solution 2024-02-08 13:15:14 -06:00
Paul Frazee 15d0f2e90b Add (WIP) debug mod page 2024-02-07 21:26:28 -08:00
Eric Bailey 270476a126 Remove log 2024-02-07 22:17:07 -06:00
Eric Bailey 5bd64fbc85 Dialogs side quest 2024-02-07 22:11:59 -06:00
Eric Bailey f4ce9e22e8 Fix scroll view 2024-02-07 20:16:37 -06:00
Eric Bailey fabf6809fc Add all groups 2024-02-07 20:14:23 -06:00
Eric Bailey e0061056dc Clean up 2024-02-07 19:28:31 -06:00
Eric Bailey 97bfa40ce3 Complete header 2024-02-07 19:28:31 -06:00
Eric Bailey 9a57155339 Header 2024-02-07 19:28:31 -06:00
Eric Bailey 8e1575f5ba Subheader 2024-02-07 19:28:31 -06:00
Eric Bailey af5b545e25 Reorg, add icon 2024-02-07 19:28:31 -06:00
Eric Bailey 5998b34471 Not found state 2024-02-07 19:28:31 -06:00
Paul Frazee dd56beeebb Add label & label group descriptions 2024-02-07 14:44:23 -08:00
Paul Frazee 981fc67138 Remove ununsed constants 2024-02-07 11:56:42 -08:00
Paul Frazee c0771deb9b Drop the guidelines for now 2024-02-07 11:56:42 -08:00
Paul Frazee 81820479f3 Add modservice screen and profile-header-card 2024-02-07 11:56:39 -08:00
306 changed files with 21090 additions and 56266 deletions
+1 -82
View File
@@ -1,5 +1,3 @@
const bskyEslint = require('./eslint')
module.exports = {
root: true,
extends: [
@@ -9,85 +7,10 @@ module.exports = {
'prettier',
],
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
'detox',
'react',
'lingui',
'simple-import-sort',
'bsky-internal',
],
plugins: ['@typescript-eslint', 'detox', 'react', 'lingui'],
rules: {
// Temporary until https://github.com/facebook/react-native/pull/43756 gets into a release.
'prettier/prettier': 0,
'react/no-unescaped-entities': 0,
'react-native/no-inline-styles': 0,
'bsky-internal/avoid-unwrapped-text': [
'error',
{
impliedTextComponents: [
'Button', // TODO: Not always safe.
'ButtonText',
'DateField.Label',
'Description',
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'InlineLink',
'Label',
'P',
'Prompt.Title',
'Prompt.Description',
'Prompt.Cancel', // TODO: Not always safe.
'Prompt.Action', // TODO: Not always safe.
'TextField.Label',
'TextField.Suffix',
'Title',
'Toggle.Label',
'ToggleButton.Button', // TODO: Not always safe.
],
impliedTextProps: ['FormContainer title'],
},
],
'simple-import-sort/imports': [
'warn',
{
groups: [
// Side effect imports.
['^\\u0000'],
// Node.js builtins prefixed with `node:`.
['^node:'],
// Packages.
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
// React/React Native priortized, followed by expo
// Followed by all packages excluding unprefixed relative ones
[
'^(react\\/(.*)$)|^(react$)|^(react-native(.*)$)',
'^(expo(.*)$)|^(expo$)',
'^(?!(?:alf|components|lib|locale|logger|platform|screens|state|view)(?:$|\\/))@?\\w',
],
// Relative imports.
// Ideally, anything that starts with a dot or #
// due to unprefixed relative imports being used, we whitelist the relative paths we use
// (?:$|\\/) matches end of string or /
[
'^(?:#\\/)?(?:lib|state|logger|platform|locale)(?:$|\\/)',
'^(?:#\\/)?view(?:$|\\/)',
'^(?:#\\/)?screens(?:$|\\/)',
'^(?:#\\/)?alf(?:$|\\/)',
'^(?:#\\/)?components(?:$|\\/)',
'^#\\/',
'^\\.',
],
// anything else - hopefully we don't have any of these
['^'],
],
},
],
'simple-import-sort/exports': 'warn',
},
ignorePatterns: [
'**/__mocks__/*.ts',
@@ -108,8 +31,4 @@ module.exports = {
settings: {
componentWrapperFunctions: ['observer'],
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
},
}
+1 -36
View File
@@ -8,7 +8,6 @@ on:
type: choice
description: Build profile to use
options:
- testflight-android
- production
jobs:
@@ -60,41 +59,7 @@ jobs:
echo "$json" > google-services.json
- name: 🏗️ EAS Build
run: yarn use-build-number-with-bump eas build -p android --profile ${{ inputs.profile || 'testflight-android' }} --local --output build.aab --non-interactive
run: yarn use-build-number eas build -p android --profile production --local --output build.aab --non-interactive
- name: 🚀 Deploy
if: ${{ inputs.profile == 'production' }}
run: eas submit -p android --non-interactive --path build.aab
- name: ✍️ Rename bundle
if: ${{ inputs.profile != 'production' }}
run: mv build.aab build.apk
- name: ⏰ Get a timestamp
id: timestamp
if: ${{ inputs.profile != 'production' }}
uses: nanzm/get-time-action@master
with:
format: 'MM-DD-HH-mm-ss'
- name: 🚀 Upload Artifact
id: upload-artifact
if: ${{ inputs.profile != 'production' }}
uses: actions/upload-artifact@v4
with:
retention-days: 30
compression-level: 0
name: build-${{ steps.timestamp.outputs.time }}.apk
path: build.apk
- name: 🔔 Notify Slack
if: ${{ inputs.profile != 'production' }}
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
+3 -2
View File
@@ -2,13 +2,14 @@
name: Build and Submit iOS
on:
schedule:
- cron: '0 5 * * *'
workflow_dispatch:
inputs:
profile:
type: choice
description: Build profile to use
options:
- testflight
- production
jobs:
@@ -68,7 +69,7 @@ jobs:
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: 🏗️ EAS Build
run: yarn use-build-number-with-bump eas build -p ios --profile ${{ inputs.profile || 'testflight' }} --local --output build.ipa --non-interactive
run: yarn use-build-number eas build -p ios --profile production --local --output build.ipa --non-interactive
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
@@ -1,285 +0,0 @@
---
name: Bundle and Deploy EAS Update
on:
push:
branches:
- main
workflow_dispatch:
inputs:
channel:
type: choice
description: Deployment channel to use
options:
- testflight
- production
runtimeVersion:
type: string
description: Runtime version (in x.x.x format) that this update is for
required: true
jobs:
bundleDeploy:
name: Bundle and Deploy EAS Update
runs-on: ubuntu-latest
outputs:
fingerprint-is-different: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different }}
steps:
- name: Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
# Validate the version if one is supplied. This should generally happen if the update is for a production client
- name: 🧐 Validate version
if: ${{ inputs.runtimeVersion }}
run: |
if [ -z "${{ inputs.runtimeVersion }}" ]; then
[[ "${{ inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 100
- name: ⬇️ Fetch commits from base branch
if: ${{ github.ref != 'refs/heads/main' }}
run: git fetch origin main:main --depth 100
# This should get the current production release's commit's hash to see if the update is compatible
- name: 🕵️ Get the base commit
id: base-commit
run: |
if [ -z "${{ inputs.channel == 'production' }}" ]; then
echo base-commit=$(git show-ref -s ${{ inputs.runtimeVersion }}) >> "$GITHUB_OUTPUT"
else
echo base-commit=$(git log -n 1 --skip 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
fi
- name: ✓ Make sure we found a base commit
run: |
if [ -z "${{ steps.base-commit.outputs.base-commit }}" ]; then
echo "Could not find a base commit for this release. Exiting."
exit 1
fi
- name: 🔧 Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: yarn
- name: ⚙️ Install Dependencies
run: yarn install
# Run the fingerprint
- name: 📷 Check fingerprint
id: fingerprint
uses: expo/expo-github-action/fingerprint@main
with:
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
args:
- name: 👀 Debug fingerprint
id: fingerprint-debug
run: |
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
fingerprintDiff="${{ steps.fingerprint.outputs.fingerprint-diff }}"
if [[ $fingerprintDiff =~ "bareRncliAutolinking" || $fingerprintDiff =~ "expoAutolinkingAndroid" || $fingerprintDiff =~ "expoAutolinkingIos" ]]; then
echo fingerprint-is-different="true" >> "$GITHUB_OUTPUT"
else
echo fingerprint-is-different="false" >> "$GITHUB_OUTPUT"
fi
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
with:
expo-version: latest
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup Expo
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: yarn global add eas-cli-local-build-plugin
- name: 🪛 Setup jq
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
uses: dcarbone/install-jq-action@v2
- name: 🔤 Compile Translations
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: yarn intl:build
- name: ✏️ Write environment variables
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "$json" > google-services.json
- name: 🏗️ Create Bundle
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
- name: 📦 Package Bundle and 🚀 Deploy
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: yarn use-build-number bash scripts/bundleUpdate.sh
env:
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
# GitHub actions are horrible so let's just copy paste this in
buildIfNecessaryIOS:
name: Build and Submit iOS
runs-on: macos-14
needs: [bundleDeploy]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.fingerprint-is-different == 'true' }}
steps:
- name: Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
- name: 🔧 Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: yarn
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
with:
expo-version: latest
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup EAS local builds
run: yarn global add eas-cli-local-build-plugin
- name: ⚙️ Install dependencies
run: yarn install
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
with:
version: 1.14.3
- name: 💾 Cache Pods
uses: actions/cache@v3
id: pods-cache
with:
path: ./ios/Pods
# We'll use the yarn.lock for our hash since we don't yet have a Podfile.lock. Pod versions will not
# change unless the yarn version changes as well.
key: ${{ runner.os }}-pods-${{ hashFiles('yarn.lock') }}
- name: 🔤 Compile translations
run: yarn intl:build
- name: ✏️ Write environment variables
run: |
echo "${{ secrets.ENV_TOKEN }}" > .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
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
buildIfNecessaryAndroid:
name: Build and Submit Android
runs-on: ubuntu-latest
needs: [ bundleDeploy ]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.fingerprint-is-different == 'true' }}
steps:
- name: Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
- name: 🔧 Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: yarn
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
with:
expo-version: latest
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup EAS local builds
run: yarn global add eas-cli-local-build-plugin
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: ⚙️ Install dependencies
run: yarn install
- name: 🔤 Compile translations
run: yarn intl:build
- name: ✏️ Write environment variables
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .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
- name: ⏰ Get a timestamp
id: timestamp
uses: nanzm/get-time-action@master
with:
format: 'MM-DD-HH-mm-ss'
- name: 🚀 Upload Artifact
id: upload-artifact
uses: actions/upload-artifact@v4
with:
retention-days: 30
compression-level: 0
name: build-${{ steps.timestamp.outputs.time }}.apk
path: build.apk
- name: 🔔 Notify Slack
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
-2
View File
@@ -24,8 +24,6 @@ jobs:
attempt_delay: 2000
- name: Lint check
run: yarn lint
- name: Prettier check
run: yarn prettier --check .
- name: Check & compile i18n
run: yarn intl:build
- name: Type check
-297
View File
@@ -1,297 +0,0 @@
# Credit for fingerprint action https://github.com/expo/expo
# https://github.com/expo/expo/blob/main/.github/workflows/pr-labeler.yml
---
name: PR labeler
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
jobs:
webpack-analyzer:
runs-on: ubuntu-22.04
if: ${{ github.event_name == 'pull_request' }}
permissions:
pull-requests: write
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🔧 Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: yarn
- name: ⚙️ Install Dependencies
run: yarn install
- name: Ensure tracking relevant branches and checkout base
run: |
git checkout ${{ github.head_ref }}
git checkout ${{ github.base_ref }}
- name: Get the base commit
id: base-commit
run: echo base-commit=$(git log -n 1 ${{ github.base_ref }} --pretty=format:'%H') >> "$GITHUB_OUTPUT"
- name: Merge PR commit
run: |
# Have to set a git config for the merge to work
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git merge --no-edit ${{ github.head_ref }}
- name: 🔦 Generate stats file for PR
run: |
yarn generate-webpack-stats-file
mv stats.json ../stats-new.json
- name: ⬇️ Get base stats from cache
id: get-base-stats
uses: actions/cache@v4
with:
path: stats-base.json
key: stats-base-${{ steps.base-commit.outputs.base-commit }}
- name: Restore to base commit
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
run: |
git reset HEAD~
git restore .
- name: 🔦 Generate stats file from base commit
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
run: |
yarn generate-webpack-stats-file
mv stats.json stats-base.json
- name: Get diff
id: get-diff
uses: NejcZdovc/bundle-size-diff@v1
with:
base_path: 'stats-base.json'
pr_path: '../stats-new.json'
excluded_assets: '(.+).chunk.js|(.+).js.map|(.+).json|(.+).png'
- name: 🔍 Find old comment if it exists
uses: peter-evans/find-comment@v2
if: ${{ github.event_name == 'pull_request' }}
id: old_comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: <!-- webpack-analyzer comment -->
- name: 💬 Add comment with diff
uses: actions/github-script@v6
if: ${{ steps.old_comment.outputs.comment-id == '' }}
with:
script: |
const body = `<!-- webpack-analyzer comment -->
| Old size | New size | Diff |
|----------|----------|-----------------------|
| ${{ steps.get-diff.outputs.base_file_string }} | ${{ steps.get-diff.outputs.pr_file_string }} | ${{ steps.get-diff.outputs.diff_file_string }} (${{ steps.get-diff.outputs.percent }}%) |
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
- name: 💬 Update comment with fingerprint
if: ${{ steps.old_comment.outputs.comment-id != '' }}
uses: actions/github-script@v6
with:
script: |
const body = `<!-- webpack-analyzer comment -->
| Old size | New size | Diff |
|----------|----------|-----------------------|
| ${{ steps.get-diff.outputs.base_file_string }} | ${{ steps.get-diff.outputs.pr_file_string }} | ${{ steps.get-diff.outputs.diff_file_string }} (${{ steps.get-diff.outputs.percent }}%) |
`;
github.rest.issues.updateComment({
issue_number: context.issue.number,
comment_id: '${{ steps.old_comment.outputs.comment-id }}',
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
test-suite-fingerprint:
runs-on: ubuntu-22.04
if: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
# REQUIRED: limit concurrency when pushing main(default) branch to prevent conflict for this action to update its fingerprint database
concurrency: fingerprint-${{ github.event_name != 'pull_request' && 'main' || github.run_id }}
permissions:
# REQUIRED: Allow comments of PRs
pull-requests: write
# REQUIRED: Allow updating fingerprint in acton caches
actions: write
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 100
- name: ⬇️ Fetch commits from base branch
run: git fetch origin main:main --depth 100
if: github.event_name == 'pull_request'
- name: 🔧 Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: yarn
- name: ⚙️ Install Dependencies
run: yarn install
- name: Get the base commit
id: base-commit
run: echo base-commit=$(git log -n 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
- name: 📷 Check fingerprint
id: fingerprint
uses: expo/expo-github-action/fingerprint@main
with:
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
- name: 👀 Debug fingerprint
run: |
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
- name: 🏷️ Labeling PR
uses: actions/github-script@v6
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' }}
with:
script: |
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: ['bot: fingerprint changed']
})
} catch (e) {
if (e.status != 404) {
throw e;
}
}
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['bot: fingerprint compatible']
})
- name: 🏷️ Labeling PR
uses: actions/github-script@v6
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' }}
with:
script: |
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: ['bot: fingerprint compatible']
})
} catch (e) {
if (e.status != 404) {
throw e;
}
}
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['bot: fingerprint changed']
})
- name: 🔍 Find old comment if it exists
uses: peter-evans/find-comment@v2
if: ${{ github.event_name == 'pull_request' }}
id: old_comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: <!-- pr-labeler comment -->
- name: 💬 Add comment with fingerprint
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' && steps.old_comment.outputs.comment-id == '' }}
uses: actions/github-script@v6
with:
script: |
const diff = JSON.stringify(${{ steps.fingerprint.outputs.fingerprint-diff}}, null, 2);
const body = `<!-- pr-labeler comment -->
The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }}
<details><summary>Fingerprint diff</summary>
\`\`\`json
${diff}
\`\`\`
</details>
---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
- name: 💬 Update comment with fingerprint
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' && steps.old_comment.outputs.comment-id != '' }}
uses: actions/github-script@v6
with:
script: |
const diff = JSON.stringify(${{ steps.fingerprint.outputs.fingerprint-diff}}, null, 2);
const body = `<!-- pr-labeler comment -->
The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }}
<details><summary>Fingerprint diff</summary>
\`\`\`json
${diff}
\`\`\`
</details>
---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
`;
github.rest.issues.updateComment({
issue_number: context.issue.number,
comment_id: '${{ steps.old_comment.outputs.comment-id }}',
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
- name: 💬 Delete comment with fingerprint
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' && steps.old_comment.outputs.comment-id != '' }}
uses: actions/github-script@v6
with:
script: |
github.rest.issues.deleteComment({
issue_number: context.issue.number,
comment_id: '${{ steps.old_comment.outputs.comment-id }}',
owner: context.repo.owner,
repo: context.repo.repo,
});
+1 -9
View File
@@ -18,6 +18,7 @@ xcuserdata
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
# Android/IntelliJ
@@ -77,7 +78,6 @@ dist/
*.mobileprovision
*.orig.*
web-build/
stats.json
# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*
@@ -104,14 +104,6 @@ google-services.json
# Performance results (Flashlight)
.perf/
# ESLint
.eslintcache
# i18n
src/locale/locales/_build/
src/locale/locales/**/*.js
# local builds
*.apk
*.aab
*.ipa
+11 -13
View File
@@ -1,14 +1,12 @@
# Ignore everything except JS-ey code.
# Based on https://stackoverflow.com/a/70715829/458193
*
!**/*.js
!**/*.jsx
!**/*.ts
!**/*.tsx
!*/
# More specific ignores go below.
.expo
android
ios
src/locale/locales
android
src/third-party
src/app.json
public
/bskyweb/templates
/dist/
/.watchmanconfig
/app.json
web/index.html
web-build/*
+2 -2
View File
@@ -64,7 +64,7 @@ describe('Curate lists', () => {
await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Upload from Library')).tap()
await element(by.text('Library')).tap()
await sleep(3e3)
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
@@ -81,7 +81,7 @@ describe('Curate lists', () => {
await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Remove Avatar')).tap()
await element(by.text('Remove')).tap()
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await expect(element(by.id('userAvatarFallback'))).toExist()
+1 -1
View File
@@ -17,7 +17,7 @@ describe('Home screen', () => {
it('Can go to feeds page using feeds button in tab bar', async () => {
await element(by.id('homeScreenFeedTabs-Feeds ✨')).tap()
await expect(element(by.text('Discover New Feeds'))).toBeVisible()
await expect(element(by.text('Discover new feeds'))).toBeVisible()
})
it('Feeds button disappears after pinning a feed', async () => {
+4 -4
View File
@@ -70,10 +70,10 @@ describe('Profile screen', () => {
await element(by.id('profileHeaderEditProfileButton')).tap()
await expect(element(by.id('editProfileModal'))).toBeVisible()
await element(by.id('changeBannerBtn')).tap()
await element(by.text('Upload from Library')).tap()
await element(by.text('Library')).tap()
await sleep(3e3)
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Upload from Library')).tap()
await element(by.text('Library')).tap()
await sleep(3e3)
await element(by.id('editProfileSaveBtn')).tap()
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
@@ -87,9 +87,9 @@ describe('Profile screen', () => {
await element(by.id('profileHeaderEditProfileButton')).tap()
await expect(element(by.id('editProfileModal'))).toBeVisible()
await element(by.id('changeBannerBtn')).tap()
await element(by.text('Remove Banner')).tap()
await element(by.text('Remove')).tap()
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Remove Avatar')).tap()
await element(by.text('Remove')).tap()
await element(by.id('editProfileSaveBtn')).tap()
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
await expect(element(by.id('userBannerFallback'))).toExist()
@@ -12,7 +12,6 @@ describe('Create account', () => {
})
it('I can create a new account with text verification', async () => {
console.log('SERVICE IS', service)
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
@@ -29,18 +28,17 @@ describe('Create account', () => {
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('text-verification-test')
await device.takeScreenshot('5- entered handle')
await element(by.id('nextBtn')).tap()
await element(by.id('phoneInput')).typeText('8042221111')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('6- requested code')
await device.takeScreenshot('5- requested code')
await element(by.id('codeInput')).typeText('000000')
await device.takeScreenshot('7- entered code')
await device.takeScreenshot('6- entered code')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('text-verification-test')
await device.takeScreenshot('7- entered handle')
await element(by.id('nextBtn')).tap()
await expect(element(by.id('onboardingInterests'))).toBeVisible()
-38
View File
@@ -4,7 +4,6 @@ import {
linkRequiresWarning,
isPossiblyAUrl,
splitApexDomain,
isTrustedUrl,
} from '../../../src/lib/strings/url-helpers'
describe('linkRequiresWarning', () => {
@@ -75,10 +74,6 @@ describe('linkRequiresWarning', () => {
// bad uri inputs, default to true
['', '', true],
['example.com', 'example.com', true],
['/profile', 'Username', false],
['#', 'Show More', false],
['https://docs.bsky.app', 'https://docs.bsky.app', false],
['https://bsky.app/compose/intent?text=test', 'Compose a post', false],
]
it.each(cases)(
@@ -144,36 +139,3 @@ describe('splitApexDomain', () => {
},
)
})
describe('isTrustedUrl', () => {
const cases = [
['#', true],
['#profile', true],
['/', true],
['/profile', true],
['/profile/', true],
['/profile/bob.test', true],
['https://bsky.app', true],
['https://bsky.app/', true],
['https://bsky.app/profile/bob.test', true],
['https://www.bsky.app', true],
['https://www.bsky.app/', true],
['https://docs.bsky.app', true],
['https://bsky.social', true],
['https://bsky.social/blog', true],
['https://blueskyweb.xyz', true],
['https://blueskyweb.zendesk.com', true],
['http://bsky.app', true],
['http://bsky.social', true],
['http://blueskyweb.xyz', true],
['http://blueskyweb.zendesk.com', true],
['https://google.com', false],
['https://docs.google.com', false],
['https://google.com/#', false],
]
it.each(cases)('given input uri %p, returns %p', (str, expected) => {
const output = isTrustedUrl(str)
expect(output).toEqual(expected)
})
})
+9 -17
View File
@@ -41,9 +41,6 @@ module.exports = function (config) {
: process.env.BSKY_IOS_BUILD_NUMBER
const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
const UPDATES_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production'
return {
expo: {
@@ -125,20 +122,10 @@ module.exports = function (config) {
favicon: './assets/favicon.png',
},
updates: {
url: 'https://updates.bsky.app/manifest',
// TODO Eventually we want to enable this for all environments, but for now it will only be used for
// TestFlight builds
enabled: IS_TESTFLIGHT,
fallbackToCacheTimeout: 30000,
codeSigningCertificate: './code-signing/certificate.pem',
codeSigningMetadata: {
keyid: 'main',
alg: 'rsa-v1_5-sha256',
},
checkAutomatically: 'NEVER',
channel: UPDATES_CHANNEL,
enabled: true,
fallbackToCacheTimeout: 1000,
url: 'https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302',
},
assetBundlePatterns: ['**/*'],
plugins: [
'expo-localization',
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
@@ -158,6 +145,12 @@ module.exports = function (config) {
},
},
],
[
'expo-updates',
{
username: 'blueskysocial',
},
],
[
'expo-notifications',
{
@@ -168,7 +161,6 @@ module.exports = function (config) {
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
'./plugins/shareExtension/withShareExtensions.js',
].filter(Boolean),
extra: {
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8 2a1 1 0 0 1 1 1v1h6V3a1 1 0 1 1 2 0v1h2a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2V3a1 1 0 0 1 1-1ZM5 6v3h14V6H5Zm14 5H5v8h14v-8Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 296 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4.568 4h14.864c.252 0 .498 0 .706.017.229.019.499.063.77.201a2 2 0 0 1 .874.874c.138.271.182.541.201.77.017.208.017.454.017.706v10.864c0 .252 0 .498-.017.706a2.022 2.022 0 0 1-.201.77 2 2 0 0 1-.874.874 2.022 2.022 0 0 1-.77.201c-.208.017-.454.017-.706.017H4.568c-.252 0-.498 0-.706-.017a2.022 2.022 0 0 1-.77-.201 2 2 0 0 1-.874-.874 2.022 2.022 0 0 1-.201-.77C2 17.93 2 17.684 2 17.432V6.568c0-.252 0-.498.017-.706.019-.229.063-.499.201-.77a2 2 0 0 1 .874-.874c.271-.138.541-.182.77-.201C4.07 4 4.316 4 4.568 4Zm.456 2L12 11.708 18.976 6H5.024ZM20 7.747l-6.733 5.509a2 2 0 0 1-2.534 0L4 7.746V17.4a8.187 8.187 0 0 0 .011.589h.014c.116.01.278.011.575.011h14.8a8.207 8.207 0 0 0 .589-.012v-.013c.01-.116.011-.279.011-.575V7.747Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 871 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7 7a5 5 0 0 1 10 0v2h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h1V7Zm-1 4v9h12v-9H6Zm9-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 327 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M15.586 2.5a2 2 0 0 1 2.828 0L21.5 5.586a2 2 0 0 1 0 2.828l-13 13A2 2 0 0 1 7.086 22H3a1 1 0 0 1-1-1v-4.086a2 2 0 0 1 .586-1.414l13-13ZM17 3.914l-13 13V20h3.086l13-13L17 3.914ZM13 21a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 373 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" stroke="#000" stroke-linejoin="round" d="M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12a3.5 3.5 0 0 1 1.75-3.032.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z"/></svg>

Before

Width:  |  Height:  |  Size: 768 B

+7 -5
View File
@@ -5,14 +5,16 @@
}
.container {
position: relative;
overflow: hidden;
width: 100vw;
height: 100vh;
width: 100%;
height: 0;
padding-bottom: 56.25%;
}
.video {
position: absolute;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
<div class="container"><div class="video" id="player"></div></div>
+2 -2
View File
@@ -44,7 +44,7 @@
scrollbar-gutter: stable both-edges;
}
html, body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
@@ -141,7 +141,7 @@
/* ProseMirror */
.ProseMirror {
font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
min-height: 140px;
}
.ProseMirror-dark {
-18
View File
@@ -1,18 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIC0TCCAbmgAwIBAgIJcMN2yt5KNDqTMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV
BAMTB0JsdWVza3kwHhcNMjQwMzE0MDA1OTU4WhcNMzQwMzE0MDA1OTU4WjASMRAw
DgYDVQQDEwdCbHVlc2t5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
izSAWEc3wRoa3eTBEh/kE9pH0d6jhEGw9GrYfei60MHT1pSq2cTdyUM1yUZchAeW
gFFtqFxX0pfIZQyMlIZbjkaOxOqzWhB0aCsxngnhbSahFwRxkVwTAuonhqIpaLBL
hrCCCQ2IfZUpy8QeasqlTlmvmijuCC34fXxJlxNcj8SqzIZi+civ7U5PMPfIMMnD
tCDIBy1vxMk57m25X2ikcWUFW64qNVLkFAL36xEnmFTL4Ivqpz23gUcUIe1zbesY
jAgDtlwnAE7mU3oagCUDcSuOveT4POhT35Xp3Y/07I68kmXtrPxwd5k0L0zbisEm
poKZ87E2X29BitihicMpBwIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0l
AQH/BAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQADggEBAED1gdMF0yr8Gy87
RgyaeVpPySwSsO0selmXXrcmOWgiPA05lubyhFEa4P5kdzBEByG2MT+pJkjGYpvK
XRnqXM5VvdS2RhYYFH0cFOIUqBKwCnzViCMuGQeoGUx4oPcKFS0PQ1WjW2d4pS75
51GBfB6LOepsCHUG0A9XEk7EAyUWc4M2ITCJsTtJh8CVn2pTks2q14ETDs86YQv4
peDaJv8nhIe8oQkeGn2o/P/ctkwJg/uBydQUsWgjjGTQZTilVjGTW1mwDr9FucAE
d5gKIk4rtR/3Zd/NDdqp8PrkoWeVM7Hwr789/mpUOeqa/j7YNkDYQh7x+M/odd1D
KY0bQEQ=
-----END CERTIFICATE-----
+4 -30
View File
@@ -16,20 +16,14 @@
"ios": {
"simulator": true,
"resourceClass": "large"
},
"env": {
"EXPO_PUBLIC_ENV": "production"
}
},
"preview": {
"extends": "base",
"distribution": "internal",
"channel": "production",
"channel": "preview",
"ios": {
"resourceClass": "large"
},
"env": {
"EXPO_PUBLIC_ENV": "production"
}
},
"production": {
@@ -41,12 +35,9 @@
"android": {
"autoIncrement": true
},
"channel": "production",
"env": {
"EXPO_PUBLIC_ENV": "production"
}
"channel": "production"
},
"testflight": {
"github": {
"extends": "base",
"ios": {
"autoIncrement": true
@@ -54,24 +45,7 @@
"android": {
"autoIncrement": true
},
"channel": "testflight",
"env": {
"EXPO_PUBLIC_ENV": "testflight"
}
},
"testflight-android": {
"extends": "base",
"distribution": "internal",
"ios": {
"autoIncrement": true
},
"android": {
"autoIncrement": true
},
"channel": "testflight",
"env": {
"EXPO_PUBLIC_ENV": "testflight"
}
"channel": "production"
}
},
"submit": {
@@ -1,423 +0,0 @@
const {RuleTester} = require('eslint')
const avoidUnwrappedText = require('../avoid-unwrapped-text')
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 6,
sourceType: 'module',
},
})
describe('avoid-unwrapped-text', () => {
const tests = {
valid: [
{
code: `
<Text>
foo
</Text>
`,
},
{
code: `
<Text>
<Trans>
foo
</Trans>
</Text>
`,
},
{
code: `
<Text>
<>
foo
</>
</Text>
`,
},
{
code: `
<Text>
{foo && <Trans>foo</Trans>}
</Text>
`,
},
{
code: `
<Text>
{foo ? <Trans>foo</Trans> : <Trans>bar</Trans>}
</Text>
`,
},
{
code: `
<Trans>
<Text>
foo
</Text>
</Trans>
`,
},
{
code: `
<Trans>
{foo && <Text>foo</Text>}
</Trans>
`,
},
{
code: `
<Trans>
{foo ? <Text>foo</Text> : <Text>bar</Text>}
</Trans>
`,
},
{
code: `
<CustomText>
foo
</CustomText>
`,
},
{
code: `
<CustomText>
<Trans>
foo
</Trans>
</CustomText>
`,
},
{
code: `
<Text>
{bar}
</Text>
`,
},
{
code: `
<View>
{bar}
</View>
`,
},
{
code: `
<Text>
foo {bar}
</Text>
`,
},
{
code: `
<View>
<Text>
foo
</Text>
</View>
`,
},
{
code: `
<View>
<Text>
{bar}
</Text>
</View>
`,
},
{
code: `
<View>
<Text>
foo {bar}
</Text>
</View>
`,
},
{
code: `
<View>
<CustomText>
foo
</CustomText>
</View>
`,
},
{
code: `
<View prop={
<Text>foo</Text>
}>
<Bar />
</View>
`,
},
{
code: `
<View prop={
foo && <Text>foo</Text>
}>
<Bar />
</View>
`,
},
{
code: `
<View prop={
foo ? <Text>foo</Text> : <Text>bar</Text>
}>
<Bar />
</View>
`,
},
{
code: `
<View prop={
<Trans><Text>foo</Text></Trans>
}>
<Bar />
</View>
`,
},
{
code: `
<View prop={
<Text><Trans>foo</Trans></Text>
}>
<Bar />
</View>
`,
},
{
code: `
<Foo propText={
<Trans>foo</Trans>
}>
<Bar />
</Foo>
`,
},
{
code: `
<Foo propText={
foo && <Trans>foo</Trans>
}>
<Bar />
</Foo>
`,
},
{
code: `
<Foo propText={
foo ? <Trans>foo</Trans> : <Trans>bar</Trans>
}>
<Bar />
</Foo>
`,
},
],
invalid: [
{
code: `
<View> </View>
`,
errors: 1,
},
{
code: `
<View>
foo
</View>
`,
errors: 1,
},
{
code: `
<View>
<>
foo
</>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>
foo
</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
{foo && <Trans>foo</Trans>}
</View>
`,
errors: 1,
},
{
code: `
<View>
{foo ? <Trans>foo</Trans> : <Trans>bar</Trans>}
</View>
`,
errors: 2,
},
{
code: `
<Trans>
<View>
foo
</View>
</Trans>
`,
errors: 1,
},
{
code: `
<View>
foo {bar}
</View>
`,
errors: 1,
},
{
code: `
<View>
<View>
foo
</View>
</View>
`,
errors: 1,
},
{
code: `
<Text>
<View>
foo
</View>
</Text>
`,
errors: 1,
},
{
code: `
<Text prop={
<View>foo</View>
}>
<Bar />
</Text>
`,
errors: 1,
},
{
code: `
<Text prop={
foo && <View>foo</View>
}>
<Bar />
</Text>
`,
errors: 1,
},
{
code: `
<Text prop={
foo ? <View>foo</View> : <View>bar</View>
}>
<Bar />
</Text>
`,
errors: 2,
},
{
code: `
<Foo prop={
<Trans>foo</Trans>
}>
<Bar />
</Foo>
`,
errors: 1,
},
],
}
// For easier local testing
if (!process.env.CI) {
let only = []
let skipped = []
;[...tests.valid, ...tests.invalid].forEach(t => {
if (t.skip) {
delete t.skip
skipped.push(t)
}
if (t.only) {
delete t.only
only.push(t)
}
})
const predicate = t => {
if (only.length > 0) {
return only.indexOf(t) !== -1
}
if (skipped.length > 0) {
return skipped.indexOf(t) === -1
}
return true
}
tests.valid = tests.valid.filter(predicate)
tests.invalid = tests.invalid.filter(predicate)
}
ruleTester.run('avoid-unwrapped-text', avoidUnwrappedText, tests)
})
-111
View File
@@ -1,111 +0,0 @@
'use strict'
// Partially based on eslint-plugin-react-native.
// Portions of code by Alex Zhukov, MIT license.
function hasOnlyLineBreak(value) {
return /^[\r\n\t\f\v]+$/.test(value.replace(/ /g, ''))
}
function getTagName(node) {
const reversedIdentifiers = []
if (
node.type === 'JSXElement' &&
node.openingElement.type === 'JSXOpeningElement'
) {
let object = node.openingElement.name
while (object.type === 'JSXMemberExpression') {
if (object.property.type === 'JSXIdentifier') {
reversedIdentifiers.push(object.property.name)
}
object = object.object
}
if (object.type === 'JSXIdentifier') {
reversedIdentifiers.push(object.name)
}
}
return reversedIdentifiers.reverse().join('.')
}
exports.create = function create(context) {
const options = context.options[0] || {}
const impliedTextProps = options.impliedTextProps ?? []
const impliedTextComponents = options.impliedTextComponents ?? []
const textProps = [...impliedTextProps]
const textComponents = ['Text', ...impliedTextComponents]
return {
JSXText(node) {
if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value)) {
return
}
let parent = node.parent
while (parent) {
if (parent.type === 'JSXElement') {
const tagName = getTagName(parent)
if (textComponents.includes(tagName) || tagName.endsWith('Text')) {
// We're good.
return
}
if (tagName === 'Trans') {
// Skip over it and check above.
// TODO: Maybe validate that it's present.
parent = parent.parent
continue
}
let message = 'Wrap this string in <Text>.'
if (tagName !== 'View') {
message +=
' If <' +
tagName +
'> is guaranteed to render <Text>, ' +
'rename it to <' +
tagName +
'Text> or add it to impliedTextComponents.'
}
context.report({
node,
message,
})
return
}
if (
parent.type === 'JSXAttribute' &&
parent.name.type === 'JSXIdentifier' &&
parent.parent.type === 'JSXOpeningElement' &&
parent.parent.parent.type === 'JSXElement'
) {
const tagName = getTagName(parent.parent.parent)
const propName = parent.name.name
if (
textProps.includes(tagName + ' ' + propName) ||
propName === 'text' ||
propName.endsWith('Text')
) {
// We're good.
return
}
const message =
'Wrap this string in <Text>.' +
' If `' +
propName +
'` is guaranteed to be wrapped in <Text>, ' +
'rename it to `' +
propName +
'Text' +
'` or add it to impliedTextProps.'
context.report({
node,
message,
})
return
}
parent = parent.parent
continue
}
},
}
}
-7
View File
@@ -1,7 +0,0 @@
'use strict'
module.exports = {
rules: {
'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
},
}
-2
View File
@@ -88,5 +88,3 @@ jest.mock('sentry-expo', () => ({
ReactNavigationInstrumentation: jest.fn(),
},
}))
jest.mock('crypto', () => ({}))
+3 -33
View File
@@ -72,7 +72,6 @@ export async function createServer(
const phoneParams = phoneRequired
? {
phoneVerificationRequired: true,
phoneVerificationProvider: 'twilio',
twilioAccountSid: 'ACXXXXXXX',
twilioAuthToken: 'AUTH',
twilioServiceSid: 'VAXXXXXXXX',
@@ -96,35 +95,6 @@ export async function createServer(
})
mockTwilio(testNet.pds)
// add the test mod authority
if (!phoneRequired) {
const agent = new BskyAgent({service: pdsUrl})
const res = await agent.api.com.atproto.server.createAccount({
email: 'mod-authority@test.com',
handle: 'mod-authority.test',
password: 'hunter2',
})
agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
await agent.api.app.bsky.actor.profile.create(
{repo: res.data.did},
{
displayName: 'Dev-env Moderation',
description: `The pretend version of mod.bsky.app`,
},
)
await agent.api.app.bsky.labeler.service.create(
{repo: res.data.did, rkey: 'self'},
{
policies: {
labelValues: ['!hide', '!warn'],
labelValueDefinitions: [],
},
createdAt: new Date().toISOString(),
},
)
}
const pic = fs.readFileSync(
path.join(__dirname, '..', 'assets', 'default-avatar.png'),
)
@@ -485,13 +455,13 @@ async function getPort(start = 3000) {
}
export const mockTwilio = (pds: TestPds) => {
if (!pds.ctx.phoneVerifier) return
if (!pds.ctx.twilio) return
pds.ctx.phoneVerifier.sendCode = async (_number: string) => {
pds.ctx.twilio.sendCode = async (_number: string) => {
// do nothing
}
pds.ctx.phoneVerifier.verifyCode = async (_number: string, code: string) => {
pds.ctx.twilio.verifyCode = async (_number: string, code: string) => {
return code === '000000'
}
}
+2 -6
View File
@@ -2,22 +2,18 @@
module.exports = {
locales: [
'en',
'ca',
'de',
'es',
'fi',
'fr',
'ga',
'hi',
'id',
'it',
'ja',
'ko',
'pt-BR',
'tr',
'uk',
'ca',
'zh-CN',
'zh-TW',
'it',
],
catalogs: [
{
@@ -0,0 +1,61 @@
# React Native UITextView
Drop in replacement for `<Text>` that renders a `UITextView`, support selection and native translation features on iOS.
## Installation
In this project, no installation is required. The pod will be installed automatically during a `pod install`.
In another project, clone the repo and copy the `modules/react-native-ui-text-view` directory to your own project
directory. Afterward, run `pod install`.
## Usage
Replace the outermost `<Text>` with `<UITextView>`. Styles and press events should be handled the same way they would
with `<Text>`. Both `<UITextView>` and `<Text>` are supported as children of the root `<UITextView>`.
## Technical
React Native's `Text` component allows for "infinite" nesting of further `Text` components. To make a true "drop-in",
we want to do the same thing.
To achieve this, we first need to handle determining if we are dealing with an ancestor or root `UITextView` component.
We can implement similar logic to the `Text` component [see Text.js](https://github.com/facebook/react-native/blob/7f2529de7bc9ab1617eaf571e950d0717c3102a6/packages/react-native/Libraries/Text/Text.js).
We create a context that contains a boolean to tell us if we have already rendered the root `UITextView`. We also store
the root styles so that we can apply those styles if the ancestor `UITextView`s have not overwritten those styles.
All of our children are placed into `RNUITextView`, which is the main native view that will display the iOS `UITextView`.
We next map each child into the view. We have to be careful here to check if the child's `children` prop is a string. If
it is, that means we have encountered what was once an RN `Text` component. RN doesn't let us pass plain text as
children outside of `Text`, so we instead just pass the text into the `text` prop on `RNUITextViewChild`. We continue
down the tree, until we run out of children.
On the native side, we make use of the shadow view to calculate text container dimensions before the views are mounted.
We cannot simply set the `UITextView` text first, since React will not have properly measured the layout before this
occurs.
As for `Text` props, the following props are implemented:
- All accessibility props
- `allowFontScaling`
- `adjustsFontSizeToFit`
- `ellipsizeMode`
- `numberOfLines`
- `onLayout`
- `onPress`
- `onTextLayout`
- `selectable`
All `ViewStyle` props will apply to the root `UITextView`. Individual children will respect these `TextStyle` styles:
- `color`
- `fontSize`
- `fontStyle`
- `fontWeight`
- `fontVariant`
- `letterSpacing`
- `lineHeight`
- `textDecorationLine`
@@ -0,0 +1,3 @@
#import <React/RCTViewManager.h>
#import <React/RCTBridge.h>
#import <React/RCTBridge+Private.h>
@@ -0,0 +1,153 @@
class RNUITextView: UIView {
var textView: UITextView
@objc var numberOfLines: Int = 0 {
didSet {
textView.textContainer.maximumNumberOfLines = numberOfLines
}
}
@objc var selectable: Bool = true {
didSet {
textView.isSelectable = selectable
}
}
@objc var ellipsizeMode: String = "tail" {
didSet {
textView.textContainer.lineBreakMode = self.getLineBreakMode()
}
}
@objc var onTextLayout: RCTDirectEventBlock?
override init(frame: CGRect) {
if #available(iOS 16.0, *) {
textView = UITextView(usingTextLayoutManager: false)
} else {
textView = UITextView()
}
// Disable scrolling
textView.isScrollEnabled = false
// Remove all the padding
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
// Remove other properties
textView.isEditable = false
textView.backgroundColor = .clear
// Init
super.init(frame: frame)
self.clipsToBounds = true
// Add the view
addSubview(textView)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
tapGestureRecognizer.isEnabled = true
textView.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Resolves some animation issues
override func reactSetFrame(_ frame: CGRect) {
UIView.performWithoutAnimation {
super.reactSetFrame(frame)
}
}
func setText(string: NSAttributedString, size: CGSize, numberOfLines: Int) -> Void {
self.textView.frame.size = size
self.textView.textContainer.maximumNumberOfLines = numberOfLines
self.textView.attributedText = string
self.textView.selectedTextRange = nil
if let onTextLayout = self.onTextLayout {
var lines: [String] = []
textView.layoutManager.enumerateLineFragments(
forGlyphRange: NSRange(location: 0, length: textView.attributedText.length))
{ (rect, usedRect, textContainer, glyphRange, stop) in
let characterRange = self.textView.layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
let line = (self.textView.text as NSString).substring(with: characterRange)
lines.append(line)
}
onTextLayout([
"lines": lines
])
}
}
@IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void {
// If we find a child, then call onPress
if let child = getPressed(sender) {
if textView.selectedTextRange == nil, let onPress = child.onPress {
onPress(["": ""])
} else {
// Clear the selected text range if we are not pressing on a link
textView.selectedTextRange = nil
}
}
}
// Try to get the pressed segment
func getPressed(_ sender: UITapGestureRecognizer) -> RNUITextViewChild? {
let layoutManager = textView.layoutManager
var location = sender.location(in: textView)
// Remove the padding
location.x -= textView.textContainerInset.left
location.y -= textView.textContainerInset.top
// Get the index of the char
let charIndex = layoutManager.characterIndex(
for: location,
in: textView.textContainer,
fractionOfDistanceBetweenInsertionPoints: nil
)
var lastUpperBound: String.Index? = nil
for child in self.reactSubviews() {
if let child = child as? RNUITextViewChild, let childText = child.text {
let fullText = self.textView.attributedText.string
// We want to skip over the children we have already checked, otherwise we could run into
// collisions of similar strings (i.e. links that get shortened to the same hostname but
// different paths)
let range = fullText.range(of: childText, options: [], range: (lastUpperBound ?? String.Index(utf16Offset: 0, in: fullText) )..<fullText.endIndex)
if let lowerBound = range?.lowerBound, let upperBound = range?.upperBound {
let lowerOffset = lowerBound.utf16Offset(in: fullText)
let upperOffset = upperBound.utf16Offset(in: fullText)
if charIndex >= lowerOffset,
charIndex <= upperOffset
{
return child
} else {
lastUpperBound = upperBound
}
}
}
}
return nil
}
func getLineBreakMode() -> NSLineBreakMode {
switch self.ellipsizeMode {
case "head":
return .byTruncatingHead
case "middle":
return .byTruncatingMiddle
case "tail":
return .byTruncatingTail
case "clip":
return .byClipping
default:
return .byTruncatingTail
}
}
}
@@ -0,0 +1,4 @@
class RNUITextViewChild: UIView {
@objc var text: String?
@objc var onPress: RCTDirectEventBlock?
}
@@ -0,0 +1,56 @@
// We want all of our props to be available in the child's shadow view so we
// can create the attributed text before mount and calculate the needed size
// for the view.
class RNUITextViewChildShadow: RCTShadowView {
@objc var text: String = ""
@objc var color: UIColor = .black
@objc var fontSize: CGFloat = 16.0
@objc var fontStyle: String = "normal"
@objc var fontWeight: String = "normal"
@objc var letterSpacing: CGFloat = 0.0
@objc var lineHeight: CGFloat = 0.0
@objc var pointerEvents: NSString?
override func isYogaLeafNode() -> Bool {
return true
}
override func didSetProps(_ changedProps: [String]!) {
guard let superview = self.superview as? RNUITextViewShadow else {
return
}
if !YGNodeIsDirty(superview.yogaNode) {
superview.setAttributedText()
}
}
func getFontWeight() -> UIFont.Weight {
switch self.fontWeight {
case "bold":
return .bold
case "normal":
return .regular
case "100":
return .ultraLight
case "200":
return .ultraLight
case "300":
return .light
case "400":
return .regular
case "500":
return .medium
case "600":
return .semibold
case "700":
return .semibold
case "800":
return .bold
case "900":
return .heavy
default:
return .regular
}
}
}
@@ -0,0 +1,26 @@
#import <React/RCTViewManager.h>
@interface RCT_EXTERN_MODULE(RNUITextViewManager, RCTViewManager)
RCT_REMAP_SHADOW_PROPERTY(numberOfLines, numberOfLines, NSInteger)
RCT_REMAP_SHADOW_PROPERTY(allowsFontScaling, allowsFontScaling, BOOL)
RCT_EXPORT_VIEW_PROPERTY(numberOfLines, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(onTextLayout, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(ellipsizeMode, NSString)
RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL)
@end
@interface RCT_EXTERN_MODULE(RNUITextViewChildManager, RCTViewManager)
RCT_REMAP_SHADOW_PROPERTY(text, text, NSString)
RCT_REMAP_SHADOW_PROPERTY(color, color, UIColor)
RCT_REMAP_SHADOW_PROPERTY(fontSize, fontSize, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(fontStyle, fontStyle, NSString)
RCT_REMAP_SHADOW_PROPERTY(fontWeight, fontWeight, NSString)
RCT_REMAP_SHADOW_PROPERTY(letterSpacing, letterSpacing, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(lineHeight, lineHeight, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(pointerEvents, pointerEvents, NSString)
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
@end
@@ -0,0 +1,30 @@
@objc(RNUITextViewManager)
class RNUITextViewManager: RCTViewManager {
override func view() -> (RNUITextView) {
return RNUITextView()
}
@objc override static func requiresMainQueueSetup() -> Bool {
return true
}
override func shadowView() -> RCTShadowView {
// Pass the bridge to the shadow view
return RNUITextViewShadow(bridge: self.bridge)
}
}
@objc(RNUITextViewChildManager)
class RNUITextViewChildManager: RCTViewManager {
override func view() -> (RNUITextViewChild) {
return RNUITextViewChild()
}
@objc override static func requiresMainQueueSetup() -> Bool {
return true
}
override func shadowView() -> RCTShadowView {
return RNUITextViewChildShadow()
}
}
@@ -0,0 +1,152 @@
class RNUITextViewShadow: RCTShadowView {
// Props
@objc var numberOfLines: Int = 0 {
didSet {
if !YGNodeIsDirty(self.yogaNode) {
self.setAttributedText()
}
}
}
@objc var allowsFontScaling: Bool = true
var attributedText: NSAttributedString = NSAttributedString()
var frameSize: CGSize = CGSize()
var lineHeight: CGFloat = 0
var bridge: RCTBridge
init(bridge: RCTBridge) {
self.bridge = bridge
super.init()
// We need to set a custom measure func here to calculate the height correctly
YGNodeSetMeasureFunc(self.yogaNode) { node, width, widthMode, height, heightMode in
// Get the shadowview and determine the needed size to set
let shadowView = Unmanaged<RNUITextViewShadow>.fromOpaque(YGNodeGetContext(node)).takeUnretainedValue()
return shadowView.getNeededSize(maxWidth: width)
}
// Subscribe to ynamic type size changes
NotificationCenter.default.addObserver(
self,
selector: #selector(preferredContentSizeChanged(_:)),
name: UIContentSizeCategory.didChangeNotification,
object: nil
)
}
@objc func preferredContentSizeChanged(_ notification: Notification) {
self.setAttributedText()
}
// Returning true here will tell Yoga to not use flexbox and instead use our custom measure func.
override func isYogaLeafNode() -> Bool {
return true
}
// We should only insert children that are UITextView shadows
override func insertReactSubview(_ subview: RCTShadowView!, at atIndex: Int) {
if subview.isKind(of: RNUITextViewChildShadow.self) {
super.insertReactSubview(subview, at: atIndex)
}
}
// Every time the subviews change, we need to reformat and render the text.
override func didUpdateReactSubviews() {
self.setAttributedText()
}
// Whenever we layout, update the UI
override func layoutSubviews(with layoutContext: RCTLayoutContext) {
// Don't do anything if the layout is dirty
if(YGNodeIsDirty(self.yogaNode)) {
return
}
// Since we are inside the shadow view here, we have to find the real view and update the text.
self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in
guard let textView = viewRegistry?[self.reactTag] as? RNUITextView else {
return
}
textView.setText(string: self.attributedText, size: self.frameSize, numberOfLines: self.numberOfLines)
}
}
override func dirtyLayout() {
super.dirtyLayout()
YGNodeMarkDirty(self.yogaNode)
}
// Update the attributed text whenever changes are made to the subviews.
func setAttributedText() -> Void {
// Create an attributed string to store each of the segments
let finalAttributedString = NSMutableAttributedString()
self.reactSubviews().forEach { child in
guard let child = child as? RNUITextViewChildShadow else {
return
}
let scaledFontSize = self.allowsFontScaling ?
UIFontMetrics.default.scaledValue(for: child.fontSize) : child.fontSize
let font = UIFont.systemFont(ofSize: scaledFontSize, weight: child.getFontWeight())
// Set some generic attributes that don't need ranges
let attributes: [NSAttributedString.Key:Any] = [
.font: font,
.foregroundColor: child.color,
]
// Create the attributed string with the generic attributes
let string = NSMutableAttributedString(string: child.text, attributes: attributes)
// Set the paragraph style attributes if necessary. We can check this by seeing if the provided
// line height is not 0.0.
let paragraphStyle = NSMutableParagraphStyle()
if child.lineHeight != 0.0 {
// Whenever we change the line height for the text, we are also removing the DynamicType
// adjustment for line height. We need to get the multiplier and apply that to the
// line height.
let scaleMultiplier = scaledFontSize / child.fontSize
paragraphStyle.minimumLineHeight = child.lineHeight * scaleMultiplier
paragraphStyle.maximumLineHeight = child.lineHeight * scaleMultiplier
string.addAttribute(
NSAttributedString.Key.paragraphStyle,
value: paragraphStyle,
range: NSMakeRange(0, string.length)
)
// To calcualte the size of the text without creating a new UILabel or UITextView, we have
// to store this line height for later.
self.lineHeight = child.lineHeight
} else {
self.lineHeight = font.lineHeight
}
finalAttributedString.append(string)
}
self.attributedText = finalAttributedString
self.dirtyLayout()
}
// To create the needed size we need to:
// 1. Get the max size that we can use for the view
// 2. Calculate the height of the text based on that max size
// 3. Determine how many lines the text is, and limit that number if it exceeds the max
// 4. Set the frame size and return the YGSize. YGSize requires Float values while CGSize needs CGFloat
func getNeededSize(maxWidth: Float) -> YGSize {
let maxSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(MAXFLOAT))
let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil)
var totalLines = Int(ceil(textSize.height / self.lineHeight))
if self.numberOfLines != 0, totalLines > self.numberOfLines {
totalLines = self.numberOfLines
}
self.frameSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(totalLines) * self.lineHeight))
return YGSize(width: Float(self.frameSize.width), height: Float(self.frameSize.height))
}
}
@@ -0,0 +1,9 @@
{
"name": "react-native-ui-text-view",
"version": "0.1.0",
"description": "UITextView in React Native on iOS",
"main": "src/index",
"author": "haileyok",
"license": "MIT",
"homepage": "https://github.com/bluesky-social/social-app/modules/react-native-ui-text-view"
}
@@ -0,0 +1,42 @@
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
Pod::Spec.new do |s|
s.name = "react-native-ui-text-view"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.authors = package["author"]
s.platforms = { :ios => "11.0" }
s.source = { :git => ".git", :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm,swift}"
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
if respond_to?(:install_modules_dependencies, true)
install_modules_dependencies(s)
else
s.dependency "React-Core"
# Don't install the dependencies when we run `pod install` in the old architecture.
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
}
s.dependency "React-RCTFabric"
s.dependency "React-Codegen"
s.dependency "RCT-Folly"
s.dependency "RCTRequired"
s.dependency "RCTTypeSafety"
s.dependency "ReactCommon/turbomodule/core"
end
end
end
@@ -0,0 +1,76 @@
import React from 'react'
import {Platform, StyleSheet, TextProps, ViewStyle} from 'react-native'
import {RNUITextView, RNUITextViewChild} from './index'
const TextAncestorContext = React.createContext<[boolean, ViewStyle]>([
false,
StyleSheet.create({}),
])
const useTextAncestorContext = () => React.useContext(TextAncestorContext)
const textDefaults: TextProps = {
allowFontScaling: true,
selectable: true,
}
export function UITextView({style, children, ...rest}: TextProps) {
const [isAncestor, rootStyle] = useTextAncestorContext()
// Flatten the styles, and apply the root styles when needed
const flattenedStyle = React.useMemo(
() => StyleSheet.flatten([rootStyle, style]),
[rootStyle, style],
)
if (Platform.OS !== 'ios') {
throw new Error('UITextView is only available on iOS')
}
if (!isAncestor) {
return (
<TextAncestorContext.Provider value={[true, flattenedStyle]}>
<RNUITextView
{...textDefaults}
{...rest}
ellipsizeMode={rest.ellipsizeMode ?? rest.lineBreakMode ?? 'tail'}
style={[{flex: 1}, flattenedStyle]}
onPress={undefined} // We want these to go to the children only
onLongPress={undefined}>
{React.Children.toArray(children).map((c, index) => {
if (React.isValidElement(c)) {
return c
} else if (typeof c === 'string') {
return (
<RNUITextViewChild
key={index}
style={flattenedStyle}
text={c}
{...rest}
/>
)
}
})}
</RNUITextView>
</TextAncestorContext.Provider>
)
} else {
return (
<>
{React.Children.toArray(children).map((c, index) => {
if (React.isValidElement(c)) {
return c
} else if (typeof c === 'string') {
return (
<RNUITextViewChild
key={index}
style={flattenedStyle}
text={c}
{...rest}
/>
)
}
})}
</>
)
}
}
@@ -0,0 +1,42 @@
import {
requireNativeComponent,
UIManager,
Platform,
type ViewStyle,
TextProps,
} from 'react-native'
const LINKING_ERROR =
`The package 'react-native-ui-text-view' doesn't seem to be linked. Make sure: \n\n` +
Platform.select({ios: "- You have run 'pod install'\n", default: ''}) +
'- You rebuilt the app after installing the package\n' +
'- You are not using Expo Go\n'
export interface RNUITextViewProps extends TextProps {
children: React.ReactNode
style: ViewStyle[]
}
export interface RNUITextViewChildProps extends TextProps {
text: string
onTextPress?: (...args: any[]) => void
onTextLongPress?: (...args: any[]) => void
}
export const RNUITextView =
UIManager.getViewManagerConfig &&
UIManager.getViewManagerConfig('RNUITextView') != null
? requireNativeComponent<RNUITextViewProps>('RNUITextView')
: () => {
throw new Error(LINKING_ERROR)
}
export const RNUITextViewChild =
UIManager.getViewManagerConfig &&
UIManager.getViewManagerConfig('RNUITextViewChild') != null
? requireNativeComponent<RNUITextViewChildProps>('RNUITextViewChild')
: () => {
throw new Error(LINKING_ERROR)
}
export * from './UITextView'
+12 -24
View File
@@ -1,11 +1,10 @@
{
"name": "bsky.app",
"version": "1.76.0",
"version": "1.73.0",
"private": true,
"engines": {
"node": ">=18"
},
"packageManager": "yarn@1.22.19",
"scripts": {
"prepare": "is-ci || husky install",
"postinstall": "patch-package && yarn intl:compile",
@@ -14,12 +13,11 @@
"ios": "expo run:ios",
"web": "expo start --web",
"use-build-number": "./scripts/useBuildNumberEnv.sh",
"use-build-number-with-bump": "./scripts/useBuildNumberEnvWithBump.sh",
"build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/",
"build-all": "yarn intl:build && yarn use-build-number-with-bump eas build --platform all",
"build-ios": "yarn use-build-number-with-bump eas build -p ios",
"build-android": "yarn use-build-number-with-bump eas build -p android",
"build": "yarn use-build-number-with-bump eas build",
"build-all": "yarn intl:build && yarn use-build-number eas build --platform all",
"build-ios": "yarn use-build-number eas build -p ios",
"build-android": "yarn use-build-number eas build -p android",
"build": "yarn use-build-number eas build",
"start": "expo start --dev-client",
"start:prod": "expo start --dev-client --no-dev --minify",
"clean-cache": "rm -rf node_modules/.cache/babel-loader/*",
@@ -27,7 +25,7 @@
"test-watch": "NODE_ENV=test jest --watchAll",
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
"test-coverage": "NODE_ENV=test jest --coverage",
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src",
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
"typecheck": "tsc --project ./tsconfig.check.json",
"e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts",
"e2e:metro": "NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
@@ -39,20 +37,16 @@
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
"perf:measure": "NODE_ENV=test flashlight measure",
"intl:build": "yarn intl:extract && yarn intl:compile",
"intl:check": "yarn intl:extract && git diff-index -G'(^[^\\*# /])|(^#\\w)|(^\\s+[^\\*#/])' HEAD || (echo '\n⚠️ i18n detected un-extracted translations\n' && exit 1)",
"intl:extract": "lingui extract",
"intl:compile": "lingui compile",
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
"update-extensions": "bash scripts/updateExtensions.sh",
"export": "npx expo export",
"make-deploy-bundle": "bash scripts/bundleUpdate.sh",
"generate-webpack-stats-file": "EXPO_PUBLIC_GENERATE_STATS=1 yarn build-web",
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
"update-extensions": "scripts/updateExtensions.sh"
},
"dependencies": {
"@atproto/api": "^0.12.2",
"@atproto/api": "^0.11.1",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "https://github.com/bluesky-social/react-native-bottom-sheet.git#discord-fork-4.6.1",
"@emoji-mart/react": "^1.1.1",
"@expo/html-elements": "^0.4.2",
"@expo/webpack-config": "^19.0.0",
@@ -60,6 +54,7 @@
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-native-fontawesome": "^0.3.0",
"@gorhom/bottom-sheet": "^4.5.1",
"@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.6.4",
"@miblanchard/react-native-slider": "^2.3.1",
@@ -96,7 +91,6 @@
"@tiptap/pm": "^2.0.0-beta.220",
"@tiptap/react": "^2.0.0-beta.220",
"@tiptap/suggestion": "^2.0.0-beta.220",
"@types/invariant": "^2.2.37",
"@types/node": "^18.16.2",
"@zxing/text-encoding": "^0.9.0",
"array.prototype.findlast": "^1.2.3",
@@ -176,7 +170,7 @@
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "~3.29.0",
"react-native-svg": "14.1.0",
"react-native-uitextview": "^1.1.6",
"react-native-ui-text-view": "link:./modules/react-native-ui-text-view",
"react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.1",
"react-native-version-number": "^0.3.6",
@@ -236,13 +230,11 @@
"babel-preset-expo": "^10.0.0",
"detox": "^20.14.8",
"eslint": "^8.19.0",
"eslint-plugin-bsky-internal": "link:./eslint",
"eslint-plugin-detox": "^1.0.0",
"eslint-plugin-ft-flow": "^2.0.3",
"eslint-plugin-lingui": "^0.2.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"html-webpack-plugin": "^5.5.0",
"husky": "^8.0.3",
"is-ci": "^3.0.1",
@@ -260,7 +252,6 @@
"typescript": "^5.3.3",
"url-loader": "^4.1.1",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
},
@@ -318,9 +309,6 @@
]
},
"lint-staged": {
"*{.js,.jsx,.ts,.tsx}": [
"eslint --cache --fix",
"prettier --cache --write --ignore-unknown"
]
"*{.js,.jsx,.ts,.tsx}": "yarn eslint --fix"
}
}
@@ -3594,19 +3594,3 @@ index 19b61ff..04a9951 100644
PasteInput_compileSdkVersion=30
PasteInput_buildToolsVersion=30.0.2
PasteInput_targetSdkVersion=30
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m b/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
index e916023..0564d97 100644
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
@@ -22,6 +22,11 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
_backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_backedTextInputView.textInputDelegate = self;
+ // Disable inline predictions to prevent jank in the composer
+ if (@available(iOS 17.0, *)) {
+ _backedTextInputView.inlinePredictionType = UITextInlinePredictionTypeNo;
+ }
+
[self addSubview:_backedTextInputView];
}
+7 -55
View File
@@ -1,56 +1,8 @@
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
index 3f50f8c..ee47fa1 100644
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
@@ -33,7 +33,9 @@ import kotlin.coroutines.resumeWithException
// TODO(@bbarthec): rename to ExpoImagePicker
private const val moduleName = "ExponentImagePicker"
+
class ImagePickerModule : Module() {
+ private var isPickerOpen = false
override fun definition() = ModuleDefinition {
Name(moduleName)
@@ -129,6 +131,11 @@ class ImagePickerModule : Module() {
options: ImagePickerOptions
): Any {
return try {
+ if(isPickerOpen) {
+ return ImagePickerResponse(canceled = true)
+ }
+
+ isPickerOpen = true
var result = launchPicker(pickerLauncher)
if (
!options.allowsMultipleSelection &&
@@ -143,6 +150,8 @@ class ImagePickerModule : Module() {
mediaHandler.readExtras(result.data, options)
} catch (cause: OperationCanceledException) {
return ImagePickerResponse(canceled = true)
+ } finally {
+ isPickerOpen = false
}
}
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
index ff15c91..9763012 100644
index ff15c91..41aaf12 100644
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
@@ -5,12 +5,7 @@ import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.net.Uri
-import androidx.activity.result.PickVisualMediaRequest
-import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
-import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia
import expo.modules.imagepicker.ImagePickerOptions
-import expo.modules.imagepicker.MediaTypes
-import expo.modules.imagepicker.UNLIMITED_SELECTION
import expo.modules.imagepicker.getAllDataUris
import expo.modules.imagepicker.toMediaType
import expo.modules.kotlin.activityresult.AppContextActivityResultContract
@@ -26,51 +21,26 @@ import java.io.Serializable
@@ -26,51 +26,26 @@ import java.io.Serializable
* @see [androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents]
*/
internal class ImageLibraryContract(
@@ -60,7 +12,7 @@ index ff15c91..9763012 100644
private val contentResolver: ContentResolver
get() = appContextProvider.appContext.reactContext?.contentResolver
?: throw Exceptions.ReactContextLost()
override fun createIntent(context: Context, input: ImageLibraryContractOptions): Intent {
- val request = PickVisualMediaRequest.Builder()
- .setMediaType(
@@ -82,7 +34,7 @@ index ff15c91..9763012 100644
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("image/*")
if (input.options.allowsMultipleSelection) {
- val selectionLimit = input.options.selectionLimit
-
@@ -93,7 +45,7 @@ index ff15c91..9763012 100644
+ if(input.options.selectionLimit == 1) {
+ return intent
}
- if (selectionLimit > 1) {
- return PickMultipleVisualMedia(selectionLimit).createIntent(context, request)
- }
@@ -104,9 +56,9 @@ index ff15c91..9763012 100644
- }
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
- return PickVisualMedia().createIntent(context, request)
+ return intent
}
override fun parseResult(input: ImageLibraryContractOptions, resultCode: Int, intent: Intent?) =
-26
View File
@@ -1,26 +0,0 @@
diff --git a/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift b/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
index 189a5f5..8d5b8e6 100644
--- a/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
+++ b/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
@@ -68,13 +68,20 @@ public final class NewUpdate: Update {
processedAssets.append(asset)
}
+ // Instead of relying on various hacks to get the correct format for the specific
+ // platform on the backend, we can just add this little patch..
+ let dateFormatter = DateFormatter()
+ dateFormatter.locale = Locale(identifier: "en_US_POSIX")
+ dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
+ let date = dateFormatter.date(from:commitTime) ?? RCTConvert.nsDate(commitTime)!
+
return Update(
manifest: manifest,
config: config,
database: database,
updateId: uuid,
scopeKey: config.scopeKey,
- commitTime: RCTConvert.nsDate(commitTime),
+ commitTime: date,
runtimeVersion: runtimeVersion,
keep: true,
status: UpdateStatus.StatusPending,
-7
View File
@@ -1,7 +0,0 @@
# Expo-Updates Patch
This is a small patch to convert timestamp formats that are returned from the backend. Instead of relying on the
backend to return the correct format for a specific format (the format required on Android is not the same as on iOS)
we can just add this conversion in.
Don't remove unless we make changes on the backend to support both platforms.
+88 -15
View File
@@ -1,19 +1,92 @@
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
index b09e653..d290dab 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
@@ -198,6 +198,14 @@ - (void)refreshControlValueChanged
[self setCurrentRefreshingState:super.refreshing];
_refreshingProgrammatically = NO;
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
index 9dca6a5..090bda5 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
@@ -266,11 +266,10 @@ - (void)textViewDidChange:(__unused UITextView *)textView
+ if (@available(iOS 17.4, *)) {
+ if (_currentRefreshingState) {
+ UIImpactFeedbackGenerator *feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
+ [feedbackGenerator prepare];
+ [feedbackGenerator impactOccurred];
- (void)textViewDidChangeSelection:(__unused UITextView *)textView
{
- if (_lastStringStateWasUpdatedWith && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
+ if (![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
[self textViewDidChange:_backedTextInputView];
_ignoreNextTextInputCall = YES;
}
- _lastStringStateWasUpdatedWith = _backedTextInputView.attributedText;
[self textViewProbablyDidChangeSelection];
}
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
index 1f06b79..ab458f3 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
@@ -87,7 +87,7 @@ - (void)invalidateContentSize
return;
}
- CGSize maximumSize = self.layoutMetrics.frame.size;
+ CGSize maximumSize = self.layoutMetrics.contentFrame.size;
if (_maximumNumberOfLines == 1) {
maximumSize.width = CGFLOAT_MAX;
@@ -158,6 +158,8 @@ - (void)uiManagerWillPerformMounting
[attributedText insertAttributedString:propertyAttributedText atIndex:0];
}
+ [self postprocessAttributedText:attributedText];
+
NSAttributedString *newAttributedText;
if (![_previousAttributedText isEqualToAttributedString:attributedText]) {
// We have to follow `set prop` pattern:
@@ -191,6 +193,52 @@ - (void)uiManagerWillPerformMounting
}];
}
+- (void)postprocessAttributedText:(NSMutableAttributedString *)attributedText
+{
+ __block CGFloat maximumLineHeight = 0;
+
+ [attributedText enumerateAttribute:NSParagraphStyleAttributeName
+ inRange:NSMakeRange(0, attributedText.length)
+ options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+ usingBlock:^(NSParagraphStyle *paragraphStyle, __unused NSRange range, __unused BOOL *stop) {
+ if (!paragraphStyle) {
+ return;
+ }
+
+ maximumLineHeight = MAX(paragraphStyle.maximumLineHeight, maximumLineHeight);
+ }];
+
+ if (maximumLineHeight == 0) {
+ // `lineHeight` was not specified, nothing to do.
+ return;
+ }
+
if (_onRefresh) {
_onRefresh(nil);
}
+ __block CGFloat maximumFontLineHeight = 0;
+
+ [attributedText enumerateAttribute:NSFontAttributeName
+ inRange:NSMakeRange(0, attributedText.length)
+ options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+ usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) {
+ if (!font) {
+ return;
+ }
+
+ if (maximumFontLineHeight <= font.lineHeight) {
+ maximumFontLineHeight = font.lineHeight;
+ }
+ }];
+
+ if (maximumLineHeight < maximumFontLineHeight) {
+ return;
+ }
+
+ CGFloat baseLineOffset = maximumLineHeight / 2.0 - maximumFontLineHeight / 2.0;
+
+ [attributedText addAttribute:NSBaselineOffsetAttributeName
+ value:@(baseLineOffset)
+ range:NSMakeRange(0, attributedText.length)];
+}
+
#pragma mark -
- (NSAttributedString *)measurableAttributedText
+4 -4
View File
@@ -1,5 +1,5 @@
# RefreshControl Patch
# TextInput Patch
Patching `RCTRefreshControl.mm` temporarily to play an impact haptic on refresh when using iOS 17.4 or higher. Since
17.4, there has been a regression somewhere causing haptics to not play on iOS on refresh. Should monitor for an update
in the RN repo: https://github.com/facebook/react-native/issues/43388
Patching `RCTBaseTextShadowInput.mm` from https://github.com/facebook/react-native/pull/38359. This fixes some text
getting cut off inside the composer. This was merged in December, so we should be able to remove this patch when RN
ships the next release.
@@ -1,28 +0,0 @@
const {withStringsXml, AndroidConfig} = require('@expo/config-plugins')
module.exports = function withAndroidSplashScreenStatusBarTranslucentPlugin(
appConfig,
) {
return withStringsXml(appConfig, function (decoratedAppConfig) {
try {
decoratedAppConfig.modResults = AndroidConfig.Strings.setStringItem(
[
{
_: 'true',
$: {
name: 'expo_splash_screen_status_bar_translucent',
translatable: 'false',
},
},
],
decoratedAppConfig.modResults,
)
} catch (e) {
console.error(
`withAndroidSplashScreenStatusBarTranslucentPlugin failed`,
e,
)
}
return decoratedAppConfig
})
}
-104
View File
@@ -1,104 +0,0 @@
const crypto = require('crypto')
const fs = require('fs')
const fsp = fs.promises
const path = require('path')
const DIST_DIR = './dist'
const BUNDLES_DIR = '/_expo/static/js'
const IOS_BUNDLE_DIR = path.join(DIST_DIR, BUNDLES_DIR, '/ios')
const ANDROID_BUNDLE_DIR = path.join(DIST_DIR, BUNDLES_DIR, '/android')
const METADATA_PATH = path.join(DIST_DIR, '/metadata.json')
const DEST_DIR = './bundleTempDir'
// Weird, don't feel like figuring out _why_ it wants this
const METADATA = require(`../${METADATA_PATH}`)
const IOS_METADATA_ASSETS = METADATA.fileMetadata.ios.assets
const ANDROID_METADATA_ASSETS = METADATA.fileMetadata.android.assets
const getMd5 = async path => {
return new Promise(res => {
const hash = crypto.createHash('md5')
const rStream = fs.createReadStream(path)
rStream.on('data', data => {
hash.update(data)
})
rStream.on('end', () => {
res(hash.digest('hex'))
})
})
}
const moveFiles = async () => {
console.log('Making directory...')
await fsp.mkdir(DEST_DIR)
await fsp.mkdir(path.join(DEST_DIR, '/assets'))
console.log('Getting ios md5...')
const iosCurrPath = path.join(
IOS_BUNDLE_DIR,
(await fsp.readdir(IOS_BUNDLE_DIR))[0],
)
const iosMd5 = await getMd5(iosCurrPath)
const iosNewPath = `bundles/${iosMd5}.bundle`
console.log('Copying ios bundle...')
await fsp.cp(iosCurrPath, path.join(DEST_DIR, iosNewPath))
console.log('Getting android md5...')
const androidCurrPath = path.join(
ANDROID_BUNDLE_DIR,
(await fsp.readdir(ANDROID_BUNDLE_DIR))[0],
)
const androidMd5 = await getMd5(androidCurrPath)
const androidNewPath = `bundles/${androidMd5}.bundle`
console.log('Copying android bundle...')
await fsp.cp(androidCurrPath, path.join(DEST_DIR, androidNewPath))
const iosAssets = []
const androidAssets = []
console.log('Getting ios asset md5s and moving them...')
for (const asset of IOS_METADATA_ASSETS) {
const currPath = path.join(DIST_DIR, asset.path)
const md5 = await getMd5(currPath)
const withExtPath = `assets/${md5}.${asset.ext}`
iosAssets.push(withExtPath)
await fsp.cp(currPath, path.join(DEST_DIR, withExtPath))
}
console.log('Getting android asset md5s and moving them...')
for (const asset of ANDROID_METADATA_ASSETS) {
const currPath = path.join(DIST_DIR, asset.path)
const md5 = await getMd5(currPath)
const withExtPath = `assets/${md5}.${asset.ext}`
androidAssets.push(withExtPath)
await fsp.cp(currPath, path.join(DEST_DIR, withExtPath))
}
const result = {
version: 0,
bundler: 'metro',
fileMetadata: {
ios: {
bundle: iosNewPath,
assets: iosAssets,
},
android: {
bundle: androidNewPath,
assets: androidAssets,
},
},
}
console.log('Writing metadata...')
await fsp.writeFile(
path.join(DEST_DIR, 'metadata.json'),
JSON.stringify(result),
)
console.log('Finished!')
console.log('Metadata:', result)
}
moveFiles()
-29
View File
@@ -1,29 +0,0 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
rm -rf bundleTempDir
rm -rf bundle.tar.gz
echo "Creating tarball..."
node scripts/bundleUpdate.js
if [ -z "$RUNTIME_VERSION" ]; then
RUNTIME_VERSION=$(cat package.json | jq '.version' -r)
fi
cd bundleTempDir || exit
BUNDLE_VERSION=$(date +%s)
DEPLOYMENT_URL="https://updates.bsky.app/v1/upload?runtime-version=$RUNTIME_VERSION&bundle-version=$BUNDLE_VERSION&channel=$CHANNEL_NAME&ios-build-number=$BSKY_IOS_BUILD_NUMBER&android-build-number=$BSKY_ANDROID_VERSION_CODE"
tar czvf bundle.tar.gz ./*
echo "Deploying to $DEPLOYMENT_URL..."
curl -o - --form "bundle=@./bundle.tar.gz" --user "bsky:$DENIS_API_KEY" --basic "$DEPLOYMENT_URL"
cd ..
rm -rf bundleTempDir
rm -rf bundle.tar.gz
+6 -2
View File
@@ -1,7 +1,11 @@
#!/bin/bash
outputIos=$(eas build:version:get -p ios)
outputAndroid=$(eas build:version:get -p android)
BSKY_IOS_BUILD_NUMBER=${outputIos#*buildNumber - }
BSKY_ANDROID_VERSION_CODE=${outputAndroid#*versionCode - }
currentIosVersion=${outputIos#*buildNumber - }
currentAndroidVersion=${outputAndroid#*versionCode - }
BSKY_IOS_BUILD_NUMBER=$((currentIosVersion+1))
BSKY_ANDROID_VERSION_CODE=$((currentAndroidVersion+1))
bash -c "BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE $*"
-11
View File
@@ -1,11 +0,0 @@
#!/bin/bash
outputIos=$(eas build:version:get -p ios)
outputAndroid=$(eas build:version:get -p android)
currentIosVersion=${outputIos#*buildNumber - }
currentAndroidVersion=${outputAndroid#*versionCode - }
BSKY_IOS_BUILD_NUMBER=$((currentIosVersion+1))
BSKY_ANDROID_VERSION_CODE=$((currentAndroidVersion+1))
bash -c "BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE $*"
+87 -87
View File
@@ -1,53 +1,59 @@
import 'react-native-url-polyfill/auto'
import 'lib/sentry' // must be near top
import React, {useState, useEffect} from 'react'
import {RootSiblingParent} from 'react-native-root-siblings'
import * as SplashScreen from 'expo-splash-screen'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {PersistQueryClientProvider} from '@tanstack/react-query-persist-client'
import {
SafeAreaProvider,
initialWindowMetrics,
} from 'react-native-safe-area-context'
import 'view/icons'
import React, {useEffect, useState} from 'react'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {RootSiblingParent} from 'react-native-root-siblings'
import {
initialWindowMetrics,
SafeAreaProvider,
} from 'react-native-safe-area-context'
import * as SplashScreen from 'expo-splash-screen'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {init as initPersistedState} from '#/state/persisted'
import * as persisted from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {useOTAUpdates} from 'lib/hooks/useOTAUpdates'
import {useNotificationsListener} from 'lib/notifications/notifications'
import {QueryProvider} from 'lib/react-query'
import {s} from 'lib/styles'
import {listenSessionDropped} from './state/events'
import {ThemeProvider} from 'lib/ThemeContext'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {s} from 'lib/styles'
import {Shell} from 'view/shell'
import * as notifications from 'lib/notifications/notifications'
import * as Toast from 'view/com/util/Toast'
import {
queryClient,
asyncStoragePersister,
dehydrateOptions,
} from 'lib/react-query'
import {TestCtrls} from 'view/com/testing/TestCtrls'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import I18nProvider from './locale/i18nProvider'
import {
Provider as SessionProvider,
useSession,
useSessionApi,
} from 'state/session'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import {TestCtrls} from 'view/com/testing/TestCtrls'
import * as Toast from 'view/com/util/Toast'
import {Shell} from 'view/shell'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted'
import {Splash} from '#/Splash'
import I18nProvider from './locale/i18nProvider'
import {listenSessionDropped} from './state/events'
import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {StatusBar} from 'expo-status-bar'
import {isAndroid} from 'platform/detection'
SplashScreen.preventAutoHideAsync()
@@ -56,12 +62,11 @@ function InnerApp() {
const {resumeSession} = useSessionApi()
const theme = useColorModeTheme()
const {_} = useLingui()
useIntentHandler()
useOTAUpdates()
// init
useEffect(() => {
notifications.init(queryClient)
listenSessionDropped(() => {
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
})
@@ -72,34 +77,31 @@ function InnerApp() {
return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
{isAndroid && <StatusBar />}
<Alf theme={theme}>
<Splash isReady={!isInitialLoad}>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<PushNotificationsListener>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</StatsigProvider>
</PushNotificationsListener>
</QueryProvider>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</StatsigProvider>
</React.Fragment>
</Splash>
</Alf>
@@ -107,12 +109,6 @@ function InnerApp() {
)
}
function PushNotificationsListener({children}: {children: React.ReactNode}) {
const queryClient = useQueryClient()
useNotificationsListener(queryClient)
return children
}
function App() {
const [isReady, setReady] = useState(false)
@@ -129,27 +125,31 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
<PersistQueryClientProvider
client={queryClient}
persistOptions={{persister: asyncStoragePersister, dehydrateOptions}}>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
</PersistQueryClientProvider>
)
}
+71 -63
View File
@@ -1,38 +1,44 @@
import 'lib/sentry' // must be near top
import React, {useState, useEffect} from 'react'
import {PersistQueryClientProvider} from '@tanstack/react-query-persist-client'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {RootSiblingParent} from 'react-native-root-siblings'
import 'view/icons'
import React, {useEffect, useState} from 'react'
import {RootSiblingParent} from 'react-native-root-siblings'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {init as initPersistedState} from '#/state/persisted'
import * as persisted from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {QueryProvider} from 'lib/react-query'
import {Shell} from 'view/shell/index'
import {ToastContainer} from 'view/com/util/Toast.web'
import {ThemeProvider} from 'lib/ThemeContext'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {
queryClient,
asyncStoragePersister,
dehydrateOptions,
} from 'lib/react-query'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import I18nProvider from './locale/i18nProvider'
import {
Provider as SessionProvider,
useSession,
useSessionApi,
} from 'state/session'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import {ToastContainer} from 'view/com/util/Toast.web'
import {Shell} from 'view/shell/index'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted'
import {Provider as PortalProvider} from '#/components/Portal'
import I18nProvider from './locale/i18nProvider'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
function InnerApp() {
const {isInitialLoad, currentAccount} = useSession()
@@ -54,27 +60,25 @@ function InnerApp() {
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
</RootSiblingParent>
<ToastContainer />
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</StatsigProvider>
</QueryProvider>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
</RootSiblingParent>
<ToastContainer />
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</StatsigProvider>
</React.Fragment>
</Alf>
)
@@ -96,27 +100,31 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
<PersistQueryClientProvider
client={queryClient}
persistOptions={{persister: asyncStoragePersister, dehydrateOptions}}>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
</PersistQueryClientProvider>
)
}
+60 -68
View File
@@ -1,86 +1,86 @@
import * as React from 'react'
import {JSX} from 'react/jsx-runtime'
import {i18n, MessageDescriptor} from '@lingui/core'
import {msg} from '@lingui/macro'
import {
NavigationContainer,
createNavigationContainerRef,
CommonActions,
StackActions,
DefaultTheme,
DarkTheme,
} from '@react-navigation/native'
import {
BottomTabBarProps,
createBottomTabNavigator,
} from '@react-navigation/bottom-tabs'
import {
CommonActions,
createNavigationContainerRef,
DarkTheme,
DefaultTheme,
NavigationContainer,
StackActions,
} from '@react-navigation/native'
import {timeout} from 'lib/async/timeout'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {usePalette} from 'lib/hooks/usePalette'
import {buildStateObject} from 'lib/routes/helpers'
import {
AllNavigatorParams,
BottomTabNavigatorParams,
FeedsTabNavigatorParams,
FlatNavigatorParams,
HomeTabNavigatorParams,
MyProfileTabNavigatorParams,
NotificationsTabNavigatorParams,
SearchTabNavigatorParams,
FeedsTabNavigatorParams,
NotificationsTabNavigatorParams,
FlatNavigatorParams,
AllNavigatorParams,
MyProfileTabNavigatorParams,
BottomTabNavigatorParams,
} from 'lib/routes/types'
import {RouteParams, State} from 'lib/routes/types'
import {bskyTitle} from 'lib/strings/headings'
import {BottomBar} from './view/shell/bottom-bar/BottomBar'
import {buildStateObject} from 'lib/routes/helpers'
import {State, RouteParams} from 'lib/routes/types'
import {isAndroid, isNative} from 'platform/detection'
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
import {AppPasswords} from 'view/screens/AppPasswords'
import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts'
import {PreferencesFollowingFeed} from 'view/screens/PreferencesFollowingFeed'
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
import {SavedFeeds} from 'view/screens/SavedFeeds'
import HashtagScreen from '#/screens/Hashtag'
import {ModerationScreen} from '#/screens/Moderation'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
import {init as initAnalytics} from './lib/analytics/analytics'
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {router} from './routes'
import {useModalControls} from './state/modals'
import {usePalette} from 'lib/hooks/usePalette'
import {bskyTitle} from 'lib/strings/headings'
import {JSX} from 'react/jsx-runtime'
import {timeout} from 'lib/async/timeout'
import {useUnreadNotifications} from './state/queries/notifications/unread'
import {useSession} from './state/session'
import {useModalControls} from './state/modals'
import {
setEmailConfirmationRequested,
shouldRequestEmailConfirmation,
setEmailConfirmationRequested,
} from './state/shell/reminders'
import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines'
import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy'
import {DebugModScreen} from './view/screens/DebugMod'
import {FeedsScreen} from './view/screens/Feeds'
import {init as initAnalytics} from './lib/analytics/analytics'
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
import {HomeScreen} from './view/screens/Home'
import {LanguageSettingsScreen} from './view/screens/LanguageSettings'
import {SearchScreen} from './view/screens/Search'
import {FeedsScreen} from './view/screens/Feeds'
import {NotificationsScreen} from './view/screens/Notifications'
import {ListsScreen} from './view/screens/Lists'
import {LogScreen} from './view/screens/Log'
import {ModerationScreen} from '#/screens/Moderation'
import {ModerationModlistsScreen} from './view/screens/ModerationModlists'
import {NotFoundScreen} from './view/screens/NotFound'
import {NotificationsScreen} from './view/screens/Notifications'
import {PostLikedByScreen} from './view/screens/PostLikedBy'
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
import {PostThreadScreen} from './view/screens/PostThread'
import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy'
import {SettingsScreen} from './view/screens/Settings'
import {LanguageSettingsScreen} from './view/screens/LanguageSettings'
import {ProfileScreen} from './view/screens/Profile'
import {ProfileFeedScreen} from './view/screens/ProfileFeed'
import {ProfileFeedLikedByScreen} from './view/screens/ProfileFeedLikedBy'
import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
import {ProfileFeedScreen} from './view/screens/ProfileFeed'
import {ProfileFeedLikedByScreen} from './view/screens/ProfileFeedLikedBy'
import {ProfileListScreen} from './view/screens/ProfileList'
import {SearchScreen} from './view/screens/Search'
import {SettingsScreen} from './view/screens/Settings'
import {PostThreadScreen} from './view/screens/PostThread'
import {PostLikedByScreen} from './view/screens/PostLikedBy'
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
import {Storybook} from './view/screens/Storybook'
import {DebugModScreen} from './view/screens/DebugMod'
import {LogScreen} from './view/screens/Log'
import {SupportScreen} from './view/screens/Support'
import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy'
import {TermsOfServiceScreen} from './view/screens/TermsOfService'
import {BottomBar} from './view/shell/bottom-bar/BottomBar'
import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines'
import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy'
import {AppPasswords} from 'view/screens/AppPasswords'
import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts'
import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
import {SavedFeeds} from 'view/screens/SavedFeeds'
import {PreferencesFollowingFeed} from 'view/screens/PreferencesFollowingFeed'
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth'
import {msg} from '@lingui/macro'
import {i18n, MessageDescriptor} from '@lingui/core'
import HashtagScreen from '#/screens/Hashtag'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
import {logEvent, attachRouteToLogEvents} from './lib/statsig/statsig'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@@ -554,14 +554,10 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
ref={navigationRef}
linking={LINKING}
theme={theme}
onStateChange={() => {
logEvent('router:navigate', {})
}}
onReady={() => {
attachRouteToLogEvents(getCurrentRouteName)
logModuleInitTime()
onReady()
logEvent('router:navigate', {})
}}>
{children}
</NavigationContainer>
@@ -569,11 +565,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
}
function getCurrentRouteName() {
if (navigationRef.isReady()) {
return navigationRef.getCurrentRoute()?.name
} else {
return undefined
}
return navigationRef.getCurrentRoute()?.name
}
/**
@@ -697,11 +689,11 @@ function logModuleInitTime() {
}
export {
FlatNavigator,
handleLink,
navigate,
reset,
resetToTab,
RoutesContainer,
reset,
handleLink,
TabsNavigator,
FlatNavigator,
RoutesContainer,
}
+6 -78
View File
@@ -1,7 +1,6 @@
import {Platform} from 'react-native'
import {web, native} from '#/alf/util/platform'
import * as tokens from '#/alf/tokens'
import {native, web} from '#/alf/util/platform'
export const atoms = {
/*
@@ -158,12 +157,6 @@ export const atoms = {
align_end: {
alignItems: 'flex-end',
},
align_baseline: {
alignItems: 'baseline',
},
align_stretch: {
alignItems: 'stretch',
},
self_auto: {
alignSelf: 'auto',
},
@@ -254,10 +247,10 @@ export const atoms = {
fontWeight: tokens.fontWeight.normal,
},
font_semibold: {
fontWeight: tokens.fontWeight.semibold,
fontWeight: '500',
},
font_bold: {
fontWeight: tokens.fontWeight.bold,
fontWeight: tokens.fontWeight.semibold,
},
italic: {
fontStyle: 'italic',
@@ -307,9 +300,6 @@ export const atoms = {
/*
* Padding
*/
p_0: {
padding: 0,
},
p_2xs: {
padding: tokens.space._2xs,
},
@@ -340,10 +330,6 @@ export const atoms = {
p_5xl: {
padding: tokens.space._5xl,
},
px_0: {
paddingLeft: 0,
paddingRight: 0,
},
px_2xs: {
paddingLeft: tokens.space._2xs,
paddingRight: tokens.space._2xs,
@@ -384,10 +370,6 @@ export const atoms = {
paddingLeft: tokens.space._5xl,
paddingRight: tokens.space._5xl,
},
py_0: {
paddingTop: 0,
paddingBottom: 0,
},
py_2xs: {
paddingTop: tokens.space._2xs,
paddingBottom: tokens.space._2xs,
@@ -428,9 +410,6 @@ export const atoms = {
paddingTop: tokens.space._5xl,
paddingBottom: tokens.space._5xl,
},
pt_0: {
paddingTop: 0,
},
pt_2xs: {
paddingTop: tokens.space._2xs,
},
@@ -461,9 +440,6 @@ export const atoms = {
pt_5xl: {
paddingTop: tokens.space._5xl,
},
pb_0: {
paddingBottom: 0,
},
pb_2xs: {
paddingBottom: tokens.space._2xs,
},
@@ -494,9 +470,6 @@ export const atoms = {
pb_5xl: {
paddingBottom: tokens.space._5xl,
},
pl_0: {
paddingLeft: 0,
},
pl_2xs: {
paddingLeft: tokens.space._2xs,
},
@@ -527,9 +500,6 @@ export const atoms = {
pl_5xl: {
paddingLeft: tokens.space._5xl,
},
pr_0: {
paddingRight: 0,
},
pr_2xs: {
paddingRight: tokens.space._2xs,
},
@@ -564,8 +534,9 @@ export const atoms = {
/*
* Margin
*/
m_0: {
margin: 0,
mx_auto: {
marginLeft: 'auto',
marginRight: 'auto',
},
m_2xs: {
margin: tokens.space._2xs,
@@ -597,13 +568,6 @@ export const atoms = {
m_5xl: {
margin: tokens.space._5xl,
},
m_auto: {
margin: 'auto',
},
mx_0: {
marginLeft: 0,
marginRight: 0,
},
mx_2xs: {
marginLeft: tokens.space._2xs,
marginRight: tokens.space._2xs,
@@ -644,14 +608,6 @@ export const atoms = {
marginLeft: tokens.space._5xl,
marginRight: tokens.space._5xl,
},
mx_auto: {
marginLeft: 'auto',
marginRight: 'auto',
},
my_0: {
marginTop: 0,
marginBottom: 0,
},
my_2xs: {
marginTop: tokens.space._2xs,
marginBottom: tokens.space._2xs,
@@ -692,13 +648,6 @@ export const atoms = {
marginTop: tokens.space._5xl,
marginBottom: tokens.space._5xl,
},
my_auto: {
marginTop: 'auto',
marginBottom: 'auto',
},
mt_0: {
marginTop: 0,
},
mt_2xs: {
marginTop: tokens.space._2xs,
},
@@ -729,12 +678,6 @@ export const atoms = {
mt_5xl: {
marginTop: tokens.space._5xl,
},
mt_auto: {
marginTop: 'auto',
},
mb_0: {
marginBottom: 0,
},
mb_2xs: {
marginBottom: tokens.space._2xs,
},
@@ -765,12 +708,6 @@ export const atoms = {
mb_5xl: {
marginBottom: tokens.space._5xl,
},
mb_auto: {
marginBottom: 'auto',
},
ml_0: {
marginLeft: 0,
},
ml_2xs: {
marginLeft: tokens.space._2xs,
},
@@ -801,12 +738,6 @@ export const atoms = {
ml_5xl: {
marginLeft: tokens.space._5xl,
},
ml_auto: {
marginLeft: 'auto',
},
mr_0: {
marginRight: 0,
},
mr_2xs: {
marginRight: tokens.space._2xs,
},
@@ -837,7 +768,4 @@ export const atoms = {
mr_5xl: {
marginRight: tokens.space._5xl,
},
mr_auto: {
marginRight: 'auto',
},
} as const
-4
View File
@@ -16,7 +16,6 @@ type BreakpointName = keyof typeof breakpoints
const breakpoints: {
[key: string]: number
} = {
gtPhone: 500,
gtMobile: 800,
gtTablet: 1300,
}
@@ -27,7 +26,6 @@ function getActiveBreakpoints({width}: {width: number}) {
return {
active: active[active.length - 1],
gtPhone: active.includes('gtPhone'),
gtMobile: active.includes('gtMobile'),
gtTablet: active.includes('gtTablet'),
}
@@ -41,7 +39,6 @@ export const Context = React.createContext<{
theme: themes.Theme
breakpoints: {
active: BreakpointName | undefined
gtPhone: boolean
gtMobile: boolean
gtTablet: boolean
}
@@ -50,7 +47,6 @@ export const Context = React.createContext<{
theme: themes.light,
breakpoints: {
active: undefined,
gtPhone: false,
gtMobile: false,
gtTablet: false,
},
+4 -4
View File
@@ -1,8 +1,8 @@
import {
BLUE_HUE,
generateScale,
GREEN_HUE,
RED_HUE,
GREEN_HUE,
generateScale,
} from '#/alf/util/colorGeneration'
export const scale = generateScale(6, 100)
@@ -116,8 +116,8 @@ export const borderRadius = {
export const fontWeight = {
normal: '400',
semibold: '500',
bold: '600',
semibold: '600',
bold: '900',
} as const
export const gradients = {
-141
View File
@@ -1,141 +0,0 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useProfileQuery} from '#/state/queries/profile'
import {type SessionAccount, useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
import {Button} from './Button'
import {Text} from './Typography'
export function AccountList({
onSelectAccount,
onSelectOther,
otherLabel,
}: {
onSelectAccount: (account: SessionAccount) => void
onSelectOther: () => void
otherLabel?: string
}) {
const {isSwitchingAccounts, currentAccount, accounts} = useSession()
const t = useTheme()
const {_} = useLingui()
const onPressAddAccount = useCallback(() => {
onSelectOther()
}, [onSelectOther])
return (
<View
style={[
a.rounded_md,
a.overflow_hidden,
a.border,
t.atoms.border_contrast_low,
]}>
{accounts.map(account => (
<React.Fragment key={account.did}>
<AccountItem
account={account}
onSelect={onSelectAccount}
isCurrentAccount={account.did === currentAccount?.did}
/>
<View style={[a.border_b, t.atoms.border_contrast_low]} />
</React.Fragment>
))}
<Button
testID="chooseAddAccountBtn"
style={[a.flex_1]}
onPress={isSwitchingAccounts ? undefined : onPressAddAccount}
label={_(msg`Login to account that is not listed`)}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.flex_row,
a.align_center,
{height: 48},
(hovered || pressed || isSwitchingAccounts) &&
t.atoms.bg_contrast_25,
]}>
<Text
style={[
a.align_baseline,
a.flex_1,
a.flex_row,
a.py_sm,
{paddingLeft: 48},
]}>
{otherLabel ?? <Trans>Other account</Trans>}
</Text>
<Chevron size="sm" style={[t.atoms.text, a.mr_md]} />
</View>
)}
</Button>
</View>
)
}
function AccountItem({
account,
onSelect,
isCurrentAccount,
}: {
account: SessionAccount
onSelect: (account: SessionAccount) => void
isCurrentAccount: boolean
}) {
const t = useTheme()
const {_} = useLingui()
const {data: profile} = useProfileQuery({did: account.did})
const onPress = React.useCallback(() => {
onSelect(account)
}, [account, onSelect])
return (
<Button
testID={`chooseAccountBtn-${account.handle}`}
key={account.did}
style={[a.flex_1]}
onPress={onPress}
label={
isCurrentAccount
? _(msg`Continue as ${account.handle} (currently signed in)`)
: _(msg`Sign in as ${account.handle}`)
}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.flex_row,
a.align_center,
{height: 48},
(hovered || pressed) && t.atoms.bg_contrast_25,
]}>
<View style={a.p_md}>
<UserAvatar avatar={profile?.avatar} size={24} />
</View>
<Text style={[a.align_baseline, a.flex_1, a.flex_row, a.py_sm]}>
<Text style={[a.font_bold]}>
{profile?.displayName || account.handle}{' '}
</Text>
<Text style={[t.atoms.text_contrast_medium]}>{account.handle}</Text>
</Text>
{isCurrentAccount ? (
<Check
size="sm"
style={[{color: t.palette.positive_600}, a.mr_md]}
/>
) : (
<Chevron size="sm" style={[t.atoms.text, a.mr_md]} />
)}
</View>
)}
</Button>
)
}
@@ -1,20 +1,19 @@
import React from 'react'
import {
AccessibilityProps,
Pressable,
PressableProps,
StyleProp,
StyleSheet,
Text,
PressableProps,
TextProps,
TextStyle,
View,
ViewStyle,
AccessibilityProps,
View,
TextStyle,
StyleSheet,
StyleProp,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {Trans} from '@lingui/macro'
import {android, atoms as a, flatten, tokens, useTheme} from '#/alf'
import {useTheme, atoms as a, tokens, android, flatten} from '#/alf'
import {Props as SVGIconProps} from '#/components/icons/common'
import {normalizeTextStyles} from '#/components/Typography'
@@ -404,9 +403,7 @@ export function Button({
</View>
)}
<Context.Provider value={context}>
{/* @ts-ignore */}
{typeof children === 'string' || children?.type === Trans ? (
/* @ts-ignore */
{typeof children === 'string' ? (
<ButtonText>{children}</ButtonText>
) : typeof children === 'function' ? (
children(context)
-61
View File
@@ -1,61 +0,0 @@
# Button
`Button` is intended to be very flexible and can be used to create consistent
button styles, as seen in the Storybook, or can be used to create custom
buttons with totally independent styles.
## Usage
Typical usage looks like this, where the `variant`, `size`, `color`, and/or
`shape` are defined. Normal usage also should make use of the `ButtonText` and
`ButtonIcon` components so that they inherit styles from the top level props
like `color`.
```tsx
<Button
label='Typical button'
onPress={() => {}}
size='large'
variant='solid'
color='primary'
>
<ButtonIcon icon={Plus} position='left' />
<ButtonText>Hello world</ButtonText>
</Button>
```
Or:
```tsx
<Button
label='Typical button'
onPress={() => {}}
size='large'
variant='solid'
color='primary'
>
<ButtonText>Hello world</ButtonText>
<ButtonIcon icon={Plus} position='right' />
</Button>
```
Each of those top-level props props is optional in order to allow for "custom"
buttons, like this:
```tsx
<Button label='Custom button' onPress={() => {}}>
{({ hovered, focused, pressed, disabled }) => (
<View style={[
t.atoms.bg_contrast_25,
hovered && t.atoms.bg_contrast_50,
]}>
<Text>{disabled ? 'Disabled' : 'Click me!'}</Text>
</View>
))}
</Button>
```
In the custom button case, you have the _option_ to use `ButtonIcon` and
`ButtonText`, but without top-level props like `color`, they serve little
purpose, and it may be perferrable to define your own components for these
purposes.
+23 -21
View File
@@ -1,31 +1,30 @@
import React, {useImperativeHandle} from 'react'
import {Dimensions, Pressable, View} from 'react-native'
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {View, Dimensions, Keyboard, Pressable} from 'react-native'
import BottomSheet, {
BottomSheetBackdropProps,
BottomSheetScrollView,
BottomSheetScrollViewMethods,
BottomSheetTextInput,
BottomSheetView,
useBottomSheet,
WINDOW_HEIGHT,
} from '@discord/bottom-sheet/src'
} from '@gorhom/bottom-sheet'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
import {useTheme, atoms as a, flatten} from '#/alf'
import {Portal} from '#/components/Portal'
import {createInput} from '#/components/forms/TextField'
import {logger} from '#/logger'
import {useDialogStateControlContext} from '#/state/dialogs'
import {isNative} from 'platform/detection'
import {atoms as a, flatten, useTheme} from '#/alf'
import {Context} from '#/components/Dialog/context'
import {
DialogOuterProps,
DialogControlProps,
DialogInnerProps,
DialogOuterProps,
} from '#/components/Dialog/types'
import {createInput} from '#/components/forms/TextField'
import {Portal} from '#/components/Portal'
import {Context} from '#/components/Dialog/context'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
export * from '#/components/Dialog/types'
// @ts-ignore
export const Input = createInput(BottomSheetTextInput)
@@ -122,6 +121,7 @@ export function Outer({
)
const onCloseInner = React.useCallback(() => {
Keyboard.dismiss()
try {
closeCallback.current?.()
} catch (e: any) {
@@ -205,14 +205,12 @@ export function Inner({children, style}: DialogInnerProps) {
)
}
export const ScrollableInner = React.forwardRef<
BottomSheetScrollViewMethods,
DialogInnerProps
>(function ScrollableInner({children, style}, ref) {
export function ScrollableInner({children, style}: DialogInnerProps) {
const insets = useSafeAreaInsets()
return (
<BottomSheetScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
style={[
a.flex_1, // main diff is this
a.p_xl,
@@ -223,20 +221,24 @@ export const ScrollableInner = React.forwardRef<
borderTopRightRadius: 40,
},
flatten(style),
]}
contentContainerStyle={isNative ? a.pb_4xl : undefined}
ref={ref}>
]}>
{children}
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
</BottomSheetScrollView>
)
})
}
export function Handle() {
const t = useTheme()
const onTouchStart = React.useCallback(() => {
Keyboard.dismiss()
}, [])
return (
<View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}>
<View
style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}
onTouchStart={onTouchStart}>
<View
style={[
a.rounded_sm,
+3 -15
View File
@@ -1,10 +1,6 @@
import React from 'react'
import type {
AccessibilityProps,
GestureResponderEvent,
ScrollViewProps,
} from 'react-native'
import {BottomSheetProps} from '@discord/bottom-sheet/src'
import type {AccessibilityProps} from 'react-native'
import {BottomSheetProps} from '@gorhom/bottom-sheet'
import {ViewStyleProp} from '#/alf'
@@ -14,15 +10,9 @@ type A11yProps = Required<AccessibilityProps>
* Mutated by useImperativeHandle to provide a public API for controlling the
* dialog. The methods here will actually become the handlers defined within
* the `Dialog.Outer` component.
*
* `Partial<GestureResponderEvent>` here allows us to add this directly to the
* `onPress` prop of a button, for example. If this type was not added, we
* would need to create a function to wrap `.open()` with.
*/
export type DialogControlRefProps = {
open: (
options?: DialogControlOpenOptions & Partial<GestureResponderEvent>,
) => void
open: (options?: DialogControlOpenOptions) => void
close: (callback?: () => void) => void
}
@@ -65,11 +55,9 @@ export type DialogInnerProps =
label?: undefined
accessibilityLabelledBy: A11yProps['aria-labelledby']
accessibilityDescribedBy: string
keyboardDismissMode?: ScrollViewProps['keyboardDismissMode']
}>
| DialogInnerPropsBase<{
label: string
accessibilityLabelledBy?: undefined
accessibilityDescribedBy?: undefined
keyboardDismissMode?: ScrollViewProps['keyboardDismissMode']
}>
-97
View File
@@ -1,97 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/core'
import {StackActions} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography'
import {router} from '#/routes'
export function Error({
title,
message,
onRetry,
}: {
title?: string
message?: string
onRetry?: () => unknown
}) {
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const canGoBack = navigation.canGoBack()
const onGoBack = React.useCallback(() => {
if (canGoBack) {
navigation.goBack()
} else {
navigation.navigate('HomeTab')
// Checking the state for routes ensures that web doesn't encounter errors while going back
if (navigation.getState()?.routes) {
navigation.dispatch(StackActions.push(...router.matchPath('/')))
} else {
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
}
}
}, [navigation, canGoBack])
return (
<CenteredView
style={[
a.flex_1,
a.align_center,
!gtMobile ? a.justify_between : a.gap_5xl,
t.atoms.border_contrast_low,
{paddingTop: 175, paddingBottom: 110},
]}
sideBorders>
<View style={[a.w_full, a.align_center, a.gap_lg]}>
<Text style={[a.font_bold, a.text_3xl]}>{title}</Text>
<Text
style={[
a.text_md,
a.text_center,
t.atoms.text_contrast_high,
{lineHeight: 1.4},
gtMobile && {width: 450},
]}>
{message}
</Text>
</View>
<View style={[a.gap_md, gtMobile ? {width: 350} : [a.w_full, a.px_lg]]}>
{onRetry && (
<Button
variant="solid"
color="primary"
label={_(msg`Press to retry`)}
onPress={onRetry}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
</Button>
)}
<Button
variant="solid"
color={onRetry ? 'secondary' : 'primary'}
label={_(msg`Return to previous page`)}
onPress={onGoBack}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
<ButtonText>
<Trans>Go Back</Trans>
</ButtonText>
</Button>
</View>
</CenteredView>
)
}
+1 -1
View File
@@ -104,7 +104,7 @@ export function Default({
}: LabelingServiceProps & ViewStyleProp) {
return (
<Outer style={style}>
<Avatar avatar={labeler.creator.avatar} />
<Avatar />
<Content>
<Title
value={getLabelingServiceTitle({
+79 -49
View File
@@ -1,48 +1,42 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useLikedByQuery} from '#/state/queries/post-liked-by'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {cleanError} from 'lib/strings/errors'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {List} from '#/view/com/util/List'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {useLikedByQuery} from '#/state/queries/post-liked-by'
import {cleanError} from '#/lib/strings/errors'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Refresh} from './icons/ArrowRotateCounterClockwise'
function renderItem({item}: {item: GetLikes.Like}) {
return <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
}
function keyExtractor(item: GetLikes.Like) {
return item.actor.did
}
import {atoms as a, useTheme} from '#/alf'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
export function LikedByList({uri}: {uri: string}) {
const t = useTheme()
const {_} = useLingui()
const initialNumToRender = useInitialNumToRender()
const [isPTRing, setIsPTRing] = React.useState(false)
const {
data: resolvedUri,
error: resolveError,
isLoading: isUriLoading,
isFetching: isFetchingResolvedUri,
} = useResolveUriQuery(uri)
const {
data,
isLoading: isLikedByLoading,
isFetching,
isFetched,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
error: likedByError,
isError,
error,
refetch,
} = useLikedByQuery(resolvedUri?.uri)
const error = resolveError || likedByError
const isError = !!resolveError || !!likedByError
const likes = React.useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.likes)
@@ -61,47 +55,83 @@ export function LikedByList({uri}: {uri: string}) {
}, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetchingNextPage || !hasNextPage || isError) return
if (isFetching || !hasNextPage || isError) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more likes', {message: err})
}
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
}, [isFetching, hasNextPage, isError, fetchNextPage])
if (likes.length < 1) {
const renderItem = React.useCallback(({item}: {item: GetLikes.Like}) => {
return (
<ListMaybePlaceholder
isLoading={isUriLoading || isLikedByLoading}
isError={isError}
emptyType="results"
emptyMessage={_(
msg`Nobody has liked this yet. Maybe you should be the first!`,
)}
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
/>
<ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
)
}, [])
if (isFetchingResolvedUri || !isFetched) {
return (
<View style={[a.w_full, a.align_center, a.p_lg]}>
<Loader size="xl" />
</View>
)
}
return (
if (resolveError || isError) {
return (
<View style={[a.p_lg]}>
<View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}>
<Text style={[a.text_md, a.pb_lg]}>
{cleanError(resolveError || error)}
</Text>
<View style={[a.flex_row, a.justify_end]}>
<Button
label={_(msg``)}
onPress={onRefresh}
size="small"
variant="solid"
color="primary">
<ButtonText>
<Trans>Try again</Trans>
</ButtonText>
<ButtonIcon icon={Refresh} position="right" />
</Button>
</View>
</View>
</View>
)
}
return likes.length ? (
<List
data={likes}
renderItem={renderItem}
keyExtractor={keyExtractor}
keyExtractor={item => item.actor.did}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
}
onEndReachedThreshold={3}
initialNumToRender={initialNumToRender}
windowSize={11}
renderItem={renderItem}
initialNumToRender={15}
contentContainerStyle={{borderWidth: 0}}
// FIXME(dan)
// eslint-disable-next-line react/no-unstable-nested-components
ListFooterComponent={() => (
<View style={[a.w_full, a.align_center, a.p_lg]}>
{(isFetching || isFetchingNextPage) && <Loader size="xl" />}
</View>
)}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
) : (
<View style={[a.p_lg]}>
<View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
Nobody has liked this yet. Maybe you should be the first!
</Trans>
</Text>
</View>
</View>
)
}
+9 -43
View File
@@ -1,24 +1,23 @@
import React from 'react'
import {GestureResponderEvent} from 'react-native'
import {useLinkProps, StackActions} from '@react-navigation/native'
import {sanitizeUrl} from '@braintree/sanitize-url'
import {StackActions, useLinkProps} from '@react-navigation/native'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {isWeb} from '#/platform/detection'
import {useTheme, web, flatten, TextStyleProp, atoms as a} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
import {AllNavigatorParams} from '#/lib/routes/types'
import {shareUrl} from '#/lib/sharing'
import {
convertBskyAppUrlIfNeeded,
isExternalUrl,
linkRequiresWarning,
} from '#/lib/strings/url-helpers'
import {isNative, isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {useOpenLink} from '#/state/preferences/in-app-browser'
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Text, TextProps} from '#/components/Typography'
import {router} from '#/routes'
import {Text, TextProps} from '#/components/Typography'
import {useOpenLink} from 'state/preferences/in-app-browser'
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
/**
* Only available within a `Link`, since that inherits from `Button`.
@@ -61,11 +60,6 @@ type BaseLinkProps = Pick<
* Web-only attribute. Sets `download` attr on web.
*/
download?: string
/**
* Native-only attribute. If true, will open the share sheet on long press.
*/
shareOnLongPress?: boolean
}
export function useLink({
@@ -74,7 +68,6 @@ export function useLink({
action = 'push',
disableMismatchWarning,
onPress: outerOnPress,
shareOnLongPress,
}: BaseLinkProps & {
displayText: string
}) {
@@ -164,34 +157,10 @@ export function useLink({
],
)
const handleLongPress = React.useCallback(() => {
const requiresWarning = Boolean(
!disableMismatchWarning &&
displayText &&
isExternal &&
linkRequiresWarning(href, displayText),
)
if (requiresWarning) {
openModal({
name: 'link-warning',
text: displayText,
href: href,
share: true,
})
} else {
shareUrl(href)
}
}, [disableMismatchWarning, displayText, href, isExternal, openModal])
const onLongPress =
isNative && isExternal && shareOnLongPress ? handleLongPress : undefined
return {
isExternal,
href,
onPress,
onLongPress,
}
}
@@ -260,18 +229,16 @@ export function InlineLink({
download,
selectable,
label,
shareOnLongPress,
...rest
}: InlineLinkProps) {
const t = useTheme()
const stringChildren = typeof children === 'string'
const {href, isExternal, onPress, onLongPress} = useLink({
const {href, isExternal, onPress} = useLink({
to,
displayText: stringChildren ? children : '',
action,
disableMismatchWarning,
onPress: outerOnPress,
shareOnLongPress,
})
const {
state: hovered,
@@ -303,7 +270,6 @@ export function InlineLink({
]}
role="link"
onPress={download ? undefined : onPress}
onLongPress={onLongPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
onFocus={onFocus}
+131 -77
View File
@@ -1,26 +1,26 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {cleanError} from 'lib/strings/errors'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Error} from '#/components/Error'
import {View} from 'react-native'
import {CenteredView} from 'view/com/util/Views'
import {Loader} from '#/components/Loader'
import {Trans} from '@lingui/macro'
import {cleanError} from 'lib/strings/errors'
import {Button} from '#/components/Button'
import {Text} from '#/components/Typography'
import {StackActions} from '@react-navigation/native'
import {router} from '#/routes'
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
export function ListFooter({
isFetchingNextPage,
isFetching,
isError,
error,
onRetry,
height,
}: {
isFetchingNextPage?: boolean
isFetching: boolean
isError: boolean
error?: string
onRetry?: () => Promise<unknown>
height?: number
}) {
const t = useTheme()
@@ -29,31 +29,37 @@ export function ListFooter({
style={[
a.w_full,
a.align_center,
a.justify_center,
a.border_t,
a.pb_lg,
t.atoms.border_contrast_low,
{height: height ?? 180, paddingTop: 30},
{height: 100},
]}>
{isFetchingNextPage ? (
{isFetching ? (
<Loader size="xl" />
) : (
<ListFooterMaybeError error={error} onRetry={onRetry} />
<ListFooterMaybeError
isError={isError}
error={error}
onRetry={onRetry}
/>
)}
</View>
)
}
function ListFooterMaybeError({
isError,
error,
onRetry,
}: {
isError: boolean
error?: string
onRetry?: () => Promise<unknown>
}) {
const t = useTheme()
const {_} = useLingui()
if (!error) return null
if (!isError) return null
return (
<View style={[a.w_full, a.px_lg]}>
@@ -77,7 +83,7 @@ function ListFooterMaybeError({
</Text>
<Button
variant="gradient"
label={_(msg`Press to retry`)}
label="Press to retry"
style={[
a.align_center,
a.justify_center,
@@ -87,7 +93,7 @@ function ListFooterMaybeError({
a.py_sm,
]}
onPress={onRetry}>
<Trans>Retry</Trans>
Retry
</Button>
</View>
</View>
@@ -120,75 +126,123 @@ export function ListHeaderDesktop({
export function ListMaybePlaceholder({
isLoading,
noEmpty,
isEmpty,
isError,
emptyTitle,
emptyMessage,
errorTitle,
errorMessage,
emptyType = 'page',
empty,
error,
notFoundType = 'page',
onRetry,
}: {
isLoading: boolean
noEmpty?: boolean
isError?: boolean
emptyTitle?: string
emptyMessage?: string
errorTitle?: string
errorMessage?: string
emptyType?: 'page' | 'results'
isEmpty: boolean
isError: boolean
empty?: string
error?: string
notFoundType?: 'page' | 'results'
onRetry?: () => Promise<unknown>
}) {
const navigation = useNavigationDeduped()
const t = useTheme()
const {_} = useLingui()
const {gtMobile, gtTablet} = useBreakpoints()
if (isLoading) {
return (
<CenteredView
style={[
a.flex_1,
a.align_center,
!gtMobile ? a.justify_between : a.gap_5xl,
t.atoms.border_contrast_low,
{paddingTop: 175, paddingBottom: 110},
]}
sideBorders={gtMobile}
topBorder={!gtTablet}>
const canGoBack = navigation.canGoBack()
const onGoBack = React.useCallback(() => {
if (canGoBack) {
navigation.goBack()
} else {
navigation.navigate('HomeTab')
// Checking the state for routes ensures that web doesn't encounter errors while going back
if (navigation.getState()?.routes) {
navigation.dispatch(StackActions.push(...router.matchPath('/')))
} else {
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
}
}
}, [navigation, canGoBack])
if (!isEmpty) return null
return (
<CenteredView
style={[
a.flex_1,
a.align_center,
!gtMobile ? a.justify_between : a.gap_5xl,
t.atoms.border_contrast_low,
{paddingTop: 175, paddingBottom: 110},
]}
sideBorders={gtMobile}
topBorder={!gtTablet}>
{isLoading ? (
<View style={[a.w_full, a.align_center, {top: 100}]}>
<Loader size="xl" />
</View>
</CenteredView>
)
}
) : (
<>
<View style={[a.w_full, a.align_center, a.gap_lg]}>
<Text style={[a.font_bold, a.text_3xl]}>
{isError ? (
<Trans>Oops!</Trans>
) : isEmpty ? (
<>
{notFoundType === 'results' ? (
<Trans>No results found</Trans>
) : (
<Trans>Page not found</Trans>
)}
</>
) : undefined}
</Text>
if (isError) {
return (
<Error
title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)}
onRetry={onRetry}
/>
)
}
if (!noEmpty) {
return (
<Error
title={
emptyTitle ??
(emptyType === 'results'
? _(msg`No results found`)
: _(msg`Page not found`))
}
message={
emptyMessage ??
_(msg`We're sorry! We can't find the page you were looking for.`)
}
onRetry={onRetry}
/>
)
}
return null
{isError ? (
<Text
style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
{error ? error : <Trans>Something went wrong!</Trans>}
</Text>
) : isEmpty ? (
<Text
style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
{empty ? (
empty
) : (
<Trans>
We're sorry! We can't find the page you were looking for.
</Trans>
)}
</Text>
) : undefined}
</View>
<View
style={[a.gap_md, !gtMobile ? [a.w_full, a.px_lg] : {width: 350}]}>
{isError && onRetry && (
<Button
variant="solid"
color="primary"
label="Click here"
onPress={onRetry}
size="large"
style={[
a.rounded_sm,
a.overflow_hidden,
{paddingVertical: 10},
]}>
Retry
</Button>
)}
<Button
variant="solid"
color={isError && onRetry ? 'secondary' : 'primary'}
label="Click here"
onPress={onGoBack}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
Go Back
</Button>
</View>
</>
)}
</CenteredView>
)
}
+2 -4
View File
@@ -17,7 +17,7 @@ import {
ItemIconProps,
} from '#/components/Menu/types'
import {Button, ButtonText} from '#/components/Button'
import {Trans, msg} from '@lingui/macro'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isNative} from 'platform/detection'
@@ -209,9 +209,7 @@ function Cancel() {
variant="ghost"
color="secondary"
onPress={() => control.close()}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
<ButtonText>Cancel</ButtonText>
</Button>
)
}
+7 -4
View File
@@ -3,10 +3,12 @@ import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonColor, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {isNative} from '#/platform/detection'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {Button, ButtonColor, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
export {useDialogControl as usePromptControl} from '#/components/Dialog'
@@ -79,11 +81,12 @@ export function Actions({children}: React.PropsWithChildren<{}>) {
<View
style={[
a.w_full,
a.gap_md,
a.gap_sm,
a.justify_end,
gtMobile
? [a.flex_row, a.flex_row_reverse, a.justify_start]
: [a.flex_col],
isNative && [a.pb_4xl],
]}>
{children}
</View>
@@ -1,87 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyLabelerDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, useButtonContext} from '#/components/Button'
import {Divider} from '#/components/Divider'
import * as LabelingServiceCard from '#/components/LabelingServiceCard'
import {Text} from '#/components/Typography'
import {ReportDialogProps} from './types'
export function SelectLabelerView({
...props
}: ReportDialogProps & {
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
onSelectLabeler: (v: string) => void
}) {
const t = useTheme()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
return (
<View style={[a.gap_lg]}>
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Select moderator</Trans>
</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
<Trans>To whom would you like to send this report?</Trans>
</Text>
</View>
<Divider />
<View style={[a.gap_sm]}>
{props.labelers.map(labeler => {
return (
<Button
key={labeler.creator.did}
label={_(msg`Send report to ${labeler.creator.displayName}`)}
onPress={() => props.onSelectLabeler(labeler.creator.did)}>
<LabelerButton labeler={labeler} />
</Button>
)
})}
</View>
</View>
)
}
function LabelerButton({
labeler,
}: {
labeler: AppBskyLabelerDefs.LabelerViewDetailed
}) {
const t = useTheme()
const {hovered, pressed} = useButtonContext()
const interacted = hovered || pressed
return (
<LabelingServiceCard.Outer
style={[
a.p_md,
a.rounded_sm,
t.atoms.bg_contrast_25,
interacted && t.atoms.bg_contrast_50,
]}>
<LabelingServiceCard.Avatar avatar={labeler.creator.avatar} />
<LabelingServiceCard.Content>
<LabelingServiceCard.Title
value={getLabelingServiceTitle({
displayName: labeler.creator.displayName,
handle: labeler.creator.handle,
})}
/>
<Text
style={[t.atoms.text_contrast_medium, a.text_sm, a.font_semibold]}>
@{labeler.creator.handle}
</Text>
</LabelingServiceCard.Content>
</LabelingServiceCard.Outer>
)
}
@@ -1,15 +1,16 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyLabelerDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyLabelerDefs} from '@atproto/api'
import {ReportOption, useReportOptions} from '#/lib/moderation/useReportOptions'
import {Link} from '#/components/Link'
import {useReportOptions, ReportOption} from '#/lib/moderation/useReportOptions'
import {DMCA_LINK} from '#/components/ReportDialog/const'
import {Link} from '#/components/Link'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {
Button,
ButtonIcon,
@@ -17,12 +18,9 @@ import {
useButtonContext,
} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {
ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft,
ChevronRight_Stroke2_Corner0_Rounded as ChevronRight,
} from '#/components/icons/Chevron'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight'
import {Text} from '#/components/Typography'
import {ReportDialogProps} from './types'
export function SelectReportOptionView({
@@ -30,11 +28,9 @@ export function SelectReportOptionView({
}: ReportDialogProps & {
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
onSelectReportOption: (reportOption: ReportOption) => void
goBack: () => void
}) {
const t = useTheme()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const allReportOptions = useReportOptions()
const reportOptions = allReportOptions[props.params.type]
@@ -64,19 +60,7 @@ export function SelectReportOptionView({
return (
<View style={[a.gap_lg]}>
{props.labelers?.length > 1 ? (
<Button
size="small"
variant="solid"
color="secondary"
shape="round"
label={_(msg`Go back to previous step`)}
onPress={props.goBack}>
<ButtonIcon icon={ChevronLeft} />
</Button>
) : null}
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
<View style={[a.justify_center, a.gap_sm]}>
<Text style={[a.text_2xl, a.font_bold]}>{i18n.title}</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
{i18n.description}
@@ -85,7 +69,7 @@ export function SelectReportOptionView({
<Divider />
<View style={[a.gap_sm]}>
<View style={[a.gap_sm, {marginHorizontal: a.p_md.padding * -1}]}>
{reportOptions.map(reportOption => {
return (
<Button
@@ -101,37 +85,37 @@ export function SelectReportOptionView({
})}
{(props.params.type === 'post' || props.params.type === 'account') && (
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.p_md,
a.pl_lg,
a.rounded_md,
t.atoms.bg_contrast_900,
]}>
<Text
<View style={[a.pt_md, a.px_md]}>
<View
style={[
a.flex_1,
t.atoms.text_inverted,
a.italic,
a.leading_snug,
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.p_md,
a.pl_lg,
a.rounded_md,
t.atoms.bg_contrast_900,
]}>
<Trans>Need to report a copyright violation?</Trans>
</Text>
<Link
to={DMCA_LINK}
label={_(msg`View details for reporting a copyright violation`)}
size="small"
variant="solid"
color="secondary">
<ButtonText>
<Trans>View details</Trans>
</ButtonText>
<ButtonIcon position="right" icon={SquareArrowTopRight} />
</Link>
<Text
style={[
a.flex_1,
t.atoms.text_inverted,
a.italic,
a.leading_snug,
]}>
<Trans>Need to report a copyright violation?</Trans>
</Text>
<Link
to={DMCA_LINK}
label={_(msg`View details for reporting a copyright violation`)}
size="small"
variant="solid"
color="secondary">
<ButtonText>View details</ButtonText>
<ButtonIcon position="right" icon={SquareArrowTopRight} />
</Link>
</View>
</View>
)}
</View>
@@ -150,6 +134,14 @@ function ReportOptionButton({
const {hovered, pressed} = useButtonContext()
const interacted = hovered || pressed
const styles = React.useMemo(() => {
return {
interacted: {
backgroundColor: t.palette.contrast_50,
},
}
}, [t])
return (
<View
style={[
@@ -160,8 +152,7 @@ function ReportOptionButton({
a.p_md,
a.rounded_md,
{paddingRight: 70},
t.atoms.bg_contrast_25,
interacted && t.atoms.bg_contrast_50,
interacted && styles.interacted,
]}>
<View style={[a.flex_1, a.gap_xs]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
@@ -178,7 +169,12 @@ function ReportOptionButton({
a.pr_md,
{left: 'auto'},
]}>
<ChevronRight size="md" fill={t.atoms.text_contrast_low.color} />
<ChevronRight
size="md"
fill={
hovered ? t.palette.primary_500 : t.atoms.text_contrast_low.color
}
/>
</View>
</View>
)
+3 -5
View File
@@ -24,13 +24,11 @@ import {getAgent} from '#/state/session'
export function SubmitView({
params,
labelers,
selectedLabeler,
selectedReportOption,
goBack,
onSubmitComplete,
}: ReportDialogProps & {
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
selectedLabeler: string
selectedReportOption: ReportOption
goBack: () => void
onSubmitComplete: () => void
@@ -39,9 +37,9 @@ export function SubmitView({
const {_} = useLingui()
const [details, setDetails] = React.useState<string>('')
const [submitting, setSubmitting] = React.useState<boolean>(false)
const [selectedServices, setSelectedServices] = React.useState<string[]>([
selectedLabeler,
])
const [selectedServices, setSelectedServices] = React.useState<string[]>(
labelers?.map(labeler => labeler.creator.did) || [],
)
const [error, setError] = React.useState('')
const submit = React.useCallback(async () => {
+26 -55
View File
@@ -1,25 +1,20 @@
import React from 'react'
import {Pressable, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {View, Pressable} from 'react-native'
import {Trans} from '@lingui/macro'
import {ReportOption} from '#/lib/moderation/useReportOptions'
import {useMyLabelersQuery} from '#/state/queries/preferences'
import {ReportOption} from '#/lib/moderation/useReportOptions'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {AppBskyLabelerDefs} from '@atproto/api'
import {BottomSheetScrollViewMethods} from '@discord/bottom-sheet/src'
import {atoms as a} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
import {useOnKeyboardDidShow} from '#/components/hooks/useOnKeyboard'
import {Loader} from '#/components/Loader'
import * as Dialog from '#/components/Dialog'
import {Text} from '#/components/Typography'
import {SelectLabelerView} from './SelectLabelerView'
import {ReportDialogProps} from './types'
import {SelectReportOptionView} from './SelectReportOptionView'
import {SubmitView} from './SubmitView'
import {ReportDialogProps} from './types'
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
export function ReportDialog(props: ReportDialogProps) {
return (
@@ -32,21 +27,18 @@ export function ReportDialog(props: ReportDialogProps) {
}
function ReportDialogInner(props: ReportDialogProps) {
const {_} = useLingui()
const {
isLoading: isLabelerLoading,
data: labelers,
error,
} = useMyLabelersQuery()
const isLoading = useDelayedLoading(500, isLabelerLoading)
const ref = React.useRef<BottomSheetScrollViewMethods>(null)
useOnKeyboardDidShow(() => {
ref.current?.scrollToEnd({animated: true})
})
const [selectedReportOption, setSelectedReportOption] = React.useState<
ReportOption | undefined
>()
return (
<Dialog.ScrollableInner label={_(msg`Report dialog`)} ref={ref}>
<Dialog.ScrollableInner label="Report Dialog">
{isLoading ? (
<View style={[a.align_center, {height: 100}]}>
<Loader size="xl" />
@@ -59,44 +51,23 @@ function ReportDialogInner(props: ReportDialogProps) {
<Trans>Something went wrong, please try again.</Trans>
</Text>
</View>
) : selectedReportOption ? (
<SubmitView
{...props}
labelers={labelers}
selectedReportOption={selectedReportOption}
goBack={() => setSelectedReportOption(undefined)}
onSubmitComplete={() => props.control.close()}
/>
) : (
<ReportDialogLoaded labelers={labelers} {...props} />
<SelectReportOptionView
{...props}
labelers={labelers}
onSelectReportOption={setSelectedReportOption}
/>
)}
<Dialog.Close />
</Dialog.ScrollableInner>
)
}
function ReportDialogLoaded(
props: ReportDialogProps & {
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
},
) {
const [selectedLabeler, setSelectedLabeler] = React.useState<
string | undefined
>(props.labelers.length === 1 ? props.labelers[0].creator.did : undefined)
const [selectedReportOption, setSelectedReportOption] = React.useState<
ReportOption | undefined
>()
if (selectedReportOption && selectedLabeler) {
return (
<SubmitView
{...props}
selectedLabeler={selectedLabeler}
selectedReportOption={selectedReportOption}
goBack={() => setSelectedReportOption(undefined)}
onSubmitComplete={() => props.control.close()}
/>
)
}
if (selectedLabeler) {
return (
<SelectReportOptionView
{...props}
goBack={() => setSelectedLabeler(undefined)}
onSelectReportOption={setSelectedReportOption}
/>
)
}
return <SelectLabelerView {...props} onSelectLabeler={setSelectedLabeler} />
}
+8 -9
View File
@@ -1,15 +1,15 @@
import React from 'react'
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
import {msg} from '@lingui/macro'
import {RichText as RichTextAPI, AppBskyRichtextFacet} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {toShortUrl} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {atoms as a, TextStyleProp, flatten, useTheme, web, native} from '#/alf'
import {InlineLink} from '#/components/Link'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
import {Text, TextProps} from '#/components/Typography'
import {toShortUrl} from 'lib/strings/url-helpers'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
import {isNative} from '#/platform/detection'
import {useInteractionState} from '#/components/hooks/useInteractionState'
const WORD_WRAP = {wordWrap: 1}
@@ -105,8 +105,7 @@ export function RichText({
to={link.uri}
style={[...styles, {pointerEvents: 'auto'}]}
// @ts-ignore TODO
dataSet={WORD_WRAP}
shareOnLongPress>
dataSet={WORD_WRAP}>
{toShortUrl(segment.text)}
</InlineLink>,
)
+1 -3
View File
@@ -264,9 +264,7 @@ export function TagMenu({
variant="ghost"
color="secondary"
onPress={() => control.close()}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
<ButtonText>Cancel</ButtonText>
</Button>
</>
)}
+1 -1
View File
@@ -87,7 +87,7 @@ export function TagMenu({
author: authorHandle,
})
},
testID: 'tagMenuSearchByUser',
testID: 'tagMenuSeachByUser',
icon: {
ios: {
name: 'magnifyingglass',
+14 -6
View File
@@ -1,9 +1,14 @@
import React from 'react'
import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
import {UITextView} from 'react-native-uitextview'
import {
Text as RNText,
StyleProp,
TextStyle,
TextProps as RNTextProps,
} from 'react-native'
import {UITextView} from 'react-native-ui-text-view'
import {isNative} from '#/platform/detection'
import {atoms, flatten, useTheme, web} from '#/alf'
import {useTheme, atoms, web, flatten} from '#/alf'
import {isIOS, isNative} from '#/platform/detection'
export type TextProps = RNTextProps & {
/**
@@ -56,8 +61,11 @@ export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
export function Text({style, selectable, ...rest}: TextProps) {
const t = useTheme()
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)])
return <UITextView selectable={selectable} uiTextView style={s} {...rest} />
return selectable && isIOS ? (
<UITextView style={s} {...rest} />
) : (
<RNText selectable={selectable} style={s} {...rest} />
)
}
export function createHeadingElement({level}: {level: number}) {
@@ -1,132 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isIOS, isWeb} from '#/platform/detection'
import {
usePreferencesQuery,
UsePreferencesQueryResponse,
usePreferencesSetBirthDateMutation,
} from '#/state/queries/preferences'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {DateInput} from '#/view/com/util/forms/DateInput'
import {atoms as a, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {Loader} from '#/components/Loader'
import {Button, ButtonIcon, ButtonText} from '../Button'
import {Text} from '../Typography'
export function BirthDateSettingsDialog({
control,
}: {
control: Dialog.DialogControlProps
}) {
const t = useTheme()
const {_} = useLingui()
const {isLoading, error, data: preferences} = usePreferencesQuery()
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`My Birthday`)}>
<View style={[a.gap_sm, a.pb_lg]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>My Birthday</Trans>
</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
<Trans>This information is not shared with other users.</Trans>
</Text>
</View>
{isLoading ? (
<Loader size="xl" />
) : error || !preferences ? (
<ErrorMessage
message={
error?.toString() ||
_(
msg`We were unable to load your birth date preferences. Please try again.`,
)
}
style={[a.rounded_sm]}
/>
) : (
<BirthdayInner control={control} preferences={preferences} />
)}
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
function BirthdayInner({
control,
preferences,
}: {
control: Dialog.DialogControlProps
preferences: UsePreferencesQueryResponse
}) {
const {_} = useLingui()
const [date, setDate] = React.useState(preferences.birthDate || new Date())
const {
isPending,
isError,
error,
mutateAsync: setBirthDate,
} = usePreferencesSetBirthDateMutation()
const hasChanged = date !== preferences.birthDate
const onSave = React.useCallback(async () => {
try {
// skip if date is the same
if (hasChanged) {
await setBirthDate({birthDate: date})
}
control.close()
} catch (e: any) {
logger.error(`setBirthDate failed`, {message: e.message})
}
}, [date, setBirthDate, control, hasChanged])
return (
<View style={a.gap_lg} testID="birthDateSettingsDialog">
<View style={isIOS && [a.w_full, a.align_center]}>
<DateInput
handleAsUTC
testID="birthdayInput"
value={date}
onChange={setDate}
buttonType="default-light"
buttonStyle={[a.rounded_sm]}
buttonLabelType="lg"
accessibilityLabel={_(msg`Birthday`)}
accessibilityHint={_(msg`Enter your birth date`)}
accessibilityLabelledBy="birthDate"
/>
</View>
{isError ? (
<ErrorMessage message={cleanError(error)} style={[a.rounded_sm]} />
) : undefined}
<View style={isWeb && [a.flex_row, a.justify_end]}>
<Button
label={hasChanged ? _(msg`Save birthday`) : _(msg`Done`)}
size="medium"
onPress={onSave}
variant="solid"
color="primary">
<ButtonText>
{hasChanged ? <Trans>Save</Trans> : <Trans>Done</Trans>}
</ButtonText>
{isPending && <ButtonIcon icon={Loader} />}
</Button>
</View>
</View>
)
}
-61
View File
@@ -1,61 +0,0 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {type SessionAccount, useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {atoms as a} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {AccountList} from '../AccountList'
import {Text} from '../Typography'
export function SwitchAccountDialog({
control,
}: {
control: Dialog.DialogControlProps
}) {
const {_} = useLingui()
const {currentAccount} = useSession()
const {onPressSwitchAccount} = useAccountSwitcher()
const {setShowLoggedOut} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()
const onSelectAccount = useCallback(
(account: SessionAccount) => {
if (account.did === currentAccount?.did) {
control.close()
} else {
onPressSwitchAccount(account, 'SwitchAccount')
}
},
[currentAccount, control, onPressSwitchAccount],
)
const onPressAddAccount = useCallback(() => {
setShowLoggedOut(true)
closeAllActiveElements()
}, [setShowLoggedOut, closeAllActiveElements])
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`Switch Account`)}>
<View style={[a.gap_lg]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Switch Account</Trans>
</Text>
<AccountList
onSelectAccount={onSelectAccount}
onSelectOther={onPressAddAccount}
otherLabel={_(msg`Add account`)}
/>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
@@ -1,11 +1,19 @@
import React from 'react'
import DatePicker from 'react-native-date-picker'
import {View, Pressable} from 'react-native'
import {useTheme} from '#/alf'
import {DateFieldProps} from '#/components/forms/DateField/types'
import {toSimpleDateString} from '#/components/forms/DateField/utils'
import {useTheme, atoms} from '#/alf'
import {Text} from '#/components/Typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import * as TextField from '#/components/forms/TextField'
import {DateFieldButton} from './index.shared'
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
import {DateFieldProps} from '#/components/forms/DateField/types'
import {
localizeDate,
toSimpleDateString,
} from '#/components/forms/DateField/utils'
import DatePicker from 'react-native-date-picker'
import {isAndroid} from 'platform/detection'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
@@ -16,10 +24,18 @@ export function DateField({
label,
isInvalid,
testID,
accessibilityHint,
}: DateFieldProps) {
const t = useTheme()
const [open, setOpen] = React.useState(false)
const {
state: pressed,
onIn: onPressIn,
onOut: onPressOut,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {chromeFocus, chromeError, chromeErrorHover} =
TextField.useSharedInputStyles()
const onChangeInternal = React.useCallback(
(date: Date) => {
@@ -31,29 +47,50 @@ export function DateField({
[onChangeDate, setOpen],
)
const onPress = React.useCallback(() => {
setOpen(true)
}, [])
const onCancel = React.useCallback(() => {
setOpen(false)
}, [])
return (
<>
<DateFieldButton
label={label}
value={value}
onPress={onPress}
isInvalid={isInvalid}
accessibilityHint={accessibilityHint}
/>
<View style={[atoms.relative, atoms.w_full]}>
<Pressable
aria-label={label}
accessibilityLabel={label}
accessibilityHint={undefined}
onPress={() => setOpen(true)}
onPressIn={onPressIn}
onPressOut={onPressOut}
onFocus={onFocus}
onBlur={onBlur}
style={[
{
paddingTop: 16,
paddingBottom: 16,
borderColor: 'transparent',
borderWidth: 2,
},
atoms.flex_row,
atoms.flex_1,
atoms.w_full,
atoms.px_lg,
atoms.rounded_sm,
t.atoms.bg_contrast_50,
focused || pressed ? chromeFocus : {},
isInvalid ? chromeError : {},
isInvalid && (focused || pressed) ? chromeErrorHover : {},
]}>
<TextField.Icon icon={CalendarDays} />
<Text
style={[atoms.text_md, atoms.pl_xs, t.atoms.text, {paddingTop: 3}]}>
{localizeDate(value)}
</Text>
</Pressable>
{open && (
<DatePicker
modal
open
timeZoneOffsetInMinutes={0}
modal={isAndroid}
open={isAndroid}
theme={t.name === 'light' ? 'light' : 'dark'}
date={new Date(value)}
onConfirm={onChangeInternal}
@@ -62,9 +99,9 @@ export function DateField({
testID={`${testID}-datepicker`}
aria-label={label}
accessibilityLabel={label}
accessibilityHint={accessibilityHint}
accessibilityHint={undefined}
/>
)}
</>
</View>
)
}
@@ -1,99 +0,0 @@
import React from 'react'
import {Pressable, View} from 'react-native'
import {android, atoms as a, useTheme, web} from '#/alf'
import * as TextField from '#/components/forms/TextField'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
import {Text} from '#/components/Typography'
import {localizeDate} from './utils'
// looks like a TextField.Input, but is just a button. It'll do something different on each platform on press
// iOS: open a dialog with an inline date picker
// Android: open the date picker modal
export function DateFieldButton({
label,
value,
onPress,
isInvalid,
accessibilityHint,
}: {
label: string
value: string
onPress: () => void
isInvalid?: boolean
accessibilityHint?: string
}) {
const t = useTheme()
const {
state: pressed,
onIn: onPressIn,
onOut: onPressOut,
} = useInteractionState()
const {
state: hovered,
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {chromeHover, chromeFocus, chromeError, chromeErrorHover} =
TextField.useSharedInputStyles()
return (
<View
style={[a.relative, a.w_full]}
{...web({
onMouseOver: onHoverIn,
onMouseOut: onHoverOut,
})}>
<Pressable
aria-label={label}
accessibilityLabel={label}
accessibilityHint={accessibilityHint}
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
onFocus={onFocus}
onBlur={onBlur}
style={[
{
paddingTop: 12,
paddingBottom: 12,
paddingLeft: 14,
paddingRight: 14,
borderColor: 'transparent',
borderWidth: 2,
},
android({
minHeight: 57.5,
}),
a.flex_row,
a.flex_1,
a.w_full,
a.rounded_sm,
t.atoms.bg_contrast_25,
a.align_center,
hovered ? chromeHover : {},
focused || pressed ? chromeFocus : {},
isInvalid || isInvalid ? chromeError : {},
(isInvalid || isInvalid) && (hovered || focused)
? chromeErrorHover
: {},
]}>
<TextField.Icon icon={CalendarDays} />
<Text
style={[
a.text_md,
a.pl_xs,
t.atoms.text,
{lineHeight: a.text_md.fontSize * 1.1875},
]}>
{localizeDate(value)}
</Text>
</Pressable>
</View>
)
}
+15 -51
View File
@@ -1,16 +1,11 @@
import React from 'react'
import {View} from 'react-native'
import DatePicker from 'react-native-date-picker'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {DateFieldProps} from '#/components/forms/DateField/types'
import {toSimpleDateString} from '#/components/forms/DateField/utils'
import {useTheme, atoms} from '#/alf'
import * as TextField from '#/components/forms/TextField'
import {DateFieldButton} from './index.shared'
import {toSimpleDateString} from '#/components/forms/DateField/utils'
import {DateFieldProps} from '#/components/forms/DateField/types'
import DatePicker from 'react-native-date-picker'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
@@ -27,12 +22,8 @@ export function DateField({
onChangeDate,
testID,
label,
isInvalid,
accessibilityHint,
}: DateFieldProps) {
const {_} = useLingui()
const t = useTheme()
const control = Dialog.useDialogControl()
const onChangeInternal = React.useCallback(
(date: Date | undefined) => {
@@ -45,44 +36,17 @@ export function DateField({
)
return (
<>
<DateFieldButton
label={label}
value={value}
onPress={control.open}
isInvalid={isInvalid}
accessibilityHint={accessibilityHint}
<View style={[atoms.relative, atoms.w_full]}>
<DatePicker
theme={t.name === 'light' ? 'light' : 'dark'}
date={new Date(value)}
onDateChange={onChangeInternal}
mode="date"
testID={`${testID}-datepicker`}
aria-label={label}
accessibilityLabel={label}
accessibilityHint={undefined}
/>
<Dialog.Outer control={control} testID={testID}>
<Dialog.Handle />
<Dialog.Inner label={label}>
<View style={a.gap_lg}>
<View style={[a.relative, a.w_full, a.align_center]}>
<DatePicker
timeZoneOffsetInMinutes={0}
theme={t.name === 'light' ? 'light' : 'dark'}
date={new Date(value)}
onDateChange={onChangeInternal}
mode="date"
testID={`${testID}-datepicker`}
aria-label={label}
accessibilityLabel={label}
accessibilityHint={accessibilityHint}
/>
</View>
<Button
label={_(msg`Done`)}
onPress={() => control.close()}
size="medium"
color="primary"
variant="solid">
<ButtonText>
<Trans>Done</Trans>
</ButtonText>
</Button>
</View>
</Dialog.Inner>
</Dialog.Outer>
</>
</View>
)
}
+3 -7
View File
@@ -1,12 +1,11 @@
import React from 'react'
import {StyleSheet, TextInput, TextInputProps} from 'react-native'
import {TextInput, TextInputProps, StyleSheet} from 'react-native'
// @ts-ignore
import {unstable_createElement} from 'react-native-web'
import {DateFieldProps} from '#/components/forms/DateField/types'
import {toSimpleDateString} from '#/components/forms/DateField/utils'
import * as TextField from '#/components/forms/TextField'
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
import {toSimpleDateString} from '#/components/forms/DateField/utils'
import {DateFieldProps} from '#/components/forms/DateField/types'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
@@ -38,7 +37,6 @@ export function DateField({
label,
isInvalid,
testID,
accessibilityHint,
}: DateFieldProps) {
const handleOnChange = React.useCallback(
(e: any) => {
@@ -54,14 +52,12 @@ export function DateField({
return (
<TextField.Root isInvalid={isInvalid}>
<TextField.Icon icon={CalendarDays} />
<Input
value={value}
label={label}
onChange={handleOnChange}
onChangeText={() => {}}
testID={testID}
accessibilityHint={accessibilityHint}
/>
</TextField.Root>
)
-1
View File
@@ -4,5 +4,4 @@ export type DateFieldProps = {
label: string
isInvalid?: boolean
testID?: string
accessibilityHint?: string
}
-30
View File
@@ -1,30 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, useTheme} from '#/alf'
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
import {Text} from '#/components/Typography'
export function FormError({error}: {error?: string}) {
const t = useTheme()
if (!error) return null
return (
<View
style={[
{backgroundColor: t.palette.negative_400},
a.flex_row,
a.rounded_sm,
a.p_md,
a.gap_sm,
]}>
<Warning fill={t.palette.white} size="md" />
<View style={[a.flex_1]}>
<Text style={[{color: t.palette.white}, a.font_bold, a.leading_snug]}>
{error}
</Text>
</View>
</View>
)
}
-95
View File
@@ -1,95 +0,0 @@
import React from 'react'
import {Keyboard, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {toNiceDomain} from '#/lib/strings/url-helpers'
import {isAndroid} from '#/platform/detection'
import {ServerInputDialog} from '#/view/com/auth/server-input'
import {atoms as a, useTheme} from '#/alf'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
import {PencilLine_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
import {Button} from '../Button'
import {useDialogControl} from '../Dialog'
import {Text} from '../Typography'
export function HostingProvider({
serviceUrl,
onSelectServiceUrl,
onOpenDialog,
}: {
serviceUrl: string
onSelectServiceUrl: (provider: string) => void
onOpenDialog?: () => void
}) {
const serverInputControl = useDialogControl()
const t = useTheme()
const {_} = useLingui()
const onPressSelectService = React.useCallback(() => {
Keyboard.dismiss()
serverInputControl.open()
if (onOpenDialog) {
onOpenDialog()
}
}, [onOpenDialog, serverInputControl])
return (
<>
<ServerInputDialog
control={serverInputControl}
onSelect={onSelectServiceUrl}
/>
<Button
label={toNiceDomain(serviceUrl)}
accessibilityHint={_(msg`Press to change hosting provider`)}
variant="solid"
color="secondary"
style={[
a.w_full,
a.flex_row,
a.align_center,
a.rounded_sm,
a.px_md,
a.pr_sm,
a.gap_xs,
{paddingVertical: isAndroid ? 14 : 9},
]}
onPress={onPressSelectService}>
{({hovered, pressed}) => {
const interacted = hovered || pressed
return (
<>
<View style={a.pr_xs}>
<Globe
size="md"
fill={
interacted ? t.palette.contrast_800 : t.palette.contrast_500
}
/>
</View>
<Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text>
<View
style={[
a.rounded_sm,
interacted
? t.atoms.bg_contrast_300
: t.atoms.bg_contrast_100,
{marginLeft: 'auto', padding: 6},
]}>
<Pencil
size="sm"
style={{
color: interacted
? t.palette.contrast_800
: t.palette.contrast_500,
}}
/>
</View>
</>
)
}}
</Button>
</>
)
}
+9 -15
View File
@@ -1,20 +1,19 @@
import React from 'react'
import {
AccessibilityProps,
StyleSheet,
View,
TextInput,
TextInputProps,
TextStyle,
View,
ViewStyle,
StyleSheet,
AccessibilityProps,
} from 'react-native'
import {mergeRefs} from '#/lib/merge-refs'
import {HITSLOP_20} from 'lib/constants'
import {android, atoms as a, useTheme, web} from '#/alf'
import {useTheme, atoms as a, web, android} from '#/alf'
import {Text} from '#/components/Typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography'
const Context = React.createContext<{
inputRef: React.RefObject<TextInput> | null
@@ -126,10 +125,9 @@ export function useSharedInputStyles() {
export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & {
label: string
value?: string
onChangeText?: (value: string) => void
value: string
onChangeText: (value: string) => void
isInvalid?: boolean
inputRef?: React.RefObject<TextInput>
}
export function createInput(Component: typeof TextInput) {
@@ -139,7 +137,6 @@ export function createInput(Component: typeof TextInput) {
value,
onChangeText,
isInvalid,
inputRef,
...rest
}: InputProps) {
const t = useTheme()
@@ -164,22 +161,19 @@ export function createInput(Component: typeof TextInput) {
)
}
const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean))
return (
<>
<Component
accessibilityHint={undefined}
{...rest}
accessibilityLabel={label}
ref={refs}
ref={ctx.inputRef}
value={value}
onChangeText={onChangeText}
onFocus={ctx.onFocus}
onBlur={ctx.onBlur}
placeholder={placeholder || label}
placeholderTextColor={t.palette.contrast_500}
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
hitSlop={HITSLOP_20}
style={[
a.relative,
@@ -277,7 +271,7 @@ export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
<Comp
size="md"
style={[
{color: t.palette.contrast_500, pointerEvents: 'none', flexShrink: 0},
{color: t.palette.contrast_500, pointerEvents: 'none'},
ctx.hovered ? hover : {},
ctx.focused ? focus : {},
ctx.isInvalid && ctx.hovered ? errorHover : {},
-12
View File
@@ -1,12 +0,0 @@
import React from 'react'
import {Keyboard} from 'react-native'
export function useOnKeyboardDidShow(cb: () => unknown) {
React.useEffect(() => {
const subscription = Keyboard.addListener('keyboardDidShow', cb)
return () => {
subscription.remove()
}
}, [cb])
}
-5
View File
@@ -1,5 +0,0 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Calendar_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M8 2a1 1 0 0 1 1 1v1h6V3a1 1 0 1 1 2 0v1h2a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2V3a1 1 0 0 1 1-1ZM5 6v3h14V6H5Zm14 5H5v8h14v-8Z',
})
-5
View File
@@ -1,5 +0,0 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Envelope_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4.568 4h14.864c.252 0 .498 0 .706.017.229.019.499.063.77.201a2 2 0 0 1 .874.874c.138.271.182.541.201.77.017.208.017.454.017.706v10.864c0 .252 0 .498-.017.706a2.022 2.022 0 0 1-.201.77 2 2 0 0 1-.874.874 2.022 2.022 0 0 1-.77.201c-.208.017-.454.017-.706.017H4.568c-.252 0-.498 0-.706-.017a2.022 2.022 0 0 1-.77-.201 2 2 0 0 1-.874-.874 2.022 2.022 0 0 1-.201-.77C2 17.93 2 17.684 2 17.432V6.568c0-.252 0-.498.017-.706.019-.229.063-.499.201-.77a2 2 0 0 1 .874-.874c.271-.138.541-.182.77-.201C4.07 4 4.316 4 4.568 4Zm.456 2L12 11.708 18.976 6H5.024ZM20 7.747l-6.733 5.509a2 2 0 0 1-2.534 0L4 7.746V17.4a8.187 8.187 0 0 0 .011.589h.014c.116.01.278.011.575.011h14.8a8.207 8.207 0 0 0 .589-.012v-.013c.01-.116.011-.279.011-.575V7.747Z',
})
-5
View File
@@ -1,5 +0,0 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Lock_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M7 7a5 5 0 0 1 10 0v2h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h1V7Zm-1 4v9h12v-9H6Zm9-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z',
})
-5
View File
@@ -1,5 +0,0 @@
import {createSinglePathSVG} from './TEMPLATE'
export const PencilLine_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M15.586 2.5a2 2 0 0 1 2.828 0L21.5 5.586a2 2 0 0 1 0 2.828l-13 13A2 2 0 0 1 7.086 22H3a1 1 0 0 1-1-1v-4.086a2 2 0 0 1 .586-1.414l13-13ZM17 3.914l-13 13V20h3.086l13-13L17 3.914ZM13 21a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z',
})
-5
View File
@@ -1,5 +0,0 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Ticket_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12a3.5 3.5 0 0 1 1.75-3.032.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z',
})
@@ -0,0 +1,93 @@
import React from 'react'
import {View} from 'react-native'
import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {
usePreferencesQuery,
usePreferencesSetContentLabelMutation,
} from '#/state/queries/preferences'
import {useTheme, atoms as a} from '#/alf'
import {Text} from '#/components/Typography'
import * as ToggleButton from '#/components/forms/ToggleButton'
export function GlobalModerationLabelPref({
labelValueDefinition,
disabled,
}: {
labelValueDefinition: InterpretedLabelValueDefinition
disabled?: boolean
}) {
const {_} = useLingui()
const t = useTheme()
const {identifier} = labelValueDefinition
const {data: preferences} = usePreferencesQuery()
const {mutate, variables} = usePreferencesSetContentLabelMutation()
const savedPref = preferences?.moderationPrefs.labels[identifier]
const pref = variables?.visibility ?? savedPref ?? 'warn'
const allLabelStrings = useGlobalLabelStrings()
const labelStrings =
labelValueDefinition.identifier in allLabelStrings
? allLabelStrings[labelValueDefinition.identifier]
: {
name: labelValueDefinition.identifier,
description: `Labeled "${labelValueDefinition.identifier}"`,
}
const labelOptions = {
hide: _(msg`Hide`),
warn: _(msg`Warn`),
ignore: _(msg`Show`),
}
return (
<View
style={[
a.flex_row,
a.justify_between,
a.gap_sm,
a.py_md,
a.pl_lg,
a.pr_md,
a.align_center,
]}>
<View style={[a.gap_xs, a.flex_1]}>
<Text style={[a.font_bold]}>{labelStrings.name}</Text>
<Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
{labelStrings.description}
</Text>
</View>
<View style={[a.justify_center, {minHeight: 35}]}>
{!disabled && (
<ToggleButton.Group
label={_(
msg`Configure content filtering setting for category: ${labelStrings.name.toLowerCase()}`,
)}
values={[pref]}
onChange={newPref =>
mutate({
label: identifier,
visibility: newPref[0] as LabelPreference,
labelerDid: undefined,
})
}>
<ToggleButton.Button name="ignore" label={labelOptions.ignore}>
{labelOptions.ignore}
</ToggleButton.Button>
<ToggleButton.Button name="warn" label={labelOptions.warn}>
{labelOptions.warn}
</ToggleButton.Button>
<ToggleButton.Button name="hide" label={labelOptions.hide}>
{labelOptions.hide}
</ToggleButton.Button>
</ToggleButton.Group>
)}
</View>
</View>
)
}
@@ -1,293 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription'
import {getLabelStrings} from '#/lib/moderation/useLabelInfo'
import {
usePreferencesQuery,
usePreferencesSetContentLabelMutation,
} from '#/state/queries/preferences'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {InlineLink} from '#/components/Link'
import {Text} from '#/components/Typography'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo'
export function Outer({children}: React.PropsWithChildren<{}>) {
return (
<View
style={[
a.flex_row,
a.gap_md,
a.px_lg,
a.py_lg,
a.justify_between,
a.flex_wrap,
]}>
{children}
</View>
)
}
export function Content({
children,
name,
description,
}: React.PropsWithChildren<{
name: string
description: string
}>) {
const t = useTheme()
const {gtPhone} = useBreakpoints()
return (
<View style={[a.gap_xs, a.flex_1]}>
<Text style={[a.font_bold, gtPhone ? a.text_sm : a.text_md]}>{name}</Text>
<Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
{description}
</Text>
{children}
</View>
)
}
export function Buttons({
name,
values,
onChange,
ignoreLabel,
warnLabel,
hideLabel,
}: {
name: string
values: ToggleButton.GroupProps['values']
onChange: ToggleButton.GroupProps['onChange']
ignoreLabel?: string
warnLabel?: string
hideLabel?: string
}) {
const {_} = useLingui()
const {gtPhone} = useBreakpoints()
return (
<View style={[{minHeight: 35}, gtPhone ? undefined : a.w_full]}>
<ToggleButton.Group
label={_(
msg`Configure content filtering setting for category: ${name}`,
)}
values={values}
onChange={onChange}>
{ignoreLabel && (
<ToggleButton.Button name="ignore" label={ignoreLabel}>
{ignoreLabel}
</ToggleButton.Button>
)}
{warnLabel && (
<ToggleButton.Button name="warn" label={warnLabel}>
{warnLabel}
</ToggleButton.Button>
)}
{hideLabel && (
<ToggleButton.Button name="hide" label={hideLabel}>
{hideLabel}
</ToggleButton.Button>
)}
</ToggleButton.Group>
</View>
)
}
/**
* For use on the global Moderation screen to set prefs for a "global" label,
* not scoped to a single labeler.
*/
export function GlobalLabelPreference({
labelDefinition,
disabled,
}: {
labelDefinition: InterpretedLabelValueDefinition
disabled?: boolean
}) {
const {_} = useLingui()
const {identifier} = labelDefinition
const {data: preferences} = usePreferencesQuery()
const {mutate, variables} = usePreferencesSetContentLabelMutation()
const savedPref = preferences?.moderationPrefs.labels[identifier]
const pref = variables?.visibility ?? savedPref ?? 'warn'
const allLabelStrings = useGlobalLabelStrings()
const labelStrings =
labelDefinition.identifier in allLabelStrings
? allLabelStrings[labelDefinition.identifier]
: {
name: labelDefinition.identifier,
description: `Labeled "${labelDefinition.identifier}"`,
}
const labelOptions = {
hide: _(msg`Hide`),
warn: _(msg`Warn`),
ignore: _(msg`Show`),
}
return (
<Outer>
<Content
name={labelStrings.name}
description={labelStrings.description}
/>
{!disabled && (
<Buttons
name={labelStrings.name.toLowerCase()}
values={[pref]}
onChange={values => {
mutate({
label: identifier,
visibility: values[0] as LabelPreference,
labelerDid: undefined,
})
}}
ignoreLabel={labelOptions.ignore}
warnLabel={labelOptions.warn}
hideLabel={labelOptions.hide}
/>
)}
</Outer>
)
}
/**
* For use on individual labeler pages
*/
export function LabelerLabelPreference({
labelDefinition,
disabled,
labelerDid,
}: {
labelDefinition: InterpretedLabelValueDefinition
disabled?: boolean
labelerDid?: string
}) {
const {i18n} = useLingui()
const t = useTheme()
const {gtPhone} = useBreakpoints()
const isGlobalLabel = !labelDefinition.definedBy
const {identifier} = labelDefinition
const {data: preferences} = usePreferencesQuery()
const {mutate, variables} = usePreferencesSetContentLabelMutation()
const savedPref =
labelerDid && !isGlobalLabel
? preferences?.moderationPrefs.labelers.find(l => l.did === labelerDid)
?.labels[identifier]
: preferences?.moderationPrefs.labels[identifier]
const pref =
variables?.visibility ??
savedPref ??
labelDefinition.defaultSetting ??
'warn'
// does the 'warn' setting make sense for this label?
const canWarn = !(
labelDefinition.blurs === 'none' && labelDefinition.severity === 'none'
)
// is this label adult only?
const adultOnly = labelDefinition.flags.includes('adult')
// is this label disabled because it's adult only?
const adultDisabled =
adultOnly && !preferences?.moderationPrefs.adultContentEnabled
// are there any reasons we cant configure this label here?
const cantConfigure = isGlobalLabel || adultDisabled
const showConfig = !disabled && (gtPhone || !cantConfigure)
// adjust the pref based on whether warn is available
let prefAdjusted = pref
if (adultDisabled) {
prefAdjusted = 'hide'
} else if (!canWarn && pref === 'warn') {
prefAdjusted = 'ignore'
}
// grab localized descriptions of the label and its settings
const currentPrefLabel = useLabelBehaviorDescription(
labelDefinition,
prefAdjusted,
)
const hideLabel = useLabelBehaviorDescription(labelDefinition, 'hide')
const warnLabel = useLabelBehaviorDescription(labelDefinition, 'warn')
const ignoreLabel = useLabelBehaviorDescription(labelDefinition, 'ignore')
const globalLabelStrings = useGlobalLabelStrings()
const labelStrings = getLabelStrings(
i18n.locale,
globalLabelStrings,
labelDefinition,
)
return (
<Outer>
<Content name={labelStrings.name} description={labelStrings.description}>
{cantConfigure && (
<View style={[a.flex_row, a.gap_xs, a.align_center, a.mt_xs]}>
<CircleInfo size="sm" fill={t.atoms.text_contrast_high.color} />
<Text
style={[t.atoms.text_contrast_medium, a.font_semibold, a.italic]}>
{adultDisabled ? (
<Trans>Adult content is disabled.</Trans>
) : isGlobalLabel ? (
<Trans>
Configured in{' '}
<InlineLink to="/moderation" style={a.text_sm}>
moderation settings
</InlineLink>
.
</Trans>
) : null}
</Text>
</View>
)}
</Content>
{showConfig && (
<View style={[gtPhone ? undefined : a.w_full]}>
{cantConfigure ? (
<View
style={[
{minHeight: 35},
a.px_md,
a.py_md,
a.rounded_sm,
a.border,
t.atoms.border_contrast_low,
]}>
<Text style={[a.font_bold, t.atoms.text_contrast_low]}>
{currentPrefLabel}
</Text>
</View>
) : (
<Buttons
name={labelStrings.name.toLowerCase()}
values={[pref]}
onChange={values => {
mutate({
label: identifier,
visibility: values[0] as LabelPreference,
labelerDid,
})
}}
ignoreLabel={ignoreLabel}
warnLabel={canWarn ? warnLabel : undefined}
hideLabel={hideLabel}
/>
)}
</View>
)}
</Outer>
)
}
+7 -3
View File
@@ -39,7 +39,6 @@ export function LabelsOnMe({
return null
}
const labelTarget = isAccount ? _(msg`account`) : _(msg`content`)
return (
<View style={[a.flex_row, style]}>
<LabelsOnMeDialog control={control} subject={details} labels={labels} />
@@ -56,9 +55,14 @@ export function LabelsOnMe({
<ButtonText style={[a.leading_snug]}>
{labels.length}{' '}
{labels.length === 1 ? (
<Trans>label has been placed on this {labelTarget}</Trans>
<Trans>
label has been placed on this {isAccount ? 'account' : 'content'}
</Trans>
) : (
<Trans>labels have been placed on this {labelTarget}</Trans>
<Trans>
labels have been placed on this{' '}
{isAccount ? 'account' : 'content'}
</Trans>
)}
</ButtonText>
</Button>
@@ -125,7 +125,7 @@ function ModerationDetailsDialogInner({
<Trans>
This label was applied by{' '}
{modcause.source.type === 'user' ? (
<Trans>the author</Trans>
'the author'
) : (
<InlineLink
to={makeProfileLink({did: modcause.label.src, handle: ''})}

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