Compare commits

..

224 Commits

Author SHA1 Message Date
Eric Bailey cccaed6aba Refetch labelers on session resolution 2024-03-15 16:57:48 -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
467 changed files with 31690 additions and 78111 deletions
+1 -62
View File
@@ -7,67 +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: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P'],
impliedTextProps: [],
suggestedTextWrappers: {
Button: 'ButtonText',
'ToggleButton.Button': 'ToggleButton.ButtonText',
},
},
],
'bsky-internal/use-typed-gates': 'error',
'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',
@@ -88,8 +31,4 @@ module.exports = {
settings: {
componentWrapperFunctions: ['observer'],
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
},
}
@@ -1,57 +0,0 @@
name: build-and-push-embedr-aws
on:
push:
branches:
- main
- bnewbold/embedr
- bnewbold/embedr-rebase
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}
IMAGE_NAME: embed
jobs:
embedr-container-aws:
if: github.repository == 'bluesky-social/social-app'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
password: ${{ env.PASSWORD }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
file: ./Dockerfile.embedr
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+3 -55
View File
@@ -8,7 +8,6 @@ on:
type: choice
description: Build profile to use
options:
- testflight-android
- production
jobs:
@@ -60,58 +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: ✍️ Rename Testflight bundle
if: ${{ inputs.profile != 'production' }}
run: mv build.aab build.apk
- name: ⏰ Get a timestamp
id: timestamp
uses: nanzm/get-time-action@master
with:
format: 'MM-DD-HH-mm-ss'
- name: 🚀 Upload Production Artifact
id: upload-artifact-production
if: ${{ inputs.profile == 'production' }}
uses: actions/upload-artifact@v4
with:
retention-days: 30
compression-level: 6
name: build-${{ steps.timestamp.outputs.time }}.aab
path: build.aab
- name: 🚀 Upload Testflight Artifact
id: upload-artifact-testflight
if: ${{ inputs.profile != 'production' }}
uses: actions/upload-artifact@v4
with:
retention-days: 30
compression-level: 6
name: build-${{ steps.timestamp.outputs.time }}.apk
path: build.apk
- name: 🔔 Notify Slack of Production Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
"text": "Android build is ready for submission. This is a production build! Download the artifact here: ${{ steps.upload-artifact-production.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: 🔔 Notify Slack of Testflight Build
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-testflight.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: 🚀 Deploy
run: eas submit -p android --non-interactive --path build.aab
+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,315 +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
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-deploy
cancel-in-progress: true
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: 0
- name: ⬇️ Get last successful deployment commit from the cache
id: get-base-commit
uses: actions/cache@v4
with:
path: last-successful-commit-hash.txt
key: last-successful-deployment-commit-${{ github.ref_name }}
- name: Add the last successful deployment commit to the output
id: last-successful-commit
run: echo base-commit=$(cat last-successful-commit-hash.txt) >> "$GITHUB_OUTPUT"
- 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=${{ steps.last-successful-commit.base-commit }} >> "$GITHUB_OUTPUT"
fi
- name: ✓ Make sure we found a base commit
run: |
if [ -z "${{ steps.base-commit.outputs.base-commit }}" && ${{ inputs.channel == 'production' }} ]; 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 }}
- 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='$(echo "${{ 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: Lint check
run: yarn lint
- name: Prettier check
run: yarn prettier --check .
- name: Check & compile i18n
run: yarn intl:build
- name: Type check
run: yarn typecheck
- 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: ✏️ 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' }}
- name: Save successful deployment commit hash
run: echo ${{ steps.fingerprint.outputs.current-git-commit }} > last-successful-commit-hash.txt
# GitHub actions are horrible so let's just copy paste this in
buildIfNecessaryIOS:
name: Build and Submit iOS
runs-on: macos-14
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
cancel-in-progress: false
needs: [bundleDeploy]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.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
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-android
cancel-in-progress: false
needs: [ bundleDeploy ]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.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
-159
View File
@@ -1,159 +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
permissions:
pull-requests: write
actions: write
contents: read
jobs:
webpack-analyzer:
runs-on: ubuntu-22.04
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
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: 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 }}
yarn install
- 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 install
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: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
with:
header: bundle-diff
message: |
| 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 }}%) |
---
test-suite-fingerprint:
runs-on: ubuntu-22.04
if: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }}
concurrency: fingerprint-${{ github.event_name != 'pull_request' && 'main' || github.run_id }}
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: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' }}
with:
header: fingerprint-diff
message: |
The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }}
<details><summary>Fingerprint diff</summary>
```json
${{ steps.fingerprint.outputs.fingerprint-diff }}
```
</details>
---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
- name: 💬 Delete comment
uses: marocchino/sticky-pull-request-comment@v2
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' }}
with:
header: fingerprint-diff
delete: true
+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/*
-78
View File
@@ -1,78 +0,0 @@
FROM golang:1.21-bullseye AS build-env
WORKDIR /usr/src/social-app
ENV DEBIAN_FRONTEND=noninteractive
# Node
ENV NODE_VERSION=18
ENV NVM_DIR=/usr/share/nvm
# Go
ENV GODEBUG="netdns=go"
ENV GOOS="linux"
ENV GOARCH="amd64"
ENV CGO_ENABLED=1
ENV GOEXPERIMENT="loopvar"
COPY . .
#
# Generate the JavaScript webpack. NOTE: this will change
#
RUN mkdir --parents $NVM_DIR && \
wget \
--output-document=/tmp/nvm-install.sh \
https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh && \
bash /tmp/nvm-install.sh
RUN \. "$NVM_DIR/nvm.sh" && \
nvm install $NODE_VERSION && \
nvm use $NODE_VERSION && \
npm install --global yarn && \
yarn && \
cd bskyembed && yarn install --frozen-lockfile && cd .. && \
yarn intl:build && \
yarn build-embed
# DEBUG
RUN find ./bskyweb/embedr-static && find ./bskyweb/embedr-templates && find ./bskyembed/dist
# hack around issue with empty directory and go:embed
RUN touch bskyweb/static/js/empty.txt
#
# Generate the embedr Go binary.
#
RUN cd bskyweb/ && \
go mod download && \
go mod verify
RUN cd bskyweb/ && \
go build \
-v \
-trimpath \
-tags timetzdata \
-o /embedr \
./cmd/embedr
FROM debian:bullseye-slim
ENV GODEBUG=netdns=go
ENV TZ=Etc/UTC
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install --yes \
dumb-init \
ca-certificates
ENTRYPOINT ["dumb-init", "--"]
WORKDIR /embedr
COPY --from=build-env /embedr /usr/bin/embedr
CMD ["/usr/bin/embedr"]
LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app
LABEL org.opencontainers.image.description="embed.bsky.app Web App"
LABEL org.opencontainers.image.licenses=MIT
-6
View File
@@ -13,11 +13,6 @@ build-web: ## Compile web bundle, copy to bskyweb directory
yarn intl:build
yarn build-web
.PHONY: build-web-embed
build-web-embed: ## Compile web embed bundle, copy to bskyweb/embedr* directories
yarn intl:build
yarn build-embed
.PHONY: test
test: ## Run all tests
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test
@@ -33,7 +28,6 @@ lint: ## Run style checks and verify syntax
.PHONY: deps
deps: ## Installs dependent libs using 'yarn install'
yarn install --frozen-lockfile
cd bskyembed && yarn install --frozen-lockfile
.PHONY: nvm-setup
nvm-setup: ## Use NVM to install and activate node+yarn
+2 -2
View File
@@ -1,6 +1,5 @@
import {createServer as createHTTPServer} from 'node:http'
import {parse} from 'node:url'
import {createServer, TestPDS} from '../jest/test-pds'
async function main() {
@@ -15,7 +14,8 @@ async function main() {
await server?.close()
console.log('Starting new server')
const inviteRequired = url?.query && 'invite' in url.query
server = await createServer({inviteRequired})
const phoneRequired = url?.query && 'phone' in url.query
server = await createServer({inviteRequired, phoneRequired})
console.log('Listening at', server.pdsUrl)
if (url?.query) {
if ('users' in url.query) {
+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()
+16 -14
View File
@@ -1,9 +1,8 @@
/* eslint-env detox/detox */
import {beforeAll, describe, it} from '@jest/globals'
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {createServer, loginAsAlice, openApp} from '../util'
import {openApp, loginAsAlice, createServer} from '../util'
describe('Home screen', () => {
beforeAll(async () => {
@@ -18,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 () => {
@@ -69,16 +68,19 @@ describe('Home screen', () => {
).not.toExist()
})
// TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
// it('Can report posts', async () => {
// const carlaPosts = by.id('feedItem-by-carla.test')
// await element(by.id('postDropdownBtn').withAncestor(carlaPosts))
// .atIndex(0)
// .tap()
// await element(by.text('Report post')).tap()
// await element(by.id('com.atproto.moderation.defs#reasonSpam')).tap()
// await element(by.id('sendReportBtn')).tap()
// })
it('Can report posts', async () => {
const carlaPosts = by.id('feedItem-by-carla.test')
await element(by.id('postDropdownBtn').withAncestor(carlaPosts))
.atIndex(0)
.tap()
await element(by.text('Report post')).tap()
await expect(element(by.id('reportModal'))).toBeVisible()
await element(
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
it('Can swipe between feeds', async () => {
await element(by.id('homeScreen')).swipe('left', 'fast', 0.75)
@@ -1,9 +1,8 @@
/* eslint-env detox/detox */
import {beforeAll, describe, it} from '@jest/globals'
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {createServer, loginAsAlice, openApp} from '../util'
import {openApp, loginAsAlice, createServer} from '../util'
describe('invite-codes', () => {
let service: string
@@ -0,0 +1,52 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util'
describe('invite-codes', () => {
let service: string
let inviteCode = ''
beforeAll(async () => {
service = await createServer('?users&invite&phone')
await openApp({permissions: {notifications: 'YES'}})
})
it('I can fetch invite codes', async () => {
await loginAsAlice()
await element(by.id('e2eOpenInviteCodesModal')).tap()
await expect(element(by.id('inviteCodesModal'))).toBeVisible()
const attrs = await element(by.id('inviteCode-0-code')).getAttributes()
inviteCode = attrs.text
await element(by.id('closeBtn')).tap()
await element(by.id('e2eSignOut')).tap()
})
it('I can create a new account with the invite code', async () => {
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen')
await element(by.id('selectServiceButton')).tap()
await device.takeScreenshot('2- selected other server')
await element(by.id('customSelectBtn')).tap()
await element(by.id('customServerTextInput')).typeText(service)
await element(by.id('customServerTextInput')).tapReturnKey()
await element(by.id('doneBtn')).tap()
await device.takeScreenshot('3- input test server URL')
await element(by.id('inviteCodeInput')).typeText(inviteCode)
await element(by.id('emailInput')).typeText('example@test.com')
await element(by.id('passwordInput')).typeText('hunter2')
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('phoneInput')).typeText('2345551234')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('5- requested code')
await element(by.id('codeInput')).typeText('000000')
await device.takeScreenshot('6- entered code')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('e2e-test')
await device.takeScreenshot('7- entered handle')
await element(by.id('nextBtn')).tap()
await expect(element(by.id('onboardingInterests'))).toBeVisible()
})
})
+27 -30
View File
@@ -1,9 +1,8 @@
/* eslint-env detox/detox */
import {beforeAll, describe, it} from '@jest/globals'
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {createServer, loginAsAlice, openApp, sleep} from '../util'
import {openApp, loginAsAlice, createServer, sleep} from '../util'
describe('Profile screen', () => {
beforeAll(async () => {
@@ -71,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()
@@ -88,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()
@@ -125,17 +124,16 @@ describe('Profile screen', () => {
await expect(element(by.id('profileHeaderAlert'))).not.toExist()
})
// TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
// it('Can report another user', async () => {
// await element(by.id('profileHeaderDropdownBtn')).tap()
// await element(by.text('Report Account')).tap()
// await expect(element(by.id('reportModal'))).toBeVisible()
// await element(
// by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
// ).tap()
// await element(by.id('sendReportBtn')).tap()
// await expect(element(by.id('reportModal'))).not.toBeVisible()
// })
it('Can report another user', async () => {
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Report Account')).tap()
await expect(element(by.id('reportModal'))).toBeVisible()
await element(
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
it('Can like posts', async () => {
await element(by.id('postsFeed-flatlist')).swipe(
@@ -181,16 +179,15 @@ describe('Profile screen', () => {
).not.toExist()
})
// TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
// it('Can report posts', async () => {
// const posts = by.id('feedItem-by-bob.test')
// await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap()
// await element(by.text('Report post')).tap()
// await expect(element(by.id('reportModal'))).toBeVisible()
// await element(
// by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
// ).tap()
// await element(by.id('sendReportBtn')).tap()
// await expect(element(by.id('reportModal'))).not.toBeVisible()
// })
it('Can report posts', async () => {
const posts = by.id('feedItem-by-bob.test')
await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap()
await element(by.text('Report post')).tap()
await expect(element(by.id('reportModal'))).toBeVisible()
await element(
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
})
+81
View File
@@ -0,0 +1,81 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, createServer} from '../util'
describe('Create account', () => {
let service: string
beforeAll(async () => {
service = await createServer('?phone')
await openApp({permissions: {notifications: 'YES'}})
})
it('I can create a new account with text verification', async () => {
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen')
await element(by.id('selectServiceButton')).tap()
await device.takeScreenshot('2- selected other server')
await element(by.id('customSelectBtn')).tap()
await element(by.id('customServerTextInput')).typeText(service)
await element(by.id('customServerTextInput')).tapReturnKey()
await element(by.id('doneBtn')).tap()
await device.takeScreenshot('3- input test server URL')
await element(by.id('emailInput')).typeText('text-verification@test.com')
await element(by.id('passwordInput')).typeText('hunter2')
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('phoneInput')).typeText('8042221111')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('5- requested code')
await element(by.id('codeInput')).typeText('000000')
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()
})
it('failed text verification correctly goes back to the code input screen', async () => {
await element(by.id('e2eSignOut')).tap()
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen')
await element(by.id('selectServiceButton')).tap()
await device.takeScreenshot('2- selected other server')
await element(by.id('customSelectBtn')).tap()
await element(by.id('customServerTextInput')).typeText(service)
await element(by.id('customServerTextInput')).tapReturnKey()
await element(by.id('doneBtn')).tap()
await device.takeScreenshot('3- input test server URL')
await element(by.id('emailInput')).typeText('text-verification2@test.com')
await element(by.id('passwordInput')).typeText('hunter2')
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('phoneInput')).typeText('8042221111')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('5- requested code')
await element(by.id('codeInput')).typeText('111111')
await device.takeScreenshot('6- entered code')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('text-verification-test2')
await device.takeScreenshot('7- entered handle')
await element(by.id('nextBtn')).tap()
await expect(element(by.id('codeInput'))).toBeVisible()
await device.takeScreenshot('8- got error')
})
})
+24 -27
View File
@@ -1,9 +1,8 @@
/* eslint-env detox/detox */
import {beforeAll, describe, it} from '@jest/globals'
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {createServer, loginAsAlice, openApp} from '../util'
import {openApp, loginAsAlice, createServer} from '../util'
describe('Thread screen', () => {
beforeAll(async () => {
@@ -103,29 +102,27 @@ describe('Thread screen', () => {
).not.toExist()
})
// TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
// it('Can report the root post', async () => {
// const post = by.id('postThreadItem-by-bob.test')
// await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
// await element(by.text('Report post')).tap()
// await expect(element(by.id('reportModal'))).toBeVisible()
// await element(
// by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
// ).tap()
// await element(by.id('sendReportBtn')).tap()
// await expect(element(by.id('reportModal'))).not.toBeVisible()
// })
it('Can report the root post', async () => {
const post = by.id('postThreadItem-by-bob.test')
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
await element(by.text('Report post')).tap()
await expect(element(by.id('reportModal'))).toBeVisible()
await element(
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
// TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
// it('Can report a reply post', async () => {
// const post = by.id('postThreadItem-by-carla.test')
// await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
// await element(by.text('Report post')).tap()
// await expect(element(by.id('reportModal'))).toBeVisible()
// await element(
// by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
// ).tap()
// await element(by.id('sendReportBtn')).tap()
// await expect(element(by.id('reportModal'))).not.toBeVisible()
// })
it('Can report a reply post', async () => {
const post = by.id('postThreadItem-by-carla.test')
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
await element(by.text('Report post')).tap()
await expect(element(by.id('reportModal'))).toBeVisible()
await element(
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
})
+2 -2
View File
@@ -1,5 +1,5 @@
import {execSync} from 'child_process'
import {resolveConfig} from 'detox/internals'
import {execSync} from 'child_process'
import http from 'http'
const platform = device.getPlatform()
@@ -52,7 +52,7 @@ export async function login(
if (await isVisible('chooseAccountForm')) {
await element(by.id('chooseNewAccountBtn')).tap()
}
await element(by.id('selectServiceButton')).tap()
await element(by.id('loginSelectServiceButton')).tap()
if (takeScreenshots) {
await device.takeScreenshot('2- opened service selector')
}
-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 -30
View File
@@ -41,15 +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 IS_PRODUCTION = process.env.EXPO_PUBLIC_ENV === 'production'
const UPDATES_CHANNEL = IS_TESTFLIGHT
? 'testflight'
: IS_PRODUCTION
? 'production'
: undefined
const UPDATES_ENABLED = !!UPDATES_CHANNEL
return {
expo: {
@@ -95,11 +86,6 @@ module.exports = function (config) {
barStyle: 'light-content',
backgroundColor: '#00000000',
},
// Dark nav bar in light mode is better than light nav bar in dark mode
androidNavigationBar: {
barStyle: 'light-content',
backgroundColor: DARK_SPLASH_CONFIG_ANDROID.backgroundColor,
},
android: {
icon: './assets/icon.png',
adaptiveIcon: {
@@ -136,22 +122,10 @@ module.exports = function (config) {
favicon: './assets/favicon.png',
},
updates: {
url: 'https://updates.bsky.app/manifest',
enabled: UPDATES_ENABLED,
fallbackToCacheTimeout: 30000,
codeSigningCertificate: UPDATES_ENABLED
? './code-signing/certificate.pem'
: undefined,
codeSigningMetadata: UPDATES_ENABLED
? {
keyid: 'main',
alg: 'rsa-v1_5-sha256',
}
: undefined,
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',
@@ -171,6 +145,12 @@ module.exports = function (config) {
},
},
],
[
'expo-updates',
{
username: 'blueskysocial',
},
],
[
'expo-notifications',
{
@@ -181,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" d="M19.002 3a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H12.28l-4.762 2.858A1 1 0 0 1 6.002 21v-2h-1a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h14Z"/></svg>

Before

Width:  |  Height:  |  Size: 221 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="M2.002 6a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H12.28l-4.762 2.858A1 1 0 0 1 6.002 21v-2h-1a3 3 0 0 1-3-3V6Zm3-1a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v1.234l3.486-2.092a1 1 0 0 1 .514-.142h7a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-14Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 386 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="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="M14.242 3.03a1 1 0 0 1 .728 1.213l-4 16a1 1 0 1 1-1.94-.485l4-16a1 1 0 0 1 1.213-.728ZM6.707 7.293a1 1 0 0 1 0 1.414L3.414 12l3.293 3.293a1 1 0 1 1-1.414 1.414l-4-4a1 1 0 0 1 0-1.414l4-4a1 1 0 0 1 1.414 0Zm10.586 0a1 1 0 0 1 1.414 0l4 4a1 1 0 0 1 0 1.414l-4 4a1 1 0 1 1-1.414-1.414L20.586 12l-3.293-3.293a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 466 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 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M10 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM5.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM16 11a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1ZM3.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C1.917 15.521 5.242 12 10 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 17.5 21h-15a1 1 0 0 1-.996-1.094Z" clip-rule="evenodd"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#000" fill-rule="evenodd" d="M10 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM5.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM16 11a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1ZM3.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C1.917 15.521 5.242 12 10 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 17.5 21h-15a1 1 0 0 1-.996-1.094Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 463 B

After

Width:  |  Height:  |  Size: 466 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M17.957 2.293a1 1 0 1 0-1.414 1.414L17.836 5H6a3 3 0 0 0-3 3v3a1 1 0 1 0 2 0V8a1 1 0 0 1 1-1h11.836l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.47-2.47a1.75 1.75 0 0 0 0-2.474l-2.47-2.47ZM20 12a1 1 0 0 1 1 1v3a3 3 0 0 1-3 3H6.164l1.293 1.293a1 1 0 1 1-1.414 1.414l-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47a1 1 0 0 1 1.414 1.414L6.164 17H18a1 1 0 0 0 1-1v-3a1 1 0 0 1 1-1Z"/></svg>

Before

Width:  |  Height:  |  Size: 470 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

-20
View File
@@ -1,20 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "simple-import-sort"],
"extends": [
"eslint:recommended",
"preact",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"rules": {
"simple-import-sort/imports": "warn",
"simple-import-sort/exports": "warn"
},
"parserOptions": {
"sourceType": "module",
"ecmaVersion": "latest",
"project": "./tsconfig.json"
}
}
-5
View File
@@ -1,5 +0,0 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#000" fill-rule="evenodd" d="M12 21a1 1 0 0 1-.707-.293l-6-6a1 1 0 1 1 1.414-1.414L11 17.586V4a1 1 0 1 1 2 0v13.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-6 6A1 1 0 0 1 12 21Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 287 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="rgb(10,122,255)" d="M19.002 3a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H12.28l-4.762 2.858A1 1 0 0 1 6.002 21v-2h-1a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h14Z"/></svg>

Before

Width:  |  Height:  |  Size: 232 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="rgb(66,87,108)" fill-rule="evenodd" d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8-1a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-4a1 1 0 0 1-1-1Zm1-3a1 1 0 1 0 2 0 1 1 0 0 0-2 0Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 361 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#ec4899" d="M12.489 21.372c8.528-4.78 10.626-10.47 9.022-14.47-.779-1.941-2.414-3.333-4.342-3.763-1.697-.378-3.552.003-5.169 1.287-1.617-1.284-3.472-1.665-5.17-1.287-1.927.43-3.562 1.822-4.34 3.764-1.605 4 .493 9.69 9.021 14.47a1 1 0 0 0 .978 0Z"/></svg>

Before

Width:  |  Height:  |  Size: 339 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 320 286"><path fill="rgb(10,122,255)" d="M69.364 19.146c36.687 27.806 76.147 84.186 90.636 114.439 14.489-30.253 53.948-86.633 90.636-114.439C277.107-.917 320-16.44 320 32.957c0 9.865-5.603 82.875-8.889 94.729-11.423 41.208-53.045 51.719-90.071 45.357 64.719 11.12 81.182 47.953 45.627 84.785-80 82.874-106.667-44.333-106.667-44.333s-26.667 127.207-106.667 44.333c-35.555-36.832-19.092-73.665 45.627-84.785-37.026 6.362-78.648-4.149-90.071-45.357C5.603 115.832 0 42.822 0 32.957 0-16.44 42.893-.917 69.364 19.147Z"/></svg>

Before

Width:  |  Height:  |  Size: 588 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#20bc07" d="M17.957 2.293a1 1 0 1 0-1.414 1.414L17.836 5H6a3 3 0 0 0-3 3v3a1 1 0 1 0 2 0V8a1 1 0 0 1 1-1h11.836l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.47-2.47a1.75 1.75 0 0 0 0-2.474l-2.47-2.47ZM20 12a1 1 0 0 1 1 1v3a3 3 0 0 1-3 3H6.164l1.293 1.293a1 1 0 1 1-1.414 1.414l-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47a1 1 0 0 1 1.414 1.414L6.164 17H18a1 1 0 0 0 1-1v-3a1 1 0 0 1 1-1Z"/></svg>

Before

Width:  |  Height:  |  Size: 473 B

-19
View File
@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bluesky Embed</title>
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="mask-icon" href="/static/safari-pinned-tab.svg" color="#1185fe">
<meta name="theme-color">
<meta name="application-name" content="Bluesky">
<meta name="generator" content="bskyweb">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/screens/landing.tsx"></script>
</body>
</html>
-29
View File
@@ -1,29 +0,0 @@
{
"name": "bskyembed",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build-snippet": "tsc --project tsconfig.snippet.json",
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src"
},
"dependencies": {
"@atproto/api": "^0.12.2",
"@preact/preset-vite": "^2.8.2",
"@vitejs/plugin-legacy": "^5.3.2",
"preact": "^10.4.8",
"terser": "^5.30.3"
},
"devDependencies": {
"autoprefixer": "^10.4.19",
"eslint": "^8.19.0",
"eslint-config-preact": "^1.3.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^4.0.5",
"vite": "^5.2.8",
"vite-tsconfig-paths": "^4.3.2"
}
}
-19
View File
@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bluesky Embed</title>
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="mask-icon" href="/static/safari-pinned-tab.svg" color="#1185fe">
<meta name="theme-color">
<meta name="application-name" content="Bluesky">
<meta name="generator" content="bskyweb">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/screens/post.tsx"></script>
</body>
</html>
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
-100
View File
@@ -1,100 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Window {
bluesky: {
scan: (element?: Pick<Element, 'querySelectorAll'>) => void
}
}
const EMBED_URL = 'https://embed.bsky.app'
window.bluesky = window.bluesky || {
scan,
}
/**
* Listen for messages from the Bluesky embed iframe and adjust the height of
* the iframe accordingly.
*/
window.addEventListener('message', event => {
if (event.origin !== EMBED_URL) {
return
}
const id = (event.data as {id: string}).id
if (!id) {
return
}
const embed = document.querySelector<HTMLIFrameElement>(
`[data-bluesky-id="${id}"]`,
)
if (!embed) {
return
}
const height = (event.data as {height: number}).height
if (height) {
embed.style.height = `${height}px`
}
})
/**
* Scan the document for all elements with the data-bluesky-aturi attribute,
* and initialize them as Bluesky embeds.
*
* @param element Only scan this specific element @default document @optional
* @returns
*/
function scan(node = document) {
const embeds = node.querySelectorAll('[data-bluesky-uri]')
for (let i = 0; i < embeds.length; i++) {
const id = String(Math.random()).slice(2)
const embed = embeds[i]
const aturi = embed.getAttribute('data-bluesky-uri')
if (!aturi) {
continue
}
const ref_url = location.origin + location.pathname
const searchParams = new URLSearchParams()
searchParams.set('id', id)
if (ref_url.startsWith('http')) {
searchParams.set('ref_url', encodeURIComponent(ref_url))
}
const iframe = document.createElement('iframe')
iframe.setAttribute('data-bluesky-id', id)
iframe.src = `${EMBED_URL}/embed/${aturi.slice(
'at://'.length,
)}?${searchParams.toString()}`
iframe.width = '100%'
iframe.style.border = 'none'
iframe.style.display = 'block'
iframe.style.flexGrow = '1'
iframe.frameBorder = '0'
iframe.scrolling = 'no'
const container = document.createElement('div')
container.style.maxWidth = '600px'
container.style.width = '100%'
container.style.marginTop = '10px'
container.style.marginBottom = '10px'
container.style.display = 'flex'
container.className = 'bluesky-embed'
container.appendChild(iframe)
embed.replaceWith(container)
}
}
if (['interactive', 'complete'].indexOf(document.readyState) !== -1) {
scan()
} else {
document.addEventListener('DOMContentLoaded', () => scan())
}
-55
View File
@@ -1,55 +0,0 @@
import {ComponentChildren, h} from 'preact'
import {useEffect, useRef} from 'preact/hooks'
import {Link} from './link'
export function Container({
children,
href,
}: {
children: ComponentChildren
href?: string
}) {
const ref = useRef<HTMLDivElement>(null)
const prevHeight = useRef(0)
useEffect(() => {
if (ref.current) {
const observer = new ResizeObserver(entries => {
const entry = entries[0]
if (!entry) return
let {height} = entry.contentRect
height += 2 // border top and bottom
if (height !== prevHeight.current) {
prevHeight.current = height
window.parent.postMessage(
{height, id: new URLSearchParams(window.location.search).get('id')},
'*',
)
}
})
observer.observe(ref.current)
return () => observer.disconnect()
}
}, [])
return (
<div
ref={ref}
className="w-full bg-white hover:bg-neutral-50 relative transition-colors max-w-[600px] min-w-[300px] flex border rounded-xl"
onClick={() => {
if (ref.current && href) {
// forwardRef requires preact/compat - let's keep it simple
// to keep the bundle size down
const anchor = ref.current.querySelector('a')
if (anchor) {
anchor.click()
}
}
}}>
{href && <Link href={href} />}
<div className="flex-1 px-4 pt-3 pb-2.5">{children}</div>
</div>
)
}
-351
View File
@@ -1,351 +0,0 @@
import {
AppBskyEmbedExternal,
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyGraphDefs,
AppBskyLabelerDefs,
} from '@atproto/api'
import {ComponentChildren, h} from 'preact'
import {useMemo} from 'preact/hooks'
import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg'
import {CONTENT_LABELS, labelsToInfo} from '../labels'
import {getRkey} from '../utils'
import {Link} from './link'
export function Embed({
content,
labels,
hideRecord,
}: {
content: AppBskyFeedDefs.PostView['embed']
labels: AppBskyFeedDefs.PostView['labels']
hideRecord?: boolean
}) {
const labelInfo = useMemo(() => labelsToInfo(labels), [labels])
if (!content) return null
try {
// Case 1: Image
if (AppBskyEmbedImages.isView(content)) {
return <ImageEmbed content={content} labelInfo={labelInfo} />
}
// Case 2: External link
if (AppBskyEmbedExternal.isView(content)) {
return <ExternalEmbed content={content} labelInfo={labelInfo} />
}
// Case 3: Record (quote or linked post)
if (AppBskyEmbedRecord.isView(content)) {
if (hideRecord) {
return null
}
const record = content.record
// Case 3.1: Post
if (AppBskyEmbedRecord.isViewRecord(record)) {
const pwiOptOut = !!record.author.labels?.find(
label => label.val === '!no-unauthenticated',
)
if (pwiOptOut) {
return (
<Info>
The author of the quoted post has requested their posts not be
displayed on external sites.
</Info>
)
}
let text
if (AppBskyFeedPost.isRecord(record.value)) {
text = record.value.text
}
const isAuthorLabeled = record.author.labels?.some(label =>
CONTENT_LABELS.includes(label.val),
)
return (
<Link
href={`/profile/${record.author.did}/post/${getRkey(record)}`}
className="transition-colors hover:bg-neutral-100 border rounded-lg p-2 gap-1.5 w-full flex flex-col">
<div className="flex gap-1.5 items-center">
<div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 shrink-0">
<img
src={record.author.avatar}
style={isAuthorLabeled ? {filter: 'blur(1.5px)'} : undefined}
/>
</div>
<p className="line-clamp-1 text-sm">
<span className="font-bold">{record.author.displayName}</span>
<span className="text-textLight ml-1">
@{record.author.handle}
</span>
</p>
</div>
{text && <p className="text-sm">{text}</p>}
{record.embeds?.map(embed => (
<Embed
key={embed.$type}
content={embed}
labels={record.labels}
hideRecord
/>
))}
</Link>
)
}
// Case 3.2: List
if (AppBskyGraphDefs.isListView(record)) {
return (
<GenericWithImage
image={record.avatar}
title={record.name}
href={`/profile/${record.creator.did}/lists/${getRkey(record)}`}
subtitle={
record.purpose === AppBskyGraphDefs.MODLIST
? `Moderation list by @${record.creator.handle}`
: `User list by @${record.creator.handle}`
}
description={record.description}
/>
)
}
// Case 3.3: Feed
if (AppBskyFeedDefs.isGeneratorView(record)) {
return (
<GenericWithImage
image={record.avatar}
title={record.displayName}
href={`/profile/${record.creator.did}/feed/${getRkey(record)}`}
subtitle={`Feed by @${record.creator.handle}`}
description={`Liked by ${record.likeCount ?? 0} users`}
/>
)
}
// Case 3.4: Labeler
if (AppBskyLabelerDefs.isLabelerView(record)) {
return (
<GenericWithImage
image={record.creator.avatar}
title={record.creator.displayName || record.creator.handle}
href={`/profile/${record.creator.did}`}
subtitle="Labeler"
description={`Liked by ${record.likeCount ?? 0} users`}
/>
)
}
// Case 3.5: Post not found
if (AppBskyEmbedRecord.isViewNotFound(record)) {
return <Info>Quoted post not found, it may have been deleted.</Info>
}
// Case 3.6: Post blocked
if (AppBskyEmbedRecord.isViewBlocked(record)) {
return <Info>The quoted post is blocked.</Info>
}
throw new Error('Unknown embed type')
}
// Case 4: Record with media
if (
AppBskyEmbedRecordWithMedia.isView(content) &&
AppBskyEmbedRecord.isViewRecord(content.record.record)
) {
return (
<div className="flex flex-col gap-2">
<Embed
content={content.media}
labels={labels}
hideRecord={hideRecord}
/>
<Embed
content={{
$type: 'app.bsky.embed.record#view',
record: content.record.record,
}}
labels={content.record.record.labels}
hideRecord={hideRecord}
/>
</div>
)
}
throw new Error('Unsupported embed type')
} catch (err) {
return (
<Info>{err instanceof Error ? err.message : 'An error occurred'}</Info>
)
}
}
function Info({children}: {children: ComponentChildren}) {
return (
<div className="w-full rounded-lg border py-2 px-2.5 flex-row flex gap-2 bg-neutral-50">
<img src={infoIcon as string} className="w-4 h-4 shrink-0 mt-0.5" />
<p className="text-sm text-textLight">{children}</p>
</div>
)
}
function ImageEmbed({
content,
labelInfo,
}: {
content: AppBskyEmbedImages.View
labelInfo?: string
}) {
if (labelInfo) {
return <Info>{labelInfo}</Info>
}
switch (content.images.length) {
case 1:
return (
<img
src={content.images[0].thumb}
alt={content.images[0].alt}
className="w-full rounded-lg overflow-hidden object-cover h-auto max-h-[1000px]"
/>
)
case 2:
return (
<div className="flex gap-1 rounded-lg overflow-hidden w-full aspect-[2/1]">
{content.images.map((image, i) => (
<img
key={i}
src={image.thumb}
alt={image.alt}
className="w-1/2 h-full object-cover rounded-sm"
/>
))}
</div>
)
case 3:
return (
<div className="flex gap-1 rounded-lg overflow-hidden w-full aspect-[2/1]">
<img
src={content.images[0].thumb}
alt={content.images[0].alt}
className="flex-[3] object-cover rounded-sm"
/>
<div className="flex flex-col gap-1 flex-[2]">
{content.images.slice(1).map((image, i) => (
<img
key={i}
src={image.thumb}
alt={image.alt}
className="w-full h-full object-cover rounded-sm"
/>
))}
</div>
</div>
)
case 4:
return (
<div className="grid grid-cols-2 gap-1 rounded-lg overflow-hidden">
{content.images.map((image, i) => (
<img
key={i}
src={image.thumb}
alt={image.alt}
className="aspect-square w-full object-cover rounded-sm"
/>
))}
</div>
)
default:
return null
}
}
function ExternalEmbed({
content,
labelInfo,
}: {
content: AppBskyEmbedExternal.View
labelInfo?: string
}) {
function toNiceDomain(url: string): string {
try {
const urlp = new URL(url)
return urlp.host ? urlp.host : url
} catch (e) {
return url
}
}
if (labelInfo) {
return <Info>{labelInfo}</Info>
}
return (
<Link
href={content.external.uri}
className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch">
{content.external.thumb && (
<img
src={content.external.thumb}
className="aspect-[1.91/1] object-cover"
/>
)}
<div className="py-3 px-4">
<p className="text-sm text-textLight line-clamp-1">
{toNiceDomain(content.external.uri)}
</p>
<p className="font-semibold line-clamp-3">{content.external.title}</p>
<p className="text-sm text-textLight line-clamp-2 mt-0.5">
{content.external.description}
</p>
</div>
</Link>
)
}
function GenericWithImage({
title,
subtitle,
href,
image,
description,
}: {
title: string
subtitle: string
href: string
image?: string
description?: string
}) {
return (
<Link
href={href}
className="w-full rounded-lg border py-2 px-3 flex flex-col gap-2">
<div className="flex gap-2.5 items-center">
{image ? (
<img
src={image}
alt={title}
className="w-8 h-8 rounded-md bg-neutral-300 shrink-0"
/>
) : (
<div className="w-8 h-8 rounded-md bg-brand shrink-0" />
)}
<div className="flex-1">
<p className="font-bold text-sm">{title}</p>
<p className="text-textLight text-sm">{subtitle}</p>
</div>
</div>
{description && <p className="text-textLight text-sm">{description}</p>}
</Link>
)
}
-32
View File
@@ -1,32 +0,0 @@
import {h} from 'preact'
export function Link({
href,
className,
...props
}: {
href: string
className?: string
} & h.JSX.HTMLAttributes<HTMLAnchorElement>) {
const searchParam = new URLSearchParams(window.location.search)
const ref_url = searchParam.get('ref_url')
const newSearchParam = new URLSearchParams()
newSearchParam.set('ref_src', 'embed')
if (ref_url) {
newSearchParam.set('ref_url', ref_url)
}
return (
<a
href={`${
href.startsWith('http') ? href : `https://bsky.app${href}`
}?${newSearchParam.toString()}`}
target="_blank"
rel="noopener noreferrer nofollow"
onClick={evt => evt.stopPropagation()}
className={`cursor-pointer ${className || ''}`}
{...props}
/>
)
}
-160
View File
@@ -1,160 +0,0 @@
import {AppBskyFeedDefs, AppBskyFeedPost, RichText} from '@atproto/api'
import {h} from 'preact'
import replyIcon from '../../assets/bubble_filled_stroke2_corner2_rounded.svg'
import likeIcon from '../../assets/heart2_filled_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg'
import repostIcon from '../../assets/repost_stroke2_corner2_rounded.svg'
import {CONTENT_LABELS} from '../labels'
import {getRkey, niceDate} from '../utils'
import {Container} from './container'
import {Embed} from './embed'
import {Link} from './link'
interface Props {
thread: AppBskyFeedDefs.ThreadViewPost
}
export function Post({thread}: Props) {
const post = thread.post
const isAuthorLabeled = post.author.labels?.some(label =>
CONTENT_LABELS.includes(label.val),
)
let record: AppBskyFeedPost.Record | null = null
if (AppBskyFeedPost.isRecord(post.record)) {
record = post.record
}
const href = `/profile/${post.author.did}/post/${getRkey(post)}`
return (
<Container href={href}>
<div className="flex-1 flex-col flex gap-2" lang={record?.langs?.[0]}>
<div className="flex gap-2.5 items-center cursor-pointer">
<Link href={`/profile/${post.author.did}`} className="rounded-full">
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 shrink-0">
<img
src={post.author.avatar}
style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined}
/>
</div>
</Link>
<div>
<Link
href={`/profile/${post.author.did}`}
className="font-bold text-[17px] leading-5 line-clamp-1 hover:underline underline-offset-2 decoration-2">
<p>{post.author.displayName}</p>
</Link>
<Link
href={`/profile/${post.author.did}`}
className="text-[15px] text-textLight hover:underline line-clamp-1">
<p>@{post.author.handle}</p>
</Link>
</div>
<div className="flex-1" />
<Link
href={href}
className="transition-transform hover:scale-110 shrink-0 self-start">
<img src={logo as string} className="h-8" />
</Link>
</div>
<PostContent record={record} />
<Embed content={post.embed} labels={post.labels} />
<Link href={href}>
<time
datetime={new Date(post.indexedAt).toISOString()}
className="text-textLight mt-1 text-sm hover:underline">
{niceDate(post.indexedAt)}
</time>
</Link>
<div className="border-t w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
{!!post.likeCount && (
<div className="flex items-center gap-2 cursor-pointer">
<img src={likeIcon as string} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">
{post.likeCount}
</p>
</div>
)}
{!!post.repostCount && (
<div className="flex items-center gap-2 cursor-pointer">
<img src={repostIcon as string} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">
{post.repostCount}
</p>
</div>
)}
<div className="flex items-center gap-2 cursor-pointer">
<img src={replyIcon as string} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">Reply</p>
</div>
<div className="flex-1" />
<p className="cursor-pointer text-brand font-bold hover:underline hidden min-[450px]:inline">
{post.replyCount
? `Read ${post.replyCount} ${
post.replyCount > 1 ? 'replies' : 'reply'
} on Bluesky`
: `View on Bluesky`}
</p>
<p className="cursor-pointer text-brand font-bold hover:underline min-[450px]:hidden">
<span className="hidden min-[380px]:inline">View on </span>Bluesky
</p>
</div>
</div>
</Container>
)
}
function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
if (!record) return null
const rt = new RichText({
text: record.text,
facets: record.facets,
})
const richText = []
let counter = 0
for (const segment of rt.segments()) {
if (segment.isLink() && segment.link) {
richText.push(
<Link
key={counter}
href={segment.link.uri}
className="text-blue-400 hover:underline">
{segment.text}
</Link>,
)
} else if (segment.isMention() && segment.mention) {
richText.push(
<Link
key={counter}
href={`/profile/${segment.mention.did}`}
className="text-blue-500 hover:underline">
{segment.text}
</Link>,
)
} else if (segment.isTag() && segment.tag) {
richText.push(
<Link
key={counter}
href={`/tag/${segment.tag.tag}`}
className="text-blue-500 hover:underline">
{segment.text}
</Link>,
)
} else {
richText.push(segment.text)
}
counter++
}
return (
<p className="min-[300px]:text-lg leading-6 break-word break-words whitespace-pre-wrap">
{richText}
</p>
)
}
-7
View File
@@ -1,7 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.break-word {
word-break: break-word;
}
-21
View File
@@ -1,21 +0,0 @@
import {AppBskyFeedDefs} from '@atproto/api'
export const CONTENT_LABELS = ['porn', 'sexual', 'nudity', 'graphic-media']
export function labelsToInfo(
labels?: AppBskyFeedDefs.PostView['labels'],
): string | undefined {
const label = labels?.find(label => CONTENT_LABELS.includes(label.val))
switch (label?.val) {
case 'porn':
case 'sexual':
return 'Adult Content'
case 'nudity':
return 'Non-sexual Nudity'
case 'graphic-media':
return 'Graphic Media'
default:
return undefined
}
}
-294
View File
@@ -1,294 +0,0 @@
import '../index.css'
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri, BskyAgent} from '@atproto/api'
import {h, render} from 'preact'
import {useEffect, useMemo, useRef, useState} from 'preact/hooks'
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
import {niceDate} from '../utils'
const DEFAULT_POST = 'https://bsky.app/profile/emilyliu.me/post/3jzn6g7ixgq2y'
const DEFAULT_URI =
'at://did:plc:vjug55kidv6sye7ykr5faxxn/app.bsky.feed.post/3jzn6g7ixgq2y'
export const EMBED_SERVICE = 'https://embed.bsky.app'
export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
const root = document.getElementById('app')
if (!root) throw new Error('No root element')
const agent = new BskyAgent({
service: 'https://public.api.bsky.app',
})
render(<LandingPage />, root)
function LandingPage() {
const [uri, setUri] = useState('')
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const [thread, setThread] = useState<AppBskyFeedDefs.ThreadViewPost | null>(
null,
)
useEffect(() => {
void (async () => {
setError(null)
setThread(null)
setLoading(true)
try {
let atUri = DEFAULT_URI
if (uri) {
if (uri.startsWith('at://')) {
atUri = uri
} else {
try {
const urlp = new URL(uri)
if (!urlp.hostname.endsWith('bsky.app')) {
throw new Error('Invalid hostname')
}
const split = urlp.pathname.slice(1).split('/')
if (split.length < 4) {
throw new Error('Invalid pathname')
}
const [profile, didOrHandle, type, rkey] = split
if (profile !== 'profile' || type !== 'post') {
throw new Error('Invalid profile or type')
}
let did = didOrHandle
if (!didOrHandle.startsWith('did:')) {
const resolution = await agent.resolveHandle({
handle: didOrHandle,
})
if (!resolution.data.did) {
throw new Error('No DID found')
}
did = resolution.data.did
}
atUri = `at://${did}/app.bsky.feed.post/${rkey}`
} catch (err) {
console.log(err)
throw new Error('Invalid Bluesky URL')
}
}
}
const {data} = await agent.getPostThread({
uri: atUri,
depth: 0,
parentHeight: 0,
})
if (!AppBskyFeedDefs.isThreadViewPost(data.thread)) {
throw new Error('Post not found')
}
const pwiOptOut = !!data.thread.post.author.labels?.find(
label => label.val === '!no-unauthenticated',
)
if (pwiOptOut) {
throw new Error(
'The author of this post has requested their posts not be displayed on external sites.',
)
}
setThread(data.thread)
} catch (err) {
console.error(err)
setError(err instanceof Error ? err.message : 'Invalid Bluesky URL')
} finally {
setLoading(false)
}
})()
}, [uri])
return (
<main className="w-full min-h-screen flex flex-col items-center gap-8 py-14 px-4 md:pt-32">
<Link
href="https://bsky.social/about"
className="transition-transform hover:scale-110">
<img src={logo as string} className="h-10" />
</Link>
<h1 className="text-4xl font-bold text-center">Embed a Bluesky Post</h1>
<input
type="text"
value={uri}
onInput={e => setUri(e.currentTarget.value)}
className="border rounded-lg py-3 w-full max-w-[600px] px-4"
placeholder={DEFAULT_POST}
/>
<img src={arrowBottom as string} className="w-6" />
{loading ? (
<Skeleton />
) : (
<div className="w-full max-w-[600px] gap-8 flex flex-col">
{!error && thread && uri && <Snippet thread={thread} />}
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
{error && (
<div className="w-full border border-red-500 bg-red-50 px-4 py-3 rounded-lg">
<p className="text-red-500 text-center">{error}</p>
</div>
)}
</div>
)}
</main>
)
}
function Skeleton() {
return (
<Container>
<div className="flex-1 flex-col flex gap-2 pb-8">
<div className="flex gap-2.5 items-center">
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 shrink-0 animate-pulse" />
<div className="flex-1">
<div className="bg-neutral-100 animate-pulse w-64 h-4 rounded" />
<div className="bg-neutral-100 animate-pulse w-32 h-3 mt-1 rounded" />
</div>
</div>
<div className="w-full h-4 mt-2 bg-neutral-100 rounded animate-pulse" />
<div className="w-5/6 h-4 bg-neutral-100 rounded animate-pulse" />
<div className="w-3/4 h-4 bg-neutral-100 rounded animate-pulse" />
</div>
</Container>
)
}
function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
const ref = useRef<HTMLInputElement>(null)
const [copied, setCopied] = useState(false)
// reset copied state after 2 seconds
useEffect(() => {
if (copied) {
const timeout = setTimeout(() => {
setCopied(false)
}, 2000)
return () => clearTimeout(timeout)
}
}, [copied])
const snippet = useMemo(() => {
const record = thread.post.record
if (!AppBskyFeedPost.isRecord(record)) {
return ''
}
const lang = record.langs && record.langs.length > 0 ? record.langs[0] : ''
const profileHref = toShareUrl(
['/profile', thread.post.author.did].join('/'),
)
const urip = new AtUri(thread.post.uri)
const href = toShareUrl(
['/profile', thread.post.author.did, 'post', urip.rkey].join('/'),
)
// x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x
// DO NOT ADD ANY NEW INTERPOLATIONS BELOW WITHOUT ESCAPING THEM!
// Also, keep this code synced with the app code in Embed.tsx.
// x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x
return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml(
thread.post.uri,
)}" data-bluesky-cid="${escapeHtml(thread.post.cid)}"><p lang="${escapeHtml(
lang,
)}">${escapeHtml(record.text)}${
record.embed
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
: ''
}</p>&mdash; ${escapeHtml(
thread.post.author.displayName || thread.post.author.handle,
)} (<a href="${escapeHtml(profileHref)}">@${escapeHtml(
thread.post.author.handle,
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
niceDate(thread.post.indexedAt),
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
}, [thread])
return (
<div className="flex gap-2 w-full">
<input
ref={ref}
type="text"
value={snippet}
className="border rounded-lg py-3 w-full px-4"
readOnly
autoFocus
onFocus={() => {
ref.current?.select()
}}
/>
<button
className="rounded-lg bg-brand text-white color-white py-3 px-4 whitespace-nowrap min-w-28"
onClick={() => {
ref.current?.focus()
ref.current?.select()
void navigator.clipboard.writeText(snippet)
setCopied(true)
}}>
{copied ? 'Copied!' : 'Copy code'}
</button>
</div>
)
}
function toShareUrl(path: string) {
return `https://bsky.app${path}?ref_src=embed`
}
/**
* Based on a snippet of code from React, which itself was based on the escape-html library.
* Copyright (c) Meta Platforms, Inc. and affiliates
* Copyright (c) 2012-2013 TJ Holowaychuk
* Copyright (c) 2015 Andreas Lubbe
* Copyright (c) 2015 Tiancheng "Timothy" Gu
* Licensed as MIT.
*/
const matchHtmlRegExp = /["'&<>]/
function escapeHtml(string: string) {
const str = String(string)
const match = matchHtmlRegExp.exec(str)
if (!match) {
return str
}
let escape
let html = ''
let index
let lastIndex = 0
for (index = match.index; index < str.length; index++) {
switch (str.charCodeAt(index)) {
case 34: // "
escape = '&quot;'
break
case 38: // &
escape = '&amp;'
break
case 39: // '
escape = '&#x27;'
break
case 60: // <
escape = '&lt;'
break
case 62: // >
escape = '&gt;'
break
default:
continue
}
if (lastIndex !== index) {
html += str.slice(lastIndex, index)
}
lastIndex = index + 1
html += escape
}
return lastIndex !== index ? html + str.slice(lastIndex, index) : html
}
-85
View File
@@ -1,85 +0,0 @@
import '../index.css'
import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
import {h, render} from 'preact'
import logo from '../../assets/logo.svg'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
import {getRkey} from '../utils'
const root = document.getElementById('app')
if (!root) throw new Error('No root element')
const agent = new BskyAgent({
service: 'https://public.api.bsky.app',
})
const uri = `at://${window.location.pathname.slice('/embed/'.length)}`
if (!uri) {
throw new Error('No uri in path')
}
agent
.getPostThread({
uri,
depth: 0,
parentHeight: 0,
})
.then(({data}) => {
if (!AppBskyFeedDefs.isThreadViewPost(data.thread)) {
throw new Error('Expected a ThreadViewPost')
}
const pwiOptOut = !!data.thread.post.author.labels?.find(
label => label.val === '!no-unauthenticated',
)
if (pwiOptOut) {
render(<PwiOptOut thread={data.thread} />, root)
} else {
render(<Post thread={data.thread} />, root)
}
})
.catch(err => {
console.error(err)
render(<ErrorMessage />, root)
})
function PwiOptOut({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
const href = `/profile/${thread.post.author.did}/post/${getRkey(thread.post)}`
return (
<Container href={href}>
<Link
href={href}
className="transition-transform hover:scale-110 absolute top-4 right-4">
<img src={logo as string} className="h-6" />
</Link>
<div className="w-full py-12 gap-4 flex flex-col items-center">
<p className="max-w-80 text-center w-full text-textLight">
The author of this post has requested their posts not be displayed on
external sites.
</p>
<Link
href={href}
className="max-w-80 rounded-lg bg-brand text-white color-white text-center py-1 px-4 w-full mx-auto">
View on Bluesky
</Link>
</div>
</Container>
)
}
function ErrorMessage() {
return (
<Container href="https://bsky.app/">
<Link
href="https://bsky.app/"
className="transition-transform hover:scale-110 absolute top-4 right-4">
<img src={logo as string} className="h-6" />
</Link>
<p className="my-16 text-center w-full text-textLight">
Post not found, it may have been deleted.
</p>
</Container>
)
}
-18
View File
@@ -1,18 +0,0 @@
import {AtUri} from '@atproto/api'
export function niceDate(date: number | string | Date) {
const d = new Date(date)
return `${d.toLocaleDateString('en-us', {
year: 'numeric',
month: 'short',
day: 'numeric',
})} at ${d.toLocaleTimeString(undefined, {
hour: 'numeric',
minute: '2-digit',
})}`
}
export function getRkey({uri}: {uri: string}): string {
const at = new AtUri(uri)
return at.rkey
}
-13
View File
@@ -1,13 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
brand: 'rgb(10,122,255)',
textLight: 'rgb(66,87,108)',
},
},
},
plugins: [],
}
-24
View File
@@ -1,24 +0,0 @@
{
"compilerOptions": {
"target": "ES5",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": ["vite/client"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"downlevelIteration": true
},
"include": ["src"]
}
-10
View File
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"target": "ES5",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"strict": true,
"outDir": "dist"
},
"include": ["snippet"],
}
-27
View File
@@ -1,27 +0,0 @@
import {resolve} from 'node:path'
import preact from '@preact/preset-vite'
import legacy from '@vitejs/plugin-legacy'
import type {UserConfig} from 'vite'
import paths from 'vite-tsconfig-paths'
const config: UserConfig = {
plugins: [
preact(),
paths(),
legacy({
targets: ['defaults', 'not IE 11'],
}),
],
build: {
assetsDir: 'static',
rollupOptions: {
input: {
index: resolve(__dirname, 'index.html'),
post: resolve(__dirname, 'post.html'),
},
},
},
}
export default config
-4194
View File
File diff suppressed because it is too large Load Diff
-10
View File
@@ -3,22 +3,12 @@ test-coverage.out
# Don't check in the binary.
/bskyweb
/embedr
# Don't accidentally commit JS-generated code
static/js/*.js
static/js/*.map
static/js/*.js.LICENSE.txt
static/js/empty.txt
templates/scripts.html
templates/*-embed.html
static/embed/*.html
static/embed/assets/*.js
static/embed/assets/*.css
embedr-static/post-*.js
embedr-static/post-*.css
embedr-static/index-*.js
embedr-static/polyfills-*.js
# Don't ignore this file
!.gitignore
-5
View File
@@ -14,7 +14,6 @@ help: ## Print info about all commands
.PHONY: build
build: ## Build all executables
go build ./cmd/bskyweb
go build ./cmd/embedr
.PHONY: test
test: ## Run all tests
@@ -44,7 +43,3 @@ check: ## Compile everything, checking syntax (does not output binaries)
.PHONY: run-dev-bskyweb
run-dev-bskyweb: .env ## Runs 'bskyweb' for local dev
GOLOG_LOG_LEVEL=info go run ./cmd/bskyweb serve
.PHONY: run-dev-embedr
run-dev-embedr: .env ## Runs 'embedr' for local dev
GOLOG_LOG_LEVEL=info go run ./cmd/embedr serve
-52
View File
@@ -1,52 +0,0 @@
## oEmbed
<https://oembed.com/>
* URL scheme: `https://bsky.app/profile/*/post/*`
* API endpoint: `https://embed.bsky.app/oembed`
Request params:
- `url` (required): support both AT-URI and bsky.app URL
- `maxwidth` (optional): [220..550], 325 is default
- `maxheight` (not supported!)
- `format` (optional): only `json` supported
Response format:
- `type` (required): "rich"
- `version` (required): "1.0"
- `author_name` (optional): display name
- `author_url` (optional): profile URL
- `provider_name` (optional): "Bluesky Social"
- `provider_url` (optional): "https://bsky.app"
- `cache_age` (optional, integer seconds): 86400 (24 hours) (?)
- `width` (required): ?
- `height` (required): ?
Not used:
- title (optional): A text title, describing the resource.
- thumbnail_url (optional): A URL to a thumbnail image representing the resource. The thumbnail must respect any maxwidth and maxheight parameters. If this parameter is present, thumbnail_width and thumbnail_height must also be present.
- thumbnail_width (optional): The width of the optional thumbnail. If this parameter is present, thumbnail_url and thumbnail_height must also be present.
- thumbnail_height (optional): The height of the optional thumbnail. If this parameter is present, thumbnail_url and thumbnail_width must also be present.
Only `json` is supported; `xml` is a 501.
```
<link rel="alternate" type="application/json+oembed" href="https://embed.bsky.app/oembed?format=json&url=https://bsky.app/profile/bnewbold.net/post/abc123" />
```
## iframe URL
`https://embed.bsky.app/embed/<did>/app.bsky.feed.post/<rkey>`
`https://embed.bsky.app/static/embed.js`
```
<blockquote class="bluesky-post" data-lang="en" data-align="center">
<p lang="en" dir="ltr">{{ post-text }}</p>
&mdash; US Department of the Interior (@Interior) <a href="https://twitter.com/Interior/status/463440424141459456?ref_src=twsrc%5Etfw">May 5, 2014</a>
</blockquote>
```
-1
View File
@@ -1 +0,0 @@
/bskyweb
-207
View File
@@ -1,207 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/labstack/echo/v4"
)
var ErrPostNotFound = errors.New("post not found")
var ErrPostNotPublic = errors.New("post is not publicly accessible")
func (srv *Server) getBlueskyPost(ctx context.Context, did syntax.DID, rkey syntax.RecordKey) (*appbsky.FeedDefs_PostView, error) {
// fetch the post post (with extra context)
uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey)
tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, 0, uri)
if err != nil {
log.Warnf("failed to fetch post: %s\t%v", uri, err)
// TODO: detect 404, specifically?
return nil, ErrPostNotFound
}
if tpv.Thread.FeedDefs_BlockedPost != nil {
return nil, ErrPostNotPublic
} else if tpv.Thread.FeedDefs_ThreadViewPost.Post == nil {
return nil, ErrPostNotFound
}
postView := tpv.Thread.FeedDefs_ThreadViewPost.Post
for _, label := range postView.Author.Labels {
if label.Src == postView.Author.Did && label.Val == "!no-unauthenticated" {
return nil, ErrPostNotPublic
}
}
return postView, nil
}
func (srv *Server) WebHome(c echo.Context) error {
return c.Render(http.StatusOK, "home.html", nil)
}
type OEmbedResponse struct {
Type string `json:"type"`
Version string `json:"version"`
AuthorName string `json:"author_name,omitempty"`
AuthorURL string `json:"author_url,omitempty"`
ProviderName string `json:"provider_url,omitempty"`
CacheAge int `json:"cache_age,omitempty"`
Width int `json:"width,omitempty"`
Height *int `json:"height,omitempty"`
HTML string `json:"html,omitempty"`
}
func (srv *Server) parseBlueskyURL(ctx context.Context, raw string) (*syntax.ATURI, error) {
if raw == "" {
return nil, fmt.Errorf("empty url")
}
// first try simple AT-URI
uri, err := syntax.ParseATURI(raw)
if nil == err {
return &uri, nil
}
// then try bsky.app post URL
u, err := url.Parse(raw)
if err != nil {
return nil, err
}
if u.Hostname() != "bsky.app" {
return nil, fmt.Errorf("only bsky.app URLs currently supported")
}
pathParts := strings.Split(u.Path, "/") // NOTE: pathParts[0] will be empty string
if len(pathParts) != 5 || pathParts[1] != "profile" || pathParts[3] != "post" {
return nil, fmt.Errorf("only bsky.app post URLs currently supported")
}
atid, err := syntax.ParseAtIdentifier(pathParts[2])
if err != nil {
return nil, err
}
rkey, err := syntax.ParseRecordKey(pathParts[4])
if err != nil {
return nil, err
}
var did syntax.DID
if atid.IsHandle() {
ident, err := srv.dir.Lookup(ctx, *atid)
if err != nil {
return nil, err
}
did = ident.DID
} else {
did, err = atid.AsDID()
if err != nil {
return nil, err
}
}
// TODO: don't really need to re-parse here, if we had test coverage
aturi, err := syntax.ParseATURI(fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey))
if err != nil {
return nil, err
} else {
return &aturi, nil
}
}
func (srv *Server) WebOEmbed(c echo.Context) error {
formatParam := c.QueryParam("format")
if formatParam != "" && formatParam != "json" {
return c.String(http.StatusNotImplemented, "Unsupported oEmbed format: "+formatParam)
}
// TODO: do we actually do something with width?
width := 550
maxWidthParam := c.QueryParam("maxwidth")
if maxWidthParam != "" {
maxWidthInt, err := strconv.Atoi(maxWidthParam)
if err != nil || maxWidthInt < 220 || maxWidthInt > 550 {
return c.String(http.StatusBadRequest, "Invalid maxwidth (expected integer between 220 and 550)")
}
width = maxWidthInt
}
// NOTE: maxheight ignored
aturi, err := srv.parseBlueskyURL(c.Request().Context(), c.QueryParam("url"))
if err != nil {
return c.String(http.StatusBadRequest, fmt.Sprintf("Expected 'url' to be bsky.app URL or AT-URI: %v", err))
}
if aturi.Collection() != syntax.NSID("app.bsky.feed.post") {
return c.String(http.StatusNotImplemented, "Only posts (app.bsky.feed.post records) can be embedded currently")
}
did, err := aturi.Authority().AsDID()
if err != nil {
return err
}
post, err := srv.getBlueskyPost(c.Request().Context(), did, aturi.RecordKey())
if err == ErrPostNotFound {
return c.String(http.StatusNotFound, fmt.Sprintf("%v", err))
} else if err == ErrPostNotPublic {
return c.String(http.StatusForbidden, fmt.Sprintf("%v", err))
} else if err != nil {
return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
}
html, err := srv.postEmbedHTML(post)
if err != nil {
return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
}
data := OEmbedResponse{
Type: "rich",
Version: "1.0",
AuthorName: "@" + post.Author.Handle,
AuthorURL: fmt.Sprintf("https://bsky.app/profile/%s", post.Author.Handle),
ProviderName: "Bluesky Social",
CacheAge: 86400,
Width: width,
Height: nil,
HTML: html,
}
if post.Author.DisplayName != nil {
data.AuthorName = fmt.Sprintf("%s (@%s)", *post.Author.DisplayName, post.Author.Handle)
}
return c.JSON(http.StatusOK, data)
}
func (srv *Server) WebPostEmbed(c echo.Context) error {
// sanity check arguments. don't 4xx, just let app handle if not expected format
rkeyParam := c.Param("rkey")
rkey, err := syntax.ParseRecordKey(rkeyParam)
if err != nil {
return c.String(http.StatusBadRequest, fmt.Sprintf("Invalid RecordKey: %v", err))
}
didParam := c.Param("did")
did, err := syntax.ParseDID(didParam)
if err != nil {
return c.String(http.StatusBadRequest, fmt.Sprintf("Invalid DID: %v", err))
}
_ = rkey
_ = did
// NOTE: this request was't really necessary; the JS will do the same fetch
/*
postView, err := srv.getBlueskyPost(ctx, did, rkey)
if err == ErrPostNotFound {
return c.String(http.StatusNotFound, fmt.Sprintf("%v", err))
} else if err == ErrPostNotPublic {
return c.String(http.StatusForbidden, fmt.Sprintf("%v", err))
} else if err != nil {
return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
}
*/
return c.Render(http.StatusOK, "postEmbed.html", nil)
}
-60
View File
@@ -1,60 +0,0 @@
package main
import (
"os"
_ "github.com/joho/godotenv/autoload"
logging "github.com/ipfs/go-log"
"github.com/urfave/cli/v2"
)
var log = logging.Logger("embedr")
func init() {
logging.SetAllLoggers(logging.LevelDebug)
//logging.SetAllLoggers(logging.LevelWarn)
}
func main() {
run(os.Args)
}
func run(args []string) {
app := cli.App{
Name: "embedr",
Usage: "web server for embed.bsky.app post embeds",
}
app.Commands = []*cli.Command{
&cli.Command{
Name: "serve",
Usage: "run the server",
Action: serve,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "appview-host",
Usage: "method, hostname, and port of PDS instance",
Value: "https://public.api.bsky.app",
EnvVars: []string{"ATP_APPVIEW_HOST"},
},
&cli.StringFlag{
Name: "http-address",
Usage: "Specify the local IP/port to bind to",
Required: false,
Value: ":8100",
EnvVars: []string{"HTTP_ADDRESS"},
},
&cli.BoolFlag{
Name: "debug",
Usage: "Enable debug mode",
Value: false,
Required: false,
EnvVars: []string{"DEBUG"},
},
},
},
}
app.RunAndExitOnError()
}
-16
View File
@@ -1,16 +0,0 @@
package main
import (
"html/template"
"io"
"github.com/labstack/echo/v4"
)
type Template struct {
templates *template.Template
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
-236
View File
@@ -1,236 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"html/template"
"io/fs"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/util/cliutil"
"github.com/bluesky-social/indigo/xrpc"
"github.com/bluesky-social/social-app/bskyweb"
"github.com/klauspost/compress/gzhttp"
"github.com/klauspost/compress/gzip"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/urfave/cli/v2"
)
type Server struct {
echo *echo.Echo
httpd *http.Server
xrpcc *xrpc.Client
dir identity.Directory
}
func serve(cctx *cli.Context) error {
debug := cctx.Bool("debug")
httpAddress := cctx.String("http-address")
appviewHost := cctx.String("appview-host")
// Echo
e := echo.New()
// create a new session (no auth)
xrpcc := &xrpc.Client{
Client: cliutil.NewHttpClient(),
Host: appviewHost,
}
// httpd
var (
httpTimeout = 2 * time.Minute
httpMaxHeaderBytes = 2 * (1024 * 1024)
gzipMinSizeBytes = 1024 * 2
gzipCompressionLevel = gzip.BestSpeed
gzipExceptMIMETypes = []string{"image/png"}
)
// Wrap the server handler in a gzip handler to compress larger responses.
gzipHandler, err := gzhttp.NewWrapper(
gzhttp.MinSize(gzipMinSizeBytes),
gzhttp.CompressionLevel(gzipCompressionLevel),
gzhttp.ExceptContentTypes(gzipExceptMIMETypes),
)
if err != nil {
return err
}
//
// server
//
server := &Server{
echo: e,
xrpcc: xrpcc,
dir: identity.DefaultDirectory(),
}
// Create the HTTP server.
server.httpd = &http.Server{
Handler: gzipHandler(server),
Addr: httpAddress,
WriteTimeout: httpTimeout,
ReadTimeout: httpTimeout,
MaxHeaderBytes: httpMaxHeaderBytes,
}
e.HideBanner = true
tmpl := &Template{
templates: template.Must(template.ParseFS(bskyweb.EmbedrTemplateFS, "embedr-templates/*.html")),
}
e.Renderer = tmpl
e.HTTPErrorHandler = server.errorHandler
e.IPExtractor = echo.ExtractIPFromXFFHeader()
// SECURITY: Do not modify without due consideration.
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
ContentTypeNosniff: "nosniff",
// diable XFrameOptions; we're embedding here!
HSTSMaxAge: 31536000, // 365 days
// TODO:
// ContentSecurityPolicy
// XSSProtection
}))
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
// Don't log requests for static content.
Skipper: func(c echo.Context) bool {
return strings.HasPrefix(c.Request().URL.Path, "/static")
},
}))
e.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
Skipper: middleware.DefaultSkipper,
Store: middleware.NewRateLimiterMemoryStoreWithConfig(
middleware.RateLimiterMemoryStoreConfig{
Rate: 10, // requests per second
Burst: 30, // allow bursts
ExpiresIn: 3 * time.Minute, // garbage collect entries older than 3 minutes
},
),
IdentifierExtractor: func(ctx echo.Context) (string, error) {
id := ctx.RealIP()
return id, nil
},
DenyHandler: func(c echo.Context, identifier string, err error) error {
return c.String(http.StatusTooManyRequests, "Your request has been rate limited. Please try again later. Contact support@bsky.app if you believe this was a mistake.\n")
},
}))
// redirect trailing slash to non-trailing slash.
// all of our current endpoints have no trailing slash.
e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{
RedirectCode: http.StatusFound,
}))
//
// configure routes
//
// static files
staticHandler := http.FileServer(func() http.FileSystem {
if debug {
log.Debugf("serving static file from the local file system")
return http.FS(os.DirFS("embedr-static"))
}
fsys, err := fs.Sub(bskyweb.EmbedrStaticFS, "embedr-static")
if err != nil {
log.Fatal(err)
}
return http.FS(fsys)
}())
e.GET("/robots.txt", echo.WrapHandler(staticHandler))
e.GET("/ips-v4", echo.WrapHandler(staticHandler))
e.GET("/ips-v6", echo.WrapHandler(staticHandler))
e.GET("/.well-known/*", echo.WrapHandler(staticHandler))
e.GET("/security.txt", func(c echo.Context) error {
return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt")
})
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
path := c.Request().URL.Path
maxAge := 1 * (60 * 60) // default is 1 hour
// Cache javascript and images files for 1 week, which works because
// they're always versioned (e.g. /static/js/main.64c14927.js)
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/images/") {
maxAge = 7 * (60 * 60 * 24) // 1 week
}
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
return next(c)
}
})
// actual routes
e.GET("/", server.WebHome)
e.GET("/iframe-resize.js", echo.WrapHandler(staticHandler))
e.GET("/embed.js", echo.WrapHandler(staticHandler))
e.GET("/oembed", server.WebOEmbed)
e.GET("/embed/:did/app.bsky.feed.post/:rkey", server.WebPostEmbed)
// Start the server.
log.Infof("starting server address=%s", httpAddress)
go func() {
if err := server.httpd.ListenAndServe(); err != nil {
if !errors.Is(err, http.ErrServerClosed) {
log.Errorf("HTTP server shutting down unexpectedly: %s", err)
}
}
}()
// Wait for a signal to exit.
log.Info("registering OS exit signal handler")
quit := make(chan struct{})
exitSignals := make(chan os.Signal, 1)
signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-exitSignals
log.Infof("received OS exit signal: %s", sig)
// Shut down the HTTP server.
if err := server.Shutdown(); err != nil {
log.Errorf("HTTP server shutdown error: %s", err)
}
// Trigger the return that causes an exit.
close(quit)
}()
<-quit
log.Infof("graceful shutdown complete")
return nil
}
func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
srv.echo.ServeHTTP(rw, req)
}
func (srv *Server) Shutdown() error {
log.Info("shutting down")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return srv.httpd.Shutdown(ctx)
}
func (srv *Server) errorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
}
c.Logger().Error(err)
data := map[string]interface{}{
"statusCode": code,
}
c.Render(code, "error.html", data)
}
-71
View File
@@ -1,71 +0,0 @@
package main
import (
"bytes"
"fmt"
"html/template"
appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/atproto/syntax"
)
func (srv *Server) postEmbedHTML(postView *appbsky.FeedDefs_PostView) (string, error) {
// ensure that there isn't an injection from the URI
aturi, err := syntax.ParseATURI(postView.Uri)
if err != nil {
log.Error("bad AT-URI in reponse", "aturi", aturi, "err", err)
return "", err
}
post, ok := postView.Record.Val.(*appbsky.FeedPost)
if !ok {
log.Error("bad post record value", "err", err)
return "", err
}
const tpl = `<blockquote class="bluesky-embed" data-bluesky-uri="{{ .PostURI }}" data-bluesky-cid="{{ .PostCID }}"><p{{ if .PostLang }} lang="{{ .PostLang }}"{{ end }}>{{ .PostText }}</p>&mdash; {{ .PostAuthor }} {{ .PostIndexedAt }}</blockquote><script async src="{{ .WidgetURL }}" charset="utf-8"></script>`
t, err := template.New("snippet").Parse(tpl)
if err != nil {
log.Error("template parse error", "err", err)
return "", err
}
var lang string
if len(post.Langs) > 0 {
lang = post.Langs[0]
}
var authorName string
if postView.Author.DisplayName != nil {
authorName = fmt.Sprintf("%s (@%s)", *postView.Author.DisplayName, postView.Author.Handle)
} else {
authorName = fmt.Sprintf("@%s", postView.Author.Handle)
}
fmt.Println(postView.Uri)
fmt.Println(fmt.Sprintf("%s", postView.Uri))
data := struct {
PostURI template.URL
PostCID string
PostLang string
PostText string
PostAuthor string
PostIndexedAt string
WidgetURL template.URL
}{
PostURI: template.URL(postView.Uri),
PostCID: postView.Cid,
PostLang: lang,
PostText: post.Text,
PostAuthor: authorName,
PostIndexedAt: postView.IndexedAt, // TODO: createdAt?
WidgetURL: template.URL("https://embed.bsky.app/static/embed.js"),
}
var buf bytes.Buffer
err = t.Execute(&buf, data)
if err != nil {
log.Error("template parse error", "err", err)
return "", err
}
return buf.String(), nil
}
@@ -1,4 +0,0 @@
Contact: mailto:security@bsky.app
Preferred-Languages: en
Canonical: https://bsky.app/.well-known/security.txt
Acknowledgements: https://github.com/bluesky-social/atproto/blob/main/CONTRIBUTORS.md
-1
View File
@@ -1 +0,0 @@
/* embed javascript widget will go here */
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

-1
View File
@@ -1 +0,0 @@
/* script to resize embed ifame would go here? */
-30
View File
@@ -1,30 +0,0 @@
13.59.225.103/32
3.18.47.21/32
18.191.104.94/32
3.129.134.255/32
3.129.237.113/32
3.138.56.230/32
44.218.10.163/32
54.89.116.251/32
44.217.166.202/32
54.208.221.149/32
54.166.110.54/32
54.208.146.65/32
3.129.234.15/32
3.138.168.48/32
3.23.53.192/32
52.14.89.53/32
3.18.126.246/32
3.136.69.4/32
3.22.137.152/32
3.132.247.113/32
3.141.186.104/32
18.222.43.214/32
3.14.35.197/32
3.23.182.70/32
18.224.144.69/32
3.129.98.29/32
3.130.134.20/32
3.17.197.213/32
18.223.234.21/32
3.20.248.177/32
View File
-9
View File
@@ -1,9 +0,0 @@
# Hello Friends!
# If you are considering bulk or automated crawling, you may want to look in
# to our protocol (API), including a firehose of updates. See: https://atproto.com/
# By default, may crawl anything on this domain. HTTP 429 ("backoff") status
# codes are used for rate-limiting. Up to a handful concurrent requests should
# be ok.
User-Agent: *
Allow: /
-1
View File
@@ -1 +0,0 @@
placeholder!
-8
View File
@@ -1,8 +0,0 @@
<html>
<head>
</head>
<body>
<h1>embed.bsky.app homepage</h1>
<p>could redirect to bsky.app? or show a "create embed" widget?
</body>
</html>
-1
View File
@@ -1 +0,0 @@
oembed JSON response will go here
-1
View File
@@ -1 +0,0 @@
embed post HTML will go here
-3
View File
@@ -4,6 +4,3 @@ import "embed"
//go:embed static/*
var StaticFS embed.FS
//go:embed embedr-static/*
var EmbedrStaticFS embed.FS
+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>
-3
View File
@@ -4,6 +4,3 @@ import "embed"
//go:embed templates/*
var TemplateFS embed.FS
//go:embed embedr-templates/*
var EmbedrTemplateFS embed.FS
+2 -28
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 {
@@ -220,32 +220,6 @@
.nativeDropdown-item:focus {
outline: none;
}
/* Spinner component */
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.rotate-500ms {
position: absolute;
inset:0;
animation: rotate 500ms linear infinite;
}
@keyframes avatarHoverFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes avatarHoverFadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
</style>
{% include "scripts.html" %}
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
-2
View File
@@ -36,8 +36,6 @@
<meta name="twitter:label1" content="Posted At">
<meta name="twitter:value1" content="{{ postView.IndexedAt }}">
<meta name="article:published_time" content="{{ postView.IndexedAt }}">
<meta name="article:published_time" content="{{ postView.IndexedAt }}">
<link rel="alternate" type="application/json+oembed" href="https://embed.bsky.app/oembed?format=json&url={{ postView.Uri | urlencode }}" />
{% endif -%}
{%- endblock %}
-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-----
+2 -1
View File
@@ -83,10 +83,11 @@ To run the build with Go, use staging credentials, your own, or any other accoun
```
cd social-app
yarn && yarn build-web
cp ./web-build/static/js/*.* bskyweb/static/js/
cd bskyweb/
go mod tidy
go build -v -tags timetzdata -o bskyweb ./cmd/bskyweb
./bskyweb serve --appview-host=https://public.api.bsky.app
./bskyweb serve --pds-host=https://staging.bsky.dev --handle=<HANDLE> --password=<PASSWORD>
```
On build success, access the application at [http://localhost:8100/](http://localhost:8100/). Subsequent changes require re-running the above steps in order to be reflected.
+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,825 +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 propText={
<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>
`,
},
{
code: `
function Stuff() {
return <Text>foo</Text>
}
`,
},
{
code: `
function Stuff({ foo }) {
return <View>{foo}</View>
}
`,
},
{
code: `
function MyText() {
return <Text>foo</Text>
}
`,
},
{
code: `
function MyText({ foo }) {
if (foo) {
return <Text>foo</Text>
}
return <Text>foo</Text>
}
`,
},
{
code: `
<View>
<Text>{'foo'}</Text>
</View>
`,
},
{
code: `
<View>
<Text>{foo + 'foo'}</Text>
</View>
`,
},
{
code: `
<View>
<Text><Trans>{'foo'}</Trans></Text>
</View>
`,
},
{
code: `
<View>
{foo['bar'] && <Bar />}
</View>
`,
},
{
code: `
<View>
{(foo === 'bar') && <Bar />}
</View>
`,
},
{
code: `
<View>
{(foo !== 'bar') && <Bar />}
</View>
`,
},
{
code: `
<View>
<Text>{\`foo\`}</Text>
</View>
`,
},
{
code: `
<View>
<Text><Trans>{\`foo\`}</Trans></Text>
</View>
`,
},
{
code: `
<View>
<Text>{_(msg\`foo\`)}</Text>
</View>
`,
},
{
code: `
<View>
<Text><Trans>{_(msg\`foo\`)}</Trans></Text>
</View>
`,
},
{
code: `
<Foo>
<View prop={stuff('foo')}>
<Bar />
</View>
</Foo>
`,
},
{
code: `
<Foo>
<View onClick={() => stuff('foo')}>
<Bar />
</View>
</Foo>
`,
},
{
code: `
<View>
{renderItem('foo')}
</View>
`,
},
{
code: `
<View>
{foo === 'foo' && <Bar />}
</View>
`,
},
{
code: `
<View>
{foo['foo'] && <Bar />}
</View>
`,
},
{
code: `
<View>
{check('foo') && <Bar />}
</View>
`,
},
{
code: `
<View>
{foo.bar && <Bar />}
</View>
`,
},
{
code: `
<Text>
<Trans>{renderItem('foo')}</Trans>
</Text>
`,
},
{
code: `
<View>
{null}
</View>
`,
},
{
code: `
<Text>
<Trans>{null}</Trans>
</Text>
`,
},
],
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,
},
{
code: `
function MyText() {
return <Foo />
}
`,
errors: 1,
},
{
code: `
function MyText({ foo }) {
return <Foo>{foo}</Foo>
}
`,
errors: 1,
},
{
code: `
function MyText({ foo }) {
if (foo) {
return <Foo>{foo}</Foo>
}
return <Text>foo</Text>
}
`,
errors: 1,
},
{
code: `
<View>
{'foo'}
</View>
`,
errors: 1,
},
{
code: `
<View>
{foo && 'foo'}
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{'foo'}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
{foo && <Trans>{'foo'}</Trans>}
</View>
`,
errors: 1,
},
{
code: `
<View>
{10}
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{10}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{foo + 10}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
{\`foo\`}
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{\`foo\`}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{foo + \`foo\`}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
{_(msg\`foo\`)}
</View>
`,
errors: 1,
},
{
code: `
<View>
{foo + _(msg\`foo\`)}
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{_(msg\`foo\`)}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{foo + _(msg\`foo\`)}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>foo</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans><Trans>foo</Trans></Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{foo}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{'foo'}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View prop={
<Trans><Text>foo</Text></Trans>
}>
<Bar />
</View>
`,
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)
})
-331
View File
@@ -1,331 +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 suggestedTextWrappers = options.suggestedTextWrappers ?? {}
const textProps = [...impliedTextProps]
const textComponents = ['Text', ...impliedTextComponents]
function isTextComponent(tagName) {
return textComponents.includes(tagName) || tagName.endsWith('Text')
}
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 (isTextComponent(tagName)) {
// We're good.
return
}
if (tagName === 'Trans') {
// Exit and rely on the traversal for <Trans> JSXElement (code below).
// TODO: Maybe validate that it's present.
return
}
const suggestedWrapper = suggestedTextWrappers[tagName]
let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
if (tagName !== 'View' && !suggestedWrapper) {
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
}
},
Literal(node) {
if (typeof node.value !== 'string' && typeof node.value !== 'number') {
return
}
let parent = node.parent
while (parent) {
if (parent.type === 'JSXElement') {
const tagName = getTagName(parent)
if (isTextComponent(tagName)) {
// We're good.
return
}
if (tagName === 'Trans') {
// Exit and rely on the traversal for <Trans> JSXElement (code below).
// TODO: Maybe validate that it's present.
return
}
const suggestedWrapper = suggestedTextWrappers[tagName]
let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
if (tagName !== 'View' && !suggestedWrapper) {
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 === 'BinaryExpression' && parent.operator === '+') {
parent = parent.parent
continue
}
if (
parent.type === 'JSXExpressionContainer' ||
parent.type === 'LogicalExpression'
) {
parent = parent.parent
continue
}
// Be conservative for other types.
return
}
},
TemplateLiteral(node) {
let parent = node.parent
while (parent) {
if (parent.type === 'JSXElement') {
const tagName = getTagName(parent)
if (isTextComponent(tagName)) {
// We're good.
return
}
if (tagName === 'Trans') {
// Exit and rely on the traversal for <Trans> JSXElement (code below).
// TODO: Maybe validate that it's present.
return
}
const suggestedWrapper = suggestedTextWrappers[tagName]
let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
if (tagName !== 'View' && !suggestedWrapper) {
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 === 'CallExpression' &&
parent.callee.type === 'Identifier' &&
parent.callee.name === '_'
) {
// This is a user-facing string, keep going up.
parent = parent.parent
continue
}
if (parent.type === 'BinaryExpression' && parent.operator === '+') {
parent = parent.parent
continue
}
if (
parent.type === 'JSXExpressionContainer' ||
parent.type === 'LogicalExpression' ||
parent.type === 'TaggedTemplateExpression'
) {
parent = parent.parent
continue
}
// Be conservative for other types.
return
}
},
JSXElement(node) {
if (getTagName(node) !== 'Trans') {
return
}
let parent = node.parent
while (parent) {
if (parent.type === 'JSXElement') {
const tagName = getTagName(parent)
if (isTextComponent(tagName)) {
// We're good.
return
}
if (tagName === 'Trans') {
// Exit and rely on the traversal for this JSXElement.
// TODO: Should nested <Trans> even be allowed?
return
}
const suggestedWrapper = suggestedTextWrappers[tagName]
let message = `Wrap this <Trans> in <${suggestedWrapper ?? 'Text'}>.`
if (tagName !== 'View' && !suggestedWrapper) {
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 <Trans> 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
}
},
ReturnStatement(node) {
let fnScope = context.getScope()
while (fnScope && fnScope.type !== 'function') {
fnScope = fnScope.upper
}
if (!fnScope) {
return
}
const fn = fnScope.block
if (!fn.id || fn.id.type !== 'Identifier' || !fn.id.name) {
return
}
if (!/^[A-Z]\w*Text$/.test(fn.id.name)) {
return
}
if (!node.argument || node.argument.type !== 'JSXElement') {
return
}
const openingEl = node.argument.openingElement
if (openingEl.name.type !== 'JSXIdentifier') {
return
}
const returnedComponentName = openingEl.name.name
if (!isTextComponent(returnedComponentName)) {
context.report({
node,
message:
'Components ending with *Text must return <Text> or <SomeText>.',
})
}
},
}
}
-8
View File
@@ -1,8 +0,0 @@
'use strict'
module.exports = {
rules: {
'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
'use-typed-gates': require('./use-typed-gates'),
},
}
-30
View File
@@ -1,30 +0,0 @@
'use strict'
exports.create = function create(context) {
return {
ImportSpecifier(node) {
if (
!node.local ||
node.local.type !== 'Identifier' ||
node.local.name !== 'useGate'
) {
return
}
if (
node.parent.type !== 'ImportDeclaration' ||
!node.parent.source ||
node.parent.source.type !== 'Literal'
) {
return
}
const source = node.parent.source.value
if (source.startsWith('statsig') || source.startsWith('@statsig')) {
context.report({
node,
message:
"Use useGate() from '#/lib/statsig/statsig' instead of the one on npm.",
})
}
},
}
}
-2
View File
@@ -88,5 +88,3 @@ jest.mock('sentry-expo', () => ({
ReactNavigationInstrumentation: jest.fn(),
},
}))
jest.mock('crypto', () => ({}))
+70 -63
View File
@@ -1,8 +1,8 @@
import {AtUri, BskyAgent} from '@atproto/api'
import {TestBsky, TestNetwork} from '@atproto/dev-env'
import fs from 'fs'
import net from 'net'
import path from 'path'
import fs from 'fs'
import {TestNetwork, TestPds} from '@atproto/dev-env'
import {AtUri, BskyAgent} from '@atproto/api'
export interface TestUser {
email: string
@@ -55,8 +55,12 @@ class StringIdGenerator {
const ids = new StringIdGenerator()
export async function createServer(
{inviteRequired}: {inviteRequired: boolean} = {
{
inviteRequired,
phoneRequired,
}: {inviteRequired: boolean; phoneRequired: boolean} = {
inviteRequired: false,
phoneRequired: false,
},
): Promise<TestPDS> {
const port = 3000
@@ -65,11 +69,22 @@ export async function createServer(
const pdsUrl = `http://localhost:${port}`
const id = ids.next()
const phoneParams = phoneRequired
? {
phoneVerificationRequired: true,
twilioAccountSid: 'ACXXXXXXX',
twilioAuthToken: 'AUTH',
twilioServiceSid: 'VAXXXXXXXX',
}
: {}
const testNet = await TestNetwork.create({
pds: {
port,
hostname: 'localhost',
dbPostgresSchema: `pds_${id}`,
inviteRequired,
...phoneParams,
},
bsky: {
dbPostgresSchema: `bsky_${id}`,
@@ -78,33 +93,7 @@ export async function createServer(
},
plc: {port: port2},
})
// add the test mod authority
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(),
},
)
mockTwilio(testNet.pds)
const pic = fs.readFileSync(
path.join(__dirname, '..', 'assets', 'default-avatar.png'),
@@ -162,7 +151,7 @@ class Mocker {
const inviteRes = await agent.api.com.atproto.server.createInviteCode(
{useCount: 1},
{
headers: this.pds.adminAuthHeaders(),
headers: this.pds.adminAuthHeaders('admin'),
encoding: 'application/json',
},
)
@@ -173,6 +162,8 @@ class Mocker {
email,
handle: name + '.test',
password: 'hunter2',
verificationPhone: '1234567890',
verificationCode: '000000',
})
await agent.upsertProfile(async () => {
const blob = await agent.uploadBlob(this.pic, {
@@ -337,7 +328,7 @@ class Mocker {
await agent.api.com.atproto.server.createInviteCode(
{useCount: 1, forAccount},
{
headers: this.pds.adminAuthHeaders(),
headers: this.pds.adminAuthHeaders('admin'),
encoding: 'application/json',
},
)
@@ -352,11 +343,18 @@ class Mocker {
if (!ctx) {
throw new Error('Invalid appview')
}
await createLabel(this.bsky, {
uri: did,
cid: '',
val: label,
})
const labelSrvc = ctx.services.label(ctx.db.getPrimary())
await labelSrvc.createLabels([
{
// @ts-ignore
src: ctx.cfg.labelerDid,
uri: did,
cid: '',
val: label,
neg: false,
cts: new Date().toISOString(),
},
])
}
async labelProfile(label: string, user: string) {
@@ -375,11 +373,18 @@ class Mocker {
if (!ctx) {
throw new Error('Invalid appview')
}
await createLabel(this.bsky, {
uri: profile.uri,
cid: profile.cid,
val: label,
})
const labelSrvc = ctx.services.label(ctx.db.getPrimary())
await labelSrvc.createLabels([
{
// @ts-ignore
src: ctx.cfg.labelerDid,
uri: profile.uri,
cid: profile.cid,
val: label,
neg: false,
cts: new Date().toISOString(),
},
])
}
async labelPost(label: string, {uri, cid}: {uri: string; cid: string}) {
@@ -387,11 +392,18 @@ class Mocker {
if (!ctx) {
throw new Error('Invalid appview')
}
await createLabel(this.bsky, {
uri,
cid,
val: label,
})
const labelSrvc = ctx.services.label(ctx.db.getPrimary())
await labelSrvc.createLabels([
{
// @ts-ignore
src: ctx.cfg.labelerDid,
uri,
cid,
val: label,
neg: false,
cts: new Date().toISOString(),
},
])
}
async createMuteList(user: string, name: string): Promise<string> {
@@ -442,19 +454,14 @@ async function getPort(start = 3000) {
throw new Error('Unable to find an available port')
}
const createLabel = async (
bsky: TestBsky,
opts: {uri: string; cid: string; val: string},
) => {
await bsky.db.db
.insertInto('label')
.values({
uri: opts.uri,
cid: opts.cid,
val: opts.val,
cts: new Date().toISOString(),
neg: false,
src: 'did:example:labeler',
})
.execute()
export const mockTwilio = (pds: TestPds) => {
if (!pds.ctx.twilio) return
pds.ctx.twilio.sendCode = async (_number: string) => {
// do nothing
}
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: [
{
@@ -1,6 +0,0 @@
{
"platforms": ["ios"],
"ios": {
"modules": ["ExpoScrollForwarderModule"]
}
}
-1
View File
@@ -1 +0,0 @@
export {ExpoScrollForwarderView} from './src/ExpoScrollForwarderView'
@@ -1,21 +0,0 @@
Pod::Spec.new do |s|
s.name = 'ExpoScrollForwarder'
s.version = '1.0.0'
s.summary = 'Forward scroll gesture from UIView to UIScrollView'
s.description = 'Forward scroll gesture from UIView to UIScrollView'
s.author = 'bluesky-social'
s.homepage = 'https://github.com/bluesky-social/social-app'
s.platforms = { :ios => '13.4', :tvos => '13.4' }
s.source = { git: '' }
s.static_framework = true
s.dependency 'ExpoModulesCore'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
end
@@ -1,13 +0,0 @@
import ExpoModulesCore
public class ExpoScrollForwarderModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoScrollForwarder")
View(ExpoScrollForwarderView.self) {
Prop("scrollViewTag") { (view: ExpoScrollForwarderView, prop: Int) in
view.scrollViewTag = prop
}
}
}
}

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