From f28334739b107f3e9f7b6ca2670778dba280600d Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 22 Feb 2023 14:23:57 -0600 Subject: [PATCH] Merge main into the Web PR (#230) * Update to RN 71.1.0 (#100) * Update to RN 71 * Adds missing lint plugin * Add missing native changes * Bump @atproto/api@0.0.7 (#112) * Image not loading on swipe (#114) * Adds prefetching to images * Adds image prefetch * bugfix for images not showing on swipe * Fixes prefetch bug * Update src/view/com/util/PostEmbeds.tsx --------- Co-authored-by: Paul Frazee * Fixes to session management (#117) * Update session-management to solve incorrectly dropped sessions * Reset the nav on account switch * Reset the feed on me.load() * Update tests to reflect new account-switching behavior * Increase max image resolutions and sizes (#118) * Slightly increase the hitslop for post controls * Fix character counter color in dark mode * Update login to use new session.create api, which enables email login (close #93) (#119) * Replaces the alert with dropdown for profile image and banner (#123) * replaces the alert with dropdown for profile image and banner * lint * Fix to ordering of images in the embed grid (#121) * Add explicit link-embed controls to the composer (#120) * Add explicit link-embed controls * Update the target rez/size of link embed thumbs * Remove the alert before publishing without a link card * [Draft] Fixes image failing on reupload issue (#128) * Fixes image failing on reupload issue * Use tmp folder instead of documents * lint * Image performance improvements (#126) * Switch out most images for FastImage * Add image loading placeholders * Fix tests * Collection of fixes to list rendering (#127) * Fix bug that caused endless spinners in profile feeds * Bundle fetches of suggested actors into one update * Fixes to suggested follow rendering * Fix missing replacement of flex:1 to height:100 * Fixes to navigation swipes (#129) * Nav swipe: increase the distance traveled in response to gesture movement. This causes swipes to feel faster and more responsive. * Fix: fully clamp the swipe against the edge * Improve the performance of swipes by skipping the interaction manager * Adds dark mode to the edit screen (#130) * Adds dark mode to edit screen * lint * lint * lint * Reduce render cost of post controls and improve perceived responsiveness (#132) * Move post control animations into conditional render and increase perceived responsiveness * Remove log * Adds dark mode to the dropdown (#131) * Adds dark mode to the bottom sheet * Make background button lighter (like before) * lint * Fix bug in lightbox rendering (#133) * Fix layout in onboarding to not overflow the footer * Configure feed FlatList (removeClippedSubviews=true) to improve scroll performance (#136) * Disable like/repost animations to see if theyre causing #135 (#137) * Composer: mention tagging now works in middle of text (close #105) (#139) * Implement account deletion (#141) * Fix photo & camera permission management (#140) * Check photo & camera perms and alert the user if not available (close #64) - Adds perms checks with a prompt to update settings if needed - Moves initial access of photos in the composer so that the initial prompt occurs at an intuitive time. * Add react-native-permissions test mock * Fix issue causing multiple access requests * Use longer var names * Update podfile.lock * Lint fix * Move photo perm request in composer to the gallery btn instead of when the carousel is opened * Adds more tracking all around the app (#142) * Adds more tracking all around the app * more events * lint * using better analytics naming * missed file * more fixes * Calculate image aspect ratio on load (#146) * Calculate image aspect ratio on load * Move aspect ratio bounds to constants * Adds detox testing and instructions (#147) * Adds detox testing and instructions * lint * lint * Error cleanup (close #79) (#148) * Avoid surfacing errors to the user when it's not critical * Remove now-unused GetAssertionsView * Apply cleanError() consistently * Give a better error message for Upstream Failures (http status 502) * Hide errors in notifications because they're not useful * More e2e tests (create account) (#150) * Adds respots under the 'post' tab under profile (#158) * Adds dark mode to delete account screen (#159) * 87 dark mode edit profile (#162) * Adds dark mode to delete account screen * Adds one more missed darkmode * more fixes * Remove fallback gradient on external links without thumbs (#164) * Remove fallback gradient on external links without thumbs * Remove fallback gradient on external links without thumbs in the composer preview * Fix refresh behavior around a series of models (repost, graph, vote) (#163) * Fix refresh behavior around a series of models (repost, graph, vote) * Fix cursor behavior in reposted-by view * Fixes issue where retrying on image upload fails (#166) * Fixes issue where retrying on image upload fails * Lint, longer test time * Longer waitfor time in tests * even longer timeout * longer timeout * missed file * Update src/view/com/composer/ComposePost.tsx Co-authored-by: Paul Frazee * Update src/view/com/composer/ComposePost.tsx Co-authored-by: Paul Frazee --------- Co-authored-by: Paul Frazee * 154 cached image profile (#167) * Fixes issue where retrying on image upload fails * Lint, longer test time * Longer waitfor time in tests * even longer timeout * longer timeout * missed file * Fixes image cache error on second try for profile screen * lint * lint * lint * Refactor session management to use a new "Agent" API (#165) * Add the atp-agent implementation (temporarily in this repo) * Rewrite all session & API management to use the new atp-agent * Update tests for the atp-agent refactor * Refactor management of session-related state. Includes: - More careful management of when state is cleared or fetched - Debug logging to help trace future issues - Clearer APIs overall * Bubble session-expiration events to the user and display a toast to explain * Switch to the new @atproto/api@0.1.0 * Minor aesthetic cleanup in SessionModel * Wire up ReportAccount and ReportPost (#168) * Fixes embeds for youtube channels (#169) * Bump app ios version to 1.1 (needed after app store submission) * Fix potential issues with promise guards when an error occurs (#170) * Refactor models to use bundleAsync and lock regions (#171) * Fix to an edge case with feed re-ordering for threads (#172) * 151 fix youtube channel embed (#173) * Fixes embeds for youtube channels * Tests for youtube extract meta * lint * Add 'doesnt use non-exempt encryption' to ios config * Rework the search UI and add (#174) * Add search tab and move icon to footer * Remove subtitles from view header * Remove unused code * Clean up UI of search screen * Search: give better user feedback to UI state and add a cancel button * Add WhoToFollow section to search * Add a temporary SuggestedPosts solution using the patented 'bsky team algo' * Trigger reload of suggested content in search on open * Wait five min between reloading discovery content * Reduce weight of solid search icon in footer * Fix lint * Fix tests * 151 feat youtube embed iframe (#176) * youtube embed iframe temp commit * Fixes styling and code cleanup * lint * Now clicking between the pause and settings button doesn't trigger the parent * use modest branding (less yt logos) * Stop playing the video once there's a navigation event * Make sure the iframe is unmounted on any navigation event * fixes tests * lint * Add scroll-to-top for all screens (#177) * Adds hardcoded suggested list (#178) * Adds hardcoded suggested list * Update suggested-actors-view to support page sizes smaller than the hardcoded list --------- Co-authored-by: Paul Frazee * more robust centering of the play button (#181) Co-authored-by: Aryan Goharzad * Bundle of UI modifications (#175) * Adjust visual balance of SuggestedPosts and WhoToFollow * Fix bug in the discovery load trigger * Adjust search header aesthetic and have it scroll away * More visual balance tweaks on the search page * Even more visual balance tweaks on the search page * Hide the footer on scroll in search * Ditch the composer prompt buttons in the home feed * Center the view header title * Hide header on scroll on the home feed * Fix e2e tests * Fix home feed positioning (closes #189) (#195) * Fix home feed positioning for floating header * Fix positioning of errors in home feed * Fix lint * Don't show new-content notification for reposts (close #179) (#197) * Show the splash screen during session resumption (close #186) (#199) * Fix to suggested follows: chunk the hardcoded fetches to 25 at a time (close #196) (#198) * UI updates to the floating action button (#201) * Update FAB to use a plus icon and not drop shadow * Update FAB positioning to be more consistent in different shell modes * Animate the FAB's repositioning * Remove the 'loading' placeholder from images as it degraded feed perf (#202) * Remove the 'loading' placeholder from images as it degraded feed perf * Remove references * Fix RN bug that causes home feed not to load more; also fix home feed load view. (#208) RN has a bug where rendering a flatlist with an empty array appears to break its virtual list windowing behaviors. See https://stackoverflow.com/a/67873596 * Only give the loading spinner on the home feed during PTR (#207) (cherry picked from commit b7a5da12fdfacef74873b5cf6d75f20d259bde0e) * Implement our own lifecycle tracking to ensure it never fires while the app is backgrounded (close #193) (#211) * Push notification fixes (#210) * Fix to when screen analytics events are firing * Fix: dont trigger update state when backgrounded * Small fix to notifee API usage * Fix: properly load notification info for push card * Add feedback link to main menu (close #191) (#212) * Add "follows you" information and sync follow state between views (#215) * Bump @atproto/api@0.1.2 and update API usage * Add 'follows you' pill to profile header (close #110) * Add 'follows you' to followers and follows (close #103) * Update reposted-by and liked-by views to use the same components as followers and following * Create a local follows cache MyFollowsModel to keep views in sync (close #205) * Add incremental hydration to the MyFollows model * Fix tests * Update deps * Fix lint * Fix to paginated fetches * Fix reference * Fix potential state-desync issue * Fixes to notifications (#216) * Improve push-notification for follows * Refresh notifications on screen open (close #214) * Avoid showing loader more than needed in post threads * Refactor notification polling to handle view-state more effectively * Delete a bunch of tests taht werent adding value * Remove the accounts integration test; we'll use the e2e test instead * Load latest in notifications when the screen is open rather than full refresh * Randomize hard-coded suggested follows (#226) * Ensure follows are loaded before filtering hardcoded suggestions * Randomize hard-coded suggested profiles (close #219) * Sanitizes posts on publish and render (#217) * Sanatizes posts on publish and render * lint * lint and added sanitize to thread view as well * adjusts indices based on replaced text * Woops, fixes a bug * bugfix + cleanup * comment * lint * move sanitize text to later in the flow * undo changes to compose post * Add RichText library building upon the sanitizePost library method * Add lodash.clonedeep dep * Switch to RichText processing on record load & render * Fix lint --------- Co-authored-by: Paul Frazee * A group of notifications fixes (#227) * Fix: don't group together notifications that can't visually be grouped (close #221) * Mark all notifications read on PTR * Small optimization: useCallback and useMemo in posts feed * Add loading spinner to footer of notifications (close #222) * Fix to scrolling to posts within a thread (#228) * Fix: render the entire thread at start so that scrollToIndex works always (close #270) * Visual fixes to thread 'load more' * A few small perf improvements to thread rendering * Fix lint * 1.2 * Remove unused logger lib * Remove state-mock * Type fixes * Reorganize the folder structure for lib and switch to typescript path aliases * Move build-flags into lib * Move to the state path alias * Add view path alias * Fix lint * iOS build fixes * Wrap analytics in native/web splitter and re-enable in all view code * Add web version of react-native-webview * Add web split for version number * Fix BlurView import for web * Add web split for fastimage * Create web split for permissions lib * Fix for web high priority images --------- Co-authored-by: Aryan Goharzad --- .detoxrc.js | 85 + .eslintrc.js | 2 +- .github/workflows/lint.yml | 4 +- .gitignore | 3 +- README.md | 27 +- __mocks__/react-native-svg.js | 67 + __mocks__/state-mock.ts | 856 ----- __tests__/accounts.test.tsx | 241 -- __tests__/lib/__mocks__/youtubeChannelHtml.ts | 63 + __tests__/lib/async/bundle.test.ts | 47 + __tests__/lib/errors.test.ts | 2 +- __tests__/lib/extractHtmlMeta.test.ts | 16 +- __tests__/lib/images.test.ts | 29 +- __tests__/lib/link-meta.test.ts | 17 +- __tests__/lib/string.test.ts | 45 +- __tests__/lib/strings/mention-manip.test.ts | 84 + __tests__/lib/strings/rich-text-sanitize.ts | 123 + __tests__/lib/strings/rich-text.ts | 123 + .../state/models/link-metas-view.test.ts | 72 - __tests__/state/models/log.test.ts | 153 - __tests__/state/models/me.test.ts | 180 - __tests__/state/models/navigation.test.ts | 102 +- __tests__/state/models/root-store.test.ts | 59 - __tests__/state/models/shell-ui.test.ts | 61 - .../view/com/composer/Autocomplete.test.tsx | 43 - .../view/com/composer/ComposePost.test.tsx | 118 - .../view/com/composer/SelectedPhoto.test.tsx | 70 - .../view/com/login/CreateAccount.test.tsx | 58 - .../view/com/profile/ProfileHeader.test.tsx | 110 - __tests__/view/lib/useAnimatedValue.test.tsx | 17 - __tests__/view/lib/useOnMainScroll.test.tsx | 49 - __tests__/view/screens/Login.test.tsx | 37 - __tests__/view/screens/NotFound.test.tsx | 21 - __tests__/view/screens/Search.test.tsx | 30 - __tests__/view/shell/mobile/Menu.test.tsx | 57 - .../view/shell/mobile/TabsSelector.test.tsx | 100 - android/app/build.gradle | 4 +- babel.config.js | 11 + e2e/jest.config.js | 12 + e2e/tests/happyPath.test.js | 77 + ios/Podfile | 25 + ios/Podfile.lock | 559 +-- ios/app/AppDelegate.mm | 1 + ios/app/Info.plist | 4 +- jest/jestSetup.js | 17 +- jest/test-pds.ts | 32 +- jest/test-utils.tsx | 10 +- package.json | 45 +- src/App.native.tsx | 47 +- src/{view => }/lib/ThemeContext.tsx | 2 + src/lib/analytics.tsx | 74 + src/lib/analytics.web.tsx | 16 + src/{state/lib => lib/api}/api-polyfill.ts | 4 +- .../lib => lib/api}/api-polyfill.web.ts | 0 src/{state/lib/api.ts => lib/api/index.ts} | 35 +- src/lib/app-info.ts | 4 + src/lib/app-info.web.ts | 3 + src/lib/assets.native.ts | 5 + src/lib/assets.ts | 10 + src/lib/async/bundle.ts | 24 + src/{state => }/lib/bg-scheduler.ts | 2 +- src/{state => }/lib/bg-scheduler.web.ts | 0 src/{ => lib}/build-flags.ts | 0 src/lib/constants.ts | 65 + src/lib/errors.ts | 4 - src/{view => }/lib/hooks/useAnimatedValue.ts | 0 src/{view => }/lib/hooks/useOnMainScroll.ts | 2 +- src/{view => }/lib/hooks/usePalette.ts | 0 src/{view => }/lib/icons.tsx | 0 src/lib/images.ts | 32 +- src/lib/images.web.ts | 5 +- .../{extractBskyMeta.ts => link-meta/bsky.ts} | 26 +- .../{extractHtmlMeta.ts => link-meta/html.ts} | 4 +- src/lib/{ => link-meta}/link-meta.ts | 8 +- .../twitter.ts} | 0 .../youtube.ts} | 7 +- src/{view => }/lib/notifee.ts | 28 +- src/lib/permissions.ts | 61 + src/lib/permissions.web.ts | 22 + src/{state => }/lib/storage.ts | 0 src/lib/strings.ts | 267 -- src/lib/strings/errors.ts | 23 + src/lib/strings/handles.ts | 13 + src/lib/strings/helpers.ts | 17 + src/lib/strings/mention-manip.ts | 37 + src/lib/strings/rich-text-detection.ts | 107 + src/lib/strings/rich-text-sanitize.ts | 32 + src/lib/strings/rich-text.ts | 216 ++ src/lib/strings/time.ts | 29 + src/lib/strings/url-helpers.ts | 108 + src/{view => }/lib/styles.ts | 12 +- src/{view => }/lib/themes.ts | 16 +- src/{state => }/lib/type-guards.ts | 0 src/state/index.ts | 40 +- src/state/models/feed-view.ts | 361 +- src/state/models/get-assertions-view.ts | 123 - src/state/models/link-metas-view.ts | 2 +- src/state/models/log.ts | 2 +- src/state/models/me.ts | 68 +- src/state/models/my-follows.ts | 109 + src/state/models/navigation.ts | 55 +- src/state/models/notifications-view.ts | 334 +- src/state/models/onboard.ts | 2 +- src/state/models/post-thread-view.ts | 39 +- src/state/models/post.ts | 3 +- src/state/models/profile-view.ts | 62 +- src/state/models/profiles-view.ts | 4 +- src/state/models/reposted-by-view.ts | 69 +- src/state/models/root-store.ts | 198 +- src/state/models/session.ts | 418 +- src/state/models/shell-ui.ts | 30 +- src/state/models/suggested-actors-view.ts | 173 +- src/state/models/suggested-posts-view.ts | 148 + src/state/models/user-autocomplete-view.ts | 34 +- src/state/models/user-followers-view.ts | 61 +- src/state/models/user-follows-view.ts | 61 +- src/state/models/votes-view.ts | 62 +- src/view/com/composer/Autocomplete.tsx | 4 +- src/view/com/composer/ComposePost.tsx | 141 +- src/view/com/composer/ExternalEmbed.tsx | 42 +- src/view/com/composer/Prompt.tsx | 2 +- src/view/com/composer/SelectedPhoto.tsx | 5 +- .../composer/char-progress/CharProgress.tsx | 2 +- .../char-progress/CharProgress.web.tsx | 2 +- .../composer/photos/PhotoCarouselPicker.tsx | 112 +- .../photos/PhotoCarouselPicker.web.tsx | 6 +- .../com/composer/text-input/TextInput.tsx | 14 +- .../com/composer/text-input/TextInput.web.tsx | 11 +- .../com/discover/LiteSuggestedFollows.tsx | 11 +- src/view/com/discover/SuggestedFollows.tsx | 208 +- src/view/com/discover/SuggestedPosts.tsx | 66 + src/view/com/discover/WhoToFollow.tsx | 89 + src/view/com/lightbox/ImageViewing/index.tsx | 12 +- src/view/com/lightbox/Lightbox.tsx | 6 +- src/view/com/lightbox/Lightbox.web.tsx | 6 +- src/view/com/login/CreateAccount.tsx | 30 +- src/view/com/login/Logo.tsx | 2 +- src/view/com/login/Signin.tsx | 97 +- src/view/com/modals/Confirm.tsx | 7 +- src/view/com/modals/DeleteAccount.tsx | 210 + src/view/com/modals/EditProfile.tsx | 51 +- src/view/com/modals/Modal.tsx | 33 +- src/view/com/modals/Modal.web.tsx | 18 +- src/view/com/modals/ReportAccount.tsx | 30 +- src/view/com/modals/ReportPost.tsx | 37 +- src/view/com/modals/ServerInput.tsx | 13 +- .../com/modals/crop-image/CropImage.web.tsx | 8 +- src/view/com/notifications/Feed.tsx | 82 +- src/view/com/notifications/FeedItem.tsx | 29 +- src/view/com/onboard/FeatureExplainer.tsx | 16 +- src/view/com/onboard/FeatureExplainer.web.tsx | 9 +- src/view/com/onboard/Follows.tsx | 12 +- src/view/com/onboard/Follows.web.tsx | 6 +- src/view/com/post-thread/PostRepostedBy.tsx | 69 +- src/view/com/post-thread/PostThread.tsx | 61 +- src/view/com/post-thread/PostThreadItem.tsx | 76 +- src/view/com/post-thread/PostVotedBy.tsx | 77 +- src/view/com/post/Post.tsx | 21 +- src/view/com/post/PostText.tsx | 4 +- src/view/com/posts/ComposerPrompt.tsx | 46 +- src/view/com/posts/ComposerPrompt.web.tsx | 4 +- src/view/com/posts/Feed.tsx | 157 +- src/view/com/posts/FeedItem.tsx | 27 +- src/view/com/profile/ProfileCard.tsx | 138 +- src/view/com/profile/ProfileFollowers.tsx | 68 +- src/view/com/profile/ProfileFollows.tsx | 75 +- src/view/com/profile/ProfileHeader.tsx | 57 +- src/view/com/util/BlurView.web.tsx | 2 +- src/view/com/util/EmptyState.tsx | 4 +- src/view/com/util/FAB.tsx | 55 +- src/view/com/util/Link.tsx | 16 +- src/view/com/util/LoadLatestBtn.tsx | 4 +- src/view/com/util/LoadLatestBtn.web.tsx | 2 +- src/view/com/util/LoadingPlaceholder.tsx | 8 +- src/view/com/util/Picker.tsx | 2 +- src/view/com/util/PostCtrls.tsx | 207 +- .../com/util/PostEmbeds/ExternalLinkEmbed.tsx | 69 + src/view/com/util/PostEmbeds/YoutubeEmbed.tsx | 119 + .../{PostEmbeds.tsx => PostEmbeds/index.tsx} | 99 +- src/view/com/util/PostMeta.tsx | 4 +- src/view/com/util/Selector.tsx | 2 +- src/view/com/util/UserAvatar.tsx | 115 +- src/view/com/util/UserBanner.tsx | 97 +- src/view/com/util/UserInfoText.tsx | 4 +- src/view/com/util/ViewHeader.tsx | 152 +- src/view/com/util/ViewHeader.web.tsx | 46 +- src/view/com/util/ViewSelector.tsx | 9 +- src/view/com/util/Views.web.tsx | 5 +- .../com/util/anim/TriggerableAnimated.tsx | 73 + src/view/com/util/error/ErrorMessage.tsx | 4 +- src/view/com/util/error/ErrorScreen.tsx | 6 +- src/view/com/util/forms/Button.tsx | 4 +- src/view/com/util/forms/DropdownButton.tsx | 113 +- src/view/com/util/forms/RadioButton.tsx | 4 +- src/view/com/util/forms/RadioGroup.tsx | 2 +- src/view/com/util/forms/ToggleButton.tsx | 6 +- src/view/com/util/gestures/HorzSwipe.tsx | 5 +- src/view/com/util/gestures/SwipeAndZoom.tsx | 2 +- src/view/com/util/images/AutoSizedImage.tsx | 138 +- src/view/com/util/images/Image.tsx | 12 + src/view/com/util/images/Image.web.tsx | 11 + src/view/com/util/images/ImageHorzList.tsx | 2 +- src/view/com/util/images/ImageLayoutGrid.tsx | 36 +- src/view/com/util/images/constants.ts | 1 - .../image-crop-picker/ImageCropPicker.tsx | 2 +- .../image-crop-picker/ImageCropPicker.web.tsx | 12 +- src/view/com/util/text/RichText.tsx | 28 +- src/view/com/util/text/Text.tsx | 4 +- src/view/index.ts | 4 + src/view/lib/addStyle.ts | 11 - src/view/lib/assets.native.ts | 5 - src/view/lib/assets.ts | 7 - src/view/routes.ts | 4 +- src/view/screens/Contacts.tsx | 6 +- src/view/screens/Debug.tsx | 15 +- src/view/screens/Home.tsx | 58 +- src/view/screens/Log.tsx | 8 +- src/view/screens/Login.tsx | 47 +- src/view/screens/Login.web.tsx | 14 +- src/view/screens/NotFound.tsx | 2 +- src/view/screens/Notifications.tsx | 81 +- src/view/screens/Onboard.tsx | 4 +- src/view/screens/PostDownvotedBy.tsx | 4 +- src/view/screens/PostRepostedBy.tsx | 4 +- src/view/screens/PostThread.tsx | 14 +- src/view/screens/PostUpvotedBy.tsx | 4 +- src/view/screens/Profile.tsx | 19 +- src/view/screens/ProfileFollowers.tsx | 4 +- src/view/screens/ProfileFollows.tsx | 4 +- src/view/screens/Search.tsx | 234 +- src/view/screens/Settings.tsx | 40 +- src/view/shell/mobile/Composer.tsx | 6 +- src/view/shell/mobile/Menu.tsx | 77 +- src/view/shell/mobile/TabsSelector.tsx | 8 +- src/view/shell/mobile/index.tsx | 127 +- src/view/shell/web/Composer.tsx | 4 +- src/view/shell/web/DesktopLeftColumn.tsx | 10 +- src/view/shell/web/DesktopRightColumn.tsx | 6 +- src/view/shell/web/index.tsx | 8 +- tsconfig.json | 9 +- web/webpack.config.js | 17 +- yarn.lock | 3401 +++++++++-------- 242 files changed, 8400 insertions(+), 7454 deletions(-) create mode 100644 .detoxrc.js create mode 100644 __mocks__/react-native-svg.js delete mode 100644 __mocks__/state-mock.ts delete mode 100644 __tests__/accounts.test.tsx create mode 100644 __tests__/lib/__mocks__/youtubeChannelHtml.ts create mode 100644 __tests__/lib/async/bundle.test.ts create mode 100644 __tests__/lib/strings/mention-manip.test.ts create mode 100644 __tests__/lib/strings/rich-text-sanitize.ts create mode 100644 __tests__/lib/strings/rich-text.ts delete mode 100644 __tests__/state/models/link-metas-view.test.ts delete mode 100644 __tests__/state/models/log.test.ts delete mode 100644 __tests__/state/models/me.test.ts delete mode 100644 __tests__/state/models/root-store.test.ts delete mode 100644 __tests__/state/models/shell-ui.test.ts delete mode 100644 __tests__/view/com/composer/Autocomplete.test.tsx delete mode 100644 __tests__/view/com/composer/ComposePost.test.tsx delete mode 100644 __tests__/view/com/composer/SelectedPhoto.test.tsx delete mode 100644 __tests__/view/com/login/CreateAccount.test.tsx delete mode 100644 __tests__/view/com/profile/ProfileHeader.test.tsx delete mode 100644 __tests__/view/lib/useAnimatedValue.test.tsx delete mode 100644 __tests__/view/lib/useOnMainScroll.test.tsx delete mode 100644 __tests__/view/screens/Login.test.tsx delete mode 100644 __tests__/view/screens/NotFound.test.tsx delete mode 100644 __tests__/view/screens/Search.test.tsx delete mode 100644 __tests__/view/shell/mobile/Menu.test.tsx delete mode 100644 __tests__/view/shell/mobile/TabsSelector.test.tsx create mode 100644 e2e/jest.config.js create mode 100644 e2e/tests/happyPath.test.js rename src/{view => }/lib/ThemeContext.tsx (98%) create mode 100644 src/lib/analytics.tsx create mode 100644 src/lib/analytics.web.tsx rename src/{state/lib => lib/api}/api-polyfill.ts (96%) rename src/{state/lib => lib/api}/api-polyfill.web.ts (100%) rename src/{state/lib/api.ts => lib/api/index.ts} (86%) create mode 100644 src/lib/app-info.ts create mode 100644 src/lib/app-info.web.ts create mode 100644 src/lib/assets.native.ts create mode 100644 src/lib/assets.ts create mode 100644 src/lib/async/bundle.ts rename src/{state => }/lib/bg-scheduler.ts (88%) rename src/{state => }/lib/bg-scheduler.web.ts (100%) rename src/{ => lib}/build-flags.ts (100%) create mode 100644 src/lib/constants.ts delete mode 100644 src/lib/errors.ts rename src/{view => }/lib/hooks/useAnimatedValue.ts (100%) rename src/{view => }/lib/hooks/useOnMainScroll.ts (94%) rename src/{view => }/lib/hooks/usePalette.ts (100%) rename src/{view => }/lib/icons.tsx (100%) rename src/lib/{extractBskyMeta.ts => link-meta/bsky.ts} (75%) rename src/lib/{extractHtmlMeta.ts => link-meta/html.ts} (93%) rename src/lib/{ => link-meta}/link-meta.ts (99%) rename src/lib/{extractTwitterMeta.ts => link-meta/twitter.ts} (100%) rename src/lib/{extractYoutubeMeta.ts => link-meta/youtube.ts} (78%) rename src/{view => }/lib/notifee.ts (58%) create mode 100644 src/lib/permissions.ts create mode 100644 src/lib/permissions.web.ts rename src/{state => }/lib/storage.ts (100%) delete mode 100644 src/lib/strings.ts create mode 100644 src/lib/strings/errors.ts create mode 100644 src/lib/strings/handles.ts create mode 100644 src/lib/strings/helpers.ts create mode 100644 src/lib/strings/mention-manip.ts create mode 100644 src/lib/strings/rich-text-detection.ts create mode 100644 src/lib/strings/rich-text-sanitize.ts create mode 100644 src/lib/strings/rich-text.ts create mode 100644 src/lib/strings/time.ts create mode 100644 src/lib/strings/url-helpers.ts rename src/{view => }/lib/styles.ts (94%) rename src/{view => }/lib/themes.ts (96%) rename src/{state => }/lib/type-guards.ts (100%) delete mode 100644 src/state/models/get-assertions-view.ts create mode 100644 src/state/models/my-follows.ts create mode 100644 src/state/models/suggested-posts-view.ts create mode 100644 src/view/com/discover/SuggestedPosts.tsx create mode 100644 src/view/com/discover/WhoToFollow.tsx create mode 100644 src/view/com/modals/DeleteAccount.tsx create mode 100644 src/view/com/util/PostEmbeds/ExternalLinkEmbed.tsx create mode 100644 src/view/com/util/PostEmbeds/YoutubeEmbed.tsx rename src/view/com/util/{PostEmbeds.tsx => PostEmbeds/index.tsx} (58%) create mode 100644 src/view/com/util/anim/TriggerableAnimated.tsx create mode 100644 src/view/com/util/images/Image.tsx create mode 100644 src/view/com/util/images/Image.web.tsx delete mode 100644 src/view/com/util/images/constants.ts delete mode 100644 src/view/lib/addStyle.ts delete mode 100644 src/view/lib/assets.native.ts delete mode 100644 src/view/lib/assets.ts diff --git a/.detoxrc.js b/.detoxrc.js new file mode 100644 index 00000000..fc9cf042 --- /dev/null +++ b/.detoxrc.js @@ -0,0 +1,85 @@ +/** @type {Detox.DetoxConfig} */ +module.exports = { + testRunner: { + args: { + $0: 'jest', + config: 'e2e/jest.config.js', + }, + jest: { + setupTimeout: 120000, + }, + }, + apps: { + 'ios.debug': { + type: 'ios.app', + binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/app.app', + build: + 'xcodebuild -workspace ios/app.xcworkspace -scheme app -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', + }, + 'ios.release': { + type: 'ios.app', + binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/app.app', + build: + 'xcodebuild -workspace ios/app.xcworkspace -scheme app -configuration Release -sdk iphonesimulator -derivedDataPath ios/build', + }, + 'android.debug': { + type: 'android.apk', + binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', + build: + 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', + reversePorts: [8081], + }, + 'android.release': { + type: 'android.apk', + binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', + build: + 'cd android ; ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd -', + }, + }, + devices: { + simulator: { + type: 'ios.simulator', + device: { + type: 'iPhone 14', + }, + }, + attached: { + type: 'android.attached', + device: { + adbName: '.*', + }, + }, + emulator: { + type: 'android.emulator', + device: { + avdName: 'Pixel_3a_API_30_x86', + }, + }, + }, + configurations: { + 'ios.sim.debug': { + device: 'simulator', + app: 'ios.debug', + }, + 'ios.sim.release': { + device: 'simulator', + app: 'ios.release', + }, + 'android.att.debug': { + device: 'attached', + app: 'android.debug', + }, + 'android.att.release': { + device: 'attached', + app: 'android.release', + }, + 'android.emu.debug': { + device: 'emulator', + app: 'android.debug', + }, + 'android.emu.release': { + device: 'emulator', + app: 'android.release', + }, + }, +} diff --git a/.eslintrc.js b/.eslintrc.js index 9a9b2fe4..93348b0d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,7 +2,7 @@ module.exports = { root: true, extends: '@react-native-community', parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], + plugins: ['@typescript-eslint', 'detox'], ignorePatterns: [ '**/__mocks__/*.ts', 'src/third-party', diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1d985988..207e076e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out Git repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Yarn install run: yarn - name: Lint Reporter @@ -30,7 +30,7 @@ jobs: with: node-version: 18 - name: Check out Git repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Yarn install run: yarn - name: Run tests diff --git a/.gitignore b/.gitignore index cd2e89f8..7453726d 100644 --- a/.gitignore +++ b/.gitignore @@ -62,4 +62,5 @@ buck-out/ # Testing coverage/ -junit.xml \ No newline at end of file +junit.xml +artifacts \ No newline at end of file diff --git a/README.md b/README.md index a7ff9e1f..8ae03e70 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,12 @@ -# Social App - -In-progress social app. - -Uses: - -- [React Native](https://reactnative.dev) -- [React Native for Web](https://necolas.github.io/react-native-web/) -- [React Navigation](https://reactnative.dev/docs/navigation#react-navigation) -- [MobX](https://mobx.js.org/README.html) -- [Async Storage](https://github.com/react-native-async-storage/async-storage) +# Bluesky ## Build instructions - Setup your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup). +- Setup your environment [for e2e testing using detox](https://wix.github.io/Detox/docs/introduction/getting-started): + - yarn global add detox-cli + - brew tap wix/brew + - brew install applesimutils - After initial setup: - `cd ios ; pod install` - Start the dev servers @@ -34,6 +28,17 @@ Uses: ## Various notes +### Debugging + +- Note that since 0.70, debugging using the old debugger (which shows up using CMD+D) doesn't work anymore. Follow the instructions below to debug the code: https://reactnative.dev/docs/next/hermes#debugging-js-on-hermes-using-google-chromes-devtools + +### Running E2E Tests + +- Make sure you've setup your environment following above +- Make sure Metro and the dev server are running +- Run `yarn e2e` +- Find the artifacts in the `artifact` folder + ### Polyfills `./platform/polyfills.*.ts` adds polyfills to the environment. Currently this includes: diff --git a/__mocks__/react-native-svg.js b/__mocks__/react-native-svg.js new file mode 100644 index 00000000..91a10e64 --- /dev/null +++ b/__mocks__/react-native-svg.js @@ -0,0 +1,67 @@ +// @flow + +// https://github.com/FormidableLabs/react-native-svg-mock +import React from 'react' + +const createComponent = function (name) { + return class extends React.Component { + // overwrite the displayName, since this is a class created dynamically + static displayName = name + + render() { + return React.createElement(name, this.props, this.props.children) + } + } +} + +// Mock all react-native-svg exports +// from https://github.com/magicismight/react-native-svg/blob/master/index.js +const Svg = createComponent('Svg') +const Circle = createComponent('Circle') +const Ellipse = createComponent('Ellipse') +const G = createComponent('G') +const Text = createComponent('Text') +const TextPath = createComponent('TextPath') +const TSpan = createComponent('TSpan') +const Path = createComponent('Path') +const Polygon = createComponent('Polygon') +const Polyline = createComponent('Polyline') +const Line = createComponent('Line') +const Rect = createComponent('Rect') +const Use = createComponent('Use') +const Image = createComponent('Image') +const Symbol = createComponent('Symbol') +const Defs = createComponent('Defs') +const LinearGradient = createComponent('LinearGradient') +const RadialGradient = createComponent('RadialGradient') +const Stop = createComponent('Stop') +const ClipPath = createComponent('ClipPath') +const Pattern = createComponent('Pattern') +const Mask = createComponent('Mask') + +export { + Svg, + Circle, + Ellipse, + G, + Text, + TextPath, + TSpan, + Path, + Polygon, + Polyline, + Line, + Rect, + Use, + Image, + Symbol, + Defs, + LinearGradient, + RadialGradient, + Stop, + ClipPath, + Pattern, + Mask, +} + +export default Svg diff --git a/__mocks__/state-mock.ts b/__mocks__/state-mock.ts deleted file mode 100644 index f269bfc6..00000000 --- a/__mocks__/state-mock.ts +++ /dev/null @@ -1,856 +0,0 @@ -import {LogModel} from './../src/state/models/log' -import {LRUMap} from 'lru_map' -import {RootStoreModel} from './../src/state/models/root-store' -import {NavigationTabModel} from './../src/state/models/navigation' -import {SessionModel} from '../src/state/models/session' -import {NavigationModel} from '../src/state/models/navigation' -import {ShellUiModel} from '../src/state/models/shell-ui' -import {MeModel} from '../src/state/models/me' -import {OnboardModel} from '../src/state/models/onboard' -import {ProfilesViewModel} from '../src/state/models/profiles-view' -import {LinkMetasViewModel} from '../src/state/models/link-metas-view' -import {FeedModel} from '../src/state/models/feed-view' -import {NotificationsViewModel} from '../src/state/models/notifications-view' -import {ProfileViewModel} from '../src/state/models/profile-view' -import {ProfileUiModel, Sections} from '../src/state/models/profile-ui' -import {SessionServiceClient} from '@atproto/api' -import {UserAutocompleteViewModel} from '../src/state/models/user-autocomplete-view' -import {UserLocalPhotosModel} from '../src/state/models/user-local-photos' -import {SuggestedActorsViewModel} from '../src/state/models/suggested-actors-view' -import {UserFollowersViewModel} from '../src/state/models/user-followers-view' -import {UserFollowsViewModel} from '../src/state/models/user-follows-view' -import {NotificationsViewItemModel} from './../src/state/models/notifications-view' -import { - PostThreadViewModel, - PostThreadViewPostModel, -} from '../src/state/models/post-thread-view' -import {FeedItemModel} from '../src/state/models/feed-view' -import {RepostedByViewModel} from '../src/state/models/reposted-by-view' -import {VotesViewModel} from '../src/state/models/votes-view' - -export const mockedProfileStore = { - isLoading: false, - isRefreshing: false, - hasLoaded: true, - error: '', - params: { - actor: '', - }, - did: 'test did', - handle: 'testhandle', - declaration: { - cid: '', - actorType: '', - }, - creator: 'test did', - displayName: '', - description: '', - avatar: '', - banner: '', - followersCount: 0, - followsCount: 0, - membersCount: 0, - postsCount: 0, - myState: { - follow: '', - member: '', - }, - rootStore: {} as RootStoreModel, - hasContent: true, - hasError: false, - isEmpty: false, - isUser: true, - isScene: false, - setup: jest.fn().mockResolvedValue({aborted: false}), - refresh: jest.fn().mockResolvedValue({}), - toggleFollowing: jest.fn().mockResolvedValue({}), - updateProfile: jest.fn(), - // unknown required because of the missing private methods: _xLoading, _xIdle, _load, _replaceAll -} as unknown as ProfileViewModel - -export const mockedFeedItemStore = { - _reactKey: 'item-1', - _isThreadParent: false, - _isThreadChildElided: false, - _isThreadChild: false, - _hideParent: false, - _isRenderingAsThread: false, - post: { - uri: 'testuri', - cid: 'test cid', - author: { - did: 'test did', - handle: 'test.handle', - displayName: 'test name', - declaration: {cid: '', actorType: ''}, - }, - record: { - $type: 'app.bsky.feed.post', - createdAt: '2022-12-29T16:39:57.919Z', - text: 'Sup', - }, - replyCount: 0, - repostCount: 0, - upvoteCount: 0, - downvoteCount: 0, - indexedAt: '2022-12-29T16:39:57.919Z', - viewer: {}, - }, - postRecord: { - $type: 'app.bsky.feed.post', - text: 'test text', - createdAt: '1', - reply: { - root: { - uri: 'testuri', - cid: 'tes cid', - }, - parent: { - uri: 'testuri', - cid: 'tes cid', - }, - }, - }, - rootStore: {} as RootStoreModel, - copy: jest.fn(), - toggleUpvote: jest.fn().mockResolvedValue({}), - toggleDownvote: jest.fn(), - toggleRepost: jest.fn().mockResolvedValue({}), - delete: jest.fn().mockResolvedValue({}), - reasonRepost: { - by: { - did: 'test did', - handle: 'test.handle', - declaration: {cid: '', actorType: ''}, - }, - indexedAt: '', - }, - reasonTrend: { - by: { - did: 'test did', - handle: 'test.handle', - declaration: {cid: '', actorType: ''}, - }, - indexedAt: '', - }, - reply: { - parent: { - author: { - did: 'test did', - handle: 'test.handle', - displayName: 'test name', - declaration: {cid: '', actorType: ''}, - }, - cid: '', - downvoteCount: 0, - indexedAt: '2023-01-10T11:17:46.945Z', - record: {}, - replyCount: 1, - repostCount: 0, - upvoteCount: 0, - uri: 'testuri', - viewer: {}, - }, - root: { - author: { - did: 'test did', - handle: 'test.handle', - displayName: 'test name', - declaration: {cid: '', actorType: ''}, - }, - cid: '', - downvoteCount: 0, - indexedAt: '2023-01-10T11:17:46.739Z', - record: {}, - replyCount: 1, - repostCount: 0, - upvoteCount: 1, - uri: 'testuri', - viewer: {}, - }, - }, -} as FeedItemModel - -export const mockedFeedStore = { - isLoading: false, - isRefreshing: false, - hasNewLatest: false, - hasLoaded: true, - error: '', - hasMore: true, - params: { - actor: '', - limit: 1, - before: '', - }, - feed: [], - rootStore: {} as RootStoreModel, - feedType: 'home', - hasContent: true, - hasError: false, - isEmpty: false, - nonReplyFeed: [ - { - _reactKey: 'item-1', - post: { - author: { - handle: 'handle.test', - displayName: 'test name', - avatar: '', - }, - cid: 'bafyreihkwjoy2vbfqld2lp3tv4ce6yfr354sqgp32qoplrudso4gyyjiwe', - downvoteCount: 0, - indexedAt: '2022-12-29T16:35:55.270Z', - record: { - $type: 'app.bsky.feed.post', - createdAt: '2022-12-29T16:39:57.919Z', - text: 'Sup', - }, - replyCount: 0, - repostCount: 0, - upvoteCount: 0, - uri: 'at://did:plc:wcizmlgv3rdslk64t6q4silu/app.bsky.feed.post/3jkzce5kfvn2h', - viewer: { - handle: 'handle.test', - displayName: 'test name', - avatar: '', - }, - }, - reason: undefined, - reply: undefined, - }, - ], - setHasNewLatest: jest.fn(), - setup: jest.fn().mockResolvedValue({}), - refresh: jest.fn().mockResolvedValue({}), - loadMore: jest.fn().mockResolvedValue({}), - loadLatest: jest.fn(), - update: jest.fn(), - checkForLatest: jest.fn().mockRejectedValue('Error checking for latest'), - registerListeners: jest.fn().mockReturnValue(jest.fn()), - // unknown required because of the missing private methods: _xLoading, _xIdle, _pendingWork, _initialLoad, _loadLatest, _loadMore, _update, _replaceAll, _appendAll, _prependAll, _updateAll, _getFeed, loadMoreCursor, pollCursor, _loadPromise, _updatePromise, _loadLatestPromise, _loadMorePromise -} as unknown as FeedModel - -export const mockedPostThreadViewPostStore = { - _reactKey: 'item-1', - _depth: 0, - _isHighlightedPost: false, - _hasMore: false, - postRecord: { - text: 'test text', - createdAt: '', - reply: { - root: { - uri: 'testuri', - cid: 'tes cid', - }, - parent: { - uri: 'testuri', - cid: 'tes cid', - }, - }, - }, - post: { - uri: 'testuri', - cid: 'testcid', - record: {}, - author: { - did: 'test did', - handle: 'test.handle', - declaration: {cid: '', actorType: ''}, - viewer: { - muted: true, - }, - }, - replyCount: 0, - repostCount: 0, - upvoteCount: 0, - downvoteCount: 0, - indexedAt: '', - viewer: { - repost: '', - upvote: '', - downvote: '', - }, - }, - rootStore: {} as RootStoreModel, - assignTreeModels: jest.fn(), - toggleRepost: jest.fn().mockRejectedValue({}), - toggleUpvote: jest.fn().mockRejectedValue({}), - toggleDownvote: jest.fn(), - delete: jest.fn(), -} as PostThreadViewPostModel - -export const mockedPostThreadViewStore = { - isLoading: false, - isRefreshing: false, - hasLoaded: false, - error: '', - notFound: false, - resolvedUri: 'testuri', - params: { - uri: 'testuri', - }, - thread: mockedPostThreadViewPostStore, - hasContent: true, - hasError: false, - setup: jest.fn(), - refresh: jest.fn().mockResolvedValue({}), - update: jest.fn(), - // unknown required because of the missing private methods: _xLoading, _xIdle, _resolveUri, _load, _replaceAll -} as unknown as PostThreadViewModel - -export const mockedNotificationsViewItemStore = { - _reactKey: 'item-1', - uri: 'testuri', - cid: '', - author: { - did: 'test did', - handle: 'test.handle', - declaration: {cid: '', actorType: ''}, - }, - rootStore: {} as RootStoreModel, - copy: jest.fn(), - reason: 'test reason', - isRead: true, - indexedAt: '', - isUpvote: true, - isRepost: false, - isTrend: false, - isMention: false, - isReply: false, - isFollow: false, - isAssertion: false, - needsAdditionalData: false, - isInvite: false, - subjectUri: 'testuri', - toSupportedRecord: jest.fn().mockReturnValue({ - text: 'test text', - createdAt: '', - }), - fetchAdditionalData: jest.fn(), - toNotifeeOpts: jest.fn(), -} as NotificationsViewItemModel - -export const mockedNotificationsStore = { - isLoading: false, - isRefreshing: false, - hasLoaded: true, - error: '', - params: { - limit: 1, - before: '', - }, - hasMore: true, - notifications: [mockedNotificationsViewItemStore], - rootStore: {} as RootStoreModel, - hasContent: true, - hasError: false, - isEmpty: false, - setup: jest.fn().mockResolvedValue({aborted: false}), - refresh: jest.fn().mockResolvedValue({}), - loadMore: jest.fn().mockResolvedValue({}), - update: jest.fn().mockResolvedValue(null), - updateReadState: jest.fn(), - // unknown required because of the missing private methods: _xLoading, _xIdle, _pendingWork, _initialLoad, _loadMore, _update, _replaceAll, _appendAll, _updateAll, loadMoreCursor, _loadPromise, _updatePromise, _loadLatestPromise, _loadMorePromise -} as unknown as NotificationsViewModel - -export const mockedSessionStore = { - serialize: jest.fn(), - hydrate: jest.fn(), - data: { - service: '', - refreshJwt: '', - accessJwt: '', - handle: '', - did: 'test did', - }, - online: false, - attemptingConnect: false, - rootStore: {} as RootStoreModel, - hasSession: true, - clear: jest.fn(), - setState: jest.fn(), - setOnline: jest.fn(), - updateAuthTokens: jest.fn(), - connect: jest.fn(), - describeService: jest.fn().mockResolvedValue({ - availableUserDomains: ['test'], - links: { - termsOfService: 'https://testTermsOfService', - privacyPolicy: 'https://testPrivacyPolicy', - }, - }), - login: jest.fn(), - createAccount: jest.fn(), - logout: jest.fn(), - - // unknown required because of the missing private methods: _connectPromise, configureApi & _connect -} as unknown as SessionModel - -export const mockedNavigationTabStore = { - serialize: jest.fn(), - hydrate: jest.fn(), - id: '0', - history: [ - { - url: '', - ts: 0, - title: '', - id: '0', - }, - ], - index: 0, - isNewTab: false, - current: { - url: '', - ts: 0, - title: '', - id: '0', - }, - canGoBack: false, - canGoForward: false, - backTen: [ - { - url: '', - title: '', - index: 0, - id: '0', - }, - ], - forwardTen: [ - { - url: '', - title: '', - index: 0, - id: '0', - }, - ], - navigate: jest.fn(), - refresh: jest.fn().mockResolvedValue({}), - goBack: jest.fn(), - fixedTabReset: jest.fn(), - goForward: jest.fn(), - goToIndex: jest.fn(), - setTitle: jest.fn(), - setIsNewTab: jest.fn(), - fixedTabPurpose: 0, - getBackList: () => [ - { - url: '/', - title: '', - index: 1, - id: '1', - }, - ], - getForwardList: jest.fn(), -} as NavigationTabModel - -export const mockedNavigationStore = { - serialize: jest.fn(), - hydrate: jest.fn(), - tabs: [mockedNavigationTabStore], - tabIndex: 0, - clear: jest.fn(), - tab: mockedNavigationTabStore, - tabCount: 1, - isCurrentScreen: jest.fn(), - navigate: jest.fn(), - refresh: jest.fn().mockResolvedValue({}), - setTitle: jest.fn(), - handleLink: jest.fn(), - switchTo: jest.fn(), - setActiveTab: jest.fn(), - closeTab: jest.fn(), - newTab: jest.fn(), -} as NavigationModel - -export const mockedShellStore = { - serialize: jest.fn(), - hydrate: jest.fn(), - minimalShellMode: false, - isMainMenuOpen: false, - isModalActive: false, - activeModal: undefined, - isLightboxActive: false, - activeLightbox: undefined, - isComposerActive: false, - composerOpts: undefined, - darkMode: false, - setDarkMode: jest.fn(), - setMainMenuOpen: jest.fn(), - setMinimalShellMode: jest.fn(), - openModal: jest.fn(), - closeModal: jest.fn(), - closeComposer: jest.fn(), - closeLightbox: jest.fn(), - openComposer: jest.fn(), - openLightbox: jest.fn(), -} as ShellUiModel - -export const mockedMeStore = { - serialize: jest.fn(), - hydrate: jest.fn(), - did: 'test did', - handle: 'test', - displayName: 'test', - description: 'test', - avatar: '', - notificationCount: 0, - rootStore: {} as RootStoreModel, - mainFeed: mockedFeedStore, - notifications: mockedNotificationsStore, - clear: jest.fn(), - load: jest.fn(), - clearNotificationCount: jest.fn(), - fetchNotifications: jest.fn(), - bgFetchNotifications: jest.fn(), - refreshMemberships: jest.fn(), -} as MeModel - -export const mockedOnboardStore = { - serialize: jest.fn(), - hydrate: jest.fn(), - isOnboarding: false, - stage: '', - start: jest.fn(), - stop: jest.fn(), - next: jest.fn(), -} as OnboardModel - -export const mockedProfilesStore = { - hydrate: jest.fn(), - serialize: jest.fn(), - cache: new LRUMap(100), - rootStore: {} as RootStoreModel, - getProfile: jest.fn().mockResolvedValue({data: {}}), - overwrite: jest.fn(), -} as ProfilesViewModel - -export const mockedLinkMetasStore = { - hydrate: jest.fn(), - serialize: jest.fn(), - cache: new LRUMap(100), - rootStore: {} as RootStoreModel, - getLinkMeta: jest.fn(), -} as LinkMetasViewModel - -export const mockedLogStore = { - entries: [], - serialize: jest.fn(), - hydrate: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - // unknown required because of the missing private methods: add -} as unknown as LogModel - -export const mockedRootStore = { - api: { - com: {}, - app: { - bsky: { - actor: { - searchTypeahead: jest.fn().mockResolvedValue({data: {users: []}}), - }, - graph: { - getFollows: jest.fn().mockResolvedValue({data: {follows: []}}), - getFollowers: jest.fn().mockResolvedValue({}), - getMembers: jest.fn().mockResolvedValue({}), - }, - }, - }, - } as unknown as SessionServiceClient, - resolveName: jest.fn(), - serialize: jest.fn(), - hydrate: jest.fn(), - fetchStateUpdate: jest.fn(), - clearAll: jest.fn(), - onPostDeleted: jest.fn(), - emitPostDeleted: jest.fn(), - initBgFetch: jest.fn(), - onBgFetch: jest.fn(), - onBgFetchTimeout: jest.fn(), - session: mockedSessionStore, - nav: mockedNavigationStore, - shell: mockedShellStore, - me: mockedMeStore, - onboard: mockedOnboardStore, - profiles: mockedProfilesStore, - linkMetas: mockedLinkMetasStore, - log: mockedLogStore, -} as RootStoreModel - -export const mockedProfileUiStore = { - profile: mockedProfileStore, - feed: mockedFeedStore, - selectedViewIndex: 0, - rootStore: mockedRootStore, - params: { - user: 'test user', - }, - currentView: mockedFeedStore, - isInitialLoading: false, - isRefreshing: false, - isUser: true, - isScene: false, - selectorItems: [Sections.Posts, Sections.PostsWithReplies], - selectedView: Sections.Posts, - setSelectedViewIndex: jest.fn(), - setup: jest.fn().mockResolvedValue({aborted: false}), - update: jest.fn(), - refresh: jest.fn().mockResolvedValue({}), - loadMore: jest.fn(), -} as ProfileUiModel - -export const mockedAutocompleteViewStore = { - isLoading: false, - isActive: true, - prefix: '', - follows: [ - { - did: 'test did', - declaration: { - cid: '', - actorType: 'app.bsky.system.actorUser', - }, - handle: '', - displayName: '', - createdAt: '', - indexedAt: '', - }, - ], - searchRes: [ - { - did: 'test did', - declaration: { - cid: '', - actorType: 'app.bsky.system.actorUser', - }, - handle: '', - displayName: '', - }, - ], - knownHandles: new Set(), - suggestions: [ - { - handle: 'handle.test', - displayName: 'Test Display', - }, - { - handle: 'handle2.test', - displayName: 'Test Display 2', - }, - ], - rootStore: {} as RootStoreModel, - setup: jest.fn(), - setActive: jest.fn(), - setPrefix: jest.fn(), - // unknown required because of the missing private methods: _searchPromise, _getFollows , _search -} as unknown as UserAutocompleteViewModel - -export const mockedLocalPhotosStore = { - photos: { - node: { - type: '', - group_name: '', - image: { - filename: '', - extension: '', - uri: '', - height: 1000, - width: 1000, - fileSize: null, - playableDuration: 0, - }, - timestamp: 1672847197, - location: null, - }, - }, - rootStore: {} as RootStoreModel, - setup: jest.fn(), - // unknown required because of the missing private methods: _getPhotos -} as unknown as UserLocalPhotosModel - -export const mockedSuggestedActorsStore = { - isLoading: false, - isRefreshing: false, - hasLoaded: false, - error: '', - suggestions: [ - { - did: '1', - declaration: { - cid: '', - actorType: 'app.bsky.system.actorUser', - }, - handle: 'handle1.test', - displayName: 'test name 1', - description: 'desc', - indexedAt: '', - _reactKey: '1', - }, - { - did: '2', - declaration: { - cid: '', - actorType: 'app.bsky.system.actorUser', - }, - handle: '', - displayName: 'handle2.test', - description: 'desc', - indexedAt: '', - _reactKey: '2', - }, - ], - rootStore: {} as RootStoreModel, - hasContent: true, - hasError: false, - isEmpty: false, - setup: jest.fn().mockResolvedValue(null), - refresh: jest.fn().mockResolvedValue({}), - // unknown required because of the missing private methods: _xLoading, _xIdle, _fetch, _appendAll, _append -} as unknown as SuggestedActorsViewModel - -export const mockedUserFollowersStore = { - isLoading: false, - isRefreshing: false, - hasLoaded: false, - error: '', - params: { - user: 'test user', - }, - subject: { - did: 'test did', - handle: '', - declaration: {cid: '', actorType: ''}, - }, - followers: [ - { - did: 'test did', - declaration: {cid: '', actorType: ''}, - handle: 'testhandle', - displayName: 'test name', - indexedAt: '', - _reactKey: '1', - }, - { - did: 'test did2', - declaration: {cid: '', actorType: ''}, - handle: 'testhandle2', - displayName: 'test name 2', - indexedAt: '', - _reactKey: '2', - }, - ], - rootStore: {} as RootStoreModel, - hasContent: true, - hasError: false, - isEmpty: false, - setup: jest.fn(), - refresh: jest.fn().mockResolvedValue({}), - loadMore: jest.fn(), - // unknown required because of the missing private methods: _xIdle, _xLoading, _fetch, _replaceAll, _append -} as unknown as UserFollowersViewModel - -export const mockedUserFollowsStore = { - isLoading: false, - isRefreshing: false, - hasLoaded: false, - error: '', - params: { - user: 'test user', - }, - subject: { - did: 'test did', - handle: '', - declaration: {cid: '', actorType: ''}, - }, - follows: [ - { - did: 'test did', - declaration: {cid: '', actorType: ''}, - handle: 'testhandle', - displayName: 'test name', - indexedAt: '', - _reactKey: '1', - }, - { - did: 'test did2', - declaration: {cid: '', actorType: ''}, - handle: 'testhandle2', - displayName: 'test name 2', - indexedAt: '', - _reactKey: '2', - }, - ], - rootStore: {} as RootStoreModel, - hasContent: true, - hasError: false, - isEmpty: false, - setup: jest.fn(), - refresh: jest.fn().mockResolvedValue({}), - loadMore: jest.fn(), - // unknown required because of the missing private methods: _xIdle, _xLoading, _fetch, _replaceAll, _append -} as unknown as UserFollowsViewModel - -export const mockedRepostedByViewStore = { - isLoading: false, - isRefreshing: false, - hasLoaded: false, - error: '', - resolvedUri: '', - params: { - uri: 'testuri', - }, - uri: '', - repostedBy: [ - { - _reactKey: '', - did: '', - handle: '', - displayName: '', - declaration: {cid: '', actorType: ''}, - indexedAt: '', - }, - ], - hasContent: false, - hasError: false, - isEmpty: false, - setup: jest.fn().mockResolvedValue({}), - refresh: jest.fn().mockResolvedValue({}), - loadMore: jest.fn().mockResolvedValue({}), - // unknown required because of the missing private methods: _xIdle, _xLoading, _resolveUri, _fetch, _refresh, _replaceAll, _append -} as unknown as RepostedByViewModel - -export const mockedVotesViewStore = { - isLoading: false, - isRefreshing: false, - hasLoaded: false, - error: '', - resolvedUri: '', - params: { - uri: 'testuri', - }, - uri: '', - votes: [ - { - _reactKey: '', - direction: 'up', - indexedAt: '', - createdAt: '', - actor: { - did: '', - handle: '', - declaration: {cid: '', actorType: ''}, - }, - }, - ], - hasContent: false, - hasError: false, - isEmpty: false, - setup: jest.fn(), - refresh: jest.fn().mockResolvedValue({}), - loadMore: jest.fn().mockResolvedValue({}), - // unknown required because of the missing private methods: _xIdle, _xLoading, _resolveUri, _fetch, _replaceAll, _append -} as unknown as VotesViewModel diff --git a/__tests__/accounts.test.tsx b/__tests__/accounts.test.tsx deleted file mode 100644 index f3ecb6af..00000000 --- a/__tests__/accounts.test.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import React from 'react' -import {MobileShell} from '../src/view/shell/mobile' -import {cleanup, fireEvent, render, waitFor} from '../jest/test-utils' -import {createServer, TestPDS} from '../jest/test-pds' -import {RootStoreModel, setupState} from '../src/state' - -const WAIT_OPTS = {timeout: 5e3} - -describe('Account flows', () => { - let pds: TestPDS | undefined - let rootStore: RootStoreModel | undefined - beforeAll(async () => { - jest.useFakeTimers() - pds = await createServer() - rootStore = await setupState(pds.pdsUrl) - }) - - afterAll(async () => { - jest.clearAllMocks() - cleanup() - await pds?.close() - }) - - it('renders initial screen', () => { - const {getByTestId} = render(, rootStore) - const signUpScreen = getByTestId('signinOrCreateAccount') - - expect(signUpScreen).toBeTruthy() - }) - - it('completes signin to the server', async () => { - const {getByTestId} = render(, rootStore) - - // move to signin view - fireEvent.press(getByTestId('signInButton')) - expect(getByTestId('signIn')).toBeTruthy() - expect(getByTestId('loginForm')).toBeTruthy() - - // input the target server - expect(getByTestId('loginSelectServiceButton')).toBeTruthy() - fireEvent.press(getByTestId('loginSelectServiceButton')) - expect(getByTestId('serverInputModal')).toBeTruthy() - fireEvent.changeText( - getByTestId('customServerTextInput'), - pds?.pdsUrl || '', - ) - fireEvent.press(getByTestId('customServerSelectBtn')) - await waitFor(() => { - expect(getByTestId('loginUsernameInput')).toBeTruthy() - }, WAIT_OPTS) - - // enter username & pass - fireEvent.changeText(getByTestId('loginUsernameInput'), 'alice') - fireEvent.changeText(getByTestId('loginPasswordInput'), 'hunter2') - await waitFor(() => { - expect(getByTestId('loginNextButton')).toBeTruthy() - }, WAIT_OPTS) - fireEvent.press(getByTestId('loginNextButton')) - - // signed in - await waitFor(() => { - expect(getByTestId('homeFeed')).toBeTruthy() - expect(rootStore?.me?.displayName).toBe('Alice') - expect(rootStore?.me?.handle).toBe('alice.test') - expect(rootStore?.session.accounts.length).toBe(1) - }, WAIT_OPTS) - expect(rootStore?.me?.displayName).toBe('Alice') - expect(rootStore?.me?.handle).toBe('alice.test') - expect(rootStore?.session.accounts.length).toBe(1) - }) - - it('opens the login screen when "add account" is pressed', async () => { - const {getByTestId, getAllByTestId} = render(, rootStore) - await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS) - - // open side menu - fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0]) - await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS) - - // nav to settings - fireEvent.press(getByTestId('menuItemButton-Settings')) - await waitFor( - () => expect(getByTestId('settingsScreen')).toBeTruthy(), - WAIT_OPTS, - ) - - // press '+ new account' in switcher - fireEvent.press(getByTestId('switchToNewAccountBtn')) - await waitFor( - () => expect(getByTestId('signinOrCreateAccount')).toBeTruthy(), - WAIT_OPTS, - ) - }) - - it('shows the "choose account" form when a previous session has been created', async () => { - const {getByTestId} = render(, rootStore) - - // move to signin view - fireEvent.press(getByTestId('signInButton')) - expect(getByTestId('signIn')).toBeTruthy() - expect(getByTestId('chooseAccountForm')).toBeTruthy() - }) - - it('logs directly into the account due to still possessing session tokens', async () => { - const {getByTestId} = render(, rootStore) - - // move to signin view - fireEvent.press(getByTestId('signInButton')) - expect(getByTestId('signIn')).toBeTruthy() - expect(getByTestId('chooseAccountForm')).toBeTruthy() - - // select the previous account - fireEvent.press(getByTestId('chooseAccountBtn-alice.test')) - - // signs in immediately - await waitFor(() => { - expect(getByTestId('homeFeed')).toBeTruthy() - expect(rootStore?.me?.displayName).toBe('Alice') - expect(rootStore?.me?.handle).toBe('alice.test') - expect(rootStore?.session.accounts.length).toBe(1) - }, WAIT_OPTS) - expect(rootStore?.me?.displayName).toBe('Alice') - expect(rootStore?.me?.handle).toBe('alice.test') - expect(rootStore?.session.accounts.length).toBe(1) - }) - - it('logs into a second account via the switcher', async () => { - const {getByTestId, getAllByTestId} = render(, rootStore) - await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS) - - // open side menu - fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0]) - await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS) - - // nav to settings - fireEvent.press(getByTestId('menuItemButton-Settings')) - await waitFor( - () => expect(getByTestId('settingsScreen')).toBeTruthy(), - WAIT_OPTS, - ) - - // press '+ new account' in switcher - fireEvent.press(getByTestId('switchToNewAccountBtn')) - await waitFor( - () => expect(getByTestId('signinOrCreateAccount')).toBeTruthy(), - WAIT_OPTS, - ) - - // move to signin view - fireEvent.press(getByTestId('signInButton')) - expect(getByTestId('signIn')).toBeTruthy() - expect(getByTestId('chooseAccountForm')).toBeTruthy() - - // select a new account - fireEvent.press(getByTestId('chooseNewAccountBtn')) - expect(getByTestId('loginForm')).toBeTruthy() - - // input the target server - expect(getByTestId('loginSelectServiceButton')).toBeTruthy() - fireEvent.press(getByTestId('loginSelectServiceButton')) - expect(getByTestId('serverInputModal')).toBeTruthy() - fireEvent.changeText( - getByTestId('customServerTextInput'), - pds?.pdsUrl || '', - ) - fireEvent.press(getByTestId('customServerSelectBtn')) - await waitFor( - () => expect(getByTestId('loginUsernameInput')).toBeTruthy(), - WAIT_OPTS, - ) - - // enter username & pass - fireEvent.changeText(getByTestId('loginUsernameInput'), 'bob') - fireEvent.changeText(getByTestId('loginPasswordInput'), 'hunter2') - await waitFor( - () => expect(getByTestId('loginNextButton')).toBeTruthy(), - WAIT_OPTS, - ) - fireEvent.press(getByTestId('loginNextButton')) - - // signed in - await waitFor(() => { - expect(getByTestId('settingsScreen')).toBeTruthy() // we go back to settings in this situation - expect(rootStore?.me?.displayName).toBe('Bob') - expect(rootStore?.me?.handle).toBe('bob.test') - expect(rootStore?.session.accounts.length).toBe(2) - }, WAIT_OPTS) - expect(rootStore?.me?.displayName).toBe('Bob') - expect(rootStore?.me?.handle).toBe('bob.test') - expect(rootStore?.session.accounts.length).toBe(2) - }) - - it('can instantly switch between accounts', async () => { - const {getByTestId} = render(, rootStore) - await waitFor( - () => expect(getByTestId('settingsScreen')).toBeTruthy(), - WAIT_OPTS, - ) - - // select the alice account - fireEvent.press(getByTestId('switchToAccountBtn-alice.test')) - - // swapped account - await waitFor(() => { - expect(rootStore?.me?.displayName).toBe('Alice') - expect(rootStore?.me?.handle).toBe('alice.test') - expect(rootStore?.session.accounts.length).toBe(2) - }, WAIT_OPTS) - expect(rootStore?.me?.displayName).toBe('Alice') - expect(rootStore?.me?.handle).toBe('alice.test') - expect(rootStore?.session.accounts.length).toBe(2) - }) - - it('will prompt for a password if you sign out', async () => { - const {getByTestId} = render(, rootStore) - await waitFor( - () => expect(getByTestId('settingsScreen')).toBeTruthy(), - WAIT_OPTS, - ) - - // press the sign out button - fireEvent.press(getByTestId('signOutBtn')) - - // in the logged out state - await waitFor( - () => expect(getByTestId('signinOrCreateAccount')).toBeTruthy(), - WAIT_OPTS, - ) - - // move to signin view - fireEvent.press(getByTestId('signInButton')) - expect(getByTestId('signIn')).toBeTruthy() - expect(getByTestId('chooseAccountForm')).toBeTruthy() - - // select an existing account - fireEvent.press(getByTestId('chooseAccountBtn-alice.test')) - - // goes to login screen instead of straight back to settings - expect(getByTestId('loginForm')).toBeTruthy() - }) -}) diff --git a/__tests__/lib/__mocks__/youtubeChannelHtml.ts b/__tests__/lib/__mocks__/youtubeChannelHtml.ts new file mode 100644 index 00000000..cc71995c --- /dev/null +++ b/__tests__/lib/__mocks__/youtubeChannelHtml.ts @@ -0,0 +1,63 @@ +export const youtubeChannelHtml = ` + +
AboutPressCopyrightContact usCreatorsAdvertiseDevelopersTermsPrivacyPolicy & SafetyHow YouTube worksTest new features
penguinz0 - YouTube
+ +` diff --git a/__tests__/lib/async/bundle.test.ts b/__tests__/lib/async/bundle.test.ts new file mode 100644 index 00000000..8718f54a --- /dev/null +++ b/__tests__/lib/async/bundle.test.ts @@ -0,0 +1,47 @@ +import {bundleAsync} from '../../../src/lib/async/bundle' + +describe('bundle', () => { + it('bundles multiple simultaneous calls into one execution', async () => { + let calls = 0 + const fn = bundleAsync(async () => { + calls++ + await new Promise(r => setTimeout(r, 1)) + return 'hello' + }) + const [res1, res2, res3] = await Promise.all([fn(), fn(), fn()]) + expect(calls).toEqual(1) + expect(res1).toEqual('hello') + expect(res2).toEqual('hello') + expect(res3).toEqual('hello') + }) + it('does not bundle non-simultaneous calls', async () => { + let calls = 0 + const fn = bundleAsync(async () => { + calls++ + await new Promise(r => setTimeout(r, 1)) + return 'hello' + }) + const res1 = await fn() + const res2 = await fn() + const res3 = await fn() + expect(calls).toEqual(3) + expect(res1).toEqual('hello') + expect(res2).toEqual('hello') + expect(res3).toEqual('hello') + }) + it('is not affected by rejections', async () => { + let calls = 0 + const fn = bundleAsync(async () => { + calls++ + await new Promise(r => setTimeout(r, 1)) + throw new Error() + }) + const res1 = await fn().catch(() => 'reject') + const res2 = await fn().catch(() => 'reject') + const res3 = await fn().catch(() => 'reject') + expect(calls).toEqual(3) + expect(res1).toEqual('reject') + expect(res2).toEqual('reject') + expect(res3).toEqual('reject') + }) +}) diff --git a/__tests__/lib/errors.test.ts b/__tests__/lib/errors.test.ts index b9549e6d..39e8d189 100644 --- a/__tests__/lib/errors.test.ts +++ b/__tests__/lib/errors.test.ts @@ -1,4 +1,4 @@ -import {isNetworkError} from '../../src/lib/errors' +import {isNetworkError} from '../../src/lib/strings/errors' describe('isNetworkError', () => { const inputs = [ diff --git a/__tests__/lib/extractHtmlMeta.test.ts b/__tests__/lib/extractHtmlMeta.test.ts index 18a0df4c..cdd2a338 100644 --- a/__tests__/lib/extractHtmlMeta.test.ts +++ b/__tests__/lib/extractHtmlMeta.test.ts @@ -1,7 +1,8 @@ -import {extractHtmlMeta} from '../../src/lib/extractHtmlMeta' +import {extractHtmlMeta} from '../../src/lib/link-meta/html' import {exampleComHtml} from './__mocks__/exampleComHtml' import {youtubeHTML} from './__mocks__/youtubeHtml' import {tiktokHtml} from './__mocks__/tiktokHtml' +import {youtubeChannelHtml} from './__mocks__/youtubeChannelHtml' describe('extractHtmlMeta', () => { const cases = [ @@ -82,6 +83,19 @@ describe('extractHtmlMeta', () => { expect(output).toEqual(expectedOutput) }) + it('extracts avatar from a youtube channel', () => { + const input = youtubeChannelHtml + const expectedOutput = { + title: 'penguinz0', + description: + 'Clips channel: https://www.youtube.com/channel/UC4EQHfzIbkL_Skit_iKt1aA\n\nTwitter: https://twitter.com/MoistCr1TiKaL\n\nInstagram: https://www.instagram.com/bigmoistcr1tikal/?hl=en\n\nTwitch: https://www.twitch.tv/moistcr1tikal\n\nSnapchat: Hugecharles\n\nTik Tok: Hugecharles\n\nI don't have any other public accounts.', + image: + 'https://yt3.googleusercontent.com/ytc/AL5GRJWOhJOuUC6C2b7gP-5D2q6ypXbcOOckyAE1En4RUQ=s176-c-k-c0x00ffffff-no-rj', + } + const output = extractHtmlMeta({html: input, hostname: 'youtube.com'}) + expect(output).toEqual(expectedOutput) + }) + it('extracts username from the url a twitter profile page', () => { const expectedOutput = { title: '@bluesky on Twitter', diff --git a/__tests__/lib/images.test.ts b/__tests__/lib/images.test.ts index 952b0ca4..ad8556ef 100644 --- a/__tests__/lib/images.test.ts +++ b/__tests__/lib/images.test.ts @@ -78,8 +78,14 @@ describe('downloadAndResize', () => { }) it('should return undefined for unsupported file type', async () => { + const mockedFetch = RNFetchBlob.fetch as jest.Mock + mockedFetch.mockResolvedValueOnce({ + path: jest.fn().mockReturnValue('file://downloaded-image'), + flush: jest.fn(), + }) + const opts: DownloadAndResizeOpts = { - uri: 'https://example.com/image.bmp', + uri: 'https://example.com/image', width: 100, height: 100, maxSize: 500000, @@ -88,6 +94,25 @@ describe('downloadAndResize', () => { } const result = await downloadAndResize(opts) - expect(result).toBeUndefined() + expect(result).toEqual(mockResizedImage) + expect(RNFetchBlob.config).toHaveBeenCalledWith({ + fileCache: true, + appendExt: 'jpeg', + }) + expect(RNFetchBlob.fetch).toHaveBeenCalledWith( + 'GET', + 'https://example.com/image', + ) + expect(ImageResizer.createResizedImage).toHaveBeenCalledWith( + 'file://downloaded-image', + 100, + 100, + 'JPEG', + 100, + undefined, + undefined, + undefined, + {mode: 'cover'}, + ) }) }) diff --git a/__tests__/lib/link-meta.test.ts b/__tests__/lib/link-meta.test.ts index eaf8732b..ce7da415 100644 --- a/__tests__/lib/link-meta.test.ts +++ b/__tests__/lib/link-meta.test.ts @@ -1,8 +1,19 @@ -import {LikelyType, getLinkMeta, getLikelyType} from '../../src/lib/link-meta' +import { + LikelyType, + getLinkMeta, + getLikelyType, +} from '../../src/lib/link-meta/link-meta' import {exampleComHtml} from './__mocks__/exampleComHtml' -import {mockedRootStore} from '../../__mocks__/state-mock' +import AtpAgent from '@atproto/api' +import {DEFAULT_SERVICE, RootStoreModel} from '../../src/state' describe('getLinkMeta', () => { + let rootStore: RootStoreModel + + beforeEach(() => { + rootStore = new RootStoreModel(new AtpAgent({service: DEFAULT_SERVICE})) + }) + const inputs = [ '', 'httpbadurl', @@ -88,7 +99,7 @@ describe('getLinkMeta', () => { }) }) const input = inputs[i] - const output = await getLinkMeta(mockedRootStore, input) + const output = await getLinkMeta(rootStore, input) expect(output).toEqual(outputs[i]) } }) diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts index 0032ebf3..4f6fd62d 100644 --- a/__tests__/lib/string.test.ts +++ b/__tests__/lib/string.test.ts @@ -1,17 +1,18 @@ import { - extractEntities, - detectLinkables, - pluralize, + getYoutubeVideoId, makeRecordUri, - ago, - makeValidHandle, - createFullHandle, - enforceLen, - cleanError, toNiceDomain, toShortUrl, toShareUrl, -} from '../../src/lib/strings' +} from '../../src/lib/strings/url-helpers' +import {pluralize, enforceLen} from '../../src/lib/strings/helpers' +import {ago} from '../../src/lib/strings/time' +import { + extractEntities, + detectLinkables, +} from '../../src/lib/strings/rich-text-detection' +import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles' +import {cleanError} from '../../src/lib/strings/errors' describe('extractEntities', () => { const knownHandles = new Set(['handle.com', 'full123.test-of-chars']) @@ -487,3 +488,29 @@ describe('toShareUrl', () => { } }) }) + +describe('getYoutubeVideoId', () => { + it(' should return undefined for invalid youtube links', () => { + expect(getYoutubeVideoId('')).toBeUndefined() + expect(getYoutubeVideoId('https://www.google.com')).toBeUndefined() + expect(getYoutubeVideoId('https://www.youtube.com')).toBeUndefined() + expect( + getYoutubeVideoId('https://www.youtube.com/channelName'), + ).toBeUndefined() + expect( + getYoutubeVideoId('https://www.youtube.com/channel/channelName'), + ).toBeUndefined() + }) + + it('getYoutubeVideoId should return video id for valid youtube links', () => { + expect(getYoutubeVideoId('https://www.youtube.com/watch?v=videoId')).toBe( + 'videoId', + ) + expect( + getYoutubeVideoId( + 'https://www.youtube.com/watch?v=videoId&feature=share', + ), + ).toBe('videoId') + expect(getYoutubeVideoId('https://youtu.be/videoId')).toBe('videoId') + }) +}) diff --git a/__tests__/lib/strings/mention-manip.test.ts b/__tests__/lib/strings/mention-manip.test.ts new file mode 100644 index 00000000..f9075763 --- /dev/null +++ b/__tests__/lib/strings/mention-manip.test.ts @@ -0,0 +1,84 @@ +import { + getMentionAt, + insertMentionAt, +} from '../../../src/lib/strings/mention-manip' + +describe('getMentionAt', () => { + type Case = [string, number, string | undefined] + const cases: Case[] = [ + ['hello @alice goodbye', 0, undefined], + ['hello @alice goodbye', 1, undefined], + ['hello @alice goodbye', 2, undefined], + ['hello @alice goodbye', 3, undefined], + ['hello @alice goodbye', 4, undefined], + ['hello @alice goodbye', 5, undefined], + ['hello @alice goodbye', 6, 'alice'], + ['hello @alice goodbye', 7, 'alice'], + ['hello @alice goodbye', 8, 'alice'], + ['hello @alice goodbye', 9, 'alice'], + ['hello @alice goodbye', 10, 'alice'], + ['hello @alice goodbye', 11, 'alice'], + ['hello @alice goodbye', 12, 'alice'], + ['hello @alice goodbye', 13, undefined], + ['hello @alice goodbye', 14, undefined], + ['@alice', 0, 'alice'], + ['@alice hello', 0, 'alice'], + ['@alice hello', 1, 'alice'], + ['@alice hello', 2, 'alice'], + ['@alice hello', 3, 'alice'], + ['@alice hello', 4, 'alice'], + ['@alice hello', 5, 'alice'], + ['@alice hello', 6, 'alice'], + ['@alice hello', 7, undefined], + ['alice@alice', 0, undefined], + ['alice@alice', 6, undefined], + ] + + it.each(cases)( + 'given input string %p and cursor position %p, returns %p', + (str, cursorPos, expected) => { + const output = getMentionAt(str, cursorPos) + expect(output?.value).toEqual(expected) + }, + ) +}) + +describe('insertMentionAt', () => { + type Case = [string, number, string] + const cases: Case[] = [ + ['hello @alice goodbye', 0, 'hello @alice goodbye'], + ['hello @alice goodbye', 1, 'hello @alice goodbye'], + ['hello @alice goodbye', 2, 'hello @alice goodbye'], + ['hello @alice goodbye', 3, 'hello @alice goodbye'], + ['hello @alice goodbye', 4, 'hello @alice goodbye'], + ['hello @alice goodbye', 5, 'hello @alice goodbye'], + ['hello @alice goodbye', 6, 'hello @alice.com goodbye'], + ['hello @alice goodbye', 7, 'hello @alice.com goodbye'], + ['hello @alice goodbye', 8, 'hello @alice.com goodbye'], + ['hello @alice goodbye', 9, 'hello @alice.com goodbye'], + ['hello @alice goodbye', 10, 'hello @alice.com goodbye'], + ['hello @alice goodbye', 11, 'hello @alice.com goodbye'], + ['hello @alice goodbye', 12, 'hello @alice.com goodbye'], + ['hello @alice goodbye', 13, 'hello @alice goodbye'], + ['hello @alice goodbye', 14, 'hello @alice goodbye'], + ['@alice', 0, '@alice.com '], + ['@alice hello', 0, '@alice.com hello'], + ['@alice hello', 1, '@alice.com hello'], + ['@alice hello', 2, '@alice.com hello'], + ['@alice hello', 3, '@alice.com hello'], + ['@alice hello', 4, '@alice.com hello'], + ['@alice hello', 5, '@alice.com hello'], + ['@alice hello', 6, '@alice.com hello'], + ['@alice hello', 7, '@alice hello'], + ['alice@alice', 0, 'alice@alice'], + ['alice@alice', 6, 'alice@alice'], + ] + + it.each(cases)( + 'given input string %p and cursor position %p, returns %p', + (str, cursorPos, expected) => { + const output = insertMentionAt(str, cursorPos, 'alice.com') + expect(output).toEqual(expected) + }, + ) +}) diff --git a/__tests__/lib/strings/rich-text-sanitize.ts b/__tests__/lib/strings/rich-text-sanitize.ts new file mode 100644 index 00000000..d0bbae5e --- /dev/null +++ b/__tests__/lib/strings/rich-text-sanitize.ts @@ -0,0 +1,123 @@ +import {AppBskyFeedPost} from '@atproto/api' +type Entity = AppBskyFeedPost.Entity +import {RichText} from '../../../src/lib/strings/rich-text' +import {removeExcessNewlines} from '../../../src/lib/strings/rich-text-sanitize' + +describe('removeExcessNewlines', () => { + it('removes more than two consecutive new lines', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) + + it('removes more than two consecutive new lines with spaces', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n \n \n \n \n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n \n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) + + it('returns original string if there are no consecutive new lines', () => { + const input = new RichText('test\n\ntest\n\ntest\n\ntest\n\ntest') + const output = removeExcessNewlines(input) + expect(output.text).toEqual(input.text) + }) + + it('returns original string if there are no new lines', () => { + const input = new RichText('test test test test test') + const output = removeExcessNewlines(input) + expect(output.text).toEqual(input.text) + }) + + it('returns empty string if input is empty', () => { + const input = new RichText('') + const output = removeExcessNewlines(input) + expect(output.text).toEqual('') + }) + + it('works with different types of new line characters', () => { + const input = new RichText( + 'test\r\ntest\n\rtest\rtest\n\n\n\ntest\n\r \n \n \n \n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\r\ntest\n\rtest\rtest\n\ntest\n\ntest') + }) + + it('removes more than two consecutive new lines with zero width space', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\u200B\u200B\n\n\n\ntest\n \u200B\u200B \n\n\n\ntest\n\n\n\n\n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) + + it('removes more than two consecutive new lines with zero width non-joiner', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\u200C\u200C\n\n\n\ntest\n \u200C\u200C \n\n\n\ntest\n\n\n\n\n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) + + it('removes more than two consecutive new lines with zero width joiner', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\u200D\u200D\n\n\n\ntest\n \u200D\u200D \n\n\n\ntest\n\n\n\n\n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) + + it('removes more than two consecutive new lines with soft hyphen', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\u00AD\u00AD\n\n\n\ntest\n \u00AD\u00AD \n\n\n\ntest\n\n\n\n\n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) + + it('removes more than two consecutive new lines with word joiner', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\u2060\u2060\n\n\n\ntest\n \u2060\u2060 \n\n\n\ntest\n\n\n\n\n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) +}) + +describe('removeExcessNewlines w/entities', () => { + it('preserves entities as expected', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest', + [ + {index: {start: 0, end: 13}, type: '', value: ''}, + {index: {start: 13, end: 24}, type: '', value: ''}, + {index: {start: 9, end: 15}, type: '', value: ''}, + {index: {start: 4, end: 9}, type: '', value: ''}, + ], + ) + const output = removeExcessNewlines(input) + expect(entToStr(input.text, input.entities?.[0])).toEqual( + 'test\n\n\n\n\ntest', + ) + expect(entToStr(input.text, input.entities?.[1])).toEqual( + '\n\n\n\n\n\n\ntest', + ) + expect(entToStr(input.text, input.entities?.[2])).toEqual('test\n\n') + expect(entToStr(input.text, input.entities?.[3])).toEqual('\n\n\n\n\n') + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + expect(entToStr(output.text, output.entities?.[0])).toEqual('test\n\ntest') + expect(entToStr(output.text, output.entities?.[1])).toEqual('test') + expect(entToStr(output.text, output.entities?.[2])).toEqual('test') + expect(output.entities?.[3]).toEqual(undefined) + }) +}) + +function entToStr(str: string, ent?: Entity) { + if (!ent) { + return '' + } + return str.slice(ent.index.start, ent.index.end) +} diff --git a/__tests__/lib/strings/rich-text.ts b/__tests__/lib/strings/rich-text.ts new file mode 100644 index 00000000..e52ac6ce --- /dev/null +++ b/__tests__/lib/strings/rich-text.ts @@ -0,0 +1,123 @@ +import {RichText} from '../../../src/lib/strings/rich-text' + +describe('richText.insert', () => { + const input = new RichText('hello world', [ + {index: {start: 2, end: 7}, type: '', value: ''}, + ]) + + it('correctly adjusts entities (scenario A - before)', () => { + const output = input.clone().insert(0, 'test') + expect(output.text).toEqual('testhello world') + expect(output.entities?.[0].index.start).toEqual(6) + expect(output.entities?.[0].index.end).toEqual(11) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('llo w') + }) + + it('correctly adjusts entities (scenario B - inner)', () => { + const output = input.clone().insert(4, 'test') + expect(output.text).toEqual('helltesto world') + expect(output.entities?.[0].index.start).toEqual(2) + expect(output.entities?.[0].index.end).toEqual(11) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('lltesto w') + }) + + it('correctly adjusts entities (scenario C - after)', () => { + const output = input.clone().insert(8, 'test') + expect(output.text).toEqual('hello wotestrld') + expect(output.entities?.[0].index.start).toEqual(2) + expect(output.entities?.[0].index.end).toEqual(7) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('llo w') + }) +}) + +describe('richText.delete', () => { + const input = new RichText('hello world', [ + {index: {start: 2, end: 7}, type: '', value: ''}, + ]) + + it('correctly adjusts entities (scenario A - entirely outer)', () => { + const output = input.clone().delete(0, 9) + expect(output.text).toEqual('ld') + expect(output.entities?.length).toEqual(0) + }) + + it('correctly adjusts entities (scenario B - entirely after)', () => { + const output = input.clone().delete(7, 11) + expect(output.text).toEqual('hello w') + expect(output.entities?.[0].index.start).toEqual(2) + expect(output.entities?.[0].index.end).toEqual(7) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('llo w') + }) + + it('correctly adjusts entities (scenario C - partially after)', () => { + const output = input.clone().delete(4, 11) + expect(output.text).toEqual('hell') + expect(output.entities?.[0].index.start).toEqual(2) + expect(output.entities?.[0].index.end).toEqual(4) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('ll') + }) + + it('correctly adjusts entities (scenario D - entirely inner)', () => { + const output = input.clone().delete(3, 5) + expect(output.text).toEqual('hel world') + expect(output.entities?.[0].index.start).toEqual(2) + expect(output.entities?.[0].index.end).toEqual(5) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('l w') + }) + + it('correctly adjusts entities (scenario E - partially before)', () => { + const output = input.clone().delete(1, 5) + expect(output.text).toEqual('h world') + expect(output.entities?.[0].index.start).toEqual(1) + expect(output.entities?.[0].index.end).toEqual(3) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual(' w') + }) + + it('correctly adjusts entities (scenario F - entirely before)', () => { + const output = input.clone().delete(0, 2) + expect(output.text).toEqual('llo world') + expect(output.entities?.[0].index.start).toEqual(0) + expect(output.entities?.[0].index.end).toEqual(5) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('llo w') + }) +}) diff --git a/__tests__/state/models/link-metas-view.test.ts b/__tests__/state/models/link-metas-view.test.ts deleted file mode 100644 index 0e5fb8da..00000000 --- a/__tests__/state/models/link-metas-view.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import {RootStoreModel} from '../../../src/state/models/root-store' -import {LinkMetasViewModel} from '../../../src/state/models/link-metas-view' -import * as LinkMetaLib from '../../../src/lib/link-meta' -import {LikelyType} from './../../../src/lib/link-meta' -import {sessionClient, SessionServiceClient} from '@atproto/api' -import {DEFAULT_SERVICE} from '../../../src/state' - -describe('LinkMetasViewModel', () => { - let viewModel: LinkMetasViewModel - let rootStore: RootStoreModel - - const getLinkMetaMockSpy = jest.spyOn(LinkMetaLib, 'getLinkMeta') - const mockedMeta = { - title: 'Test Title', - url: 'testurl', - likelyType: LikelyType.Other, - } - - beforeEach(() => { - const api = sessionClient.service(DEFAULT_SERVICE) as SessionServiceClient - rootStore = new RootStoreModel(api) - viewModel = new LinkMetasViewModel(rootStore) - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - describe('getLinkMeta', () => { - it('should return link meta if it is cached', async () => { - const url = 'http://example.com' - - viewModel.cache.set(url, mockedMeta) - - const result = await viewModel.getLinkMeta(url) - - expect(getLinkMetaMockSpy).not.toHaveBeenCalled() - expect(result).toEqual(mockedMeta) - }) - - it('should return link meta if it is not cached', async () => { - getLinkMetaMockSpy.mockResolvedValueOnce(mockedMeta) - - const result = await viewModel.getLinkMeta(mockedMeta.url) - - expect(getLinkMetaMockSpy).toHaveBeenCalledWith(rootStore, mockedMeta.url) - expect(result).toEqual(mockedMeta) - }) - - it('should cache the link meta if it is successfully returned', async () => { - getLinkMetaMockSpy.mockResolvedValueOnce(mockedMeta) - - await viewModel.getLinkMeta(mockedMeta.url) - - expect(viewModel.cache.get(mockedMeta.url)).toEqual(mockedMeta) - }) - - it('should not cache the link meta if it fails to return', async () => { - const url = 'http://example.com' - const error = new Error('Failed to fetch link meta') - getLinkMetaMockSpy.mockRejectedValueOnce(error) - - try { - await viewModel.getLinkMeta(url) - fail('Error was not thrown') - } catch (e) { - expect(e).toEqual(error) - expect(viewModel.cache.get(url)).toBeUndefined() - } - }) - }) -}) diff --git a/__tests__/state/models/log.test.ts b/__tests__/state/models/log.test.ts deleted file mode 100644 index b5a6d0db..00000000 --- a/__tests__/state/models/log.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import {LogModel} from '../../../src/state/models/log' - -describe('LogModel', () => { - let logModel: LogModel - - beforeEach(() => { - logModel = new LogModel() - jest.spyOn(console, 'debug') - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it('should call a log method and add a log entry to the entries array', () => { - logModel.debug('Test log') - expect(logModel.entries.length).toEqual(1) - expect(logModel.entries[0]).toEqual({ - id: logModel.entries[0].id, - type: 'debug', - summary: 'Test log', - details: undefined, - ts: logModel.entries[0].ts, - }) - - logModel.warn('Test log') - expect(logModel.entries.length).toEqual(2) - expect(logModel.entries[1]).toEqual({ - id: logModel.entries[1].id, - type: 'warn', - summary: 'Test log', - details: undefined, - ts: logModel.entries[1].ts, - }) - - logModel.error('Test log') - expect(logModel.entries.length).toEqual(3) - expect(logModel.entries[2]).toEqual({ - id: logModel.entries[2].id, - type: 'error', - summary: 'Test log', - details: undefined, - ts: logModel.entries[2].ts, - }) - }) - - it('should call the console.debug after calling the debug method', () => { - logModel.debug('Test log') - expect(console.debug).toHaveBeenCalledWith('Test log', '') - }) - - it('should call the serialize method', () => { - logModel.debug('Test log') - expect(logModel.serialize()).toEqual({ - entries: [ - { - id: logModel.entries[0].id, - type: 'debug', - summary: 'Test log', - details: undefined, - ts: logModel.entries[0].ts, - }, - ], - }) - }) - - it('should call the hydrate method with valid properties', () => { - logModel.hydrate({ - entries: [ - { - id: '123', - type: 'debug', - summary: 'Test log', - details: undefined, - ts: 123, - }, - ], - }) - expect(logModel.entries).toEqual([ - { - id: '123', - type: 'debug', - summary: 'Test log', - details: undefined, - ts: 123, - }, - ]) - }) - - it('should call the hydrate method with invalid properties', () => { - logModel.hydrate({ - entries: [ - { - id: '123', - type: 'debug', - summary: 'Test log', - details: undefined, - ts: 123, - }, - { - summary: 'Invalid entry', - }, - ], - }) - expect(logModel.entries).toEqual([ - { - id: '123', - type: 'debug', - summary: 'Test log', - details: undefined, - ts: 123, - }, - ]) - }) - - it('should stringify the details if it is not a string', () => { - logModel.debug('Test log', {details: 'test'}) - expect(logModel.entries[0].details).toEqual('{\n "details": "test"\n}') - }) - - it('should stringify the details object if it is of a specific error', () => { - class TestError extends Error { - constructor() { - super() - this.name = 'TestError' - } - } - const error = new TestError() - logModel.error('Test error log', error) - expect(logModel.entries[0].details).toEqual('TestError') - - class XRPCInvalidResponseErrorMock { - validationError = {toString: () => 'validationError'} - lexiconNsid = 'test' - } - const xrpcInvalidResponseError = new XRPCInvalidResponseErrorMock() - logModel.error('Test error log', xrpcInvalidResponseError) - expect(logModel.entries[1].details).toEqual( - '{\n "validationError": {},\n "lexiconNsid": "test"\n}', - ) - - class XRPCErrorMock { - status = 'status' - error = 'error' - message = 'message' - } - const xrpcError = new XRPCErrorMock() - logModel.error('Test error log', xrpcError) - expect(logModel.entries[2].details).toEqual( - '{\n "status": "status",\n "error": "error",\n "message": "message"\n}', - ) - }) -}) diff --git a/__tests__/state/models/me.test.ts b/__tests__/state/models/me.test.ts deleted file mode 100644 index b69e4347..00000000 --- a/__tests__/state/models/me.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import {RootStoreModel} from '../../../src/state/models/root-store' -import {MeModel} from '../../../src/state/models/me' -import {NotificationsViewModel} from './../../../src/state/models/notifications-view' -import {sessionClient, SessionServiceClient} from '@atproto/api' -import {DEFAULT_SERVICE} from './../../../src/state/index' - -describe('MeModel', () => { - let rootStore: RootStoreModel - let meModel: MeModel - - beforeEach(() => { - const api = sessionClient.service(DEFAULT_SERVICE) as SessionServiceClient - rootStore = new RootStoreModel(api) - meModel = new MeModel(rootStore) - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it('should clear() correctly', () => { - meModel.did = '123' - meModel.handle = 'handle' - meModel.displayName = 'John Doe' - meModel.description = 'description' - meModel.avatar = 'avatar' - meModel.notificationCount = 1 - meModel.clear() - expect(meModel.did).toEqual('') - expect(meModel.handle).toEqual('') - expect(meModel.displayName).toEqual('') - expect(meModel.description).toEqual('') - expect(meModel.avatar).toEqual('') - expect(meModel.notificationCount).toEqual(0) - }) - - it('should hydrate() successfully with valid properties', () => { - meModel.hydrate({ - did: '123', - handle: 'handle', - displayName: 'John Doe', - description: 'description', - avatar: 'avatar', - }) - expect(meModel.did).toEqual('123') - expect(meModel.handle).toEqual('handle') - expect(meModel.displayName).toEqual('John Doe') - expect(meModel.description).toEqual('description') - expect(meModel.avatar).toEqual('avatar') - }) - - it('should not hydrate() with invalid properties', () => { - meModel.hydrate({ - did: '', - handle: 'handle', - displayName: 'John Doe', - description: 'description', - avatar: 'avatar', - }) - expect(meModel.did).toEqual('') - expect(meModel.handle).toEqual('') - expect(meModel.displayName).toEqual('') - expect(meModel.description).toEqual('') - expect(meModel.avatar).toEqual('') - - meModel.hydrate({ - did: '123', - displayName: 'John Doe', - description: 'description', - avatar: 'avatar', - }) - expect(meModel.did).toEqual('') - expect(meModel.handle).toEqual('') - expect(meModel.displayName).toEqual('') - expect(meModel.description).toEqual('') - expect(meModel.avatar).toEqual('') - }) - - it('should load() successfully', async () => { - jest - .spyOn(rootStore.api.app.bsky.actor, 'getProfile') - .mockImplementationOnce((): Promise => { - return Promise.resolve({ - data: { - displayName: 'John Doe', - description: 'description', - avatar: 'avatar', - }, - }) - }) - rootStore.session.data = { - did: '123', - handle: 'handle', - service: 'test service', - accessJwt: 'test token', - refreshJwt: 'test token', - } - await meModel.load() - expect(meModel.did).toEqual('123') - expect(meModel.handle).toEqual('handle') - expect(meModel.displayName).toEqual('John Doe') - expect(meModel.description).toEqual('description') - expect(meModel.avatar).toEqual('avatar') - }) - - it('should load() successfully without profile data', async () => { - jest - .spyOn(rootStore.api.app.bsky.actor, 'getProfile') - .mockImplementationOnce((): Promise => { - return Promise.resolve({ - data: null, - }) - }) - rootStore.session.data = { - did: '123', - handle: 'handle', - service: 'test service', - accessJwt: 'test token', - refreshJwt: 'test token', - } - await meModel.load() - expect(meModel.did).toEqual('123') - expect(meModel.handle).toEqual('handle') - expect(meModel.displayName).toEqual('') - expect(meModel.description).toEqual('') - expect(meModel.avatar).toEqual('') - }) - - it('should load() to nothing when no session', async () => { - rootStore.session.data = null - await meModel.load() - expect(meModel.did).toEqual('') - expect(meModel.handle).toEqual('') - expect(meModel.displayName).toEqual('') - expect(meModel.description).toEqual('') - expect(meModel.avatar).toEqual('') - expect(meModel.notificationCount).toEqual(0) - }) - - it('should serialize() key information', () => { - meModel.did = '123' - meModel.handle = 'handle' - meModel.displayName = 'John Doe' - meModel.description = 'description' - meModel.avatar = 'avatar' - - expect(meModel.serialize()).toEqual({ - did: '123', - handle: 'handle', - displayName: 'John Doe', - description: 'description', - avatar: 'avatar', - }) - }) - - it('should clearNotificationCount() successfully', () => { - meModel.clearNotificationCount() - expect(meModel.notificationCount).toBe(0) - }) - - it('should update notifs count with fetchStateUpdate()', async () => { - meModel.notifications = { - refresh: jest.fn().mockResolvedValue({}), - } as unknown as NotificationsViewModel - - jest - .spyOn(rootStore.api.app.bsky.notification, 'getCount') - .mockImplementationOnce((): Promise => { - return Promise.resolve({ - data: { - count: 1, - }, - }) - }) - - await meModel.fetchNotifications() - expect(meModel.notificationCount).toBe(1) - expect(meModel.notifications.refresh).toHaveBeenCalled() - }) -}) diff --git a/__tests__/state/models/navigation.test.ts b/__tests__/state/models/navigation.test.ts index 80f66d49..16a20206 100644 --- a/__tests__/state/models/navigation.test.ts +++ b/__tests__/state/models/navigation.test.ts @@ -1,11 +1,16 @@ +import {RootStoreModel} from './../../../src/state/models/root-store' import {NavigationModel} from './../../../src/state/models/navigation' -import * as flags from '../../../src/build-flags' +import * as flags from '../../../src/lib/build-flags' +import AtpAgent from '@atproto/api' +import {DEFAULT_SERVICE} from '../../../src/state' describe('NavigationModel', () => { let model: NavigationModel + let rootStore: RootStoreModel beforeEach(() => { - model = new NavigationModel() + rootStore = new RootStoreModel(new AtpAgent({service: DEFAULT_SERVICE})) + model = new NavigationModel(rootStore) model.setTitle('0-0', 'title') }) @@ -15,7 +20,7 @@ describe('NavigationModel', () => { it('should clear() to the correct base state', async () => { await model.clear() - expect(model.tabCount).toBe(2) + expect(model.tabCount).toBe(3) expect(model.tab).toEqual({ fixedTabPurpose: 0, history: [ @@ -64,7 +69,7 @@ describe('NavigationModel', () => { }) it('should call the tabCount getter', () => { - expect(model.tabCount).toBe(2) + expect(model.tabCount).toBe(3) }) describe('tabs not enabled', () => { @@ -87,7 +92,7 @@ describe('NavigationModel', () => { it('should not change the active tab', () => { // @ts-expect-error flags.TABS_ENABLED = false - model.setActiveTab(2) + model.setActiveTab(3) expect(model.tabIndex).toBe(0) }) @@ -95,57 +100,58 @@ describe('NavigationModel', () => { // @ts-expect-error flags.TABS_ENABLED = false model.closeTab(0) - expect(model.tabCount).toBe(2) + expect(model.tabCount).toBe(3) }) }) - describe('tabs enabled', () => { - jest.mock('../../../src/build-flags', () => ({ - TABS_ENABLED: true, - })) + // TODO restore when tabs get re-enabled + // describe('tabs enabled', () => { + // jest.mock('../../../src/build-flags', () => ({ + // TABS_ENABLED: true, + // })) - afterAll(() => { - jest.clearAllMocks() - }) + // afterAll(() => { + // jest.clearAllMocks() + // }) - it('should create new tabs', () => { - // @ts-expect-error - flags.TABS_ENABLED = true + // it('should create new tabs', () => { + // // @ts-expect-error + // flags.TABS_ENABLED = true - model.newTab('testurl', 'title') - expect(model.tab.isNewTab).toBe(true) - expect(model.tabIndex).toBe(2) - }) + // model.newTab('testurl', 'title') + // expect(model.tab.isNewTab).toBe(true) + // expect(model.tabIndex).toBe(2) + // }) - it('should change the current tab', () => { - // @ts-expect-error - flags.TABS_ENABLED = true + // it('should change the current tab', () => { + // // @ts-expect-error + // flags.TABS_ENABLED = true - model.setActiveTab(0) - expect(model.tabIndex).toBe(0) - }) + // model.setActiveTab(0) + // expect(model.tabIndex).toBe(0) + // }) - it('should close tabs', () => { - // @ts-expect-error - flags.TABS_ENABLED = true + // it('should close tabs', () => { + // // @ts-expect-error + // flags.TABS_ENABLED = true - model.closeTab(0) - expect(model.tabs).toEqual([ - { - fixedTabPurpose: 1, - history: [ - { - id: expect.anything(), - ts: expect.anything(), - url: '/notifications', - }, - ], - id: expect.anything(), - index: 0, - isNewTab: false, - }, - ]) - expect(model.tabIndex).toBe(0) - }) - }) + // model.closeTab(0) + // expect(model.tabs).toEqual([ + // { + // fixedTabPurpose: 1, + // history: [ + // { + // id: expect.anything(), + // ts: expect.anything(), + // url: '/notifications', + // }, + // ], + // id: expect.anything(), + // index: 0, + // isNewTab: false, + // }, + // ]) + // expect(model.tabIndex).toBe(0) + // }) + // }) }) diff --git a/__tests__/state/models/root-store.test.ts b/__tests__/state/models/root-store.test.ts deleted file mode 100644 index 56eec9cd..00000000 --- a/__tests__/state/models/root-store.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {RootStoreModel} from '../../../src/state/models/root-store' -import {setupState} from '../../../src/state' - -describe('rootStore', () => { - let rootStore: RootStoreModel - - beforeAll(() => { - jest.useFakeTimers() - }) - - beforeEach(async () => { - rootStore = await setupState() - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it('should call the clearAll() resets state correctly', () => { - rootStore.clearAll() - - expect(rootStore.session.data).toEqual(null) - expect(rootStore.nav.tabs).toEqual([ - { - fixedTabPurpose: 0, - history: [ - { - id: expect.anything(), - ts: expect.anything(), - url: '/', - }, - ], - id: expect.anything(), - index: 0, - isNewTab: false, - }, - { - fixedTabPurpose: 1, - history: [ - { - id: expect.anything(), - ts: expect.anything(), - url: '/notifications', - }, - ], - id: expect.anything(), - index: 0, - isNewTab: false, - }, - ]) - expect(rootStore.nav.tabIndex).toEqual(0) - expect(rootStore.me.did).toEqual('') - expect(rootStore.me.handle).toEqual('') - expect(rootStore.me.displayName).toEqual('') - expect(rootStore.me.description).toEqual('') - expect(rootStore.me.avatar).toEqual('') - expect(rootStore.me.notificationCount).toEqual(0) - }) -}) diff --git a/__tests__/state/models/shell-ui.test.ts b/__tests__/state/models/shell-ui.test.ts deleted file mode 100644 index b6ccd064..00000000 --- a/__tests__/state/models/shell-ui.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - ConfirmModal, - ImagesLightbox, - ShellUiModel, -} from './../../../src/state/models/shell-ui' - -describe('ShellUiModel', () => { - let model: ShellUiModel - - beforeEach(() => { - model = new ShellUiModel() - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it('should call the openModal & closeModal method', () => { - const m = new ConfirmModal('Test Modal', 'Look good?', () => {}) - model.openModal(m) - expect(model.isModalActive).toEqual(true) - expect(model.activeModal).toEqual(m) - - model.closeModal() - expect(model.isModalActive).toEqual(false) - expect(model.activeModal).toBeUndefined() - }) - - it('should call the openLightbox & closeLightbox method', () => { - const lt = new ImagesLightbox(['uri'], 0) - model.openLightbox(lt) - expect(model.isLightboxActive).toEqual(true) - expect(model.activeLightbox).toEqual(lt) - - model.closeLightbox() - expect(model.isLightboxActive).toEqual(false) - expect(model.activeLightbox).toBeUndefined() - }) - - it('should call the openComposer & closeComposer method', () => { - const composer = { - replyTo: { - uri: 'uri', - cid: 'cid', - text: 'text', - author: { - handle: 'handle', - displayName: 'name', - }, - }, - onPost: jest.fn(), - } - model.openComposer(composer) - expect(model.isComposerActive).toEqual(true) - expect(model.composerOpts).toEqual(composer) - - model.closeComposer() - expect(model.isComposerActive).toEqual(false) - expect(model.composerOpts).toBeUndefined() - }) -}) diff --git a/__tests__/view/com/composer/Autocomplete.test.tsx b/__tests__/view/com/composer/Autocomplete.test.tsx deleted file mode 100644 index 57539b75..00000000 --- a/__tests__/view/com/composer/Autocomplete.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react' -import {Autocomplete} from '../../../../src/view/com/composer/Autocomplete' -import {cleanup, fireEvent, render} from '../../../../jest/test-utils' - -describe('Autocomplete', () => { - const onSelectMock = jest.fn() - const mockedProps = { - active: true, - items: [ - { - handle: 'handle.test', - displayName: 'Test Display', - }, - { - handle: 'handle2.test', - displayName: 'Test Display 2', - }, - ], - onSelect: onSelectMock, - } - - afterAll(() => { - jest.clearAllMocks() - cleanup() - }) - - it('renders a button for each user', async () => { - const {findAllByTestId} = render() - const autocompleteButton = await findAllByTestId('autocompleteButton') - expect(autocompleteButton.length).toBe(2) - }) - - it('triggers onSelect by pressing the button', async () => { - const {findAllByTestId} = render() - const autocompleteButton = await findAllByTestId('autocompleteButton') - - fireEvent.press(autocompleteButton[0]) - expect(onSelectMock).toHaveBeenCalledWith('handle.test') - - fireEvent.press(autocompleteButton[1]) - expect(onSelectMock).toHaveBeenCalledWith('handle2.test') - }) -}) diff --git a/__tests__/view/com/composer/ComposePost.test.tsx b/__tests__/view/com/composer/ComposePost.test.tsx deleted file mode 100644 index 5c5a6181..00000000 --- a/__tests__/view/com/composer/ComposePost.test.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react' -import {ComposePost} from '../../../../src/view/com/composer/ComposePost' -import {cleanup, fireEvent, render, waitFor} from '../../../../jest/test-utils' -import * as apilib from '../../../../src/state/lib/api' -import { - mockedAutocompleteViewStore, - mockedRootStore, -} from '../../../../__mocks__/state-mock' -import Toast from 'react-native-root-toast' - -describe('ComposePost', () => { - const mockedProps = { - replyTo: { - uri: 'testUri', - cid: 'testCid', - text: 'testText', - author: { - handle: 'test.handle', - displayName: 'test name', - avatar: '', - }, - }, - onPost: jest.fn(), - onClose: jest.fn(), - } - - afterAll(() => { - jest.clearAllMocks() - cleanup() - }) - - it('renders post composer', async () => { - const {findByTestId} = render() - const composePostView = await findByTestId('composePostView') - expect(composePostView).toBeTruthy() - }) - - it('closes composer', async () => { - const {findByTestId} = render() - const composerCancelButton = await findByTestId('composerCancelButton') - fireEvent.press(composerCancelButton) - expect(mockedProps.onClose).toHaveBeenCalled() - }) - - it('changes text and publishes post', async () => { - const postSpy = jest.spyOn(apilib, 'post').mockResolvedValue({ - uri: '', - cid: '', - }) - const toastSpy = jest.spyOn(Toast, 'show') - - const wrapper = render() - - const composerTextInput = await wrapper.findByTestId('composerTextInput') - fireEvent.changeText(composerTextInput, 'testing publish') - - const composerPublishButton = await wrapper.findByTestId( - 'composerPublishButton', - ) - fireEvent.press(composerPublishButton) - - expect(postSpy).toHaveBeenCalledWith( - mockedRootStore, - 'testing publish', - 'testUri', - undefined, - [], - new Set(), - expect.anything(), - ) - - // Waits for request to be resolved - await waitFor(() => { - expect(mockedProps.onPost).toHaveBeenCalled() - expect(mockedProps.onClose).toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalledWith('Your reply has been published', { - animation: true, - duration: 3500, - hideOnPress: true, - position: 50, - shadow: true, - }) - }) - }) - - it('selects autocomplete item', async () => { - jest - .spyOn(React, 'useMemo') - .mockReturnValueOnce(mockedAutocompleteViewStore) - - const {findAllByTestId} = render() - const autocompleteButton = await findAllByTestId('autocompleteButton') - - fireEvent.press(autocompleteButton[0]) - expect(mockedAutocompleteViewStore.setActive).toHaveBeenCalledWith(false) - }) - - it('selects photos', async () => { - const {findByTestId, queryByTestId} = render( - , - ) - let photoCarouselPickerView = queryByTestId('photoCarouselPickerView') - expect(photoCarouselPickerView).toBeFalsy() - - const composerSelectPhotosButton = await findByTestId( - 'composerSelectPhotosButton', - ) - fireEvent.press(composerSelectPhotosButton) - - photoCarouselPickerView = await findByTestId('photoCarouselPickerView') - expect(photoCarouselPickerView).toBeTruthy() - - fireEvent.press(composerSelectPhotosButton) - - photoCarouselPickerView = queryByTestId('photoCarouselPickerView') - expect(photoCarouselPickerView).toBeFalsy() - }) -}) diff --git a/__tests__/view/com/composer/SelectedPhoto.test.tsx b/__tests__/view/com/composer/SelectedPhoto.test.tsx deleted file mode 100644 index 26059ae3..00000000 --- a/__tests__/view/com/composer/SelectedPhoto.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react' -import {SelectedPhoto} from '../../../../src/view/com/composer/SelectedPhoto' -import {cleanup, fireEvent, render} from '../../../../jest/test-utils' - -describe('SelectedPhoto', () => { - const mockedProps = { - selectedPhotos: ['mock-uri', 'mock-uri-2'], - onSelectPhotos: jest.fn(), - } - - afterAll(() => { - jest.clearAllMocks() - cleanup() - }) - - it('has no photos to render', () => { - const {queryByTestId} = render( - , - ) - const selectedPhotosView = queryByTestId('selectedPhotosView') - expect(selectedPhotosView).toBeNull() - - const selectedPhotoImage = queryByTestId('selectedPhotoImage') - expect(selectedPhotoImage).toBeNull() - }) - - it('has 1 photos to render', async () => { - const {findByTestId} = render( - , - ) - const selectedPhotosView = await findByTestId('selectedPhotosView') - expect(selectedPhotosView).toBeTruthy() - - const selectedPhotoImage = await findByTestId('selectedPhotoImage') - expect(selectedPhotoImage).toBeTruthy() - // @ts-expect-error - expect(selectedPhotoImage).toHaveStyle({width: 250}) - }) - - it('has 2 photos to render', async () => { - const {findAllByTestId} = render() - const selectedPhotoImage = await findAllByTestId('selectedPhotoImage') - expect(selectedPhotoImage[0]).toBeTruthy() - // @ts-expect-error - expect(selectedPhotoImage[0]).toHaveStyle({width: 175}) - }) - - it('has 3 photos to render', async () => { - const {findAllByTestId} = render( - , - ) - const selectedPhotoImage = await findAllByTestId('selectedPhotoImage') - expect(selectedPhotoImage[0]).toBeTruthy() - // @ts-expect-error - expect(selectedPhotoImage[0]).toHaveStyle({width: 85}) - }) - - it('removes a photo', async () => { - const {findAllByTestId} = render() - const removePhotoButton = await findAllByTestId('removePhotoButton') - fireEvent.press(removePhotoButton[0]) - expect(mockedProps.onSelectPhotos).toHaveBeenCalledWith(['mock-uri-2']) - }) -}) diff --git a/__tests__/view/com/login/CreateAccount.test.tsx b/__tests__/view/com/login/CreateAccount.test.tsx deleted file mode 100644 index 0c13db6a..00000000 --- a/__tests__/view/com/login/CreateAccount.test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react' -import {Keyboard} from 'react-native' -import {CreateAccount} from '../../../../src/view/com/login/CreateAccount' -import {cleanup, fireEvent, render} from '../../../../jest/test-utils' -import { - mockedSessionStore, - mockedShellStore, -} from '../../../../__mocks__/state-mock' - -describe('CreateAccount', () => { - const mockedProps = { - onPressBack: jest.fn(), - } - afterAll(() => { - jest.clearAllMocks() - cleanup() - }) - - it('renders form and creates new account', async () => { - const {findByTestId} = render() - - const registerEmailInput = await findByTestId('registerEmailInput') - expect(registerEmailInput).toBeTruthy() - fireEvent.changeText(registerEmailInput, 'test@email.com') - - const registerHandleInput = await findByTestId('registerHandleInput') - expect(registerHandleInput).toBeTruthy() - fireEvent.changeText(registerHandleInput, 'test.handle') - - const registerPasswordInput = await findByTestId('registerPasswordInput') - expect(registerPasswordInput).toBeTruthy() - fireEvent.changeText(registerPasswordInput, 'testpass') - - const registerIs13Input = await findByTestId('registerIs13Input') - expect(registerIs13Input).toBeTruthy() - fireEvent.press(registerIs13Input) - - const createAccountButton = await findByTestId('createAccountButton') - expect(createAccountButton).toBeTruthy() - fireEvent.press(createAccountButton) - - expect(mockedSessionStore.createAccount).toHaveBeenCalled() - }) - - it('renders and selects service', async () => { - const keyboardSpy = jest.spyOn(Keyboard, 'dismiss') - const {findByTestId} = render() - - const registerSelectServiceButton = await findByTestId( - 'registerSelectServiceButton', - ) - expect(registerSelectServiceButton).toBeTruthy() - fireEvent.press(registerSelectServiceButton) - - expect(mockedShellStore.openModal).toHaveBeenCalled() - expect(keyboardSpy).toHaveBeenCalled() - }) -}) diff --git a/__tests__/view/com/profile/ProfileHeader.test.tsx b/__tests__/view/com/profile/ProfileHeader.test.tsx deleted file mode 100644 index 6ffe1a9a..00000000 --- a/__tests__/view/com/profile/ProfileHeader.test.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react' -import {cleanup, fireEvent, render} from '../../../../jest/test-utils' -import {ProfileViewModel} from '../../../../src/state/models/profile-view' -import {ProfileHeader} from '../../../../src/view/com/profile/ProfileHeader' -import { - mockedNavigationStore, - mockedProfileStore, - mockedShellStore, -} from '../../../../__mocks__/state-mock' - -describe('ProfileHeader', () => { - const mockedProps = { - view: mockedProfileStore, - onRefreshAll: jest.fn(), - } - afterAll(() => { - jest.clearAllMocks() - cleanup() - }) - - it('renders ErrorMessage on error', async () => { - const {findByTestId} = render( - , - ) - - const profileHeaderHasError = await findByTestId('profileHeaderHasError') - expect(profileHeaderHasError).toBeTruthy() - }) - - it('presses and opens edit profile', async () => { - const {findByTestId} = render() - - const profileHeaderEditProfileButton = await findByTestId( - 'profileHeaderEditProfileButton', - ) - expect(profileHeaderEditProfileButton).toBeTruthy() - fireEvent.press(profileHeaderEditProfileButton) - - expect(mockedShellStore.openModal).toHaveBeenCalled() - }) - - it('presses and opens followers page', async () => { - const {findByTestId} = render() - - const profileHeaderFollowersButton = await findByTestId( - 'profileHeaderFollowersButton', - ) - expect(profileHeaderFollowersButton).toBeTruthy() - fireEvent.press(profileHeaderFollowersButton) - - expect(mockedNavigationStore.navigate).toHaveBeenCalledWith( - '/profile/testhandle/followers', - ) - }) - - // TODO - this will only pass if the profile has an avatar image set - // it('presses and opens avatar modal', async () => { - // const {findByTestId} = render() - - // const profileHeaderAviButton = await findByTestId('profileHeaderAviButton') - // expect(profileHeaderAviButton).toBeTruthy() - // fireEvent.press(profileHeaderAviButton) - - // expect(mockedShellStore.openLightbox).toHaveBeenCalled() - // }) - - it('presses and opens follows page', async () => { - const {findByTestId} = render() - - const profileHeaderFollowsButton = await findByTestId( - 'profileHeaderFollowsButton', - ) - expect(profileHeaderFollowsButton).toBeTruthy() - fireEvent.press(profileHeaderFollowsButton) - - expect(mockedNavigationStore.navigate).toHaveBeenCalledWith( - '/profile/testhandle/follows', - ) - }) - - it('toggles following', async () => { - const {findByTestId} = render( - , - ) - - const profileHeaderToggleFollowButton = await findByTestId( - 'profileHeaderToggleFollowButton', - ) - expect(profileHeaderToggleFollowButton).toBeTruthy() - fireEvent.press(profileHeaderToggleFollowButton) - - expect(mockedProps.view.toggleFollowing).toHaveBeenCalled() - }) -}) diff --git a/__tests__/view/lib/useAnimatedValue.test.tsx b/__tests__/view/lib/useAnimatedValue.test.tsx deleted file mode 100644 index 762dcc8f..00000000 --- a/__tests__/view/lib/useAnimatedValue.test.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import {renderHook} from '../../../jest/test-utils' -import {useAnimatedValue} from '../../../src/view/lib/hooks/useAnimatedValue' - -describe('useAnimatedValue', () => { - it('creates an Animated.Value with the initial value passed to the hook', () => { - const {result} = renderHook(() => useAnimatedValue(10)) - // @ts-expect-error - expect(result.current.__getValue()).toEqual(10) - }) - - it('returns the same Animated.Value instance on subsequent renders', () => { - const {result, rerender} = renderHook(() => useAnimatedValue(10)) - const firstValue = result.current - rerender({}) - expect(result.current).toBe(firstValue) - }) -}) diff --git a/__tests__/view/lib/useOnMainScroll.test.tsx b/__tests__/view/lib/useOnMainScroll.test.tsx deleted file mode 100644 index 6fae0378..00000000 --- a/__tests__/view/lib/useOnMainScroll.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react' -import {fireEvent, render} from '../../../jest/test-utils' -import {Home} from '../../../src/view/screens/Home' -import {mockedRootStore, mockedShellStore} from '../../../__mocks__/state-mock' - -describe('useOnMainScroll', () => { - const mockedProps = { - navIdx: '0-0', - params: {}, - visible: true, - } - - it('toggles minimalShellMode to true', () => { - jest.useFakeTimers() - const {getByTestId} = render() - - fireEvent.scroll(getByTestId('homeFeed'), { - nativeEvent: { - contentOffset: {y: 20}, - contentSize: {height: 100}, - layoutMeasurement: {height: 50}, - }, - }) - - expect(mockedRootStore.shell.setMinimalShellMode).toHaveBeenCalledWith(true) - }) - - it('toggles minimalShellMode to false', () => { - jest.useFakeTimers() - const {getByTestId} = render(, { - ...mockedRootStore, - shell: { - ...mockedShellStore, - minimalShellMode: true, - }, - }) - - fireEvent.scroll(getByTestId('homeFeed'), { - nativeEvent: { - contentOffset: {y: 0}, - contentSize: {height: 100}, - layoutMeasurement: {height: 50}, - }, - }) - expect(mockedRootStore.shell.setMinimalShellMode).toHaveBeenCalledWith( - false, - ) - }) -}) diff --git a/__tests__/view/screens/Login.test.tsx b/__tests__/view/screens/Login.test.tsx deleted file mode 100644 index e347534b..00000000 --- a/__tests__/view/screens/Login.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react' -import {Login} from '../../../src/view/screens/Login' -import {cleanup, fireEvent, render} from '../../../jest/test-utils' - -describe('Login', () => { - afterAll(() => { - jest.clearAllMocks() - cleanup() - }) - - it('renders initial screen', () => { - const {getByTestId} = render() - const signUpScreen = getByTestId('signinOrCreateAccount') - - expect(signUpScreen).toBeTruthy() - }) - - it('renders Signin screen', () => { - const {getByTestId} = render() - const signInButton = getByTestId('signInButton') - - fireEvent.press(signInButton) - - const signInScreen = getByTestId('signIn') - expect(signInScreen).toBeTruthy() - }) - - it('renders CreateAccount screen', () => { - const {getByTestId} = render() - const createAccountButton = getByTestId('createAccountButton') - - fireEvent.press(createAccountButton) - - const createAccountScreen = getByTestId('createAccount') - expect(createAccountScreen).toBeTruthy() - }) -}) diff --git a/__tests__/view/screens/NotFound.test.tsx b/__tests__/view/screens/NotFound.test.tsx deleted file mode 100644 index fd3c84b0..00000000 --- a/__tests__/view/screens/NotFound.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' -import {NotFound} from '../../../src/view/screens/NotFound' -import {cleanup, fireEvent, render} from '../../../jest/test-utils' -import {mockedNavigationStore} from '../../../__mocks__/state-mock' - -describe('NotFound', () => { - afterAll(() => { - jest.clearAllMocks() - cleanup() - }) - - it('navigates home', async () => { - const navigationSpy = jest.spyOn(mockedNavigationStore, 'navigate') - const {getByTestId} = render() - const navigateHomeButton = getByTestId('navigateHomeButton') - - fireEvent.press(navigateHomeButton) - - expect(navigationSpy).toHaveBeenCalledWith('/') - }) -}) diff --git a/__tests__/view/screens/Search.test.tsx b/__tests__/view/screens/Search.test.tsx deleted file mode 100644 index c53e80b4..00000000 --- a/__tests__/view/screens/Search.test.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react' -import {Search} from '../../../src/view/screens/Search' -import {cleanup, fireEvent, render} from '../../../jest/test-utils' - -describe('Search', () => { - jest.useFakeTimers() - const mockedProps = { - navIdx: '0-0', - params: { - name: 'test name', - }, - visible: true, - } - - afterAll(() => { - jest.clearAllMocks() - cleanup() - }) - - it('renders with query', async () => { - const {findByTestId} = render() - const searchTextInput = await findByTestId('searchTextInput') - - expect(searchTextInput).toBeTruthy() - fireEvent.changeText(searchTextInput, 'test') - - const searchScrollView = await findByTestId('searchScrollView') - expect(searchScrollView).toBeTruthy() - }) -}) diff --git a/__tests__/view/shell/mobile/Menu.test.tsx b/__tests__/view/shell/mobile/Menu.test.tsx deleted file mode 100644 index 31325904..00000000 --- a/__tests__/view/shell/mobile/Menu.test.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react' -import {Menu} from '../../../../src/view/shell/mobile/Menu' -import {cleanup, fireEvent, render} from '../../../../jest/test-utils' -import {mockedNavigationStore} from '../../../../__mocks__/state-mock' - -describe('Menu', () => { - const onCloseMock = jest.fn() - - const mockedProps = { - visible: true, - onClose: onCloseMock, - } - - afterAll(() => { - jest.clearAllMocks() - cleanup() - }) - - it('renders menu', () => { - const {getByTestId} = render() - - const menuView = getByTestId('menuView') - - expect(menuView).toBeTruthy() - }) - - it('presses profile card button', () => { - const {getByTestId} = render() - - const profileCardButton = getByTestId('profileCardButton') - fireEvent.press(profileCardButton) - - expect(onCloseMock).toHaveBeenCalled() - expect(mockedNavigationStore.switchTo).toHaveBeenCalledWith(0, true) - }) - - it('presses search button', () => { - const {getByTestId} = render() - - const searchBtn = getByTestId('searchBtn') - fireEvent.press(searchBtn) - - expect(onCloseMock).toHaveBeenCalled() - expect(mockedNavigationStore.switchTo).toHaveBeenCalledWith(0, true) - expect(mockedNavigationStore.navigate).toHaveBeenCalledWith('/search') - }) - - it("presses notifications menu item' button", () => { - const {getByTestId} = render() - - const menuItemButton = getByTestId('menuItemButton-Notifications') - fireEvent.press(menuItemButton) - - expect(onCloseMock).toHaveBeenCalled() - expect(mockedNavigationStore.switchTo).toHaveBeenCalledWith(1, true) - }) -}) diff --git a/__tests__/view/shell/mobile/TabsSelector.test.tsx b/__tests__/view/shell/mobile/TabsSelector.test.tsx deleted file mode 100644 index be6bc967..00000000 --- a/__tests__/view/shell/mobile/TabsSelector.test.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react' -import {Animated} from 'react-native' -import {TabsSelector} from '../../../../src/view/shell/mobile/TabsSelector' -import {cleanup, fireEvent, render} from '../../../../jest/test-utils' -import {mockedNavigationStore} from '../../../../__mocks__/state-mock' - -describe('TabsSelector', () => { - const onCloseMock = jest.fn() - - const mockedProps = { - active: true, - tabMenuInterp: new Animated.Value(0), - onClose: onCloseMock, - } - - afterAll(() => { - jest.clearAllMocks() - cleanup() - }) - - it('renders tabs selector', () => { - const {getByTestId} = render() - - const tabsSelectorView = getByTestId('tabsSelectorView') - - expect(tabsSelectorView).toBeTruthy() - }) - - it('renders nothing if inactive', () => { - const {getByTestId} = render( - , - ) - - const emptyView = getByTestId('emptyView') - - expect(emptyView).toBeTruthy() - }) - - // TODO - this throws currently, but the tabs selector isnt being used atm so I just disabled -prf - // it('presses share button', () => { - // const shareSpy = jest.spyOn(Share, 'share') - // const {getByTestId} = render() - - // const shareButton = getByTestId('shareButton') - // fireEvent.press(shareButton) - - // expect(onCloseMock).toHaveBeenCalled() - // expect(shareSpy).toHaveBeenCalledWith({url: 'https://bsky.app/'}) - // }) - - it('presses clone button', () => { - const {getByTestId} = render() - - const cloneButton = getByTestId('cloneButton') - fireEvent.press(cloneButton) - - expect(onCloseMock).toHaveBeenCalled() - expect(mockedNavigationStore.newTab).toHaveBeenCalled() - }) - - it('presses new tab button', () => { - const {getByTestId} = render() - - const newTabButton = getByTestId('newTabButton') - fireEvent.press(newTabButton) - - expect(onCloseMock).toHaveBeenCalled() - expect(mockedNavigationStore.newTab).toHaveBeenCalledWith('/') - }) - - it('presses change tab button', () => { - const {getAllByTestId} = render() - - const changeTabButton = getAllByTestId('changeTabButton') - fireEvent.press(changeTabButton[0]) - - expect(onCloseMock).toHaveBeenCalled() - expect(mockedNavigationStore.newTab).toHaveBeenCalledWith('/') - }) - - it('presses close tab button', () => { - const {getAllByTestId} = render() - - const closeTabButton = getAllByTestId('closeTabButton') - fireEvent.press(closeTabButton[0]) - - expect(onCloseMock).toHaveBeenCalled() - expect(mockedNavigationStore.setActiveTab).toHaveBeenCalledWith(0) - }) - - it('presses swipes to close the tab', () => { - const {getByTestId} = render() - - const tabsSwipable = getByTestId('tabsSwipable') - fireEvent(tabsSwipable, 'swipeableRightOpen') - - expect(onCloseMock).toHaveBeenCalled() - expect(mockedNavigationStore.setActiveTab).toHaveBeenCalledWith(0) - }) -}) diff --git a/android/app/build.gradle b/android/app/build.gradle index ab46f0ec..2c96dc30 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -14,9 +14,9 @@ react { // The root of your project, i.e. where "package.json" lives. Default is '..' // root = file("../") // The folder where the react-native NPM package is. Default is ../node_modules/react-native - // reactNativeDir = file("../node-modules/react-native") + // reactNativeDir = file("../node_modules/react-native") // The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen - // codegenDir = file("../node-modules/react-native-codegen") + // codegenDir = file("../node_modules/react-native-codegen") // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js // cliFile = file("../node_modules/react-native/cli.js") /* Variants */ diff --git a/babel.config.js b/babel.config.js index 16f63e6c..6ba90e98 100644 --- a/babel.config.js +++ b/babel.config.js @@ -14,6 +14,17 @@ module.exports = { verbose: false, }, ], + [ + 'module-resolver', + { + alias: { + // This needs to be mirrored in tsconfig.json + lib: './src/lib', + state: './src/state', + view: './src/view', + }, + }, + ], 'react-native-reanimated/plugin', // NOTE: this plugin MUST be last ], } diff --git a/e2e/jest.config.js b/e2e/jest.config.js new file mode 100644 index 00000000..3472f716 --- /dev/null +++ b/e2e/jest.config.js @@ -0,0 +1,12 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + rootDir: '..', + testMatch: ['/e2e/**/*.test.js'], + testTimeout: 120000, + maxWorkers: 1, + globalSetup: 'detox/runners/jest/globalSetup', + globalTeardown: 'detox/runners/jest/globalTeardown', + reporters: ['detox/runners/jest/reporter'], + testEnvironment: 'detox/runners/jest/testEnvironment', + verbose: true, +} diff --git a/e2e/tests/happyPath.test.js b/e2e/tests/happyPath.test.js new file mode 100644 index 00000000..15d0b3e3 --- /dev/null +++ b/e2e/tests/happyPath.test.js @@ -0,0 +1,77 @@ +/* eslint-env detox/detox */ + +describe('Example', () => { + async function grantAccessToUserWithValidCredentials( + username, + {takeScreenshots} = {takeScreenshots: false}, + ) { + await element(by.id('signInButton')).tap() + if (takeScreenshots) { + await device.takeScreenshot('1- opened sign-in screen') + } + await element(by.id('loginSelectServiceButton')).tap() + if (takeScreenshots) { + await device.takeScreenshot('2- opened service selector') + } + await element(by.id('localDevServerButton')).tap() + if (takeScreenshots) { + await device.takeScreenshot('3- selected local dev server') + } + await element(by.id('loginUsernameInput')).typeText(username) + await element(by.id('loginPasswordInput')).typeText('hunter2') + if (takeScreenshots) { + await device.takeScreenshot('4- entered username and password') + } + await element(by.id('loginNextButton')).tap() + } + + beforeEach(async () => { + await device.uninstallApp() + await device.installApp() + await device.launchApp({permissions: {notifications: 'YES'}}) + }) + + it('As Alice, I can login', async () => { + await expect(element(by.id('signInButton'))).toBeVisible() + await grantAccessToUserWithValidCredentials('alice', { + takeScreenshots: true, + }) + await device.takeScreenshot('5- opened home screen') + }) + + it('As Alice, I can login, and post a text', async () => { + await grantAccessToUserWithValidCredentials('alice') + await element(by.id('composeFAB')).tap() + await device.takeScreenshot('1- opened composer') + await element(by.id('composerTextInput')).typeText( + 'Greetings earthlings, I come in peace... and to run some tests.', + ) + await device.takeScreenshot('2- entered text') + await element(by.id('composerPublishButton')).tap() + await device.takeScreenshot('3- opened general section') + await expect(element(by.id('composeFAB'))).toBeVisible() + }) + + it('I can create a new account', async () => { + await element(by.id('createAccountButton')).tap() + await device.takeScreenshot('1- opened create account screen') + await element(by.id('registerSelectServiceButton')).tap() + await device.takeScreenshot('2- opened service selector') + await element(by.id('localDevServerButton')).tap() + await device.takeScreenshot('3- selected local dev server') + await element(by.id('registerEmailInput')).typeText('example@test.com') + await element(by.id('registerPasswordInput')).typeText('hunter2') + await element(by.id('registerHandleInput')).typeText('e2e-test') + await element(by.id('registerIs13Input')).tap() + await device.takeScreenshot('4- entered account details') + await element(by.id('createAccountButton')).tap() + await expect(element(by.id('onboardFeatureExplainerSkipBtn'))).toBeVisible() + await expect(element(by.id('onboardFeatureExplainerNextBtn'))).toBeVisible() + await device.takeScreenshot('5- onboard feature explainer') + await element(by.id('onboardFeatureExplainerSkipBtn')).tap() + await expect(element(by.id('onboardFollowsSkipBtn'))).toBeVisible() + await expect(element(by.id('onboardFollowsNextBtn'))).toBeVisible() + await device.takeScreenshot('6- onboard follows recommender') + await element(by.id('onboardFollowsSkipBtn')).tap() + }) +}) diff --git a/ios/Podfile b/ios/Podfile index bc4be9f1..2e80c145 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -4,6 +4,15 @@ require_relative '../node_modules/@react-native-community/cli-platform-ios/nativ platform :ios, min_ios_version_supported prepare_react_native_project! +# If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. +# because `react-native-flipper` depends on (FlipperKit,...) that will be excluded +# +# To fix this you can also exclude `react-native-flipper` using a `react-native.config.js` +# ```js +# module.exports = { +# dependencies: { +# ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), +# ``` flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled linkage = ENV['USE_FRAMEWORKS'] @@ -35,12 +44,28 @@ target 'app' do :app_path => "#{Pod::Config.instance.installation_root}/.." ) + # react-native-permissions settings + permissions_path = '../node_modules/react-native-permissions/ios' + pod 'Permission-Camera', :path => "#{permissions_path}/Camera" + pod 'Permission-PhotoLibrary', :path => "#{permissions_path}/PhotoLibrary" + target 'appTests' do inherit! :complete # Pods for testing end post_install do |installer| + # Temporary fix until CocoaPods 1.12.0 is released. + # https://github.com/CocoaPods/CocoaPods/issues/11402#issuecomment-1201464693 + installer.pods_project.targets.each do |target| + if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle" + target.build_configurations.each do |config| + config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' + end + end + end + + react_native_post_install( installer, # Set `mac_catalyst_enabled` to `true` in order to apply patches diff --git a/ios/Podfile.lock b/ios/Podfile.lock index deb15e4d..fe16f141 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,20 +3,33 @@ PODS: - BVLinearGradient (2.6.2): - React-Core - DoubleConversion (1.1.6) - - FBLazyVector (0.71.0) - - FBReactNativeSpec (0.71.0): + - FBLazyVector (0.71.1) + - FBReactNativeSpec (0.71.1): - RCT-Folly (= 2021.07.22.00) - - RCTRequired (= 0.71.0) - - RCTTypeSafety (= 0.71.0) - - React-Core (= 0.71.0) - - React-jsi (= 0.71.0) - - ReactCommon/turbomodule/core (= 0.71.0) + - RCTRequired (= 0.71.1) + - RCTTypeSafety (= 0.71.1) + - React-Core (= 0.71.1) + - React-jsi (= 0.71.1) + - ReactCommon/turbomodule/core (= 0.71.1) - fmt (6.2.1) - glog (0.3.5) - - hermes-engine (0.71.0): - - hermes-engine/Pre-built (= 0.71.0) - - hermes-engine/Pre-built (0.71.0) + - hermes-engine (0.71.1): + - hermes-engine/Pre-built (= 0.71.1) + - hermes-engine/Pre-built (0.71.1) - libevent (2.1.12) + - libwebp (1.2.4): + - libwebp/demux (= 1.2.4) + - libwebp/mux (= 1.2.4) + - libwebp/webp (= 1.2.4) + - libwebp/demux (1.2.4): + - libwebp/webp + - libwebp/mux (1.2.4): + - libwebp/demux + - libwebp/webp (1.2.4) + - Permission-Camera (3.6.1): + - RNPermissions + - Permission-PhotoLibrary (3.6.1): + - RNPermissions - RCT-Folly (2021.07.22.00): - boost - DoubleConversion @@ -34,26 +47,26 @@ PODS: - fmt (~> 6.2.1) - glog - libevent - - RCTRequired (0.71.0) - - RCTTypeSafety (0.71.0): - - FBLazyVector (= 0.71.0) - - RCTRequired (= 0.71.0) - - React-Core (= 0.71.0) - - React (0.71.0): - - React-Core (= 0.71.0) - - React-Core/DevSupport (= 0.71.0) - - React-Core/RCTWebSocket (= 0.71.0) - - React-RCTActionSheet (= 0.71.0) - - React-RCTAnimation (= 0.71.0) - - React-RCTBlob (= 0.71.0) - - React-RCTImage (= 0.71.0) - - React-RCTLinking (= 0.71.0) - - React-RCTNetwork (= 0.71.0) - - React-RCTSettings (= 0.71.0) - - React-RCTText (= 0.71.0) - - React-RCTVibration (= 0.71.0) - - React-callinvoker (0.71.0) - - React-Codegen (0.71.0): + - RCTRequired (0.71.1) + - RCTTypeSafety (0.71.1): + - FBLazyVector (= 0.71.1) + - RCTRequired (= 0.71.1) + - React-Core (= 0.71.1) + - React (0.71.1): + - React-Core (= 0.71.1) + - React-Core/DevSupport (= 0.71.1) + - React-Core/RCTWebSocket (= 0.71.1) + - React-RCTActionSheet (= 0.71.1) + - React-RCTAnimation (= 0.71.1) + - React-RCTBlob (= 0.71.1) + - React-RCTImage (= 0.71.1) + - React-RCTLinking (= 0.71.1) + - React-RCTNetwork (= 0.71.1) + - React-RCTSettings (= 0.71.1) + - React-RCTText (= 0.71.1) + - React-RCTVibration (= 0.71.1) + - React-callinvoker (0.71.1) + - React-Codegen (0.71.1): - FBReactNativeSpec - hermes-engine - RCT-Folly @@ -64,190 +77,190 @@ PODS: - React-jsiexecutor - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - React-Core (0.71.0): + - React-Core (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - - React-Core/Default (= 0.71.0) - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-Core/Default (= 0.71.1) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-Core/CoreModulesHeaders (0.71.0): + - React-Core/CoreModulesHeaders (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-Core/Default (0.71.0): + - React-Core/Default (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-Core/DevSupport (0.71.0): + - React-Core/DevSupport (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - - React-Core/Default (= 0.71.0) - - React-Core/RCTWebSocket (= 0.71.0) - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-jsinspector (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-Core/Default (= 0.71.1) + - React-Core/RCTWebSocket (= 0.71.1) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-jsinspector (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-Core/RCTActionSheetHeaders (0.71.0): + - React-Core/RCTActionSheetHeaders (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-Core/RCTAnimationHeaders (0.71.0): + - React-Core/RCTAnimationHeaders (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-Core/RCTBlobHeaders (0.71.0): + - React-Core/RCTBlobHeaders (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-Core/RCTImageHeaders (0.71.0): + - React-Core/RCTImageHeaders (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-Core/RCTLinkingHeaders (0.71.0): + - React-Core/RCTLinkingHeaders (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-Core/RCTNetworkHeaders (0.71.0): + - React-Core/RCTNetworkHeaders (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-Core/RCTSettingsHeaders (0.71.0): + - React-Core/RCTSettingsHeaders (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-Core/RCTTextHeaders (0.71.0): + - React-Core/RCTTextHeaders (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-Core/RCTVibrationHeaders (0.71.0): + - React-Core/RCTVibrationHeaders (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-Core/RCTWebSocket (0.71.0): + - React-Core/RCTWebSocket (0.71.1): - glog - RCT-Folly (= 2021.07.22.00) - - React-Core/Default (= 0.71.0) - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-Core/Default (= 0.71.1) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-perflogger (= 0.71.1) - Yoga - - React-CoreModules (0.71.0): + - React-CoreModules (0.71.1): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.71.0) - - React-Codegen (= 0.71.0) - - React-Core/CoreModulesHeaders (= 0.71.0) - - React-jsi (= 0.71.0) - - React-RCTImage (= 0.71.0) - - ReactCommon/turbomodule/core (= 0.71.0) - - React-cxxreact (0.71.0): + - RCTTypeSafety (= 0.71.1) + - React-Codegen (= 0.71.1) + - React-Core/CoreModulesHeaders (= 0.71.1) + - React-jsi (= 0.71.1) + - React-RCTImage (= 0.71.1) + - ReactCommon/turbomodule/core (= 0.71.1) + - React-cxxreact (0.71.1): - boost (= 1.76.0) - DoubleConversion - glog - RCT-Folly (= 2021.07.22.00) - - React-callinvoker (= 0.71.0) - - React-jsi (= 0.71.0) - - React-jsinspector (= 0.71.0) - - React-logger (= 0.71.0) - - React-perflogger (= 0.71.0) - - React-runtimeexecutor (= 0.71.0) - - React-hermes (0.71.0): + - React-callinvoker (= 0.71.1) + - React-jsi (= 0.71.1) + - React-jsinspector (= 0.71.1) + - React-logger (= 0.71.1) + - React-perflogger (= 0.71.1) + - React-runtimeexecutor (= 0.71.1) + - React-hermes (0.71.1): - DoubleConversion - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - RCT-Folly/Futures (= 2021.07.22.00) - - React-cxxreact (= 0.71.0) - - React-jsiexecutor (= 0.71.0) - - React-jsinspector (= 0.71.0) - - React-perflogger (= 0.71.0) - - React-jsi (0.71.0): + - React-cxxreact (= 0.71.1) + - React-jsiexecutor (= 0.71.1) + - React-jsinspector (= 0.71.1) + - React-perflogger (= 0.71.1) + - React-jsi (0.71.1): - boost (= 1.76.0) - DoubleConversion - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - - React-jsiexecutor (0.71.0): + - React-jsiexecutor (0.71.1): - DoubleConversion - glog - RCT-Folly (= 2021.07.22.00) - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-perflogger (= 0.71.0) - - React-jsinspector (0.71.0) - - React-logger (0.71.0): + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-perflogger (= 0.71.1) + - React-jsinspector (0.71.1) + - React-logger (0.71.1): - glog - react-native-blur (4.3.0): - React-Core - - react-native-cameraroll (5.2.2): + - react-native-cameraroll (5.2.4): - React-Core - - react-native-image-resizer (3.0.4): + - react-native-image-resizer (3.0.5): - React-Core - - react-native-pager-view (6.1.2): + - react-native-pager-view (6.1.4): - React-Core - - react-native-paste-input (0.6.0): + - react-native-paste-input (0.6.2): - React-Core - Swime (= 3.0.6) - - react-native-safe-area-context (4.4.1): + - react-native-safe-area-context (4.5.0): - RCT-Folly - RCTRequired - RCTTypeSafety @@ -257,87 +270,89 @@ PODS: - React-Core - react-native-version-number (0.3.6): - React - - React-perflogger (0.71.0) - - React-RCTActionSheet (0.71.0): - - React-Core/RCTActionSheetHeaders (= 0.71.0) - - React-RCTAnimation (0.71.0): + - react-native-webview (11.26.1): + - React-Core + - React-perflogger (0.71.1) + - React-RCTActionSheet (0.71.1): + - React-Core/RCTActionSheetHeaders (= 0.71.1) + - React-RCTAnimation (0.71.1): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.71.0) - - React-Codegen (= 0.71.0) - - React-Core/RCTAnimationHeaders (= 0.71.0) - - React-jsi (= 0.71.0) - - ReactCommon/turbomodule/core (= 0.71.0) - - React-RCTAppDelegate (0.71.0): + - RCTTypeSafety (= 0.71.1) + - React-Codegen (= 0.71.1) + - React-Core/RCTAnimationHeaders (= 0.71.1) + - React-jsi (= 0.71.1) + - ReactCommon/turbomodule/core (= 0.71.1) + - React-RCTAppDelegate (0.71.1): - RCT-Folly - RCTRequired - RCTTypeSafety - React-Core - ReactCommon/turbomodule/core - - React-RCTBlob (0.71.0): + - React-RCTBlob (0.71.1): - RCT-Folly (= 2021.07.22.00) - - React-Codegen (= 0.71.0) - - React-Core/RCTBlobHeaders (= 0.71.0) - - React-Core/RCTWebSocket (= 0.71.0) - - React-jsi (= 0.71.0) - - React-RCTNetwork (= 0.71.0) - - ReactCommon/turbomodule/core (= 0.71.0) - - React-RCTImage (0.71.0): + - React-Codegen (= 0.71.1) + - React-Core/RCTBlobHeaders (= 0.71.1) + - React-Core/RCTWebSocket (= 0.71.1) + - React-jsi (= 0.71.1) + - React-RCTNetwork (= 0.71.1) + - ReactCommon/turbomodule/core (= 0.71.1) + - React-RCTImage (0.71.1): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.71.0) - - React-Codegen (= 0.71.0) - - React-Core/RCTImageHeaders (= 0.71.0) - - React-jsi (= 0.71.0) - - React-RCTNetwork (= 0.71.0) - - ReactCommon/turbomodule/core (= 0.71.0) - - React-RCTLinking (0.71.0): - - React-Codegen (= 0.71.0) - - React-Core/RCTLinkingHeaders (= 0.71.0) - - React-jsi (= 0.71.0) - - ReactCommon/turbomodule/core (= 0.71.0) - - React-RCTNetwork (0.71.0): + - RCTTypeSafety (= 0.71.1) + - React-Codegen (= 0.71.1) + - React-Core/RCTImageHeaders (= 0.71.1) + - React-jsi (= 0.71.1) + - React-RCTNetwork (= 0.71.1) + - ReactCommon/turbomodule/core (= 0.71.1) + - React-RCTLinking (0.71.1): + - React-Codegen (= 0.71.1) + - React-Core/RCTLinkingHeaders (= 0.71.1) + - React-jsi (= 0.71.1) + - ReactCommon/turbomodule/core (= 0.71.1) + - React-RCTNetwork (0.71.1): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.71.0) - - React-Codegen (= 0.71.0) - - React-Core/RCTNetworkHeaders (= 0.71.0) - - React-jsi (= 0.71.0) - - ReactCommon/turbomodule/core (= 0.71.0) - - React-RCTSettings (0.71.0): + - RCTTypeSafety (= 0.71.1) + - React-Codegen (= 0.71.1) + - React-Core/RCTNetworkHeaders (= 0.71.1) + - React-jsi (= 0.71.1) + - ReactCommon/turbomodule/core (= 0.71.1) + - React-RCTSettings (0.71.1): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.71.0) - - React-Codegen (= 0.71.0) - - React-Core/RCTSettingsHeaders (= 0.71.0) - - React-jsi (= 0.71.0) - - ReactCommon/turbomodule/core (= 0.71.0) - - React-RCTText (0.71.0): - - React-Core/RCTTextHeaders (= 0.71.0) - - React-RCTVibration (0.71.0): + - RCTTypeSafety (= 0.71.1) + - React-Codegen (= 0.71.1) + - React-Core/RCTSettingsHeaders (= 0.71.1) + - React-jsi (= 0.71.1) + - ReactCommon/turbomodule/core (= 0.71.1) + - React-RCTText (0.71.1): + - React-Core/RCTTextHeaders (= 0.71.1) + - React-RCTVibration (0.71.1): - RCT-Folly (= 2021.07.22.00) - - React-Codegen (= 0.71.0) - - React-Core/RCTVibrationHeaders (= 0.71.0) - - React-jsi (= 0.71.0) - - ReactCommon/turbomodule/core (= 0.71.0) - - React-runtimeexecutor (0.71.0): - - React-jsi (= 0.71.0) - - ReactCommon/turbomodule/bridging (0.71.0): + - React-Codegen (= 0.71.1) + - React-Core/RCTVibrationHeaders (= 0.71.1) + - React-jsi (= 0.71.1) + - ReactCommon/turbomodule/core (= 0.71.1) + - React-runtimeexecutor (0.71.1): + - React-jsi (= 0.71.1) + - ReactCommon/turbomodule/bridging (0.71.1): - DoubleConversion - glog - RCT-Folly (= 2021.07.22.00) - - React-callinvoker (= 0.71.0) - - React-Core (= 0.71.0) - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-logger (= 0.71.0) - - React-perflogger (= 0.71.0) - - ReactCommon/turbomodule/core (0.71.0): + - React-callinvoker (= 0.71.1) + - React-Core (= 0.71.1) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-logger (= 0.71.1) + - React-perflogger (= 0.71.1) + - ReactCommon/turbomodule/core (0.71.1): - DoubleConversion - glog - RCT-Folly (= 2021.07.22.00) - - React-callinvoker (= 0.71.0) - - React-Core (= 0.71.0) - - React-cxxreact (= 0.71.0) - - React-jsi (= 0.71.0) - - React-logger (= 0.71.0) - - React-perflogger (= 0.71.0) + - React-callinvoker (= 0.71.1) + - React-Core (= 0.71.1) + - React-cxxreact (= 0.71.1) + - React-jsi (= 0.71.1) + - React-logger (= 0.71.1) + - React-perflogger (= 0.71.1) - rn-fetch-blob (0.12.0): - React-Core - RNBackgroundFetch (4.1.8): @@ -346,9 +361,13 @@ PODS: - React-Core - RNCClipboard (1.11.1): - React-Core + - RNFastImage (8.6.3): + - React-Core + - SDWebImage (~> 5.11.1) + - SDWebImageWebPCoder (~> 0.8.4) - RNFS (2.20.0): - React-Core - - RNGestureHandler (2.8.0): + - RNGestureHandler (2.9.0): - React-Core - RNImageCropPicker (0.38.1): - React-Core @@ -361,14 +380,16 @@ PODS: - TOCropViewController - RNInAppBrowser (3.7.0): - React-Core - - RNNotifee (7.4.0): + - RNNotifee (7.5.0): - React-Core - - RNNotifee/NotifeeCore (= 7.4.0) - - RNNotifee/NotifeeCore (7.4.0): + - RNNotifee/NotifeeCore (= 7.5.0) + - RNNotifee/NotifeeCore (7.5.0): + - React-Core + - RNPermissions (3.6.1): - React-Core - RNReactNativeHapticFeedback (1.14.0): - React-Core - - RNReanimated (2.13.0): + - RNReanimated (2.14.4): - DoubleConversion - FBLazyVector - FBReactNativeSpec @@ -395,12 +416,18 @@ PODS: - React-RCTText - ReactCommon/turbomodule/core - Yoga - - RNScreens (3.18.2): + - RNScreens (3.20.0): - React-Core - React-RCTImage - - RNSVG (12.5.0): + - RNSVG (12.5.1): - React-Core - - segment-analytics-react-native (2.10.1): + - SDWebImage (5.11.1): + - SDWebImage/Core (= 5.11.1) + - SDWebImage/Core (5.11.1) + - SDWebImageWebPCoder (0.8.5): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.10) + - segment-analytics-react-native (2.13.0): - React-Core - sovran-react-native - sovran-react-native (0.4.5): @@ -418,6 +445,8 @@ DEPENDENCIES: - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - libevent (~> 2.1.12) + - Permission-Camera (from `../node_modules/react-native-permissions/ios/Camera`) + - Permission-PhotoLibrary (from `../node_modules/react-native-permissions/ios/PhotoLibrary`) - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) @@ -441,6 +470,7 @@ DEPENDENCIES: - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-splash-screen (from `../node_modules/react-native-splash-screen`) - react-native-version-number (from `../node_modules/react-native-version-number`) + - react-native-webview (from `../node_modules/react-native-webview`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) @@ -458,11 +488,13 @@ DEPENDENCIES: - RNBackgroundFetch (from `../node_modules/react-native-background-fetch`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" + - RNFastImage (from `../node_modules/react-native-fast-image`) - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNInAppBrowser (from `../node_modules/react-native-inappbrowser-reborn`) - "RNNotifee (from `../node_modules/@notifee/react-native`)" + - RNPermissions (from `../node_modules/react-native-permissions`) - RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) @@ -475,6 +507,9 @@ SPEC REPOS: trunk: - fmt - libevent + - libwebp + - SDWebImage + - SDWebImageWebPCoder - Swime - TOCropViewController @@ -493,6 +528,10 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" hermes-engine: :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + Permission-Camera: + :path: "../node_modules/react-native-permissions/ios/Camera" + Permission-PhotoLibrary: + :path: "../node_modules/react-native-permissions/ios/PhotoLibrary" RCT-Folly: :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTRequired: @@ -537,6 +576,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-splash-screen" react-native-version-number: :path: "../node_modules/react-native-version-number" + react-native-webview: + :path: "../node_modules/react-native-webview" React-perflogger: :path: "../node_modules/react-native/ReactCommon/reactperflogger" React-RCTActionSheet: @@ -571,6 +612,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-async-storage/async-storage" RNCClipboard: :path: "../node_modules/@react-native-clipboard/clipboard" + RNFastImage: + :path: "../node_modules/react-native-fast-image" RNFS: :path: "../node_modules/react-native-fs" RNGestureHandler: @@ -581,6 +624,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-inappbrowser-reborn" RNNotifee: :path: "../node_modules/@notifee/react-native" + RNPermissions: + :path: "../node_modules/react-native-permissions" RNReactNativeHapticFeedback: :path: "../node_modules/react-native-haptic-feedback" RNReanimated: @@ -597,69 +642,77 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - boost: a7c83b31436843459a1961bfd74b96033dc77234 + boost: 57d2868c099736d80fcd648bf211b4431e51a558 BVLinearGradient: 34a999fda29036898a09c6a6b728b0b4189e1a44 - DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662 - FBLazyVector: 61839cba7a48c570b7ac3e1cd8a4d0948382202f - FBReactNativeSpec: 5a14398ccf5e27c1ca2d7109eb920594ce93c10d + DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 + FBLazyVector: ad72713385db5289b19f1ead07e8e4aa26dcb01d + FBReactNativeSpec: df2602c11e33d310433496e28a48b4b2be652a61 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: 476ee3e89abb49e07f822b48323c51c57124b572 - hermes-engine: f6e715aa6c8bd38de6c13bc85e07b0a337edaa89 + glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b + hermes-engine: 922ccd744f50d9bfde09e9677bf0f3b562ea5fb9 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 + libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef + Permission-Camera: bf6791b17c7f614b6826019fcfdcc286d3a107f6 + Permission-PhotoLibrary: 5b34ca67279f7201ae109cef36f9806a6596002d RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 - RCTRequired: dea3e4163184ea57c50288c15c32c1529265c58f - RCTTypeSafety: a0834ab89159a346731e8aae55ad6e2cce61c327 - React: d877d055ff2137ca0325a4babdef3411e11f3cb7 - React-callinvoker: 77bd2701eee3acac154b11ec219e68d5a1f780ad - React-Codegen: bccc516adc1551ccfe04b0de27e345d38829b204 - React-Core: 4035f59e5bec8f3053583c6108d99c7516deb760 - React-CoreModules: b6a1f76423fea57a03e0d7a2f79d3b55cf193f2c - React-cxxreact: fe5f6ec8ae875bebc71309d1e8ef89bb966d61a6 - React-hermes: 3c8ea5e8f402db2a08b57051206d7f2ba9c75565 - React-jsi: dbf0f82c93bfd828fa05c50f2ee74dc81f711050 - React-jsiexecutor: 060dd495f1e2af3d87216f7ca8a94c55ec885b4f - React-jsinspector: 5061fcbec93fd672183dfb39cc2f65e55a0835db - React-logger: a6c0b3a807a8e81f6d7fea2e72660766f55daa50 + RCTRequired: fd4d923b964658aa0c4091a32c8b2004c6d9e3a6 + RCTTypeSafety: c276d85975bde3d8448907235c70bf0da257adfd + React: e481a67971af1ce9639c9f746b753dd0e84ca108 + React-callinvoker: 1051c04a94fa9d243786b86380606bad701a3b31 + React-Codegen: 14b1e716d361d5ad95e0ce1a338f3fa0733a98b5 + React-Core: 698fc3baecb80d511d987475a16d036cec6d287f + React-CoreModules: 59245305f41ff0adfeac334acc0594dea4585a7c + React-cxxreact: 49accd2954b0f532805dbcd1918fa6962f32f247 + React-hermes: d068733294581a085e95b6024e8d951b005e26d3 + React-jsi: 122b9bce14f4c6c7cb58f28f87912cfe091885fa + React-jsiexecutor: 60cf272aababc5212410e4249d17cea14fc36caa + React-jsinspector: ff56004b0c974b688a6548c156d5830ad751ae07 + React-logger: 60a0b5f8bed667ecf9e24fecca1f30d125de6d75 react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3 - react-native-cameraroll: 71d68167beb6fc7216aa564abb6d86f1d666a2c6 - react-native-image-resizer: 794abf75ec13ed1f0dbb1f134e27504ea65e9e66 - react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43 - react-native-paste-input: 5182843692fd2ec72be50f241a38a49796e225d7 - react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a + react-native-cameraroll: cb752fda6d5268f1646b4390bd5be1f27706b9a0 + react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa + react-native-pager-view: b58cb9e9f42f64e50cab3040815772c1d119a2e2 + react-native-paste-input: 3392800944a47c00dddbff23c31c281482209679 + react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457 react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f - React-perflogger: e5fc4149e9bbb972b8520277f3b23141faa47a36 - React-RCTActionSheet: 991de88216bf03ab9bb1d213d73c62ecbe64ade7 - React-RCTAnimation: b74e3d1bf5280891a573e447b487fa1db0713b5b - React-RCTAppDelegate: f52667f2dbc510f87b7988c5204e8764d50bf0c1 - React-RCTBlob: 6762787c01d5d8d18efed03764b0d58d3b79595a - React-RCTImage: 9ed7eba8dd192a49def2cad2ecaedee7e7e315b4 - React-RCTLinking: 0b58eed9af0645a161b80bf412b6b721e4585c66 - React-RCTNetwork: dc075b0eea00d8a98c928f011d9bc2458acc7092 - React-RCTSettings: 30fb3f498cfaf8a4bb47334ff9ffbe318ef78766 - React-RCTText: a631564e84a227fe24bae7c04446f36faea7fcf5 - React-RCTVibration: 55c91eccdbd435d7634efbe847086944389475b0 - React-runtimeexecutor: ac80782d9d76ba2b0f709f4de0c427fe33c352dc - ReactCommon: 20e38a9be5fe1341b5e422220877cc94034776ba + react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1 + React-perflogger: ec8eef2a8f03ecfa6361c2c5fb9197ef4a29cc85 + React-RCTActionSheet: a0c023b86cf4c862fa9c4eb0f6f91fbe878fb2de + React-RCTAnimation: 168d53718c74153947c0109f55900faa64d79439 + React-RCTAppDelegate: a8efbab128b34aa07a9491c85a41401210b1bec5 + React-RCTBlob: 9bcbfc893bfda9f6b2eb016329d38c0f6366d31a + React-RCTImage: 3fcd4570b4b0f1ac2f4b4b6308dba33ce66c5b50 + React-RCTLinking: 1edb8e1bb3fc39bf9e13c63d6aaaa3f0c3d18683 + React-RCTNetwork: 500a79e0e0f67678077df727fabba87a55c043e1 + React-RCTSettings: cc4414eb84ad756d619076c3999fecbf12896d6f + React-RCTText: 2a34261f3da6e34f47a62154def657546ebfa5e1 + React-RCTVibration: 49d531ec8498e0afa2c9b22c2205784372e3d4f3 + React-runtimeexecutor: 311feb67600774723fe10eb8801d3138cae9ad67 + ReactCommon: 03be76588338a27a88d103b35c3c44a3fd43d136 rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba RNBackgroundFetch: 8e16176ff415daac743a6eb57afc8e9e14dbe623 RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60 RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd + RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 - RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3 + RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39 RNImageCropPicker: 648356d68fbf9911a1016b3e3723885d28373eda RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364 - RNNotifee: da8dcf09f079ea22f46e239d7c406e10d4525a5f + RNNotifee: 053c0ace9c73634709a0214fd9c436a5777a562f + RNPermissions: dcdb7b99796bbeda6975a6e79ad519c41b251b1c RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c - RNReanimated: d8d9d3d3801bda5e35e85cdffc871577d044dc2e - RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d - RNSVG: 6adc5c52d2488a476248413064b7f2832e639057 - segment-analytics-react-native: cb097e393c3560a0d4cfd877044293e37b0050d9 + RNReanimated: cc5e3aa479cb9170bcccf8204291a6950a3be128 + RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f + RNSVG: d7d7bc8229af3842c9cfc3a723c815a52cdd1105 + SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d + SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d + segment-analytics-react-native: bd1f13ea95bad2313a9c7130da032af0e9a6da60 sovran-react-native: fd3dc8f1a4b14acdc4ad25fc6b4ac4f52a2a2a15 Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 - Yoga: c618b544ff8bd8865cdca602f00cbcdb92fd6d31 + Yoga: 921eb014669cf9c718ada68b08d362517d564e0c -PODFILE CHECKSUM: 0975a639c66f07f4d49706dd0bf7c3aa4dc833cf +PODFILE CHECKSUM: 95c7fde1130d862b561348cca2b3fb7f9bd84bfb COCOAPODS: 1.11.3 diff --git a/ios/app/AppDelegate.mm b/ios/app/AppDelegate.mm index 460c319c..09aad895 100644 --- a/ios/app/AppDelegate.mm +++ b/ios/app/AppDelegate.mm @@ -21,6 +21,7 @@ [[TSBackgroundFetch sharedInstance] didFinishLaunching]; self.moduleName = @"xyz.blueskyweb.app"; + self.initialProps = @{}; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } diff --git a/ios/app/Info.plist b/ios/app/Info.plist index dadcb12f..1dfe081d 100644 --- a/ios/app/Info.plist +++ b/ios/app/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + 1.2 CFBundleSignature ???? CFBundleURLTypes @@ -39,6 +39,8 @@ CFBundleVersion 1 + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/jest/jestSetup.js b/jest/jestSetup.js index 35bb1772..f7d136a8 100644 --- a/jest/jestSetup.js +++ b/jest/jestSetup.js @@ -1,10 +1,19 @@ /* global jest */ - +import {configure} from '@testing-library/react-native' import 'react-native-gesture-handler/jestSetup' + +configure({asyncUtilTimeout: 20000}) + jest.mock('@react-native-async-storage/async-storage', () => require('@react-native-async-storage/async-storage/jest/async-storage-mock'), ) -jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter') +jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => { + const {EventEmitter} = require('events') + return { + __esModule: true, + default: EventEmitter, + } +}) // Silence the warning: Animated: `useNativeDriver` is not supported jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper') @@ -55,3 +64,7 @@ jest.mock('@segment/analytics-react-native', () => ({ flush: jest.fn(), }), })) + +jest.mock('react-native-permissions', () => + require('react-native-permissions/mock'), +) diff --git a/jest/test-pds.ts b/jest/test-pds.ts index 31dcb3cb..32f3bc9b 100644 --- a/jest/test-pds.ts +++ b/jest/test-pds.ts @@ -8,7 +8,7 @@ import PDSServer, { ServerConfig as PDSServerConfig, } from '@atproto/pds' import * as plc from '@atproto/plc' -import AtpApi, {ServiceClient} from '@atproto/api' +import AtpAgent from '@atproto/api' export interface TestUser { email: string @@ -16,7 +16,7 @@ export interface TestUser { declarationCid: string handle: string password: string - api: ServiceClient + agent: AtpAgent } export interface TestUsers { @@ -87,6 +87,8 @@ export async function createServer(): Promise { dbPostgresUrl: process.env.DB_POSTGRES_URL, blobstoreLocation: `${blobstoreLoc}/blobs`, blobstoreTmp: `${blobstoreLoc}/tmp`, + maxSubscriptionBuffer: 200, + repoBackfillLimitMs: 1e3 * 60 * 60, }) const db = PDSDatabase.memory() @@ -112,11 +114,11 @@ export async function createServer(): Promise { async function genMockData(pdsUrl: string): Promise { const date = dateGen() - const clients = { - loggedout: AtpApi.service(pdsUrl), - alice: AtpApi.service(pdsUrl), - bob: AtpApi.service(pdsUrl), - carla: AtpApi.service(pdsUrl), + const agents = { + loggedout: new AtpAgent({service: pdsUrl}), + alice: new AtpAgent({service: pdsUrl}), + bob: new AtpAgent({service: pdsUrl}), + carla: new AtpAgent({service: pdsUrl}), } const users: TestUser[] = [ { @@ -125,7 +127,7 @@ async function genMockData(pdsUrl: string): Promise { declarationCid: '', handle: 'alice.test', password: 'hunter2', - api: clients.alice, + agent: agents.alice, }, { email: 'bob@test.com', @@ -133,7 +135,7 @@ async function genMockData(pdsUrl: string): Promise { declarationCid: '', handle: 'bob.test', password: 'hunter2', - api: clients.bob, + agent: agents.bob, }, { email: 'carla@test.com', @@ -141,7 +143,7 @@ async function genMockData(pdsUrl: string): Promise { declarationCid: '', handle: 'carla.test', password: 'hunter2', - api: clients.carla, + agent: agents.carla, }, ] const alice = users[0] @@ -150,18 +152,18 @@ async function genMockData(pdsUrl: string): Promise { let _i = 1 for (const user of users) { - const res = await clients.loggedout.com.atproto.account.create({ + const res = await agents.loggedout.api.com.atproto.account.create({ email: user.email, handle: user.handle, password: user.password, }) - user.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`) - const {data: profile} = await user.api.app.bsky.actor.getProfile({ + user.agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`) + const {data: profile} = await user.agent.api.app.bsky.actor.getProfile({ actor: user.handle, }) user.did = res.data.did user.declarationCid = profile.declaration.cid - await user.api.app.bsky.actor.profile.create( + await user.agent.api.app.bsky.actor.profile.create( {did: user.did}, { displayName: ucfirst(user.handle).slice(0, -5), @@ -172,7 +174,7 @@ async function genMockData(pdsUrl: string): Promise { // everybody follows everybody const follow = async (author: TestUser, subject: TestUser) => { - await author.api.app.bsky.graph.follow.create( + await author.agent.api.app.bsky.graph.follow.create( {did: author.did}, { subject: { diff --git a/jest/test-utils.tsx b/jest/test-utils.tsx index 5a74a6ef..0a22d792 100644 --- a/jest/test-utils.tsx +++ b/jest/test-utils.tsx @@ -3,17 +3,15 @@ import {render} from '@testing-library/react-native' import {GestureHandlerRootView} from 'react-native-gesture-handler' import {RootSiblingParent} from 'react-native-root-siblings' import {SafeAreaProvider} from 'react-native-safe-area-context' -import {RootStoreProvider} from '../src/state' -import {ThemeProvider} from '../src/view/lib/ThemeContext' -import {mockedRootStore} from '../__mocks__/state-mock' +import {RootStoreProvider, RootStoreModel} from '../src/state' +import {ThemeProvider} from '../src/lib/ThemeContext' -const customRender = (ui: any, rootStore?: any) => +const customRender = (ui: any, rootStore: RootStoreModel) => render( // eslint-disable-next-line react-native/no-inline-styles - + {ui} diff --git a/package.json b/package.json index 6e55b39e..6aa3add9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "0.0.1", + "version": "1.2.0", "private": true, "scripts": { "android": "react-native run-android", @@ -8,16 +8,17 @@ "web": "webpack-dev-server --config ./web/webpack.config.js -d inline-source-map --hot --color", "start": "react-native start", "clean-cache": "rm -rf node_modules/.cache/babel-loader/*", - "test": "jest --forceExit", + "test": "jest --forceExit --testTimeout=20000 --bail", "test-watch": "jest --watchAll", "test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit", "test-coverage": "jest --coverage", - "lint": "eslint . --ext .js,.jsx,.ts,.tsx" + "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "e2e": "detox test --configuration ios.sim.debug --take-screenshots all" }, "dependencies": { - "@atproto/api": "^0.0.6", + "@atproto/api": "^0.1.2", "@atproto/lexicon": "^0.0.4", - "@atproto/xrpc": "^0.0.3", + "@atproto/xrpc": "^0.0.4", "@bam.tech/react-native-image-resizer": "^3.0.4", "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.1", @@ -34,21 +35,27 @@ "@segment/analytics-react-native": "^2.10.1", "@segment/sovran-react-native": "^0.4.5", "@zxing/text-encoding": "^0.9.0", + "await-lock": "^2.2.2", "base64-js": "^1.5.1", "email-validator": "^2.0.4", "he": "^1.2.0", "lodash.chunk": "^4.2.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0", "lodash.omit": "^4.5.0", + "lodash.shuffle": "^4.2.0", "lru_map": "^0.4.1", "mobx": "^6.6.1", "mobx-react-lite": "^3.4.0", + "normalize-url": "^8.0.0", "react": "18.2.0", "react-avatar-editor": "^13.0.0", "react-circular-progressbar": "^2.1.0", "react-dom": "^18.2.0", - "react-native": "0.71.0", + "react-native": "0.71.1", "react-native-appstate-hook": "^1.0.6", "react-native-background-fetch": "^4.1.8", + "react-native-fast-image": "^8.6.3", "react-native-fs": "^2.20.0", "react-native-gesture-handler": "^2.5.0", "react-native-haptic-feedback": "^1.14.0", @@ -56,6 +63,7 @@ "react-native-inappbrowser-reborn": "^3.6.3", "react-native-linear-gradient": "^2.6.2", "react-native-pager-view": "^6.0.2", + "react-native-permissions": "^3.6.1", "react-native-progress": "^5.0.0", "react-native-reanimated": "^2.9.1", "react-native-root-siblings": "^4.1.1", @@ -66,18 +74,22 @@ "react-native-svg": "^12.4.0", "react-native-tab-view": "^3.3.0", "react-native-url-polyfill": "^1.3.0", + "react-native-uuid": "^2.0.1", "react-native-version-number": "^0.3.6", "react-native-web": "^0.18.11", "react-native-web-linear-gradient": "^1.1.2", + "react-native-web-webview": "^1.0.2", + "react-native-webview": "^11.26.1", + "react-native-youtube-iframe": "^2.2.2", "rn-fetch-blob": "^0.12.0", "tlds": "^1.234.0", "zod": "^3.20.2" }, "devDependencies": { - "@atproto/pds": "^0.0.1", - "@babel/core": "^7.12.9", - "@babel/preset-env": "^7.14.0", - "@babel/runtime": "^7.12.5", + "@atproto/pds": "^0.0.3", + "@babel/core": "^7.20.0", + "@babel/preset-env": "^7.20.0", + "@babel/runtime": "^7.20.0", "@react-native-community/eslint-config": "^3.0.0", "@testing-library/jest-native": "^5.3.3", "@testing-library/react-native": "^11.5.0", @@ -85,7 +97,10 @@ "@types/he": "^1.1.2", "@types/jest": "^26.0.23", "@types/lodash.chunk": "^4.2.7", + "@types/lodash.clonedeep": "^4.5.7", + "@types/lodash.isequal": "^4.5.6", "@types/lodash.omit": "^4.5.7", + "@types/lodash.shuffle": "^4.2.7", "@types/react-avatar-editor": "^13.0.0", "@types/react-native": "^0.67.3", "@types/react-test-renderer": "^17.0.1", @@ -95,11 +110,14 @@ "babel-loader": "^9.1.2", "babel-plugin-module-resolver": "^5.0.0", "babel-plugin-react-native-web": "^0.18.12", + "detox": "^20.1.2", "eslint": "^8.19.0", + "eslint-plugin-detox": "^1.0.0", + "eslint-plugin-ft-flow": "^2.0.3", "html-webpack-plugin": "^5.5.0", "jest": "^29.2.1", "jest-junit": "^15.0.0", - "metro-react-native-babel-preset": "0.73.5", + "metro-react-native-babel-preset": "^0.73.7", "prettier": "^2.8.3", "react-native-dotenv": "^3.3.1", "react-scripts": "^5.0.1", @@ -131,10 +149,11 @@ "node" ], "transformIgnorePatterns": [ - "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|rollbar-react-native|@fortawesome|@react-native|@react-navigation)" + "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|rollbar-react-native|@fortawesome|@react-native|@react-navigation|normalize-url)" ], "modulePathIgnorePatterns": [ - "__tests__/.*/__mocks__" + "__tests__/.*/__mocks__", + "e2e/.*" ], "coveragePathIgnorePatterns": [ "/node_modules/", diff --git a/src/App.native.tsx b/src/App.native.tsx index f00e3cad..17b8b455 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -6,35 +6,27 @@ import {GestureHandlerRootView} from 'react-native-gesture-handler' import SplashScreen from 'react-native-splash-screen' import {SafeAreaProvider} from 'react-native-safe-area-context' import {observer} from 'mobx-react-lite' -import { - createClient, - SegmentClient, - AnalyticsProvider, -} from '@segment/analytics-react-native' -import {ThemeProvider} from './view/lib/ThemeContext' +import {ThemeProvider} from 'lib/ThemeContext' import * as view from './view/index' import {RootStoreModel, setupState, RootStoreProvider} from './state' import {MobileShell} from './view/shell/mobile' -import {s} from './view/lib/styles' -import notifee, {EventType} from '@notifee/react-native' +import {s} from 'lib/styles' +import * as notifee from 'lib/notifee' +import * as analytics from 'lib/analytics' +import * as Toast from './view/com/util/Toast' const App = observer(() => { const [rootStore, setRootStore] = useState( undefined, ) - const [segment, setSegment] = useState(undefined) // init useEffect(() => { view.setup() - setSegment( - createClient({ - writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI', - trackAppLifecycleEvents: true, - }), - ) setupState().then(store => { setRootStore(store) + analytics.init(store) + notifee.init(store) SplashScreen.hide() Linking.getInitialURL().then((url: string | null) => { if (url) { @@ -44,12 +36,8 @@ const App = observer(() => { Linking.addEventListener('url', ({url}) => { store.nav.handleLink(url) }) - notifee.onForegroundEvent(async ({type}: {type: EventType}) => { - store.log.debug('Notifee foreground event', {type}) - if (type === EventType.PRESS) { - store.log.debug('User pressed a notifee, opening notifications') - store.nav.switchTo(1, true) - } + store.onSessionDropped(() => { + Toast.show('Sorry! Your session expired. Please log in again.') }) }) }, []) @@ -58,20 +46,19 @@ const App = observer(() => { if (!rootStore) { return null } - return ( - - - - + + + + - - - - + + + + ) }) diff --git a/src/view/lib/ThemeContext.tsx b/src/lib/ThemeContext.tsx similarity index 98% rename from src/view/lib/ThemeContext.tsx rename to src/lib/ThemeContext.tsx index 16a7d9cb..bcfc076f 100644 --- a/src/view/lib/ThemeContext.tsx +++ b/src/lib/ThemeContext.tsx @@ -53,6 +53,7 @@ export type TypographyVariant = | 'xs-medium' | 'xs-bold' | 'xs-heavy' + | 'title-2xl' | 'title-xl' | 'title-lg' | 'title' @@ -60,6 +61,7 @@ export type TypographyVariant = | 'post-text-lg' | 'post-text' | 'button' + | 'button-lg' | 'mono' export type Typography = Record diff --git a/src/lib/analytics.tsx b/src/lib/analytics.tsx new file mode 100644 index 00000000..441cdc45 --- /dev/null +++ b/src/lib/analytics.tsx @@ -0,0 +1,74 @@ +import React from 'react' +import {AppState, AppStateStatus} from 'react-native' +import {createClient, AnalyticsProvider} from '@segment/analytics-react-native' +import {RootStoreModel, AppInfo} from 'state/models/root-store' + +const segmentClient = createClient({ + writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI', + trackAppLifecycleEvents: false, +}) + +export {useAnalytics} from '@segment/analytics-react-native' + +export function init(store: RootStoreModel) { + // NOTE + // this method is a copy of segment's own lifecycle event tracking + // we handle it manually to ensure that it never fires while the app is backgrounded + // -prf + segmentClient.onContextLoaded(() => { + if (AppState.currentState !== 'active') { + store.log.debug('Prevented a metrics ping while the app was backgrounded') + return + } + const context = segmentClient.context.get() + if (typeof context?.app === 'undefined') { + store.log.debug('Aborted metrics ping due to unavailable context') + return + } + + const oldAppInfo = store.appInfo + const newAppInfo = context.app as AppInfo + store.setAppInfo(newAppInfo) + store.log.debug('Recording app info', {new: newAppInfo, old: oldAppInfo}) + + if (typeof oldAppInfo === 'undefined') { + segmentClient.track('Application Installed', { + version: newAppInfo.version, + build: newAppInfo.build, + }) + } else if (newAppInfo.version !== oldAppInfo.version) { + segmentClient.track('Application Updated', { + version: newAppInfo.version, + build: newAppInfo.build, + previous_version: oldAppInfo.version, + previous_build: oldAppInfo.build, + }) + } + segmentClient.track('Application Opened', { + from_background: false, + version: newAppInfo.version, + build: newAppInfo.build, + }) + }) + + let lastState: AppStateStatus = AppState.currentState + AppState.addEventListener('change', (state: AppStateStatus) => { + if (state === 'active' && lastState !== 'active') { + const context = segmentClient.context.get() + segmentClient.track('Application Opened', { + from_background: true, + version: context?.app?.version, + build: context?.app?.build, + }) + } else if (state !== 'active' && lastState === 'active') { + segmentClient.track('Application Backgrounded') + } + lastState = state + }) +} + +export function Provider({children}: React.PropsWithChildren<{}>) { + return ( + {children} + ) +} diff --git a/src/lib/analytics.web.tsx b/src/lib/analytics.web.tsx new file mode 100644 index 00000000..d7b9b264 --- /dev/null +++ b/src/lib/analytics.web.tsx @@ -0,0 +1,16 @@ +// TODO +import React from 'react' +import {RootStoreModel} from 'state/models/root-store' + +export function useAnalytics() { + return { + screen(_name: string) {}, + track(_name: string, _opts: any) {}, + } +} + +export function init(_store: RootStoreModel) {} + +export function Provider({children}: React.PropsWithChildren<{}>) { + return children +} diff --git a/src/state/lib/api-polyfill.ts b/src/lib/api/api-polyfill.ts similarity index 96% rename from src/state/lib/api-polyfill.ts rename to src/lib/api/api-polyfill.ts index df397cbd..3b5ba751 100644 --- a/src/state/lib/api-polyfill.ts +++ b/src/lib/api/api-polyfill.ts @@ -1,10 +1,10 @@ -import {sessionClient as AtpApi} from '@atproto/api' +import AtpAgent from '@atproto/api' import RNFS from 'react-native-fs' const TIMEOUT = 10e3 // 10s export function doPolyfill() { - AtpApi.xrpc.fetch = fetchHandler + AtpAgent.configure({fetch: fetchHandler}) } interface FetchHandlerResponse { diff --git a/src/state/lib/api-polyfill.web.ts b/src/lib/api/api-polyfill.web.ts similarity index 100% rename from src/state/lib/api-polyfill.web.ts rename to src/lib/api/api-polyfill.web.ts diff --git a/src/state/lib/api.ts b/src/lib/api/index.ts similarity index 86% rename from src/state/lib/api.ts rename to src/lib/api/index.ts index 7ae04f52..d800c376 100644 --- a/src/state/lib/api.ts +++ b/src/lib/api/index.ts @@ -1,16 +1,11 @@ -/** - * The environment is a place where services and shared dependencies between - * models live. They are made available to every model via dependency injection. - */ - -// import {ReactNativeStore} from './auth' import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api' import {AtUri} from '../../third-party/uri' -import {RootStoreModel} from '../models/root-store' -import {extractEntities} from '../../lib/strings' -import {isNetworkError} from '../../lib/errors' -import {LinkMeta} from '../../lib/link-meta' -import {Image} from '../../lib/images' +import {RootStoreModel} from 'state/models/root-store' +import {extractEntities} from 'lib/strings/rich-text-detection' +import {isNetworkError} from 'lib/strings/errors' +import {LinkMeta} from '../link-meta/link-meta' +import {Image} from '../images' +import {RichText} from '../strings/rich-text' export interface ExternalEmbedDraft { uri: string @@ -19,9 +14,22 @@ export interface ExternalEmbedDraft { localThumb?: Image } +export async function resolveName(store: RootStoreModel, didOrHandle: string) { + if (!didOrHandle) { + throw new Error('Invalid handle: ""') + } + if (didOrHandle.startsWith('did:')) { + return didOrHandle + } + const res = await store.api.com.atproto.handle.resolve({ + handle: didOrHandle, + }) + return res.data.did +} + export async function post( store: RootStoreModel, - text: string, + rawText: string, replyTo?: string, extLink?: ExternalEmbedDraft, images?: string[], @@ -30,6 +38,9 @@ export async function post( ) { let embed: AppBskyEmbedImages.Main | AppBskyEmbedExternal.Main | undefined let reply + const text = new RichText(rawText, undefined, { + cleanNewlines: true, + }).text.trim() onStateChange?.('Processing...') const entities = extractEntities(text, knownHandles) diff --git a/src/lib/app-info.ts b/src/lib/app-info.ts new file mode 100644 index 00000000..1ced274e --- /dev/null +++ b/src/lib/app-info.ts @@ -0,0 +1,4 @@ +import VersionNumber from 'react-native-version-number' + +export const appVersion = VersionNumber.appVersion +export const buildVersion = VersionNumber.buildVersion diff --git a/src/lib/app-info.web.ts b/src/lib/app-info.web.ts new file mode 100644 index 00000000..a2b6858d --- /dev/null +++ b/src/lib/app-info.web.ts @@ -0,0 +1,3 @@ +// TODO +export const appVersion = 'TODO' +export const buildVersion = 'TODO' diff --git a/src/lib/assets.native.ts b/src/lib/assets.native.ts new file mode 100644 index 00000000..d7f4a728 --- /dev/null +++ b/src/lib/assets.native.ts @@ -0,0 +1,5 @@ +import {ImageRequireSource} from 'react-native' + +export const DEF_AVATAR: ImageRequireSource = require('../../public/img/default-avatar.jpg') +export const TABS_EXPLAINER: ImageRequireSource = require('../../public/img/tabs-explainer.jpg') +export const CLOUD_SPLASH: ImageRequireSource = require('../../public/img/cloud-splash.png') diff --git a/src/lib/assets.ts b/src/lib/assets.ts new file mode 100644 index 00000000..21647876 --- /dev/null +++ b/src/lib/assets.ts @@ -0,0 +1,10 @@ +import {ImageRequireSource} from 'react-native' + +// @ts-ignore we need to pretend -prf +export const DEF_AVATAR: ImageRequireSource = {uri: '/img/default-avatar.jpg'} +// @ts-ignore we need to pretend -prf +export const TABS_EXPLAINER: ImageRequireSource = { + uri: '/img/tabs-explainer.jpg', +} +// @ts-ignore we need to pretend -prf +export const CLOUD_SPLASH: ImageRequireSource = {uri: '/img/cloud-splash.png'} diff --git a/src/lib/async/bundle.ts b/src/lib/async/bundle.ts new file mode 100644 index 00000000..e307cd43 --- /dev/null +++ b/src/lib/async/bundle.ts @@ -0,0 +1,24 @@ +type BundledFn = ( + ...args: Args +) => Promise + +/** + * A helper which ensures that multiple calls to an async function + * only produces one in-flight request at a time. + */ +export function bundleAsync( + fn: BundledFn, +): BundledFn { + let promise: Promise | undefined + return async (...args) => { + if (promise) { + return promise + } + promise = fn(...args) + try { + return await promise + } finally { + promise = undefined + } + } +} diff --git a/src/state/lib/bg-scheduler.ts b/src/lib/bg-scheduler.ts similarity index 88% rename from src/state/lib/bg-scheduler.ts rename to src/lib/bg-scheduler.ts index 97ccb78b..db3f2d7f 100644 --- a/src/state/lib/bg-scheduler.ts +++ b/src/lib/bg-scheduler.ts @@ -4,7 +4,7 @@ import BackgroundFetch, { export function configure( handler: (taskId: string) => Promise, - timeoutHandler: (taskId: string) => Promise, + timeoutHandler: (taskId: string) => void, ): Promise { return BackgroundFetch.configure( {minimumFetchInterval: 15}, diff --git a/src/state/lib/bg-scheduler.web.ts b/src/lib/bg-scheduler.web.ts similarity index 100% rename from src/state/lib/bg-scheduler.web.ts rename to src/lib/bg-scheduler.web.ts diff --git a/src/build-flags.ts b/src/lib/build-flags.ts similarity index 100% rename from src/build-flags.ts rename to src/lib/build-flags.ts diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 00000000..2a3043c0 --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,65 @@ +export const FEEDBACK_FORM_URL = + 'https://docs.google.com/forms/d/e/1FAIpQLSdavFRXTdB6tRobaFrRR2A1gv3b-IBHwQkBmNZTRpoqmcrPrQ/viewform?usp=sf_link' + +export const MAX_DISPLAY_NAME = 64 +export const MAX_DESCRIPTION = 256 + +export const PROD_SUGGESTED_FOLLOWS = [ + 'john', + 'visakanv', + 'saz', + 'steph', + 'ratzlaff', + 'beth', + 'weisser', + 'katherine', + 'annagat', + 'josh', + 'lurkshark', + 'amir', + 'amyxzh', + 'danielle', + 'jack-frazee', + 'vibes', + 'cat', + 'yuriy', + 'alvinreyes', + 'skoot', + 'patricia', + 'ara4n', + 'case', + 'armand', + 'ivan', + 'nicholas', + 'kelsey', + 'ericlee', + 'emily', + 'jake', + 'jennijuju', + 'ian5v', + 'bnewbold', + 'chris', + 'mtclai', + 'willscott', + 'michael', + 'kwkroeger', + 'broox', + 'iamrosewang', + 'jack-morrison', + 'pwang', + 'martin', + 'jack', + 'dan', + 'why', + 'divy', + 'jay', + 'paul', +].map(handle => `${handle}.bsky.social`) + +export const STAGING_SUGGESTED_FOLLOWS = ['arcalinea', 'paul', 'paul2'].map( + handle => `${handle}.staging.bsky.dev`, +) + +export const DEV_SUGGESTED_FOLLOWS = ['alice', 'bob', 'carla'].map( + handle => `${handle}.test`, +) diff --git a/src/lib/errors.ts b/src/lib/errors.ts deleted file mode 100644 index 216d5927..00000000 --- a/src/lib/errors.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function isNetworkError(e: unknown) { - const str = String(e) - return str.includes('Abort') || str.includes('Network request failed') -} diff --git a/src/view/lib/hooks/useAnimatedValue.ts b/src/lib/hooks/useAnimatedValue.ts similarity index 100% rename from src/view/lib/hooks/useAnimatedValue.ts rename to src/lib/hooks/useAnimatedValue.ts diff --git a/src/view/lib/hooks/useOnMainScroll.ts b/src/lib/hooks/useOnMainScroll.ts similarity index 94% rename from src/view/lib/hooks/useOnMainScroll.ts rename to src/lib/hooks/useOnMainScroll.ts index c3c16ff8..41b35dd4 100644 --- a/src/view/lib/hooks/useOnMainScroll.ts +++ b/src/lib/hooks/useOnMainScroll.ts @@ -1,6 +1,6 @@ import {useState} from 'react' import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native' -import {RootStoreModel} from '../../../state' +import {RootStoreModel} from 'state/index' export type OnScrollCb = ( event: NativeSyntheticEvent, diff --git a/src/view/lib/hooks/usePalette.ts b/src/lib/hooks/usePalette.ts similarity index 100% rename from src/view/lib/hooks/usePalette.ts rename to src/lib/hooks/usePalette.ts diff --git a/src/view/lib/icons.tsx b/src/lib/icons.tsx similarity index 100% rename from src/view/lib/icons.tsx rename to src/lib/icons.tsx diff --git a/src/lib/images.ts b/src/lib/images.ts index 8d5eaded..609e03bd 100644 --- a/src/lib/images.ts +++ b/src/lib/images.ts @@ -2,8 +2,8 @@ import RNFetchBlob from 'rn-fetch-blob' import ImageResizer from '@bam.tech/react-native-image-resizer' import {Share} from 'react-native' import RNFS from 'react-native-fs' - -import * as Toast from '../view/com/util/Toast' +import uuid from 'react-native-uuid' +import * as Toast from 'view/com/util/Toast' export interface DownloadAndResizeOpts { uri: string @@ -23,16 +23,12 @@ export interface Image { } export async function downloadAndResize(opts: DownloadAndResizeOpts) { - let appendExt + let appendExt = 'jpeg' try { const urip = new URL(opts.uri) const ext = urip.pathname.split('.').pop() - if (ext === 'jpg' || ext === 'jpeg') { - appendExt = 'jpeg' - } else if (ext === 'png') { + if (ext === 'png') { appendExt = 'png' - } else { - return } } catch (e: any) { console.error('Invalid URI', opts.uri, e) @@ -109,12 +105,18 @@ export async function compressIfNeeded( if (img.size < maxSize) { return img } - return await resize(origUri, { + const resizedImage = await resize(origUri, { width: img.width, height: img.height, mode: 'stretch', maxSize, }) + const finalImageMovedPath = await moveToPremanantPath(resizedImage.path) + const finalImg = { + ...resizedImage, + path: finalImageMovedPath, + } + return finalImg } export interface Dim { @@ -150,3 +152,15 @@ export const saveImageModal = async ({uri}: {uri: string}) => { } RNFS.unlink(imagePath) } + +export const moveToPremanantPath = async (path: string) => { + /* + Since this package stores images in a temp directory, we need to move the file to a permanent location. + Relevant: IOS bug when trying to open a second time: + https://github.com/ivpusic/react-native-image-crop-picker/issues/1199 + */ + const filename = uuid.v4() + const destinationPath = `${RNFS.TemporaryDirectoryPath}/${filename}` + RNFS.moveFile(path, destinationPath) + return destinationPath +} diff --git a/src/lib/images.web.ts b/src/lib/images.web.ts index 5158e005..4b6d93af 100644 --- a/src/lib/images.web.ts +++ b/src/lib/images.web.ts @@ -1,6 +1,5 @@ -import {Share} from 'react-native' - -import * as Toast from '../view/com/util/Toast' +// import {Share} from 'react-native' +// import * as Toast from 'view/com/util/Toast' export interface DownloadAndResizeOpts { uri: string diff --git a/src/lib/extractBskyMeta.ts b/src/lib/link-meta/bsky.ts similarity index 75% rename from src/lib/extractBskyMeta.ts rename to src/lib/link-meta/bsky.ts index e53036ae..fba41260 100644 --- a/src/lib/extractBskyMeta.ts +++ b/src/lib/link-meta/bsky.ts @@ -1,18 +1,18 @@ import {LikelyType, LinkMeta} from './link-meta' -import {match as matchRoute} from '../view/routes' -import {convertBskyAppUrlIfNeeded, makeRecordUri} from './strings' -import {RootStoreModel} from '../state' -import {PostThreadViewModel} from '../state/models/post-thread-view' +import {match as matchRoute} from 'view/routes' +import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers' +import {RootStoreModel} from 'state/index' +import {PostThreadViewModel} from 'state/models/post-thread-view' -import {Home} from '../view/screens/Home' -import {Search} from '../view/screens/Search' -import {Notifications} from '../view/screens/Notifications' -import {PostThread} from '../view/screens/PostThread' -import {PostUpvotedBy} from '../view/screens/PostUpvotedBy' -import {PostRepostedBy} from '../view/screens/PostRepostedBy' -import {Profile} from '../view/screens/Profile' -import {ProfileFollowers} from '../view/screens/ProfileFollowers' -import {ProfileFollows} from '../view/screens/ProfileFollows' +import {Home} from 'view/screens/Home' +import {Search} from 'view/screens/Search' +import {Notifications} from 'view/screens/Notifications' +import {PostThread} from 'view/screens/PostThread' +import {PostUpvotedBy} from 'view/screens/PostUpvotedBy' +import {PostRepostedBy} from 'view/screens/PostRepostedBy' +import {Profile} from 'view/screens/Profile' +import {ProfileFollowers} from 'view/screens/ProfileFollowers' +import {ProfileFollows} from 'view/screens/ProfileFollows' // NOTE // this is a hack around the lack of hosted social metadata diff --git a/src/lib/extractHtmlMeta.ts b/src/lib/link-meta/html.ts similarity index 93% rename from src/lib/extractHtmlMeta.ts rename to src/lib/link-meta/html.ts index 70387f71..220f8431 100644 --- a/src/lib/extractHtmlMeta.ts +++ b/src/lib/link-meta/html.ts @@ -1,5 +1,5 @@ -import {extractTwitterMeta} from './extractTwitterMeta' -import {extractYoutubeMeta} from './extractYoutubeMeta' +import {extractTwitterMeta} from './twitter' +import {extractYoutubeMeta} from './youtube' interface ExtractHtmlMetaInput { html: string diff --git a/src/lib/link-meta.ts b/src/lib/link-meta/link-meta.ts similarity index 99% rename from src/lib/link-meta.ts rename to src/lib/link-meta/link-meta.ts index 2826e969..6c4ad538 100644 --- a/src/lib/link-meta.ts +++ b/src/lib/link-meta/link-meta.ts @@ -1,8 +1,8 @@ import he from 'he' -import {isBskyAppUrl} from './strings' -import {RootStoreModel} from '../state' -import {extractBskyMeta} from './extractBskyMeta' -import {extractHtmlMeta} from './extractHtmlMeta' +import {isBskyAppUrl} from '../strings/url-helpers' +import {RootStoreModel} from 'state/index' +import {extractBskyMeta} from './bsky' +import {extractHtmlMeta} from './html' export enum LikelyType { HTML, diff --git a/src/lib/extractTwitterMeta.ts b/src/lib/link-meta/twitter.ts similarity index 100% rename from src/lib/extractTwitterMeta.ts rename to src/lib/link-meta/twitter.ts diff --git a/src/lib/extractYoutubeMeta.ts b/src/lib/link-meta/youtube.ts similarity index 78% rename from src/lib/extractYoutubeMeta.ts rename to src/lib/link-meta/youtube.ts index 566e3be4..42eed51e 100644 --- a/src/lib/extractYoutubeMeta.ts +++ b/src/lib/link-meta/youtube.ts @@ -4,10 +4,12 @@ export const extractYoutubeMeta = (html: string): Record => { const youtubeDescriptionRegex = /"videoDetails":.*"shortDescription":"([^"]*)"/i const youtubeThumbnailRegex = /"videoDetails":.*"url":"(.*)(default\.jpg)/i - + const youtubeAvatarRegex = + /"avatar":{"thumbnails":\[{.*?url.*?url.*?url":"([^"]*)"/i const youtubeTitleMatch = youtubeTitleRegex.exec(html) const youtubeDescriptionMatch = youtubeDescriptionRegex.exec(html) const youtubeThumbnailMatch = youtubeThumbnailRegex.exec(html) + const youtubeAvatarMatch = youtubeAvatarRegex.exec(html) if (youtubeTitleMatch && youtubeTitleMatch.length >= 1) { res.title = decodeURI(youtubeTitleMatch[1]) @@ -21,6 +23,9 @@ export const extractYoutubeMeta = (html: string): Record => { if (youtubeThumbnailMatch && youtubeThumbnailMatch.length >= 2) { res.image = youtubeThumbnailMatch[1] + 'default.jpg' } + if (!res.image && youtubeAvatarMatch && youtubeAvatarMatch.length >= 1) { + res.image = youtubeAvatarMatch[1] + } return res } diff --git a/src/view/lib/notifee.ts b/src/lib/notifee.ts similarity index 58% rename from src/view/lib/notifee.ts rename to src/lib/notifee.ts index 5e191738..fb0afdd6 100644 --- a/src/view/lib/notifee.ts +++ b/src/lib/notifee.ts @@ -1,7 +1,26 @@ -import notifee from '@notifee/react-native' +import notifee, {EventType} from '@notifee/react-native' import {AppBskyEmbedImages} from '@atproto/api' -import {NotificationsViewItemModel} from '../../state/models/notifications-view' -import {enforceLen} from '../../lib/strings' +import {RootStoreModel} from 'state/models/root-store' +import {TabPurpose} from 'state/models/navigation' +import {NotificationsViewItemModel} from 'state/models/notifications-view' +import {enforceLen} from 'lib/strings/helpers' + +export function init(store: RootStoreModel) { + store.onUnreadNotifications(count => notifee.setBadgeCount(count)) + store.onPushNotification(displayNotificationFromModel) + store.onSessionLoaded(() => { + // request notifications permission once the user has logged in + notifee.requestPermission() + }) + notifee.onForegroundEvent(async ({type}: {type: EventType}) => { + store.log.debug('Notifee foreground event', {type}) + if (type === EventType.PRESS) { + store.log.debug('User pressed a notifee, opening notifications') + store.nav.switchTo(TabPurpose.Notifs, true) + } + }) + notifee.onBackgroundEvent(async _e => {}) // notifee requires this but we handle it with onForegroundEvent +} export function displayNotification( title: string, @@ -39,7 +58,8 @@ export function displayNotificationFromModel( title = `${author} replied to your post` body = notif.additionalPost?.thread?.postRecord?.text || '' } else if (notif.isFollow) { - title = `${author} followed you` + title = 'New follower!' + body = `${author} has followed you` } else { return } diff --git a/src/lib/permissions.ts b/src/lib/permissions.ts new file mode 100644 index 00000000..ab2c73ca --- /dev/null +++ b/src/lib/permissions.ts @@ -0,0 +1,61 @@ +import {Alert} from 'react-native' +import { + check, + openSettings, + Permission, + PermissionStatus, + PERMISSIONS, + RESULTS, +} from 'react-native-permissions' + +export const PHOTO_LIBRARY = PERMISSIONS.IOS.PHOTO_LIBRARY +export const CAMERA = PERMISSIONS.IOS.CAMERA + +/** + * Returns `true` if the user has granted permission or hasn't made + * a decision yet. Returns `false` if unavailable or not granted. + */ +export async function hasAccess(perm: Permission): Promise { + const status = await check(perm) + return isntANo(status) +} + +export async function requestAccessIfNeeded( + perm: Permission, +): Promise { + if (await hasAccess(perm)) { + return true + } + let permDescription + if (perm === PHOTO_LIBRARY) { + permDescription = 'photo library' + } else if (perm === CAMERA) { + permDescription = 'camera' + } else { + return false + } + Alert.alert( + 'Permission needed', + `Bluesky does not have permission to access your ${permDescription}.`, + [ + { + text: 'Cancel', + style: 'cancel', + }, + {text: 'Open Settings', onPress: () => openSettings()}, + ], + ) + return false +} + +export async function requestPhotoAccessIfNeeded() { + return requestAccessIfNeeded(PHOTO_LIBRARY) +} + +export async function requestCameraAccessIfNeeded() { + return requestAccessIfNeeded(CAMERA) +} + +function isntANo(status: PermissionStatus): boolean { + return status !== RESULTS.UNAVAILABLE && status !== RESULTS.BLOCKED +} diff --git a/src/lib/permissions.web.ts b/src/lib/permissions.web.ts new file mode 100644 index 00000000..5b69637e --- /dev/null +++ b/src/lib/permissions.web.ts @@ -0,0 +1,22 @@ +/* +At the moment, Web doesn't have any equivalence for these. +*/ + +export const PHOTO_LIBRARY = '' +export const CAMERA = '' + +export async function hasAccess(_perm: any): Promise { + return true +} + +export async function requestAccessIfNeeded(_perm: any): Promise { + return true +} + +export async function requestPhotoAccessIfNeeded() { + return requestAccessIfNeeded(PHOTO_LIBRARY) +} + +export async function requestCameraAccessIfNeeded() { + return requestAccessIfNeeded(CAMERA) +} diff --git a/src/state/lib/storage.ts b/src/lib/storage.ts similarity index 100% rename from src/state/lib/storage.ts rename to src/lib/storage.ts diff --git a/src/lib/strings.ts b/src/lib/strings.ts deleted file mode 100644 index 8b93fa93..00000000 --- a/src/lib/strings.ts +++ /dev/null @@ -1,267 +0,0 @@ -import {AtUri} from '../third-party/uri' -import {AppBskyFeedPost} from '@atproto/api' -type Entity = AppBskyFeedPost.Entity -import {PROD_SERVICE} from '../state' -import {isNetworkError} from './errors' -import TLDs from 'tlds' - -export const MAX_DISPLAY_NAME = 64 -export const MAX_DESCRIPTION = 256 - -export function pluralize(n: number, base: string, plural?: string): string { - if (n === 1) { - return base - } - if (plural) { - return plural - } - return base + 's' -} - -export function makeRecordUri( - didOrName: string, - collection: string, - rkey: string, -) { - const urip = new AtUri('at://host/') - urip.host = didOrName - urip.collection = collection - urip.rkey = rkey - return urip.toString() -} - -const MINUTE = 60 -const HOUR = MINUTE * 60 -const DAY = HOUR * 24 -const MONTH = DAY * 30 -const YEAR = DAY * 365 -export function ago(date: number | string | Date): string { - let ts: number - if (typeof date === 'string') { - ts = Number(new Date(date)) - } else if (date instanceof Date) { - ts = Number(date) - } else { - ts = date - } - const diffSeconds = Math.floor((Date.now() - ts) / 1e3) - if (diffSeconds < MINUTE) { - return `${diffSeconds}s` - } else if (diffSeconds < HOUR) { - return `${Math.floor(diffSeconds / MINUTE)}m` - } else if (diffSeconds < DAY) { - return `${Math.floor(diffSeconds / HOUR)}h` - } else if (diffSeconds < MONTH) { - return `${Math.floor(diffSeconds / DAY)}d` - } else if (diffSeconds < YEAR) { - return `${Math.floor(diffSeconds / MONTH)}mo` - } else { - return new Date(ts).toLocaleDateString() - } -} - -export function isValidDomain(str: string): boolean { - return !!TLDs.find(tld => { - let i = str.lastIndexOf(tld) - if (i === -1) { - return false - } - return str.charAt(i - 1) === '.' && i === str.length - tld.length - }) -} - -export function extractEntities( - text: string, - knownHandles?: Set, -): Entity[] | undefined { - let match - let ents: Entity[] = [] - { - // mentions - const re = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g - while ((match = re.exec(text))) { - if (knownHandles && !knownHandles.has(match[3])) { - continue // not a known handle - } else if (!match[3].includes('.')) { - continue // probably not a handle - } - const start = text.indexOf(match[3], match.index) - 1 - ents.push({ - type: 'mention', - value: match[3], - index: {start, end: start + match[3].length + 1}, - }) - } - } - { - // links - const re = - /(^|\s|\()((https?:\/\/[\S]+)|((?[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim - while ((match = re.exec(text))) { - let value = match[2] - if (!value.startsWith('http')) { - const domain = match.groups?.domain - if (!domain || !isValidDomain(domain)) { - continue - } - value = `https://${value}` - } - const start = text.indexOf(match[2], match.index) - const index = {start, end: start + match[2].length} - // strip ending puncuation - if (/[.,;!?]$/.test(value)) { - value = value.slice(0, -1) - index.end-- - } - if (/[)]$/.test(value) && !value.includes('(')) { - value = value.slice(0, -1) - index.end-- - } - ents.push({ - type: 'link', - value, - index, - }) - } - } - return ents.length > 0 ? ents : undefined -} - -interface DetectedLink { - link: string -} -type DetectedLinkable = string | DetectedLink -export function detectLinkables(text: string): DetectedLinkable[] { - const re = - /((^|\s|\()@[a-z0-9.-]*)|((^|\s|\()https?:\/\/[\S]+)|((^|\s|\()(?[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*)/gi - const segments = [] - let match - let start = 0 - while ((match = re.exec(text))) { - let matchIndex = match.index - let matchValue = match[0] - - if (match.groups?.domain && !isValidDomain(match.groups?.domain)) { - continue - } - - if (/\s|\(/.test(matchValue)) { - // HACK - // skip the starting space - // we have to do this because RN doesnt support negative lookaheads - // -prf - matchIndex++ - matchValue = matchValue.slice(1) - } - - // strip ending puncuation - if (/[.,;!?]$/.test(matchValue)) { - matchValue = matchValue.slice(0, -1) - } - if (/[)]$/.test(matchValue) && !matchValue.includes('(')) { - matchValue = matchValue.slice(0, -1) - } - - if (start !== matchIndex) { - segments.push(text.slice(start, matchIndex)) - } - segments.push({link: matchValue}) - start = matchIndex + matchValue.length - } - if (start < text.length) { - segments.push(text.slice(start)) - } - return segments -} - -export function makeValidHandle(str: string): string { - if (str.length > 20) { - str = str.slice(0, 20) - } - str = str.toLowerCase() - return str.replace(/^[^a-z]+/g, '').replace(/[^a-z0-9-]/g, '') -} - -export function createFullHandle(name: string, domain: string): string { - name = (name || '').replace(/[.]+$/, '') - domain = (domain || '').replace(/^[.]+/, '') - return `${name}.${domain}` -} - -export function enforceLen(str: string, len: number, ellipsis = false): string { - str = str || '' - if (str.length > len) { - return str.slice(0, len) + (ellipsis ? '...' : '') - } - return str -} - -export function cleanError(str: any): string { - if (!str) { - return str - } - if (typeof str !== 'string') { - str = str.toString() - } - if (isNetworkError(str)) { - return 'Unable to connect. Please check your internet connection and try again.' - } - if (str.startsWith('Error: ')) { - return str.slice('Error: '.length) - } - return str -} - -export function toNiceDomain(url: string): string { - try { - const urlp = new URL(url) - if (`https://${urlp.host}` === PROD_SERVICE) { - return 'Bluesky Social' - } - return urlp.host - } catch (e) { - return url - } -} - -export function toShortUrl(url: string): string { - try { - const urlp = new URL(url) - const shortened = - urlp.host + - (urlp.pathname === '/' ? '' : urlp.pathname) + - urlp.search + - urlp.hash - if (shortened.length > 30) { - return shortened.slice(0, 27) + '...' - } - return shortened - } catch (e) { - return url - } -} - -export function toShareUrl(url: string): string { - if (!url.startsWith('https')) { - const urlp = new URL('https://bsky.app') - urlp.pathname = url - url = urlp.toString() - } - return url -} - -export function isBskyAppUrl(url: string): boolean { - return url.startsWith('https://bsky.app/') -} - -export function convertBskyAppUrlIfNeeded(url: string): string { - if (isBskyAppUrl(url)) { - try { - const urlp = new URL(url) - return urlp.pathname - } catch (e) { - console.error('Unexpected error in convertBskyAppUrlIfNeeded()', e) - } - } - return url -} diff --git a/src/lib/strings/errors.ts b/src/lib/strings/errors.ts new file mode 100644 index 00000000..0efcad33 --- /dev/null +++ b/src/lib/strings/errors.ts @@ -0,0 +1,23 @@ +export function cleanError(str: any): string { + if (!str) { + return '' + } + if (typeof str !== 'string') { + str = str.toString() + } + if (isNetworkError(str)) { + return 'Unable to connect. Please check your internet connection and try again.' + } + if (str.includes('Upstream Failure')) { + return 'The server appears to be experiencing issues. Please try again in a few moments.' + } + if (str.startsWith('Error: ')) { + return str.slice('Error: '.length) + } + return str +} + +export function isNetworkError(e: unknown) { + const str = String(e) + return str.includes('Abort') || str.includes('Network request failed') +} diff --git a/src/lib/strings/handles.ts b/src/lib/strings/handles.ts new file mode 100644 index 00000000..3409a031 --- /dev/null +++ b/src/lib/strings/handles.ts @@ -0,0 +1,13 @@ +export function makeValidHandle(str: string): string { + if (str.length > 20) { + str = str.slice(0, 20) + } + str = str.toLowerCase() + return str.replace(/^[^a-z]+/g, '').replace(/[^a-z0-9-]/g, '') +} + +export function createFullHandle(name: string, domain: string): string { + name = (name || '').replace(/[.]+$/, '') + domain = (domain || '').replace(/^[.]+/, '') + return `${name}.${domain}` +} diff --git a/src/lib/strings/helpers.ts b/src/lib/strings/helpers.ts new file mode 100644 index 00000000..183d53e3 --- /dev/null +++ b/src/lib/strings/helpers.ts @@ -0,0 +1,17 @@ +export function pluralize(n: number, base: string, plural?: string): string { + if (n === 1) { + return base + } + if (plural) { + return plural + } + return base + 's' +} + +export function enforceLen(str: string, len: number, ellipsis = false): string { + str = str || '' + if (str.length > len) { + return str.slice(0, len) + (ellipsis ? '...' : '') + } + return str +} diff --git a/src/lib/strings/mention-manip.ts b/src/lib/strings/mention-manip.ts new file mode 100644 index 00000000..1f7cbe43 --- /dev/null +++ b/src/lib/strings/mention-manip.ts @@ -0,0 +1,37 @@ +interface FoundMention { + value: string + index: number +} + +export function getMentionAt( + text: string, + cursorPos: number, +): FoundMention | undefined { + let re = /(^|\s)@([a-z0-9.]*)/gi + let match + while ((match = re.exec(text))) { + const spaceOffset = match[1].length + const index = match.index + spaceOffset + if ( + cursorPos >= index && + cursorPos <= index + match[0].length - spaceOffset + ) { + return {value: match[2], index} + } + } + return undefined +} + +export function insertMentionAt( + text: string, + cursorPos: number, + mention: string, +) { + const target = getMentionAt(text, cursorPos) + if (target) { + return `${text.slice(0, target.index)}@${mention} ${text.slice( + target.index + target.value.length + 1, // add 1 to include the "@" + )}` + } + return text +} diff --git a/src/lib/strings/rich-text-detection.ts b/src/lib/strings/rich-text-detection.ts new file mode 100644 index 00000000..386ed48e --- /dev/null +++ b/src/lib/strings/rich-text-detection.ts @@ -0,0 +1,107 @@ +import {AppBskyFeedPost} from '@atproto/api' +type Entity = AppBskyFeedPost.Entity +import {isValidDomain} from './url-helpers' + +export function extractEntities( + text: string, + knownHandles?: Set, +): Entity[] | undefined { + let match + let ents: Entity[] = [] + { + // mentions + const re = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g + while ((match = re.exec(text))) { + if (knownHandles && !knownHandles.has(match[3])) { + continue // not a known handle + } else if (!match[3].includes('.')) { + continue // probably not a handle + } + const start = text.indexOf(match[3], match.index) - 1 + ents.push({ + type: 'mention', + value: match[3], + index: {start, end: start + match[3].length + 1}, + }) + } + } + { + // links + const re = + /(^|\s|\()((https?:\/\/[\S]+)|((?[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim + while ((match = re.exec(text))) { + let value = match[2] + if (!value.startsWith('http')) { + const domain = match.groups?.domain + if (!domain || !isValidDomain(domain)) { + continue + } + value = `https://${value}` + } + const start = text.indexOf(match[2], match.index) + const index = {start, end: start + match[2].length} + // strip ending puncuation + if (/[.,;!?]$/.test(value)) { + value = value.slice(0, -1) + index.end-- + } + if (/[)]$/.test(value) && !value.includes('(')) { + value = value.slice(0, -1) + index.end-- + } + ents.push({ + type: 'link', + value, + index, + }) + } + } + return ents.length > 0 ? ents : undefined +} + +interface DetectedLink { + link: string +} +type DetectedLinkable = string | DetectedLink +export function detectLinkables(text: string): DetectedLinkable[] { + const re = + /((^|\s|\()@[a-z0-9.-]*)|((^|\s|\()https?:\/\/[\S]+)|((^|\s|\()(?[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*)/gi + const segments = [] + let match + let start = 0 + while ((match = re.exec(text))) { + let matchIndex = match.index + let matchValue = match[0] + + if (match.groups?.domain && !isValidDomain(match.groups?.domain)) { + continue + } + + if (/\s|\(/.test(matchValue)) { + // HACK + // skip the starting space + // we have to do this because RN doesnt support negative lookaheads + // -prf + matchIndex++ + matchValue = matchValue.slice(1) + } + + // strip ending puncuation + if (/[.,;!?]$/.test(matchValue)) { + matchValue = matchValue.slice(0, -1) + } + if (/[)]$/.test(matchValue) && !matchValue.includes('(')) { + matchValue = matchValue.slice(0, -1) + } + + if (start !== matchIndex) { + segments.push(text.slice(start, matchIndex)) + } + segments.push({link: matchValue}) + start = matchIndex + matchValue.length + } + if (start < text.length) { + segments.push(text.slice(start)) + } + return segments +} diff --git a/src/lib/strings/rich-text-sanitize.ts b/src/lib/strings/rich-text-sanitize.ts new file mode 100644 index 00000000..0b589570 --- /dev/null +++ b/src/lib/strings/rich-text-sanitize.ts @@ -0,0 +1,32 @@ +import {RichText} from './rich-text' + +const EXCESS_SPACE_RE = /[\r\n]([\u00AD\u2060\u200D\u200C\u200B\s]*[\r\n]){2,}/ +const REPLACEMENT_STR = '\n\n' + +export function removeExcessNewlines(richText: RichText): RichText { + return clean(richText, EXCESS_SPACE_RE, REPLACEMENT_STR) +} + +// TODO: check on whether this works correctly with multi-byte codepoints +export function clean( + richText: RichText, + targetRegexp: RegExp, + replacementString: string, +): RichText { + richText = richText.clone() + + let match = richText.text.match(targetRegexp) + while (match && typeof match.index !== 'undefined') { + const oldText = richText.text + const removeStartIndex = match.index + const removeEndIndex = removeStartIndex + match[0].length + richText.delete(removeStartIndex, removeEndIndex) + if (richText.text === oldText) { + break // sanity check + } + richText.insert(removeStartIndex, replacementString) + match = richText.text.match(targetRegexp) + } + + return richText +} diff --git a/src/lib/strings/rich-text.ts b/src/lib/strings/rich-text.ts new file mode 100644 index 00000000..1df2144e --- /dev/null +++ b/src/lib/strings/rich-text.ts @@ -0,0 +1,216 @@ +/* += Rich Text Manipulation + +When we sanitize rich text, we have to update the entity indices as the +text is modified. This can be modeled as inserts() and deletes() of the +rich text string. The possible scenarios are outlined below, along with +their expected behaviors. + +NOTE: Slices are start inclusive, end exclusive + +== richTextInsert() + +Target string: + + 0 1 2 3 4 5 6 7 8 910 // string indices + h e l l o w o r l d // string value + ^-------^ // target slice {start: 2, end: 7} + +Scenarios: + +A: ^ // insert "test" at 0 +B: ^ // insert "test" at 4 +C: ^ // insert "test" at 8 + +A = before -> move both by num added +B = inner -> move end by num added +C = after -> noop + +Results: + +A: 0 1 2 3 4 5 6 7 8 910 // string indices + t e s t h e l l o w // string value + ^-------^ // target slice {start: 6, end: 11} + +B: 0 1 2 3 4 5 6 7 8 910 // string indices + h e l l t e s t o w // string value + ^---------------^ // target slice {start: 2, end: 11} + +C: 0 1 2 3 4 5 6 7 8 910 // string indices + h e l l o w o t e s // string value + ^-------^ // target slice {start: 2, end: 7} + +== richTextDelete() + +Target string: + + 0 1 2 3 4 5 6 7 8 910 // string indices + h e l l o w o r l d // string value + ^-------^ // target slice {start: 2, end: 7} + +Scenarios: + +A: ^---------------^ // remove slice {start: 0, end: 9} +B: ^-----^ // remove slice {start: 7, end: 11} +C: ^-----------^ // remove slice {start: 4, end: 11} +D: ^-^ // remove slice {start: 3, end: 5} +E: ^-----^ // remove slice {start: 1, end: 5} +F: ^-^ // remove slice {start: 0, end: 2} + +A = entirely outer -> delete slice +B = entirely after -> noop +C = partially after -> move end to remove-start +D = entirely inner -> move end by num removed +E = partially before -> move start to remove-start index, move end by num removed +F = entirely before -> move both by num removed + +Results: + +A: 0 1 2 3 4 5 6 7 8 910 // string indices + l d // string value + // target slice (deleted) + +B: 0 1 2 3 4 5 6 7 8 910 // string indices + h e l l o w // string value + ^-------^ // target slice {start: 2, end: 7} + +C: 0 1 2 3 4 5 6 7 8 910 // string indices + h e l l // string value + ^-^ // target slice {start: 2, end: 4} + +D: 0 1 2 3 4 5 6 7 8 910 // string indices + h e l w o r l d // string value + ^---^ // target slice {start: 2, end: 5} + +E: 0 1 2 3 4 5 6 7 8 910 // string indices + h w o r l d // string value + ^-^ // target slice {start: 1, end: 3} + +F: 0 1 2 3 4 5 6 7 8 910 // string indices + l l o w o r l d // string value + ^-------^ // target slice {start: 0, end: 5} + */ + +import cloneDeep from 'lodash.clonedeep' +import {AppBskyFeedPost} from '@atproto/api' +import {removeExcessNewlines} from './rich-text-sanitize' + +export type Entity = AppBskyFeedPost.Entity +export interface RichTextOpts { + cleanNewlines?: boolean +} + +export class RichText { + constructor( + public text: string, + public entities?: Entity[], + opts?: RichTextOpts, + ) { + if (opts?.cleanNewlines) { + removeExcessNewlines(this).copyInto(this) + } + } + + clone() { + return new RichText(this.text, cloneDeep(this.entities)) + } + + copyInto(target: RichText) { + target.text = this.text + target.entities = cloneDeep(this.entities) + } + + insert(insertIndex: number, insertText: string) { + this.text = + this.text.slice(0, insertIndex) + + insertText + + this.text.slice(insertIndex) + + if (!this.entities?.length) { + return this + } + + const numCharsAdded = insertText.length + for (const ent of this.entities) { + // see comment at top of file for labels of each scenario + // scenario A (before) + if (insertIndex <= ent.index.start) { + // move both by num added + ent.index.start += numCharsAdded + ent.index.end += numCharsAdded + } + // scenario B (inner) + else if (insertIndex >= ent.index.start && insertIndex < ent.index.end) { + // move end by num added + ent.index.end += numCharsAdded + } + // scenario C (after) + // noop + } + return this + } + + delete(removeStartIndex: number, removeEndIndex: number) { + this.text = + this.text.slice(0, removeStartIndex) + this.text.slice(removeEndIndex) + + if (!this.entities?.length) { + return this + } + + const numCharsRemoved = removeEndIndex - removeStartIndex + for (const ent of this.entities) { + // see comment at top of file for labels of each scenario + // scenario A (entirely outer) + if ( + removeStartIndex <= ent.index.start && + removeEndIndex >= ent.index.end + ) { + // delete slice (will get removed in final pass) + ent.index.start = 0 + ent.index.end = 0 + } + // scenario B (entirely after) + else if (removeStartIndex > ent.index.end) { + // noop + } + // scenario C (partially after) + else if ( + removeStartIndex > ent.index.start && + removeStartIndex <= ent.index.end && + removeEndIndex > ent.index.end + ) { + // move end to remove start + ent.index.end = removeStartIndex + } + // scenario D (entirely inner) + else if ( + removeStartIndex >= ent.index.start && + removeEndIndex <= ent.index.end + ) { + // move end by num removed + ent.index.end -= numCharsRemoved + } + // scenario E (partially before) + else if ( + removeStartIndex < ent.index.start && + removeEndIndex >= ent.index.start && + removeEndIndex <= ent.index.end + ) { + // move start to remove-start index, move end by num removed + ent.index.start = removeStartIndex + ent.index.end -= numCharsRemoved + } + // scenario F (entirely before) + else if (removeEndIndex < ent.index.start) { + // move both by num removed + ent.index.start -= numCharsRemoved + ent.index.end -= numCharsRemoved + } + } + + // filter out any entities that were made irrelevant + this.entities = this.entities.filter(ent => ent.index.start < ent.index.end) + return this + } +} diff --git a/src/lib/strings/time.ts b/src/lib/strings/time.ts new file mode 100644 index 00000000..4f62eeba --- /dev/null +++ b/src/lib/strings/time.ts @@ -0,0 +1,29 @@ +const MINUTE = 60 +const HOUR = MINUTE * 60 +const DAY = HOUR * 24 +const MONTH = DAY * 30 +const YEAR = DAY * 365 +export function ago(date: number | string | Date): string { + let ts: number + if (typeof date === 'string') { + ts = Number(new Date(date)) + } else if (date instanceof Date) { + ts = Number(date) + } else { + ts = date + } + const diffSeconds = Math.floor((Date.now() - ts) / 1e3) + if (diffSeconds < MINUTE) { + return `${diffSeconds}s` + } else if (diffSeconds < HOUR) { + return `${Math.floor(diffSeconds / MINUTE)}m` + } else if (diffSeconds < DAY) { + return `${Math.floor(diffSeconds / HOUR)}h` + } else if (diffSeconds < MONTH) { + return `${Math.floor(diffSeconds / DAY)}d` + } else if (diffSeconds < YEAR) { + return `${Math.floor(diffSeconds / MONTH)}mo` + } else { + return new Date(ts).toLocaleDateString() + } +} diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts new file mode 100644 index 00000000..a149f49c --- /dev/null +++ b/src/lib/strings/url-helpers.ts @@ -0,0 +1,108 @@ +import {AtUri} from '../../third-party/uri' +import {PROD_SERVICE} from 'state/index' +import TLDs from 'tlds' + +export function isValidDomain(str: string): boolean { + return !!TLDs.find(tld => { + let i = str.lastIndexOf(tld) + if (i === -1) { + return false + } + return str.charAt(i - 1) === '.' && i === str.length - tld.length + }) +} + +export function makeRecordUri( + didOrName: string, + collection: string, + rkey: string, +) { + const urip = new AtUri('at://host/') + urip.host = didOrName + urip.collection = collection + urip.rkey = rkey + return urip.toString() +} + +export function toNiceDomain(url: string): string { + try { + const urlp = new URL(url) + if (`https://${urlp.host}` === PROD_SERVICE) { + return 'Bluesky Social' + } + return urlp.host + } catch (e) { + return url + } +} + +export function toShortUrl(url: string): string { + try { + const urlp = new URL(url) + const shortened = + urlp.host + + (urlp.pathname === '/' ? '' : urlp.pathname) + + urlp.search + + urlp.hash + if (shortened.length > 30) { + return shortened.slice(0, 27) + '...' + } + return shortened + } catch (e) { + return url + } +} + +export function toShareUrl(url: string): string { + if (!url.startsWith('https')) { + const urlp = new URL('https://bsky.app') + urlp.pathname = url + url = urlp.toString() + } + return url +} + +export function isBskyAppUrl(url: string): boolean { + return url.startsWith('https://bsky.app/') +} + +export function convertBskyAppUrlIfNeeded(url: string): string { + if (isBskyAppUrl(url)) { + try { + const urlp = new URL(url) + return urlp.pathname + } catch (e) { + console.error('Unexpected error in convertBskyAppUrlIfNeeded()', e) + } + } + return url +} + +export function getYoutubeVideoId(link: string): string | undefined { + let url + try { + url = new URL(link) + } catch (e) { + return undefined + } + + if ( + url.hostname !== 'www.youtube.com' && + url.hostname !== 'youtube.com' && + url.hostname !== 'youtu.be' + ) { + return undefined + } + if (url.hostname === 'youtu.be') { + const videoId = url.pathname.split('/')[1] + if (!videoId) { + return undefined + } + return videoId + } + const videoId = url.searchParams.get('v') as string + if (!videoId) { + return undefined + } + return videoId +} diff --git a/src/view/lib/styles.ts b/src/lib/styles.ts similarity index 94% rename from src/view/lib/styles.ts rename to src/lib/styles.ts index 7129867e..db6c0360 100644 --- a/src/view/lib/styles.ts +++ b/src/lib/styles.ts @@ -1,4 +1,4 @@ -import {StyleSheet, TextStyle} from 'react-native' +import {StyleProp, StyleSheet, TextStyle} from 'react-native' import {Theme, TypographyVariant} from './ThemeContext' // 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest @@ -206,3 +206,13 @@ export function lh( lineHeight: (theme.typography[type].fontSize || 16) * height, } } + +export function addStyle( + base: StyleProp, + addedStyle: StyleProp, +): StyleProp { + if (Array.isArray(base)) { + return base.concat([addedStyle]) + } + return [base, addedStyle] +} diff --git a/src/view/lib/themes.ts b/src/lib/themes.ts similarity index 96% rename from src/view/lib/themes.ts rename to src/lib/themes.ts index 84e2b788..c544eebf 100644 --- a/src/view/lib/themes.ts +++ b/src/lib/themes.ts @@ -14,7 +14,7 @@ export const defaultTheme: Theme = { link: colors.blue3, border: '#f0e9e9', borderDark: '#e0d9d9', - icon: colors.gray3, + icon: colors.gray4, // non-standard textVeryLight: colors.gray4, @@ -208,11 +208,16 @@ export const defaultTheme: Theme = { fontWeight: '800', }, - 'title-xl': { + 'title-2xl': { fontSize: 34, letterSpacing: 0.25, fontWeight: '500', }, + 'title-xl': { + fontSize: 28, + letterSpacing: 0.25, + fontWeight: '500', + }, 'title-lg': { fontSize: 22, fontWeight: '500', @@ -237,6 +242,11 @@ export const defaultTheme: Theme = { letterSpacing: 0.4, fontWeight: '400', }, + 'button-lg': { + fontWeight: '500', + fontSize: 18, + letterSpacing: 0.5, + }, button: { fontWeight: '500', fontSize: 14, @@ -263,7 +273,7 @@ export const darkTheme: Theme = { link: colors.blue3, border: colors.gray6, borderDark: colors.gray5, - icon: colors.gray5, + icon: colors.gray4, // non-standard textVeryLight: colors.gray4, diff --git a/src/state/lib/type-guards.ts b/src/lib/type-guards.ts similarity index 100% rename from src/state/lib/type-guards.ts rename to src/lib/type-guards.ts diff --git a/src/state/index.ts b/src/state/index.ts index 654c15af..61b85e51 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,9 +1,9 @@ import {autorun} from 'mobx' -import {Platform} from 'react-native' -import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api' +import {AppState, Platform} from 'react-native' +import {AtpAgent} from '@atproto/api' import {RootStoreModel} from './models/root-store' -import * as apiPolyfill from './lib/api-polyfill' -import * as storage from './lib/storage' +import * as apiPolyfill from 'lib/api/api-polyfill' +import * as storage from 'lib/storage' export const LOCAL_DEV_SERVICE = Platform.OS === 'ios' ? 'http://localhost:2583' : 'http://10.0.2.2:2583' @@ -19,8 +19,7 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) { apiPolyfill.doPolyfill() - const api = AtpApi.service(serviceUri) as SessionServiceClient - rootStore = new RootStoreModel(api) + rootStore = new RootStoreModel(new AtpAgent({service: serviceUri})) try { data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {} rootStore.log.debug('Initial hydrate', {hasSession: !!data.session}) @@ -28,25 +27,7 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) { } catch (e: any) { rootStore.log.error('Failed to load state from storage', e) } - - rootStore.session - .connect() - .then(() => { - rootStore.log.debug('Session connected') - return rootStore.fetchStateUpdate() - }) - .catch((e: any) => { - rootStore.log.warn('Failed initial connect', e) - }) - // @ts-ignore .on() is correct -prf - api.sessionManager.on('session', () => { - if (!api.sessionManager.session && rootStore.session.hasSession) { - // reset session - rootStore.session.clear() - } else if (api.sessionManager.session) { - rootStore.session.updateAuthTokens(api.sessionManager.session) - } - }) + rootStore.attemptSessionResumption() // track changes & save to storage autorun(() => { @@ -56,7 +37,14 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) { // periodic state fetch setInterval(() => { - rootStore.fetchStateUpdate() + // NOTE + // this must ONLY occur when the app is active, as the bg-fetch handler + // will wake up the thread and cause this interval to fire, which in + // turn schedules a bunch of work at a poor time + // -prf + if (AppState.currentState === 'active') { + rootStore.updateSessionState() + } }, STATE_FETCH_INTERVAL) return rootStore diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index 62105982..f80c5f2c 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -5,13 +5,16 @@ import { AppBskyFeedPost, AppBskyFeedGetAuthorFeed as GetAuthorFeed, } from '@atproto/api' +import AwaitLock from 'await-lock' +import {bundleAsync} from 'lib/async/bundle' type FeedViewPost = AppBskyFeedFeedViewPost.Main type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost type PostView = AppBskyFeedPost.View import {AtUri} from '../../third-party/uri' import {RootStoreModel} from './root-store' -import * as apilib from '../lib/api' -import {cleanError} from '../../lib/strings' +import * as apilib from 'lib/api/index' +import {cleanError} from 'lib/strings/errors' +import {RichText} from 'lib/strings/rich-text' const PAGE_SIZE = 30 @@ -37,6 +40,7 @@ export class FeedItemModel { reply?: FeedViewPost['reply'] replyParent?: FeedItemModel reason?: FeedViewPost['reason'] + richText?: RichText constructor( public rootStore: RootStoreModel, @@ -49,6 +53,11 @@ export class FeedItemModel { const valid = AppBskyFeedPost.validateRecord(this.post.record) if (valid.success) { this.postRecord = this.post.record + this.richText = new RichText( + this.postRecord.text, + this.postRecord.entities, + {cleanNewlines: true}, + ) } else { rootStore.log.warn( 'Received an invalid app.bsky.feed.post record', @@ -187,10 +196,9 @@ export class FeedModel { hasMore = true loadMoreCursor: string | undefined pollCursor: string | undefined - _loadPromise: Promise | undefined - _loadMorePromise: Promise | undefined - _loadLatestPromise: Promise | undefined - _updatePromise: Promise | undefined + + // used to linearize async modifications to state + private lock = new AwaitLock() // data feed: FeedItemModel[] = [] @@ -206,10 +214,6 @@ export class FeedModel { rootStore: false, params: false, loadMoreCursor: false, - _loadPromise: false, - _loadMorePromise: false, - _loadLatestPromise: false, - _updatePromise: false, }, {autoBind: true}, ) @@ -229,13 +233,22 @@ export class FeedModel { } get nonReplyFeed() { - return this.feed.filter( - item => + const nonReplyFeed = this.feed.filter(item => { + const params = this.params as GetAuthorFeed.QueryParams + const isRepost = + item.reply && + (item?.reasonRepost?.by?.handle === params.author || + item?.reasonRepost?.by?.did === params.author) + + return ( !item.reply || // not a reply + isRepost || ((item._isThreadParent || // but allow if it's a thread by the user item._isThreadChild) && - item.reply?.root.author.did === item.post.author.did), - ) + item.reply?.root.author.did === item.post.author.did) + ) + }) + return nonReplyFeed } setHasNewLatest(v: boolean) { @@ -245,22 +258,45 @@ export class FeedModel { // public api // = + /** + * Nuke all data + */ + clear() { + this.rootStore.log.debug('FeedModel:clear') + this.isLoading = false + this.isRefreshing = false + this.hasNewLatest = false + this.hasLoaded = false + this.error = '' + this.hasMore = true + this.loadMoreCursor = undefined + this.pollCursor = undefined + this.feed = [] + } + /** * Load for first render */ - async setup(isRefreshing = false) { + setup = bundleAsync(async (isRefreshing: boolean = false) => { + this.rootStore.log.debug('FeedModel:setup', {isRefreshing}) if (isRefreshing) { this.isRefreshing = true // set optimistically for UI } - if (this._loadPromise) { - return this._loadPromise + await this.lock.acquireAsync() + try { + this.setHasNewLatest(false) + this._xLoading(isRefreshing) + try { + const res = await this._getFeed({limit: PAGE_SIZE}) + await this._replaceAll(res) + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + } finally { + this.lock.release() } - await this._pendingWork() - this.setHasNewLatest(false) - this._loadPromise = this._initialLoad(isRefreshing) - await this._loadPromise - this._loadPromise = undefined - } + }) /** * Register any event listeners. Returns a cleanup function. @@ -280,42 +316,93 @@ export class FeedModel { /** * Load more posts to the end of the feed */ - async loadMore() { - if (this._loadMorePromise) { - return this._loadMorePromise + loadMore = bundleAsync(async () => { + await this.lock.acquireAsync() + try { + if (!this.hasMore || this.hasError) { + return + } + this._xLoading() + try { + const res = await this._getFeed({ + before: this.loadMoreCursor, + limit: PAGE_SIZE, + }) + await this._appendAll(res) + this._xIdle() + } catch (e: any) { + this._xIdle() // don't bubble the error to the user + this.rootStore.log.error('FeedView: Failed to load more', { + params: this.params, + e, + }) + } + } finally { + this.lock.release() } - await this._pendingWork() - this._loadMorePromise = this._loadMore() - await this._loadMorePromise - this._loadMorePromise = undefined - } + }) /** * Load more posts to the start of the feed */ - async loadLatest() { - if (this._loadLatestPromise) { - return this._loadLatestPromise + loadLatest = bundleAsync(async () => { + await this.lock.acquireAsync() + try { + this.setHasNewLatest(false) + this._xLoading() + try { + const res = await this._getFeed({limit: PAGE_SIZE}) + await this._prependAll(res) + this._xIdle() + } catch (e: any) { + this._xIdle() // don't bubble the error to the user + this.rootStore.log.error('FeedView: Failed to load latest', { + params: this.params, + e, + }) + } + } finally { + this.lock.release() } - await this._pendingWork() - this.setHasNewLatest(false) - this._loadLatestPromise = this._loadLatest() - await this._loadLatestPromise - this._loadLatestPromise = undefined - } + }) /** * Update content in-place */ - async update() { - if (this._updatePromise) { - return this._updatePromise + update = bundleAsync(async () => { + await this.lock.acquireAsync() + try { + if (!this.feed.length) { + return + } + this._xLoading() + let numToFetch = this.feed.length + let cursor + try { + do { + const res: GetTimeline.Response = await this._getFeed({ + before: cursor, + limit: Math.min(numToFetch, 100), + }) + if (res.data.feed.length === 0) { + break // sanity check + } + this._updateAll(res) + numToFetch -= res.data.feed.length + cursor = res.data.cursor + } while (cursor && numToFetch > 0) + this._xIdle() + } catch (e: any) { + this._xIdle() // don't bubble the error to the user + this.rootStore.log.error('FeedView: Failed to update', { + params: this.params, + e, + }) + } + } finally { + this.lock.release() } - await this._pendingWork() - this._updatePromise = this._update() - await this._updatePromise - this._updatePromise = undefined - } + }) /** * Check if new posts are available @@ -324,17 +411,18 @@ export class FeedModel { if (this.hasNewLatest) { return } - await this._pendingWork() const res = await this._getFeed({limit: 1}) const currentLatestUri = this.pollCursor - const receivedLatestUri = res.data.feed[0] - ? res.data.feed[0].post.uri - : undefined - const hasNewLatest = Boolean( - receivedLatestUri && - (this.feed.length === 0 || receivedLatestUri !== currentLatestUri), - ) - this.setHasNewLatest(hasNewLatest) + const item = res.data.feed[0] + if (!item) { + return + } + if (AppBskyFeedFeedViewPost.isReasonRepost(item.reason)) { + if (item.reason.by.did === this.rootStore.me.did) { + return // ignore reposts by the user + } + } + this.setHasNewLatest(item.post.uri !== currentLatestUri) } /** @@ -363,95 +451,15 @@ export class FeedModel { this.isLoading = false this.isRefreshing = false this.hasLoaded = true - this.error = err ? cleanError(err.toString()) : '' + this.error = cleanError(err) if (err) { this.rootStore.log.error('Posts feed request failed', err) } } - // loader functions + // helper functions // = - private async _pendingWork() { - if (this._loadPromise) { - await this._loadPromise - } - if (this._loadMorePromise) { - await this._loadMorePromise - } - if (this._loadLatestPromise) { - await this._loadLatestPromise - } - if (this._updatePromise) { - await this._updatePromise - } - } - - private async _initialLoad(isRefreshing = false) { - this._xLoading(isRefreshing) - try { - const res = await this._getFeed({limit: PAGE_SIZE}) - await this._replaceAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - } - - private async _loadLatest() { - this._xLoading() - try { - const res = await this._getFeed({limit: PAGE_SIZE}) - await this._prependAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - } - - private async _loadMore() { - if (!this.hasMore || this.hasError) { - return - } - this._xLoading() - try { - const res = await this._getFeed({ - before: this.loadMoreCursor, - limit: PAGE_SIZE, - }) - await this._appendAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - } - - private async _update() { - if (!this.feed.length) { - return - } - this._xLoading() - let numToFetch = this.feed.length - let cursor - try { - do { - const res: GetTimeline.Response = await this._getFeed({ - before: cursor, - limit: Math.min(numToFetch, 100), - }) - if (res.data.feed.length === 0) { - break // sanity check - } - this._updateAll(res) - numToFetch -= res.data.feed.length - cursor = res.data.cursor - } while (cursor && numToFetch > 0) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - } - private async _replaceAll( res: GetTimeline.Response | GetAuthorFeed.Response, ) { @@ -570,11 +578,46 @@ function preprocessFeed(feed: FeedViewPost[]): FeedViewPostWithThreadMeta[] { reorg.unshift(item) } - // phase two: identify the positions of the threads + // phase two: reorder the feed so that the timestamp of the + // last post in a thread establishes its ordering + let threadSlices: Slice[] = identifyThreadSlices(reorg) + for (const slice of threadSlices) { + const removed: FeedViewPostWithThreadMeta[] = reorg.splice( + slice.index, + slice.length, + ) + const targetDate = new Date(ts(removed[removed.length - 1])) + let newIndex = reorg.findIndex(item => new Date(ts(item)) < targetDate) + if (newIndex === -1) { + newIndex = reorg.length + } + reorg.splice(newIndex, 0, ...removed) + slice.index = newIndex + } + + // phase three: compress any threads that are longer than 3 posts + let removedCount = 0 + // phase 2 moved posts around, so we need to re-identify the slice indices + threadSlices = identifyThreadSlices(reorg) + for (const slice of threadSlices) { + if (slice.length > 3) { + reorg.splice(slice.index - removedCount + 1, slice.length - 3) + if (reorg[slice.index - removedCount]) { + // ^ sanity check + reorg[slice.index - removedCount]._isThreadChildElided = true + } + removedCount += slice.length - 3 + } + } + + return reorg +} + +function identifyThreadSlices(feed: FeedViewPost[]): Slice[] { let activeSlice = -1 let threadSlices: Slice[] = [] - for (let i = 0; i < reorg.length; i++) { - const item = reorg[i] as FeedViewPostWithThreadMeta + for (let i = 0; i < feed.length; i++) { + const item = feed[i] as FeedViewPostWithThreadMeta if (activeSlice === -1) { if (item._isThreadParent) { activeSlice = i @@ -591,39 +634,9 @@ function preprocessFeed(feed: FeedViewPost[]): FeedViewPostWithThreadMeta[] { } } if (activeSlice !== -1) { - threadSlices.push({index: activeSlice, length: reorg.length - activeSlice}) + threadSlices.push({index: activeSlice, length: feed.length - activeSlice}) } - - // phase three: reorder the feed so that the timestamp of the - // last post in a thread establishes its ordering - for (const slice of threadSlices) { - const removed: FeedViewPostWithThreadMeta[] = reorg.splice( - slice.index, - slice.length, - ) - const targetDate = new Date(ts(removed[removed.length - 1])) - let newIndex = reorg.findIndex(item => new Date(ts(item)) < targetDate) - if (newIndex === -1) { - newIndex = reorg.length - } - reorg.splice(newIndex, 0, ...removed) - slice.index = newIndex - } - - // phase four: compress any threads that are longer than 3 posts - let removedCount = 0 - for (const slice of threadSlices) { - if (slice.length > 3) { - reorg.splice(slice.index - removedCount + 1, slice.length - 3) - if (reorg[slice.index - removedCount]) { - // ^ sanity check - reorg[slice.index - removedCount]._isThreadChildElided = true - } - removedCount += slice.length - 3 - } - } - - return reorg + return threadSlices } // WARNING: mutates `feed` diff --git a/src/state/models/get-assertions-view.ts b/src/state/models/get-assertions-view.ts deleted file mode 100644 index bdb2c089..00000000 --- a/src/state/models/get-assertions-view.ts +++ /dev/null @@ -1,123 +0,0 @@ -import {makeAutoObservable} from 'mobx' -import {AppBskyGraphGetAssertions as GetAssertions} from '@atproto/api' -import {RootStoreModel} from './root-store' - -export type Assertion = GetAssertions.Assertion & { - _reactKey: string -} - -export class GetAssertionsView { - // state - isLoading = false - isRefreshing = false - hasLoaded = false - error = '' - params: GetAssertions.QueryParams - - // data - assertions: Assertion[] = [] - - constructor( - public rootStore: RootStoreModel, - params: GetAssertions.QueryParams, - ) { - makeAutoObservable( - this, - { - rootStore: false, - params: false, - }, - {autoBind: true}, - ) - this.params = params - } - - get hasContent() { - return this.assertions.length > 0 - } - - get hasError() { - return this.error !== '' - } - - get isEmpty() { - return this.hasLoaded && !this.hasContent - } - - getBySubject(did: string) { - return this.assertions.find(assertion => assertion.subject.did === did) - } - - get confirmed() { - return this.assertions.filter(assertion => !!assertion.confirmation) - } - - get unconfirmed() { - return this.assertions.filter(assertion => !assertion.confirmation) - } - - // public api - // = - - async setup() { - await this._fetch() - } - - async refresh() { - await this._fetch(true) - } - - async loadMore() { - // TODO - } - - // state transitions - // = - - private _xLoading(isRefreshing = false) { - this.isLoading = true - this.isRefreshing = isRefreshing - this.error = '' - } - - private _xIdle(err?: any) { - this.isLoading = false - this.isRefreshing = false - this.hasLoaded = true - this.error = err ? err.toString() : '' - if (err) { - this.rootStore.log.error('Failed to fetch assertions', err) - } - } - - // loader functions - // = - - private async _fetch(isRefreshing = false) { - this._xLoading(isRefreshing) - try { - const res = await this.rootStore.api.app.bsky.graph.getAssertions( - this.params, - ) - this._replaceAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - } - - private _replaceAll(res: GetAssertions.Response) { - this.assertions.length = 0 - let counter = 0 - for (const item of res.data.assertions) { - this._append({ - _reactKey: `item-${counter++}`, - ...item, - }) - } - } - - private _append(item: Assertion) { - this.assertions.push(item) - } -} diff --git a/src/state/models/link-metas-view.ts b/src/state/models/link-metas-view.ts index 6b787987..59447008 100644 --- a/src/state/models/link-metas-view.ts +++ b/src/state/models/link-metas-view.ts @@ -1,7 +1,7 @@ import {makeAutoObservable} from 'mobx' import {LRUMap} from 'lru_map' import {RootStoreModel} from './root-store' -import {LinkMeta, getLinkMeta} from '../../lib/link-meta' +import {LinkMeta, getLinkMeta} from 'lib/link-meta/link-meta' type CacheValue = Promise | LinkMeta export class LinkMetasViewModel { diff --git a/src/state/models/log.ts b/src/state/models/log.ts index 67f4a210..f2709f2f 100644 --- a/src/state/models/log.ts +++ b/src/state/models/log.ts @@ -1,6 +1,6 @@ import {makeAutoObservable} from 'mobx' import {XRPCError, XRPCInvalidResponseError} from '@atproto/xrpc' -import {isObj, hasProp} from '../lib/type-guards' +import {isObj, hasProp} from 'lib/type-guards' interface LogEntry { id: string diff --git a/src/state/models/me.ts b/src/state/models/me.ts index 0d0c1d1d..0cb84c9f 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -1,10 +1,9 @@ import {makeAutoObservable, runInAction} from 'mobx' -import notifee from '@notifee/react-native' import {RootStoreModel} from './root-store' import {FeedModel} from './feed-view' import {NotificationsViewModel} from './notifications-view' -import {isObj, hasProp} from '../lib/type-guards' -import {displayNotificationFromModel} from '../../view/lib/notifee' +import {MyFollowsModel} from './my-follows' +import {isObj, hasProp} from 'lib/type-guards' export class MeModel { did: string = '' @@ -12,9 +11,9 @@ export class MeModel { displayName: string = '' description: string = '' avatar: string = '' - notificationCount: number = 0 mainFeed: FeedModel notifications: NotificationsViewModel + follows: MyFollowsModel constructor(public rootStore: RootStoreModel) { makeAutoObservable( @@ -26,15 +25,17 @@ export class MeModel { algorithm: 'reverse-chronological', }) this.notifications = new NotificationsViewModel(this.rootStore, {}) + this.follows = new MyFollowsModel(this.rootStore) } clear() { + this.mainFeed.clear() + this.notifications.clear() this.did = '' this.handle = '' this.displayName = '' this.description = '' this.avatar = '' - this.notificationCount = 0 } serialize(): unknown { @@ -77,9 +78,10 @@ export class MeModel { async load() { const sess = this.rootStore.session - if (sess.hasSession && sess.data) { - this.did = sess.data.did || '' - this.handle = sess.data.handle + this.rootStore.log.debug('MeModel:load', {hasSession: sess.hasSession}) + if (sess.hasSession) { + this.did = sess.currentSession?.did || '' + this.handle = sess.currentSession?.handle || '' const profile = await this.rootStore.api.app.bsky.actor.getProfile({ actor: this.did, }) @@ -94,10 +96,6 @@ export class MeModel { this.avatar = '' } }) - this.mainFeed = new FeedModel(this.rootStore, 'home', { - algorithm: 'reverse-chronological', - }) - this.notifications = new NotificationsViewModel(this.rootStore, {}) await Promise.all([ this.mainFeed.setup().catch(e => { this.rootStore.log.error('Failed to setup main feed model', e) @@ -105,51 +103,13 @@ export class MeModel { this.notifications.setup().catch(e => { this.rootStore.log.error('Failed to setup notifications model', e) }), + this.follows.fetch().catch(e => { + this.rootStore.log.error('Failed to load my follows', e) + }), ]) - - // request notifications permission once the user has logged in - notifee.requestPermission() + this.rootStore.emitSessionLoaded() } else { this.clear() } } - - clearNotificationCount() { - this.notificationCount = 0 - notifee.setBadgeCount(0) - } - - async fetchNotifications() { - const res = await this.rootStore.api.app.bsky.notification.getCount() - runInAction(() => { - const newNotifications = this.notificationCount !== res.data.count - this.notificationCount = res.data.count - notifee.setBadgeCount(this.notificationCount) - if (newNotifications) { - this.notifications.refresh() - } - }) - } - - async bgFetchNotifications() { - const res = await this.rootStore.api.app.bsky.notification.getCount() - // NOTE we don't update this.notificationCount to avoid repaints during bg - // this means `newNotifications` may not be accurate, so we rely on - // `mostRecent` to determine if there really is a new notif to show -prf - const newNotifications = this.notificationCount !== res.data.count - notifee.setBadgeCount(res.data.count) - this.rootStore.log.debug( - `Background fetch received unread count = ${res.data.count}`, - ) - if (newNotifications) { - this.rootStore.log.debug( - 'Background fetch detected potentially a new notification', - ) - const mostRecent = await this.notifications.getNewMostRecent() - if (mostRecent) { - this.rootStore.log.debug('Got the notification, triggering a push') - displayNotificationFromModel(mostRecent) - } - } - } } diff --git a/src/state/models/my-follows.ts b/src/state/models/my-follows.ts new file mode 100644 index 00000000..252e8a3d --- /dev/null +++ b/src/state/models/my-follows.ts @@ -0,0 +1,109 @@ +import {makeAutoObservable, runInAction} from 'mobx' +import {FollowRecord, AppBskyActorProfile, AppBskyActorRef} from '@atproto/api' +import {RootStoreModel} from './root-store' +import {bundleAsync} from 'lib/async/bundle' + +const CACHE_TTL = 1000 * 60 * 60 // hourly +type FollowsListResponse = Awaited> +type FollowsListResponseRecord = FollowsListResponse['records'][0] +type Profile = + | AppBskyActorProfile.ViewBasic + | AppBskyActorProfile.View + | AppBskyActorRef.WithInfo + +/** + * This model is used to maintain a synced local cache of the user's + * follows. It should be periodically refreshed and updated any time + * the user makes a change to their follows. + */ +export class MyFollowsModel { + // data + followDidToRecordMap: Record = {} + lastSync = 0 + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + // public api + // = + + fetchIfNeeded = bundleAsync(async () => { + if ( + Object.keys(this.followDidToRecordMap).length === 0 || + Date.now() - this.lastSync > CACHE_TTL + ) { + return await this.fetch() + } + }) + + fetch = bundleAsync(async () => { + this.rootStore.log.debug('MyFollowsModel:fetch running full fetch') + let before + let records: FollowsListResponseRecord[] = [] + do { + const res: FollowsListResponse = + await this.rootStore.api.app.bsky.graph.follow.list({ + user: this.rootStore.me.did, + before, + }) + records = records.concat(res.records) + before = res.cursor + } while (typeof before !== 'undefined') + runInAction(() => { + this.followDidToRecordMap = {} + for (const record of records) { + this.followDidToRecordMap[record.value.subject.did] = record.uri + } + this.lastSync = Date.now() + }) + }) + + isFollowing(did: string) { + return !!this.followDidToRecordMap[did] + } + + getFollowUri(did: string): string { + const v = this.followDidToRecordMap[did] + if (!v) { + throw new Error('Not a followed user') + } + return v + } + + addFollow(did: string, recordUri: string) { + this.followDidToRecordMap[did] = recordUri + } + + removeFollow(did: string) { + delete this.followDidToRecordMap[did] + } + + /** + * Use this to incrementally update the cache as views provide information + */ + hydrate(did: string, recordUri: string | undefined) { + if (recordUri) { + this.followDidToRecordMap[did] = recordUri + } else { + delete this.followDidToRecordMap[did] + } + } + + /** + * Use this to incrementally update the cache as views provide information + */ + hydrateProfiles(profiles: Profile[]) { + for (const profile of profiles) { + if (profile.viewer) { + this.hydrate(profile.did, profile.viewer.following) + } + } + } +} diff --git a/src/state/models/navigation.ts b/src/state/models/navigation.ts index 224ffef0..feb03b36 100644 --- a/src/state/models/navigation.ts +++ b/src/state/models/navigation.ts @@ -1,5 +1,7 @@ +import {RootStoreModel} from './root-store' import {makeAutoObservable} from 'mobx' -import {TABS_ENABLED} from '../../build-flags' +import {TABS_ENABLED} from 'lib/build-flags' +import {segmentClient} from 'lib/analytics' let __id = 0 function genId() { @@ -11,13 +13,20 @@ function genId() { // we've since decided to pause that idea and do something more traditional // until we're fully sure what that is, the tabs are being repurposed into a fixed topology // - Tab 0: The "Default" tab -// - Tab 1: The "Notifications" tab +// - Tab 1: The "Search" tab +// - Tab 2: The "Notifications" tab // These tabs always retain the first item in their history. -// The default tab is used for basically everything except notifications. // -prf export enum TabPurpose { Default = 0, - Notifs = 1, + Search = 1, + Notifs = 2, +} + +export const TabPurposeMainPath: Record = { + [TabPurpose.Default]: '/', + [TabPurpose.Search]: '/search', + [TabPurpose.Notifs]: '/notifications', } interface HistoryItem { @@ -36,11 +45,9 @@ export class NavigationTabModel { isNewTab = false constructor(public fixedTabPurpose: TabPurpose) { - if (fixedTabPurpose === TabPurpose.Notifs) { - this.history = [{url: '/notifications', ts: Date.now(), id: genId()}] - } else { - this.history = [{url: '/', ts: Date.now(), id: genId()}] - } + this.history = [ + {url: TabPurposeMainPath[fixedTabPurpose], ts: Date.now(), id: genId()}, + ] makeAutoObservable(this, { serialize: false, hydrate: false, @@ -96,6 +103,13 @@ export class NavigationTabModel { // = navigate(url: string, title?: string) { + try { + const path = url.split('/')[1] + segmentClient.track('Navigation', { + path, + }) + } catch (error) {} + if (this.current?.url === url) { this.refresh() } else { @@ -104,8 +118,7 @@ export class NavigationTabModel { } // TEMP ensure the tab has its purpose's main view -prf if (this.history.length < 1) { - const fixedUrl = - this.fixedTabPurpose === TabPurpose.Notifs ? '/notifications' : '/' + const fixedUrl = TabPurposeMainPath[this.fixedTabPurpose] this.history.push({url: fixedUrl, ts: Date.now(), id: genId()}) } this.history.push({url, title, ts: Date.now(), id: genId()}) @@ -211,12 +224,14 @@ export class NavigationTabModel { export class NavigationModel { tabs: NavigationTabModel[] = [ new NavigationTabModel(TabPurpose.Default), + new NavigationTabModel(TabPurpose.Search), new NavigationTabModel(TabPurpose.Notifs), ] tabIndex = 0 - constructor() { + constructor(public rootStore: RootStoreModel) { makeAutoObservable(this, { + rootStore: false, serialize: false, hydrate: false, }) @@ -225,6 +240,7 @@ export class NavigationModel { clear() { this.tabs = [ new NavigationTabModel(TabPurpose.Default), + new NavigationTabModel(TabPurpose.Search), new NavigationTabModel(TabPurpose.Notifs), ] this.tabIndex = 0 @@ -249,6 +265,7 @@ export class NavigationModel { // = navigate(url: string, title?: string) { + this.rootStore.emitNavigation() this.tab.navigate(url, title) } @@ -286,10 +303,16 @@ export class NavigationModel { // fixed tab helper function // -prf switchTo(purpose: TabPurpose, reset: boolean) { - if (purpose === TabPurpose.Notifs) { - this.tabIndex = 1 - } else { - this.tabIndex = 0 + this.rootStore.emitNavigation() + switch (purpose) { + case TabPurpose.Notifs: + this.tabIndex = 2 + break + case TabPurpose.Search: + this.tabIndex = 1 + break + default: + this.tabIndex = 0 } if (reset) { this.tab.fixedTabReset() diff --git a/src/state/models/notifications-view.ts b/src/state/models/notifications-view.ts index 93b6a398..048de968 100644 --- a/src/state/models/notifications-view.ts +++ b/src/state/models/notifications-view.ts @@ -8,11 +8,13 @@ import { AppBskyGraphAssertion, AppBskyGraphFollow, } from '@atproto/api' +import AwaitLock from 'await-lock' +import {bundleAsync} from 'lib/async/bundle' import {RootStoreModel} from './root-store' import {PostThreadViewModel} from './post-thread-view' -import {cleanError} from '../../lib/strings' +import {cleanError} from 'lib/strings/errors' -const UNGROUPABLE_REASONS = ['assertion'] +const GROUPABLE_REASONS = ['vote', 'repost', 'follow'] const PAGE_SIZE = 30 const MS_1HR = 1e3 * 60 * 60 const MS_2DAY = MS_1HR * 48 @@ -190,15 +192,16 @@ export class NotificationsViewModel { params: ListNotifications.QueryParams hasMore = true loadMoreCursor?: string - _loadPromise: Promise | undefined - _loadMorePromise: Promise | undefined - _updatePromise: Promise | undefined + + // used to linearize async modifications to state + private lock = new AwaitLock() // data notifications: NotificationsViewItemModel[] = [] + unreadCount = 0 // this is used to help trigger push notifications - mostRecentNotification: NotificationsViewItemModel | undefined + mostRecentNotificationUri: string | undefined constructor( public rootStore: RootStoreModel, @@ -209,10 +212,7 @@ export class NotificationsViewModel { { rootStore: false, params: false, - mostRecentNotification: false, - _loadPromise: false, - _loadMorePromise: false, - _updatePromise: false, + mostRecentNotificationUri: false, }, {autoBind: true}, ) @@ -234,21 +234,48 @@ export class NotificationsViewModel { // public api // = + /** + * Nuke all data + */ + clear() { + this.rootStore.log.debug('NotificationsModel:clear') + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = false + this.error = '' + this.hasMore = true + this.loadMoreCursor = undefined + this.notifications = [] + this.unreadCount = 0 + this.rootStore.emitUnreadNotifications(0) + this.mostRecentNotificationUri = undefined + } + /** * Load for first render */ - async setup(isRefreshing = false) { + setup = bundleAsync(async (isRefreshing: boolean = false) => { + this.rootStore.log.debug('NotificationsModel:setup', {isRefreshing}) if (isRefreshing) { this.isRefreshing = true // set optimistically for UI } - if (this._loadPromise) { - return this._loadPromise + await this.lock.acquireAsync() + try { + this._xLoading(isRefreshing) + try { + const params = Object.assign({}, this.params, { + limit: PAGE_SIZE, + }) + const res = await this.rootStore.api.app.bsky.notification.list(params) + await this._replaceAll(res) + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + } finally { + this.lock.release() } - await this._pendingWork() - this._loadPromise = this._initialLoad(isRefreshing) - await this._loadPromise - this._loadPromise = undefined - } + }) /** * Reset and load @@ -260,59 +287,148 @@ export class NotificationsViewModel { /** * Load more posts to the end of the notifications */ - async loadMore() { - if (this._loadMorePromise) { - return this._loadMorePromise + loadMore = bundleAsync(async () => { + if (!this.hasMore) { + return } - await this._pendingWork() - this._loadMorePromise = this._loadMore() - await this._loadMorePromise - this._loadMorePromise = undefined - } + this.lock.acquireAsync() + try { + this._xLoading() + try { + const params = Object.assign({}, this.params, { + limit: PAGE_SIZE, + before: this.loadMoreCursor, + }) + const res = await this.rootStore.api.app.bsky.notification.list(params) + await this._appendAll(res) + this._xIdle() + } catch (e: any) { + this._xIdle() // don't bubble the error to the user + this.rootStore.log.error('NotificationsView: Failed to load more', { + params: this.params, + e, + }) + } + } finally { + this.lock.release() + } + }) + + /** + * Load more posts at the start of the notifications + */ + loadLatest = bundleAsync(async () => { + if (this.notifications.length === 0 || this.unreadCount > PAGE_SIZE) { + return this.refresh() + } + this.lock.acquireAsync() + try { + this._xLoading() + try { + const res = await this.rootStore.api.app.bsky.notification.list({ + limit: PAGE_SIZE, + }) + await this._prependAll(res) + this._xIdle() + } catch (e: any) { + this._xIdle() // don't bubble the error to the user + this.rootStore.log.error('NotificationsView: Failed to load latest', { + params: this.params, + e, + }) + } + } finally { + this.lock.release() + } + }) /** * Update content in-place */ - async update() { - if (this._updatePromise) { - return this._updatePromise + update = bundleAsync(async () => { + await this.lock.acquireAsync() + try { + if (!this.notifications.length) { + return + } + this._xLoading() + let numToFetch = this.notifications.length + let cursor + try { + do { + const res: ListNotifications.Response = + await this.rootStore.api.app.bsky.notification.list({ + before: cursor, + limit: Math.min(numToFetch, 100), + }) + if (res.data.notifications.length === 0) { + break // sanity check + } + this._updateAll(res) + numToFetch -= res.data.notifications.length + cursor = res.data.cursor + } while (cursor && numToFetch > 0) + this._xIdle() + } catch (e: any) { + this._xIdle() // don't bubble the error to the user + this.rootStore.log.error('NotificationsView: Failed to update', { + params: this.params, + e, + }) + } + } finally { + this.lock.release() } - await this._pendingWork() - this._updatePromise = this._update() - await this._updatePromise - this._updatePromise = undefined - } + }) + + // unread notification apis + // = + + /** + * Get the current number of unread notifications + * returns true if the number changed + */ + loadUnreadCount = bundleAsync(async () => { + const old = this.unreadCount + const res = await this.rootStore.api.app.bsky.notification.getCount() + runInAction(() => { + this.unreadCount = res.data.count + }) + this.rootStore.emitUnreadNotifications(this.unreadCount) + return this.unreadCount !== old + }) /** * Update read/unread state */ - async updateReadState() { + async markAllRead() { try { + this.unreadCount = 0 + this.rootStore.emitUnreadNotifications(0) await this.rootStore.api.app.bsky.notification.updateSeen({ seenAt: new Date().toISOString(), }) - this.rootStore.me.clearNotificationCount() } catch (e: any) { this.rootStore.log.warn('Failed to update notifications read state', e) } } async getNewMostRecent(): Promise { - let old = this.mostRecentNotification - const res = await this.rootStore.api.app.bsky.notification.list({limit: 1}) - if ( - !res.data.notifications[0] || - old?.uri === res.data.notifications[0].uri - ) { + let old = this.mostRecentNotificationUri + const res = await this.rootStore.api.app.bsky.notification.list({ + limit: 1, + }) + if (!res.data.notifications[0] || old === res.data.notifications[0].uri) { return } - this.mostRecentNotification = new NotificationsViewItemModel( + this.mostRecentNotificationUri = res.data.notifications[0].uri + const notif = new NotificationsViewItemModel( this.rootStore, 'mostRecent', res.data.notifications[0], ) - await this.mostRecentNotification.fetchAdditionalData() - return this.mostRecentNotification + await notif.fetchAdditionalData() + return notif } // state transitions @@ -329,93 +445,17 @@ export class NotificationsViewModel { this.isRefreshing = false this.hasLoaded = true this.error = cleanError(err) - this.error = err ? cleanError(err) : '' if (err) { this.rootStore.log.error('Failed to fetch notifications', err) } } - // loader functions + // helper functions // = - private async _pendingWork() { - if (this._loadPromise) { - await this._loadPromise - } - if (this._loadMorePromise) { - await this._loadMorePromise - } - if (this._updatePromise) { - await this._updatePromise - } - } - - private async _initialLoad(isRefreshing = false) { - this._xLoading(isRefreshing) - try { - const params = Object.assign({}, this.params, { - limit: PAGE_SIZE, - }) - const res = await this.rootStore.api.app.bsky.notification.list(params) - await this._replaceAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - } - - private async _loadMore() { - if (!this.hasMore) { - return - } - this._xLoading() - try { - const params = Object.assign({}, this.params, { - limit: PAGE_SIZE, - before: this.loadMoreCursor, - }) - const res = await this.rootStore.api.app.bsky.notification.list(params) - await this._appendAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - } - - private async _update() { - if (!this.notifications.length) { - return - } - this._xLoading() - let numToFetch = this.notifications.length - let cursor - try { - do { - const res: ListNotifications.Response = - await this.rootStore.api.app.bsky.notification.list({ - before: cursor, - limit: Math.min(numToFetch, 100), - }) - if (res.data.notifications.length === 0) { - break // sanity check - } - this._updateAll(res) - numToFetch -= res.data.notifications.length - cursor = res.data.cursor - } while (cursor && numToFetch > 0) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - } - private async _replaceAll(res: ListNotifications.Response) { if (res.data.notifications[0]) { - this.mostRecentNotification = new NotificationsViewItemModel( - this.rootStore, - 'mostRecent', - res.data.notifications[0], - ) + this.mostRecentNotificationUri = res.data.notifications[0].uri } return this._appendAll(res, true) } @@ -451,14 +491,40 @@ export class NotificationsViewModel { }) } + private async _prependAll(res: ListNotifications.Response) { + const promises = [] + const itemModels: NotificationsViewItemModel[] = [] + const dedupedNotifs = res.data.notifications.filter( + n1 => + !this.notifications.find( + n2 => isEq(n1, n2) || n2.additional?.find(n3 => isEq(n1, n3)), + ), + ) + for (const item of groupNotifications(dedupedNotifs)) { + const itemModel = new NotificationsViewItemModel( + this.rootStore, + `item-${_idCounter++}`, + item, + ) + if (itemModel.needsAdditionalData) { + promises.push(itemModel.fetchAdditionalData()) + } + itemModels.push(itemModel) + } + await Promise.all(promises).catch(e => { + this.rootStore.log.error( + 'Uncaught failure during notifications-view _prependAll()', + e, + ) + }) + runInAction(() => { + this.notifications = itemModels.concat(this.notifications) + }) + } + private _updateAll(res: ListNotifications.Response) { for (const item of res.data.notifications) { - const existingItem = this.notifications.find( - // this find function has a key subtlety- the indexedAt comparison - // the reason for this is reposts: they set the URI of the original post, not of the repost record - // the indexedAt time will be for the repost however, so we use that to help us - item2 => item.uri === item2.uri && item.indexedAt === item2.indexedAt, - ) + const existingItem = this.notifications.find(item2 => isEq(item, item2)) if (existingItem) { existingItem.copy(item, true) } @@ -473,7 +539,7 @@ function groupNotifications( for (const item of items) { const ts = +new Date(item.indexedAt) let grouped = false - if (!UNGROUPABLE_REASONS.includes(item.reason)) { + if (GROUPABLE_REASONS.includes(item.reason)) { for (const item2 of items2) { const ts2 = +new Date(item2.indexedAt) if ( @@ -495,3 +561,11 @@ function groupNotifications( } return items2 } + +type N = ListNotifications.Notification | NotificationsViewItemModel +function isEq(a: N, b: N) { + // this function has a key subtlety- the indexedAt comparison + // the reason for this is reposts: they set the URI of the original post, not of the repost record + // the indexedAt time will be for the repost however, so we use that to help us + return a.uri === b.uri && a.indexedAt === b.indexedAt +} diff --git a/src/state/models/onboard.ts b/src/state/models/onboard.ts index 5ab5ecb6..aa275c6b 100644 --- a/src/state/models/onboard.ts +++ b/src/state/models/onboard.ts @@ -1,5 +1,5 @@ import {makeAutoObservable} from 'mobx' -import {isObj, hasProp} from '../lib/type-guards' +import {isObj, hasProp} from 'lib/type-guards' export const OnboardStage = { Explainers: 'explainers', diff --git a/src/state/models/post-thread-view.ts b/src/state/models/post-thread-view.ts index 584658e1..ad989cc5 100644 --- a/src/state/models/post-thread-view.ts +++ b/src/state/models/post-thread-view.ts @@ -5,7 +5,9 @@ import { } from '@atproto/api' import {AtUri} from '../../third-party/uri' import {RootStoreModel} from './root-store' -import * as apilib from '../lib/api' +import * as apilib from 'lib/api/index' +import {cleanError} from 'lib/strings/errors' +import {RichText} from 'lib/strings/rich-text' function* reactKeyGenerator(): Generator { let counter = 0 @@ -26,6 +28,7 @@ export class PostThreadViewPostModel { postRecord?: FeedPost.Record parent?: PostThreadViewPostModel | GetPostThread.NotFoundPost replies?: (PostThreadViewPostModel | GetPostThread.NotFoundPost)[] + richText?: RichText constructor( public rootStore: RootStoreModel, @@ -38,6 +41,11 @@ export class PostThreadViewPostModel { const valid = FeedPost.validateRecord(this.post.record) if (valid.success) { this.postRecord = this.post.record + this.richText = new RichText( + this.postRecord.text, + this.postRecord.entities, + {cleanNewlines: true}, + ) } else { rootStore.log.warn( 'Received an invalid app.bsky.feed.post record', @@ -276,7 +284,7 @@ export class PostThreadViewModel { this.isLoading = false this.isRefreshing = false this.hasLoaded = true - this.error = err ? err.toString() : '' + this.error = cleanError(err) if (err) { this.rootStore.log.error('Failed to fetch post thread', err) } @@ -290,7 +298,7 @@ export class PostThreadViewModel { const urip = new AtUri(this.params.uri) if (!urip.host.startsWith('did:')) { try { - urip.host = await this.rootStore.resolveName(urip.host) + urip.host = await apilib.resolveName(this.rootStore, urip.host) } catch (e: any) { this.error = e.toString() } @@ -314,7 +322,7 @@ export class PostThreadViewModel { } private _replaceAll(res: GetPostThread.Response) { - // sortThread(res.data.thread) TODO needed? + sortThread(res.data.thread) const keyGen = reactKeyGenerator() const thread = new PostThreadViewPostModel( this.rootStore, @@ -330,36 +338,37 @@ export class PostThreadViewModel { } } -/* -TODO needed? +type MaybePost = + | GetPostThread.ThreadViewPost + | GetPostThread.NotFoundPost + | {[k: string]: unknown; $type: string} function sortThread(post: MaybePost) { if (post.notFound) { return } - post = post as GetPostThread.Post + post = post as GetPostThread.ThreadViewPost if (post.replies) { post.replies.sort((a: MaybePost, b: MaybePost) => { - post = post as GetPostThread.Post + post = post as GetPostThread.ThreadViewPost if (a.notFound) { return 1 } if (b.notFound) { return -1 } - a = a as GetPostThread.Post - b = b as GetPostThread.Post - const aIsByOp = a.author.did === post.author.did - const bIsByOp = b.author.did === post.author.did + a = a as GetPostThread.ThreadViewPost + b = b as GetPostThread.ThreadViewPost + const aIsByOp = a.post.author.did === post.post.author.did + const bIsByOp = b.post.author.did === post.post.author.did if (aIsByOp && bIsByOp) { - return a.indexedAt.localeCompare(b.indexedAt) // oldest + return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest } else if (aIsByOp) { return -1 // op's own reply } else if (bIsByOp) { return 1 // op's own reply } - return b.indexedAt.localeCompare(a.indexedAt) // newest + return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest }) post.replies.forEach(reply => sortThread(reply)) } } -*/ diff --git a/src/state/models/post.ts b/src/state/models/post.ts index 497c8e4c..749e98bb 100644 --- a/src/state/models/post.ts +++ b/src/state/models/post.ts @@ -2,7 +2,7 @@ import {makeAutoObservable} from 'mobx' import {AppBskyFeedPost as Post} from '@atproto/api' import {AtUri} from '../../third-party/uri' import {RootStoreModel} from './root-store' -import {cleanError} from '../../lib/strings' +import {cleanError} from 'lib/strings/errors' type RemoveIndex = { [P in keyof T as string extends P @@ -67,7 +67,6 @@ export class PostModel implements RemoveIndex { this.isLoading = false this.hasLoaded = true this.error = cleanError(err) - this.error = err ? cleanError(err) : '' if (err) { this.rootStore.log.error('Failed to fetch post', err) } diff --git a/src/state/models/profile-view.ts b/src/state/models/profile-view.ts index 79882a56..8630eae5 100644 --- a/src/state/models/profile-view.ts +++ b/src/state/models/profile-view.ts @@ -1,22 +1,23 @@ import {makeAutoObservable, runInAction} from 'mobx' -import {Image as PickedImage} from '../../view/com/util/images/image-crop-picker/ImageCropPicker' +import {PickedMedia} from 'view/com/util/images/image-crop-picker/ImageCropPicker' import { AppBskyActorGetProfile as GetProfile, AppBskyActorProfile as Profile, AppBskySystemDeclRef, - AppBskyFeedPost, } from '@atproto/api' type DeclRef = AppBskySystemDeclRef.Main -type Entity = AppBskyFeedPost.Entity -import {extractEntities} from '../../lib/strings' +import {extractEntities} from 'lib/strings/rich-text-detection' import {RootStoreModel} from './root-store' -import * as apilib from '../lib/api' +import * as apilib from 'lib/api/index' +import {cleanError} from 'lib/strings/errors' +import {RichText} from 'lib/strings/rich-text' export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser' -export class ProfileViewMyStateModel { - follow?: string +export class ProfileViewViewerModel { muted?: boolean + following?: string + followedBy?: string constructor() { makeAutoObservable(this) @@ -46,10 +47,10 @@ export class ProfileViewModel { followersCount: number = 0 followsCount: number = 0 postsCount: number = 0 - myState = new ProfileViewMyStateModel() + viewer = new ProfileViewViewerModel() // added data - descriptionEntities?: Entity[] + descriptionRichText?: RichText constructor( public rootStore: RootStoreModel, @@ -97,11 +98,24 @@ export class ProfileViewModel { if (!this.rootStore.me.did) { throw new Error('Not logged in') } - if (this.myState.follow) { - await apilib.unfollow(this.rootStore, this.myState.follow) + + const follows = this.rootStore.me.follows + const followUri = follows.isFollowing(this.did) + ? follows.getFollowUri(this.did) + : undefined + + // guard against this view getting out of sync with the follows cache + if (followUri !== this.viewer.following) { + this.viewer.following = followUri + return + } + + if (followUri) { + await apilib.unfollow(this.rootStore, followUri) runInAction(() => { this.followersCount-- - this.myState.follow = undefined + this.viewer.following = undefined + this.rootStore.me.follows.removeFollow(this.did) }) } else { const res = await apilib.follow( @@ -111,15 +125,16 @@ export class ProfileViewModel { ) runInAction(() => { this.followersCount++ - this.myState.follow = res.uri + this.viewer.following = res.uri + this.rootStore.me.follows.addFollow(this.did, res.uri) }) } } async updateProfile( updates: Profile.Record, - newUserAvatar: PickedImage | undefined, - newUserBanner: PickedImage | undefined, + newUserAvatar: PickedMedia | undefined, + newUserBanner: PickedMedia | undefined, ) { if (newUserAvatar) { const res = await this.rootStore.api.com.atproto.blob.upload( @@ -152,13 +167,13 @@ export class ProfileViewModel { async muteAccount() { await this.rootStore.api.app.bsky.graph.mute({user: this.did}) - this.myState.muted = true + this.viewer.muted = true await this.refresh() } async unmuteAccount() { await this.rootStore.api.app.bsky.graph.unmute({user: this.did}) - this.myState.muted = false + this.viewer.muted = false await this.refresh() } @@ -175,7 +190,7 @@ export class ProfileViewModel { this.isLoading = false this.isRefreshing = false this.hasLoaded = true - this.error = err ? err.toString() : '' + this.error = cleanError(err) if (err) { this.rootStore.log.error('Failed to fetch profile', err) } @@ -210,9 +225,14 @@ export class ProfileViewModel { this.followersCount = res.data.followersCount this.followsCount = res.data.followsCount this.postsCount = res.data.postsCount - if (res.data.myState) { - Object.assign(this.myState, res.data.myState) + if (res.data.viewer) { + Object.assign(this.viewer, res.data.viewer) + this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following) } - this.descriptionEntities = extractEntities(this.description || '') + this.descriptionRichText = new RichText( + this.description || '', + extractEntities(this.description || ''), + {cleanNewlines: true}, + ) } } diff --git a/src/state/models/profiles-view.ts b/src/state/models/profiles-view.ts index 804491c8..4241e50e 100644 --- a/src/state/models/profiles-view.ts +++ b/src/state/models/profiles-view.ts @@ -31,7 +31,9 @@ export class ProfilesViewModel { } } try { - const promise = this.rootStore.api.app.bsky.actor.getProfile({actor: did}) + const promise = this.rootStore.api.app.bsky.actor.getProfile({ + actor: did, + }) this.cache.set(did, promise) const res = await promise this.cache.set(did, res) diff --git a/src/state/models/reposted-by-view.ts b/src/state/models/reposted-by-view.ts index 1de6b7c5..69a728d6 100644 --- a/src/state/models/reposted-by-view.ts +++ b/src/state/models/reposted-by-view.ts @@ -1,11 +1,17 @@ import {makeAutoObservable, runInAction} from 'mobx' import {AtUri} from '../../third-party/uri' -import {AppBskyFeedGetRepostedBy as GetRepostedBy} from '@atproto/api' +import { + AppBskyFeedGetRepostedBy as GetRepostedBy, + AppBskyActorRef as ActorRef, +} from '@atproto/api' import {RootStoreModel} from './root-store' +import {bundleAsync} from 'lib/async/bundle' +import {cleanError} from 'lib/strings/errors' +import * as apilib from 'lib/api/index' const PAGE_SIZE = 30 -export type RepostedByItem = GetRepostedBy.RepostedBy +export type RepostedByItem = ActorRef.WithInfo export class RepostedByViewModel { // state @@ -17,7 +23,6 @@ export class RepostedByViewModel { params: GetRepostedBy.QueryParams hasMore = true loadMoreCursor?: string - private _loadMorePromise: Promise | undefined // data uri: string = '' @@ -57,17 +62,28 @@ export class RepostedByViewModel { return this.loadMore(true) } - async loadMore(isRefreshing = false) { - if (this._loadMorePromise) { - return this._loadMorePromise + loadMore = bundleAsync(async (replace: boolean = false) => { + this._xLoading(replace) + try { + if (!this.resolvedUri) { + await this._resolveUri() + } + const params = Object.assign({}, this.params, { + uri: this.resolvedUri, + limit: PAGE_SIZE, + before: replace ? undefined : this.loadMoreCursor, + }) + const res = await this.rootStore.api.app.bsky.feed.getRepostedBy(params) + if (replace) { + this._replaceAll(res) + } else { + this._appendAll(res) + } + this._xIdle() + } catch (e: any) { + this._xIdle(e) } - if (!this.resolvedUri) { - await this._resolveUri() - } - this._loadMorePromise = this._loadMore(isRefreshing) - await this._loadMorePromise - this._loadMorePromise = undefined - } + }) // state transitions // = @@ -82,20 +98,20 @@ export class RepostedByViewModel { this.isLoading = false this.isRefreshing = false this.hasLoaded = true - this.error = err ? err.toString() : '' + this.error = cleanError(err) if (err) { this.rootStore.log.error('Failed to fetch reposted by view', err) } } - // loader functions + // helper functions // = private async _resolveUri() { const urip = new AtUri(this.params.uri) if (!urip.host.startsWith('did:')) { try { - urip.host = await this.rootStore.resolveName(urip.host) + urip.host = await apilib.resolveName(this.rootStore, urip.host) } catch (e: any) { this.error = e.toString() } @@ -105,28 +121,15 @@ export class RepostedByViewModel { }) } - private async _loadMore(isRefreshing = false) { - this._xLoading(isRefreshing) - try { - const params = Object.assign({}, this.params, { - uri: this.resolvedUri, - limit: PAGE_SIZE, - before: this.loadMoreCursor, - }) - if (this.isRefreshing) { - this.repostedBy = [] - } - const res = await this.rootStore.api.app.bsky.feed.getRepostedBy(params) - await this._appendAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } + private _replaceAll(res: GetRepostedBy.Response) { + this.repostedBy = [] + this._appendAll(res) } private _appendAll(res: GetRepostedBy.Response) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor this.repostedBy = this.repostedBy.concat(res.data.repostedBy) + this.rootStore.me.follows.hydrateProfiles(res.data.repostedBy) } } diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index 2f6931cd..11d49635 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -3,71 +3,63 @@ */ import {makeAutoObservable} from 'mobx' -import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api' +import {AtpAgent} from '@atproto/api' import {createContext, useContext} from 'react' import {DeviceEventEmitter, EmitterSubscription} from 'react-native' -import * as BgScheduler from '../lib/bg-scheduler' -import {isObj, hasProp} from '../lib/type-guards' +import * as BgScheduler from 'lib/bg-scheduler' +import {z} from 'zod' +import {isObj, hasProp} from 'lib/type-guards' import {LogModel} from './log' import {SessionModel} from './session' import {NavigationModel} from './navigation' import {ShellUiModel} from './shell-ui' import {ProfilesViewModel} from './profiles-view' import {LinkMetasViewModel} from './link-metas-view' +import {NotificationsViewItemModel} from './notifications-view' import {MeModel} from './me' import {OnboardModel} from './onboard' -import {isNetworkError} from '../../lib/errors' + +export const appInfo = z.object({ + build: z.string(), + name: z.string(), + namespace: z.string(), + version: z.string(), +}) +export type AppInfo = z.infer export class RootStoreModel { + agent: AtpAgent + appInfo?: AppInfo log = new LogModel() session = new SessionModel(this) - nav = new NavigationModel() - shell = new ShellUiModel() + nav = new NavigationModel(this) + shell = new ShellUiModel(this) me = new MeModel(this) onboard = new OnboardModel() profiles = new ProfilesViewModel(this) linkMetas = new LinkMetasViewModel(this) - constructor(public api: SessionServiceClient) { + constructor(agent: AtpAgent) { + this.agent = agent makeAutoObservable(this, { api: false, - resolveName: false, serialize: false, hydrate: false, }) this.initBgFetch() } - async resolveName(didOrHandle: string) { - if (!didOrHandle) { - throw new Error('Invalid handle: ""') - } - if (didOrHandle.startsWith('did:')) { - return didOrHandle - } - const res = await this.api.com.atproto.handle.resolve({handle: didOrHandle}) - return res.data.did + get api() { + return this.agent.api } - async fetchStateUpdate() { - if (!this.session.hasSession) { - return - } - try { - if (!this.session.online) { - await this.session.connect() - } - await this.me.fetchNotifications() - } catch (e: any) { - if (isNetworkError(e)) { - this.session.setOnline(false) // connection lost - } - this.log.error('Failed to fetch latest state', e) - } + setAppInfo(info: AppInfo) { + this.appInfo = info } serialize(): unknown { return { + appInfo: this.appInfo, log: this.log.serialize(), session: this.session.serialize(), me: this.me.serialize(), @@ -79,6 +71,12 @@ export class RootStoreModel { hydrate(v: unknown) { if (isObj(v)) { + if (hasProp(v, 'appInfo')) { + const appInfoParsed = appInfo.safeParse(v.appInfo) + if (appInfoParsed.success) { + this.setAppInfo(appInfoParsed.data) + } + } if (hasProp(v, 'log')) { this.log.hydrate(v.log) } @@ -100,20 +98,131 @@ export class RootStoreModel { } } - clearAll() { + /** + * Called during init to resume any stored session. + */ + async attemptSessionResumption() { + this.log.debug('RootStoreModel:attemptSessionResumption') + try { + await this.session.attemptSessionResumption() + this.log.debug('Session initialized', { + hasSession: this.session.hasSession, + }) + this.updateSessionState() + } catch (e: any) { + this.log.warn('Failed to initialize session', e) + } + } + + /** + * Called by the session model. Refreshes session-oriented state. + */ + async handleSessionChange(agent: AtpAgent) { + this.log.debug('RootStoreModel:handleSessionChange') + this.agent = agent + this.nav.clear() + this.me.clear() + await this.me.load() + } + + /** + * Called by the session model. Handles session drops by informing the user. + */ + async handleSessionDrop() { + this.log.debug('RootStoreModel:handleSessionDrop') + this.nav.clear() + this.me.clear() + this.emitSessionDropped() + } + + /** + * Clears all session-oriented state. + */ + clearAllSessionState() { + this.log.debug('RootStoreModel:clearAllSessionState') this.session.clear() this.nav.clear() this.me.clear() } + /** + * Periodic poll for new session state. + */ + async updateSessionState() { + if (!this.session.hasSession) { + return + } + try { + await this.me.follows.fetchIfNeeded() + } catch (e: any) { + this.log.error('Failed to fetch latest state', e) + } + } + + // global event bus + // = + // - some events need to be passed around between views and models + // in order to keep state in sync; these methods are for that + + // a post was deleted by the local user onPostDeleted(handler: (uri: string) => void): EmitterSubscription { return DeviceEventEmitter.addListener('post-deleted', handler) } - emitPostDeleted(uri: string) { DeviceEventEmitter.emit('post-deleted', uri) } + // the session has started and been fully hydrated + onSessionLoaded(handler: () => void): EmitterSubscription { + return DeviceEventEmitter.addListener('session-loaded', handler) + } + emitSessionLoaded() { + DeviceEventEmitter.emit('session-loaded') + } + + // the session was dropped due to bad/expired refresh tokens + onSessionDropped(handler: () => void): EmitterSubscription { + return DeviceEventEmitter.addListener('session-dropped', handler) + } + emitSessionDropped() { + DeviceEventEmitter.emit('session-dropped') + } + + // the current screen has changed + onNavigation(handler: () => void): EmitterSubscription { + return DeviceEventEmitter.addListener('navigation', handler) + } + emitNavigation() { + DeviceEventEmitter.emit('navigation') + } + + // a "soft reset" typically means scrolling to top and loading latest + // but it can depend on the screen + onScreenSoftReset(handler: () => void): EmitterSubscription { + return DeviceEventEmitter.addListener('screen-soft-reset', handler) + } + emitScreenSoftReset() { + DeviceEventEmitter.emit('screen-soft-reset') + } + + // the unread notifications count has changed + onUnreadNotifications(handler: (count: number) => void): EmitterSubscription { + return DeviceEventEmitter.addListener('unread-notifications', handler) + } + emitUnreadNotifications(count: number) { + DeviceEventEmitter.emit('unread-notifications', count) + } + + // a notification has been queued for push + onPushNotification( + handler: (notif: NotificationsViewItemModel) => void, + ): EmitterSubscription { + return DeviceEventEmitter.addListener('push-notification', handler) + } + emitPushNotification(notif: NotificationsViewItemModel) { + DeviceEventEmitter.emit('push-notification', notif) + } + // background fetch // = // - we use this to poll for unread notifications, which is not "ideal" behavior but @@ -135,7 +244,22 @@ export class RootStoreModel { async onBgFetch(taskId: string) { this.log.debug(`Background fetch fired for task ${taskId}`) if (this.session.hasSession) { - await this.me.bgFetchNotifications() + const res = await this.api.app.bsky.notification.getCount() + const hasNewNotifs = this.me.notifications.unreadCount !== res.data.count + this.emitUnreadNotifications(res.data.count) + this.log.debug( + `Background fetch received unread count = ${res.data.count}`, + ) + if (hasNewNotifs) { + this.log.debug( + 'Background fetch detected potentially a new notification', + ) + const mostRecent = await this.me.notifications.getNewMostRecent() + if (mostRecent) { + this.log.debug('Got the notification, triggering a push') + this.emitPushNotification(mostRecent) + } + } } BgScheduler.finish(taskId) } @@ -146,7 +270,9 @@ export class RootStoreModel { } } -const throwawayInst = new RootStoreModel(AtpApi.service('http://localhost')) // this will be replaced by the loader, we just need to supply a value at init +const throwawayInst = new RootStoreModel( + new AtpAgent({service: 'http://localhost'}), +) // this will be replaced by the loader, we just need to supply a value at init const RootStoreContext = createContext(throwawayInst) export const RootStoreProvider = RootStoreContext.Provider export const useStores = () => useContext(RootStoreContext) diff --git a/src/state/models/session.ts b/src/state/models/session.ts index bc0a9123..6e816120 100644 --- a/src/state/models/session.ts +++ b/src/state/models/session.ts @@ -1,25 +1,22 @@ -import {makeAutoObservable, runInAction} from 'mobx' +import {makeAutoObservable} from 'mobx' import { - sessionClient as AtpApi, - Session, - SessionServiceClient, + AtpAgent, + AtpSessionEvent, + AtpSessionData, ComAtprotoServerGetAccountsConfig as GetAccountsConfig, } from '@atproto/api' -import {isObj, hasProp} from '../lib/type-guards' +import normalizeUrl from 'normalize-url' +import {isObj, hasProp} from 'lib/type-guards' import {z} from 'zod' import {RootStoreModel} from './root-store' -import {isNetworkError} from '../../lib/errors' export type ServiceDescription = GetAccountsConfig.OutputSchema -export const sessionData = z.object({ +export const activeSession = z.object({ service: z.string(), - refreshJwt: z.string(), - accessJwt: z.string(), - handle: z.string(), did: z.string(), }) -export type SessionData = z.infer +export type ActiveSession = z.infer export const accountData = z.object({ service: z.string(), @@ -32,18 +29,24 @@ export const accountData = z.object({ }) export type AccountData = z.infer +interface AdditionalAccountData { + displayName?: string + aviUrl?: string +} + export class SessionModel { /** - * Current session data + * Currently-active session */ - data: SessionData | null = null + data: ActiveSession | null = null /** - * A listing of the currently & previous sessions, used for account switching + * A listing of the currently & previous sessions */ accounts: AccountData[] = [] - online = false - attemptingConnect = false - private _connectPromise: Promise | undefined + /** + * Flag to indicate if we're doing our initial-load session resumption + */ + isResumingSession = false constructor(public rootStore: RootStoreModel) { makeAutoObservable(this, { @@ -53,8 +56,22 @@ export class SessionModel { }) } + get currentSession() { + if (!this.data) { + return undefined + } + const {did, service} = this.data + return this.accounts.find( + account => + normalizeUrl(account.service) === normalizeUrl(service) && + account.did === did && + !!account.accessJwt && + !!account.refreshJwt, + ) + } + get hasSession() { - return this.data !== null + return !!this.currentSession && !!this.rootStore.agent.session } get hasAccounts() { @@ -75,8 +92,8 @@ export class SessionModel { hydrate(v: unknown) { this.accounts = [] if (isObj(v)) { - if (hasProp(v, 'data') && sessionData.safeParse(v.data)) { - this.data = v.data as SessionData + if (hasProp(v, 'data') && activeSession.safeParse(v.data)) { + this.data = v.data as ActiveSession } if (hasProp(v, 'accounts') && Array.isArray(v.accounts)) { for (const account of v.accounts) { @@ -90,92 +107,96 @@ export class SessionModel { clear() { this.data = null - this.setOnline(false) - } - - setState(data: SessionData) { - this.data = data - } - - setOnline(online: boolean, attemptingConnect?: boolean) { - this.online = online - if (typeof attemptingConnect === 'boolean') { - this.attemptingConnect = attemptingConnect - } - } - - updateAuthTokens(session: Session) { - if (this.data) { - this.setState({ - ...this.data, - accessJwt: session.accessJwt, - refreshJwt: session.refreshJwt, - }) - } } /** - * Sets up the XRPC API, must be called before connecting to a service + * Attempts to resume the previous session loaded from storage */ - private configureApi(): boolean { - if (!this.data) { - return false - } - - try { - const serviceUri = new URL(this.data.service) - this.rootStore.api.xrpc.uri = serviceUri - } catch (e: any) { - this.rootStore.log.error( - `Invalid service URL: ${this.data.service}. Resetting session.`, - e, + async attemptSessionResumption() { + const sess = this.currentSession + if (sess) { + this.rootStore.log.debug( + 'SessionModel:attemptSessionResumption found stored session', + ) + this.isResumingSession = true + try { + return await this.resumeSession(sess) + } finally { + this.isResumingSession = false + } + } else { + this.rootStore.log.debug( + 'SessionModel:attemptSessionResumption has no session to resume', ) - this.clear() - return false } - - this.rootStore.api.sessionManager.set({ - refreshJwt: this.data.refreshJwt, - accessJwt: this.data.accessJwt, - }) - return true } /** - * Upserts the current session into the accounts + * Sets the active session */ - private addSessionToAccounts() { - if (!this.data) { - return + setActiveSession(agent: AtpAgent, did: string) { + this.rootStore.log.debug('SessionModel:setActiveSession') + this.data = { + service: agent.service.toString(), + did, } + this.rootStore.handleSessionChange(agent) + } + + /** + * Upserts a session into the accounts + */ + private persistSession( + service: string, + did: string, + event: AtpSessionEvent, + session?: AtpSessionData, + addedInfo?: AdditionalAccountData, + ) { + this.rootStore.log.debug('SessionModel:persistSession', { + service, + did, + event, + hasSession: !!session, + }) + + // upsert the account in our listing const existingAccount = this.accounts.find( - acc => acc.service === this.data?.service && acc.did === this.data.did, + account => account.service === service && account.did === did, ) const newAccount = { - service: this.data.service, - refreshJwt: this.data.refreshJwt, - accessJwt: this.data.accessJwt, - handle: this.data.handle, - did: this.data.did, - displayName: this.rootStore.me.displayName, - aviUrl: this.rootStore.me.avatar, + service, + did, + refreshJwt: session?.refreshJwt, + accessJwt: session?.accessJwt, + handle: session?.handle || existingAccount?.handle || '', + displayName: addedInfo + ? addedInfo.displayName + : existingAccount?.displayName || '', + aviUrl: addedInfo ? addedInfo.aviUrl : existingAccount?.aviUrl || '', } if (!existingAccount) { this.accounts.push(newAccount) } else { - this.accounts = this.accounts - .filter( - acc => - !(acc.service === this.data?.service && acc.did === this.data.did), - ) - .concat([newAccount]) + this.accounts = [ + newAccount, + ...this.accounts.filter( + account => !(account.service === service && account.did === did), + ), + ] + } + + // if the session expired, fire an event to let the user know + if (event === 'expired') { + this.rootStore.handleSessionDrop() } } /** * Clears any session tokens from the accounts; used on logout. */ - private clearSessionTokensFromAccounts() { + private clearSessionTokens() { + this.rootStore.log.debug('SessionModel:clearSessionTokens') this.accounts = this.accounts.map(acct => ({ service: acct.service, handle: acct.handle, @@ -186,144 +207,106 @@ export class SessionModel { } /** - * Fetches the current session from the service, if possible. - * Requires an existing session (.data) to be populated with access tokens. + * Fetches additional information about an account on load. */ - async connect(): Promise { - if (this._connectPromise) { - return this._connectPromise - } - this._connectPromise = this._connect() - const res = await this._connectPromise - this._connectPromise = undefined - return res - } - - private async _connect(): Promise { - this.attemptingConnect = true - if (!this.configureApi()) { - return false - } - - try { - const sess = await this.rootStore.api.com.atproto.session.get() - if (sess.success && this.data && this.data.did === sess.data.did) { - this.setOnline(true, false) - if (this.rootStore.me.did !== sess.data.did) { - this.rootStore.me.clear() - } - this.rootStore.me - .load() - .catch(e => { - this.rootStore.log.error( - 'Failed to fetch local user information', - e, - ) - }) - .then(() => { - this.addSessionToAccounts() - }) - return true // success - } - } catch (e: any) { - if (isNetworkError(e)) { - this.setOnline(false, false) // connection issue - return false - } else { - this.clear() // invalid session cached + private async loadAccountInfo(agent: AtpAgent, did: string) { + const res = await agent.api.app.bsky.actor + .getProfile({actor: did}) + .catch(_e => undefined) + if (res) { + return { + dispayName: res.data.displayName, + aviUrl: res.data.avatar, } } - - this.setOnline(false, false) - return false } /** * Helper to fetch the accounts config settings from an account. */ async describeService(service: string): Promise { - const api = AtpApi.service(service) as SessionServiceClient - const res = await api.com.atproto.server.getAccountsConfig({}) + const agent = new AtpAgent({service}) + const res = await agent.api.com.atproto.server.getAccountsConfig({}) return res.data } + /** + * Attempt to resume a session that we still have access tokens for. + */ + async resumeSession(account: AccountData): Promise { + this.rootStore.log.debug('SessionModel:resumeSession') + if (!(account.accessJwt && account.refreshJwt && account.service)) { + this.rootStore.log.debug( + 'SessionModel:resumeSession aborted due to lack of access tokens', + ) + return false + } + + const agent = new AtpAgent({ + service: account.service, + persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => { + this.persistSession(account.service, account.did, evt, sess) + }, + }) + + try { + await agent.resumeSession({ + accessJwt: account.accessJwt, + refreshJwt: account.refreshJwt, + did: account.did, + handle: account.handle, + }) + const addedInfo = await this.loadAccountInfo(agent, account.did) + this.persistSession( + account.service, + account.did, + 'create', + agent.session, + addedInfo, + ) + this.rootStore.log.debug('SessionModel:resumeSession succeeded') + } catch (e: any) { + this.rootStore.log.debug('SessionModel:resumeSession failed', { + error: e.toString(), + }) + return false + } + + this.setActiveSession(agent, account.did) + return true + } + /** * Create a new session. */ async login({ service, - handle, + identifier, password, }: { service: string - handle: string + identifier: string password: string }) { - const api = AtpApi.service(service) as SessionServiceClient - const res = await api.com.atproto.session.create({handle, password}) - if (res.data.accessJwt && res.data.refreshJwt) { - this.setState({ - service: service, - accessJwt: res.data.accessJwt, - refreshJwt: res.data.refreshJwt, - handle: res.data.handle, - did: res.data.did, - }) - this.configureApi() - this.setOnline(true, false) - this.rootStore.me - .load() - .catch(e => { - this.rootStore.log.error('Failed to fetch local user information', e) - }) - .then(() => { - this.addSessionToAccounts() - }) - } - } - - /** - * Attempt to resume a session that we still have access tokens for. - */ - async resumeSession(account: AccountData): Promise { - if (!(account.accessJwt && account.refreshJwt && account.service)) { - return false + this.rootStore.log.debug('SessionModel:login') + const agent = new AtpAgent({service}) + await agent.login({identifier, password}) + if (!agent.session) { + throw new Error('Failed to establish session') } - // test that the session is good - const api = AtpApi.service(account.service) - api.sessionManager.set({ - refreshJwt: account.refreshJwt, - accessJwt: account.accessJwt, - }) - try { - const sess = await api.com.atproto.session.get() - if ( - !sess.success || - sess.data.did !== account.did || - !api.sessionManager.session - ) { - return false - } + const did = agent.session.did + const addedInfo = await this.loadAccountInfo(agent, did) - // copy over the access tokens, as they may have refreshed during the .get() above - runInAction(() => { - account.refreshJwt = api.sessionManager.session?.refreshJwt - account.accessJwt = api.sessionManager.session?.accessJwt - }) - } catch (_e) { - return false - } + this.persistSession(service, did, 'create', agent.session, addedInfo) + agent.setPersistSessionHandler( + (evt: AtpSessionEvent, sess?: AtpSessionData) => { + this.persistSession(service, did, evt, sess) + }, + ) - // session is good, connect - this.setState({ - service: account.service, - accessJwt: account.accessJwt, - refreshJwt: account.refreshJwt, - handle: account.handle, - did: account.did, - }) - return this.connect() + this.setActiveSession(agent, did) + this.rootStore.log.debug('SessionModel:login succeeded') } async createAccount({ @@ -339,38 +322,41 @@ export class SessionModel { handle: string inviteCode?: string }) { - const api = AtpApi.service(service) as SessionServiceClient - const res = await api.com.atproto.account.create({ + this.rootStore.log.debug('SessionModel:createAccount') + const agent = new AtpAgent({service}) + await agent.createAccount({ handle, password, email, inviteCode, }) - if (res.data.accessJwt && res.data.refreshJwt) { - this.setState({ - service: service, - accessJwt: res.data.accessJwt, - refreshJwt: res.data.refreshJwt, - handle: res.data.handle, - did: res.data.did, - }) - this.rootStore.onboard.start() - this.configureApi() - this.rootStore.me - .load() - .catch(e => { - this.rootStore.log.error('Failed to fetch local user information', e) - }) - .then(() => { - this.addSessionToAccounts() - }) + if (!agent.session) { + throw new Error('Failed to establish session') } + + const did = agent.session.did + const addedInfo = await this.loadAccountInfo(agent, did) + + this.persistSession(service, did, 'create', agent.session, addedInfo) + agent.setPersistSessionHandler( + (evt: AtpSessionEvent, sess?: AtpSessionData) => { + this.persistSession(service, did, evt, sess) + }, + ) + + this.setActiveSession(agent, did) + this.rootStore.onboard.start() + this.rootStore.log.debug('SessionModel:createAccount succeeded') } /** * Close all sessions across all accounts. */ async logout() { + this.rootStore.log.debug('SessionModel:logout') + // TODO + // need to evaluate why deleting the session has caused errors at times + // -prf /*if (this.hasSession) { this.rootStore.api.com.atproto.session.delete().catch((e: any) => { this.rootStore.log.warn( @@ -379,7 +365,7 @@ export class SessionModel { ) }) }*/ - this.clearSessionTokensFromAccounts() - this.rootStore.clearAll() + this.clearSessionTokens() + this.rootStore.clearAllSessionState() } } diff --git a/src/state/models/shell-ui.ts b/src/state/models/shell-ui.ts index 09ffd265..b9f480ec 100644 --- a/src/state/models/shell-ui.ts +++ b/src/state/models/shell-ui.ts @@ -1,7 +1,8 @@ +import {RootStoreModel} from './root-store' import {makeAutoObservable} from 'mobx' import {ProfileViewModel} from './profile-view' -import {isObj, hasProp} from '../lib/type-guards' -import {PickedMedia} from '../../view/com/util/images/image-crop-picker/types' +import {isObj, hasProp} from 'lib/type-guards' +import {PickedMedia} from 'view/com/util/images/image-crop-picker/types' export class ConfirmModal { name = 'confirm' @@ -40,7 +41,7 @@ export class ServerInputModal { export class ReportPostModal { name = 'report-post' - constructor(public postUrl: string) { + constructor(public postUri: string, public postCid: string) { makeAutoObservable(this) } } @@ -59,7 +60,13 @@ export class CropImageModal { constructor( public uri: string, public onSelect: (img?: PickedMedia) => void, - ) { + ) {} +} + +export class DeleteAccountModal { + name = 'delete-account' + + constructor() { makeAutoObservable(this) } } @@ -111,14 +118,19 @@ export class ShellUiModel { | ReportPostModal | ReportAccountModal | CropImageModal + | DeleteAccountModal | undefined isLightboxActive = false activeLightbox: ProfileImageLightbox | ImagesLightbox | undefined isComposerActive = false composerOpts: ComposerOpts | undefined - constructor() { - makeAutoObservable(this, {serialize: false, hydrate: false}) + constructor(public rootStore: RootStoreModel) { + makeAutoObservable(this, { + serialize: false, + rootStore: false, + hydrate: false, + }) } serialize(): unknown { @@ -154,8 +166,10 @@ export class ShellUiModel { | ServerInputModal | ReportPostModal | ReportAccountModal - | CropImageModal, + | CropImageModal + | DeleteAccountModal, ) { + this.rootStore.emitNavigation() this.isModalActive = true this.activeModal = modal } @@ -166,6 +180,7 @@ export class ShellUiModel { } openLightbox(lightbox: ProfileImageLightbox | ImagesLightbox) { + this.rootStore.emitNavigation() this.isLightboxActive = true this.activeLightbox = lightbox } @@ -176,6 +191,7 @@ export class ShellUiModel { } openComposer(opts: ComposerOpts) { + this.rootStore.emitNavigation() this.isComposerActive = true this.composerOpts = opts } diff --git a/src/state/models/suggested-actors-view.ts b/src/state/models/suggested-actors-view.ts index 0c9e0c3e..4764f581 100644 --- a/src/state/models/suggested-actors-view.ts +++ b/src/state/models/suggested-actors-view.ts @@ -1,25 +1,48 @@ -import {makeAutoObservable} from 'mobx' -import {AppBskyActorGetSuggestions as GetSuggestions} from '@atproto/api' +import {makeAutoObservable, runInAction} from 'mobx' +import {AppBskyActorProfile as Profile} from '@atproto/api' +import shuffle from 'lodash.shuffle' import {RootStoreModel} from './root-store' +import {cleanError} from 'lib/strings/errors' +import {bundleAsync} from 'lib/async/bundle' +import { + DEV_SUGGESTED_FOLLOWS, + PROD_SUGGESTED_FOLLOWS, + STAGING_SUGGESTED_FOLLOWS, +} from 'lib/constants' const PAGE_SIZE = 30 -export type SuggestedActor = GetSuggestions.Actor +export type SuggestedActor = Profile.ViewBasic | Profile.View + +const getSuggestionList = ({serviceUrl}: {serviceUrl: string}) => { + if (serviceUrl.includes('localhost')) { + return DEV_SUGGESTED_FOLLOWS + } else if (serviceUrl.includes('staging')) { + return STAGING_SUGGESTED_FOLLOWS + } else { + return PROD_SUGGESTED_FOLLOWS + } +} export class SuggestedActorsViewModel { // state + pageSize = PAGE_SIZE isLoading = false isRefreshing = false hasLoaded = false error = '' hasMore = true loadMoreCursor?: string - private _loadMorePromise: Promise | undefined + + private hardCodedSuggestions: SuggestedActor[] | undefined // data suggestions: SuggestedActor[] = [] - constructor(public rootStore: RootStoreModel) { + constructor(public rootStore: RootStoreModel, opts?: {pageSize?: number}) { + if (opts?.pageSize) { + this.pageSize = opts.pageSize + } makeAutoObservable( this, { @@ -48,13 +71,96 @@ export class SuggestedActorsViewModel { return this.loadMore(true) } - async loadMore(isRefreshing = false) { - if (this._loadMorePromise) { - return this._loadMorePromise + loadMore = bundleAsync(async (replace: boolean = false) => { + if (!replace && !this.hasMore) { + return + } + if (replace) { + this.hardCodedSuggestions = undefined + } + this._xLoading(replace) + try { + let items: SuggestedActor[] = this.suggestions + if (replace) { + items = [] + this.loadMoreCursor = undefined + } + let res + do { + await this.fetchHardcodedSuggestions() + if (this.hardCodedSuggestions && this.hardCodedSuggestions.length > 0) { + // pull from the hard-coded suggestions + const newItems = this.hardCodedSuggestions.splice(0, this.pageSize) + items = items.concat(newItems) + this.hasMore = true + this.loadMoreCursor = undefined + } else { + // pull from the PDS' algo + res = await this.rootStore.api.app.bsky.actor.getSuggestions({ + limit: this.pageSize, + cursor: this.loadMoreCursor, + }) + this.loadMoreCursor = res.data.cursor + this.hasMore = !!this.loadMoreCursor + items = items.concat( + res.data.actors.filter( + actor => !items.find(i => i.did === actor.did), + ), + ) + } + } while (items.length < this.pageSize && this.hasMore) + runInAction(() => { + this.suggestions = items + }) + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + }) + + private async fetchHardcodedSuggestions() { + if (this.hardCodedSuggestions) { + return + } + await this.rootStore.me.follows.fetchIfNeeded() + try { + // clone the array so we can mutate it + const actors = [ + ...getSuggestionList({ + serviceUrl: this.rootStore.session.currentSession?.service || '', + }), + ] + + // fetch the profiles in chunks of 25 (the limit allowed by `getProfiles`) + let profiles: Profile.View[] = [] + do { + const res = await this.rootStore.api.app.bsky.actor.getProfiles({ + actors: actors.splice(0, 25), + }) + profiles = profiles.concat(res.data.profiles) + } while (actors.length) + + runInAction(() => { + profiles = profiles.filter(profile => { + if (this.rootStore.me.follows.isFollowing(profile.did)) { + return false + } + if (profile.did === this.rootStore.me.did) { + return false + } + return true + }) + this.hardCodedSuggestions = shuffle(profiles) + }) + } catch (e) { + this.rootStore.log.error( + 'Failed to getProfiles() for suggested follows', + {e}, + ) + runInAction(() => { + this.hardCodedSuggestions = [] + }) } - this._loadMorePromise = this._loadMore(isRefreshing) - await this._loadMorePromise - this._loadMorePromise = undefined } // state transitions @@ -70,52 +176,9 @@ export class SuggestedActorsViewModel { this.isLoading = false this.isRefreshing = false this.hasLoaded = true - this.error = err ? err.toString() : '' + this.error = cleanError(err) if (err) { this.rootStore.log.error('Failed to fetch suggested actors', err) } } - - // loader functions - // = - - private async _loadMore(isRefreshing = false) { - if (!this.hasMore) { - return - } - this._xLoading(isRefreshing) - try { - if (this.isRefreshing) { - this.suggestions = [] - } - let res - let totalAdded = 0 - do { - res = await this.rootStore.api.app.bsky.actor.getSuggestions({ - limit: PAGE_SIZE, - cursor: this.loadMoreCursor, - }) - totalAdded += await this._appendAll(res) - } while (totalAdded < PAGE_SIZE && this.hasMore) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - } - - private async _appendAll(res: GetSuggestions.Response) { - this.loadMoreCursor = res.data.cursor - this.hasMore = !!this.loadMoreCursor - const newSuggestions = res.data.actors.filter(actor => { - if (actor.did === this.rootStore.me.did) { - return false // skip self - } - if (actor.myState?.follow) { - return false // skip already-followed users - } - return true - }) - this.suggestions = this.suggestions.concat(newSuggestions) - return newSuggestions.length - } } diff --git a/src/state/models/suggested-posts-view.ts b/src/state/models/suggested-posts-view.ts new file mode 100644 index 00000000..7b44370d --- /dev/null +++ b/src/state/models/suggested-posts-view.ts @@ -0,0 +1,148 @@ +import {makeAutoObservable, runInAction} from 'mobx' +import { + AppBskyFeedFeedViewPost, + AppBskyFeedGetAuthorFeed as GetAuthorFeed, +} from '@atproto/api' +type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost +import {RootStoreModel} from './root-store' +import {FeedItemModel} from './feed-view' +import {cleanError} from 'lib/strings/errors' + +const TEAM_HANDLES = [ + 'jay.bsky.social', + 'paul.bsky.social', + 'dan.bsky.social', + 'divy.bsky.social', + 'why.bsky.social', + 'iamrosewang.bsky.social', +] + +export class SuggestedPostsView { + // state + isLoading = false + hasLoaded = false + error = '' + + // data + posts: FeedItemModel[] = [] + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + get hasContent() { + return this.posts.length > 0 + } + + get hasError() { + return this.error !== '' + } + + get isEmpty() { + return this.hasLoaded && !this.hasContent + } + + // public api + // = + + async setup() { + this._xLoading() + try { + const responses = await Promise.all( + TEAM_HANDLES.map(handle => + this.rootStore.api.app.bsky.feed + .getAuthorFeed({author: handle, limit: 10}) + .catch(_err => ({success: false, headers: {}, data: {feed: []}})), + ), + ) + runInAction(() => { + this.posts = mergeAndFilterResponses(this.rootStore, responses) + }) + this._xIdle() + } catch (e: any) { + this.rootStore.log.error('SuggestedPostsView: Failed to load posts', { + e, + }) + this._xIdle() // dont bubble to the user + } + } + + // state transitions + // = + + private _xLoading() { + this.isLoading = true + this.error = '' + } + + private _xIdle(err?: any) { + this.isLoading = false + this.hasLoaded = true + this.error = cleanError(err) + if (err) { + this.rootStore.log.error('Failed to fetch suggested posts', err) + } + } +} + +function mergeAndFilterResponses( + store: RootStoreModel, + responses: GetAuthorFeed.Response[], +): FeedItemModel[] { + let posts: AppBskyFeedFeedViewPost.Main[] = [] + + // merge into one array + for (const res of responses) { + if (res.success) { + posts = posts.concat(res.data.feed) + } + } + + // filter down to reposts of other users + const now = Date.now() + const uris = new Set() + posts = posts.filter(p => { + if (isARepostOfSomeoneElse(p) && isRecentEnough(now, p)) { + if (uris.has(p.post.uri)) { + return false + } + uris.add(p.post.uri) + return true + } + return false + }) + + // sort by index time + posts.sort((a, b) => { + return ( + Number(new Date(b.post.indexedAt)) - Number(new Date(a.post.indexedAt)) + ) + }) + + // hydrate into models and strip the reasons to hide that these are reposts + return posts.map((post, i) => { + delete post.reason + return new FeedItemModel(store, `post-${i}`, post) + }) +} + +function isARepostOfSomeoneElse(post: AppBskyFeedFeedViewPost.Main): boolean { + return ( + post.reason?.$type === 'app.bsky.feed.feedViewPost#reasonRepost' && + post.post.author.did !== (post.reason as ReasonRepost).by.did + ) +} + +const THREE_DAYS = 3 * 24 * 60 * 60 * 1000 +function isRecentEnough( + now: number, + post: AppBskyFeedFeedViewPost.Main, +): boolean { + return now - Number(new Date(post.post.indexedAt)) < THREE_DAYS +} diff --git a/src/state/models/user-autocomplete-view.ts b/src/state/models/user-autocomplete-view.ts index 8f467da6..8e4211c2 100644 --- a/src/state/models/user-autocomplete-view.ts +++ b/src/state/models/user-autocomplete-view.ts @@ -1,8 +1,6 @@ import {makeAutoObservable, runInAction} from 'mobx' -import { - AppBskyGraphGetFollows as GetFollows, - AppBskyActorSearchTypeahead as SearchTypeahead, -} from '@atproto/api' +import {AppBskyActorRef} from '@atproto/api' +import AwaitLock from 'await-lock' import {RootStoreModel} from './root-store' export class UserAutocompleteViewModel { @@ -10,11 +8,11 @@ export class UserAutocompleteViewModel { isLoading = false isActive = false prefix = '' - _searchPromise: Promise | undefined + lock = new AwaitLock() // data - follows: GetFollows.Follow[] = [] - searchRes: SearchTypeahead.User[] = [] + follows: AppBskyActorRef.WithInfo[] = [] + searchRes: AppBskyActorRef.WithInfo[] = [] knownHandles: Set = new Set() constructor(public rootStore: RootStoreModel) { @@ -58,16 +56,20 @@ export class UserAutocompleteViewModel { } async setPrefix(prefix: string) { - const origPrefix = prefix - this.prefix = prefix.trim() - if (this.prefix) { - await this._searchPromise - if (this.prefix !== origPrefix) { - return // another prefix was set before we got our chance + const origPrefix = prefix.trim() + this.prefix = origPrefix + await this.lock.acquireAsync() + try { + if (this.prefix) { + if (this.prefix !== origPrefix) { + return // another prefix was set before we got our chance + } + await this._search() + } else { + this.searchRes = [] } - this._searchPromise = this._search() - } else { - this.searchRes = [] + } finally { + this.lock.release() } } diff --git a/src/state/models/user-followers-view.ts b/src/state/models/user-followers-view.ts index 9daaf35a..7400262a 100644 --- a/src/state/models/user-followers-view.ts +++ b/src/state/models/user-followers-view.ts @@ -4,10 +4,12 @@ import { AppBskyActorRef as ActorRef, } from '@atproto/api' import {RootStoreModel} from './root-store' +import {cleanError} from 'lib/strings/errors' +import {bundleAsync} from 'lib/async/bundle' const PAGE_SIZE = 30 -export type FollowerItem = GetFollowers.Follower +export type FollowerItem = ActorRef.WithInfo export class UserFollowersViewModel { // state @@ -18,7 +20,6 @@ export class UserFollowersViewModel { params: GetFollowers.QueryParams hasMore = true loadMoreCursor?: string - private _loadMorePromise: Promise | undefined // data subject: ActorRef.WithInfo = { @@ -62,14 +63,27 @@ export class UserFollowersViewModel { return this.loadMore(true) } - async loadMore(isRefreshing = false) { - if (this._loadMorePromise) { - return this._loadMorePromise + loadMore = bundleAsync(async (replace: boolean = false) => { + if (!replace && !this.hasMore) { + return } - this._loadMorePromise = this._loadMore(isRefreshing) - await this._loadMorePromise - this._loadMorePromise = undefined - } + this._xLoading(replace) + try { + const params = Object.assign({}, this.params, { + limit: PAGE_SIZE, + before: replace ? undefined : this.loadMoreCursor, + }) + const res = await this.rootStore.api.app.bsky.graph.getFollowers(params) + if (replace) { + this._replaceAll(res) + } else { + this._appendAll(res) + } + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + }) // state transitions // = @@ -84,39 +98,24 @@ export class UserFollowersViewModel { this.isLoading = false this.isRefreshing = false this.hasLoaded = true - this.error = err ? err.toString() : '' + this.error = cleanError(err) if (err) { this.rootStore.log.error('Failed to fetch user followers', err) } } - // loader functions + // helper functions // = - private async _loadMore(isRefreshing = false) { - if (!this.hasMore) { - return - } - this._xLoading(isRefreshing) - try { - const params = Object.assign({}, this.params, { - limit: PAGE_SIZE, - before: this.loadMoreCursor, - }) - if (this.isRefreshing) { - this.followers = [] - } - const res = await this.rootStore.api.app.bsky.graph.getFollowers(params) - await this._appendAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } + private _replaceAll(res: GetFollowers.Response) { + this.followers = [] + this._appendAll(res) } - private async _appendAll(res: GetFollowers.Response) { + private _appendAll(res: GetFollowers.Response) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor this.followers = this.followers.concat(res.data.followers) + this.rootStore.me.follows.hydrateProfiles(res.data.followers) } } diff --git a/src/state/models/user-follows-view.ts b/src/state/models/user-follows-view.ts index d43a10c7..7d28d7eb 100644 --- a/src/state/models/user-follows-view.ts +++ b/src/state/models/user-follows-view.ts @@ -4,10 +4,12 @@ import { AppBskyActorRef as ActorRef, } from '@atproto/api' import {RootStoreModel} from './root-store' +import {cleanError} from 'lib/strings/errors' +import {bundleAsync} from 'lib/async/bundle' const PAGE_SIZE = 30 -export type FollowItem = GetFollows.Follow +export type FollowItem = ActorRef.WithInfo export class UserFollowsViewModel { // state @@ -18,7 +20,6 @@ export class UserFollowsViewModel { params: GetFollows.QueryParams hasMore = true loadMoreCursor?: string - private _loadMorePromise: Promise | undefined // data subject: ActorRef.WithInfo = { @@ -62,14 +63,27 @@ export class UserFollowsViewModel { return this.loadMore(true) } - async loadMore(isRefreshing = false) { - if (this._loadMorePromise) { - return this._loadMorePromise + loadMore = bundleAsync(async (replace: boolean = false) => { + if (!replace && !this.hasMore) { + return } - this._loadMorePromise = this._loadMore(isRefreshing) - await this._loadMorePromise - this._loadMorePromise = undefined - } + this._xLoading(replace) + try { + const params = Object.assign({}, this.params, { + limit: PAGE_SIZE, + before: replace ? undefined : this.loadMoreCursor, + }) + const res = await this.rootStore.api.app.bsky.graph.getFollows(params) + if (replace) { + this._replaceAll(res) + } else { + this._appendAll(res) + } + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + }) // state transitions // = @@ -84,39 +98,24 @@ export class UserFollowsViewModel { this.isLoading = false this.isRefreshing = false this.hasLoaded = true - this.error = err ? err.toString() : '' + this.error = cleanError(err) if (err) { this.rootStore.log.error('Failed to fetch user follows', err) } } - // loader functions + // helper functions // = - private async _loadMore(isRefreshing = false) { - if (!this.hasMore) { - return - } - this._xLoading(isRefreshing) - try { - const params = Object.assign({}, this.params, { - limit: PAGE_SIZE, - before: this.loadMoreCursor, - }) - if (this.isRefreshing) { - this.follows = [] - } - const res = await this.rootStore.api.app.bsky.graph.getFollows(params) - await this._appendAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } + private _replaceAll(res: GetFollows.Response) { + this.follows = [] + this._appendAll(res) } - private async _appendAll(res: GetFollows.Response) { + private _appendAll(res: GetFollows.Response) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor this.follows = this.follows.concat(res.data.follows) + this.rootStore.me.follows.hydrateProfiles(res.data.follows) } } diff --git a/src/state/models/votes-view.ts b/src/state/models/votes-view.ts index df939226..ad8698d2 100644 --- a/src/state/models/votes-view.ts +++ b/src/state/models/votes-view.ts @@ -2,6 +2,9 @@ import {makeAutoObservable, runInAction} from 'mobx' import {AtUri} from '../../third-party/uri' import {AppBskyFeedGetVotes as GetVotes} from '@atproto/api' import {RootStoreModel} from './root-store' +import {cleanError} from 'lib/strings/errors' +import {bundleAsync} from 'lib/async/bundle' +import * as apilib from 'lib/api/index' const PAGE_SIZE = 30 @@ -17,7 +20,6 @@ export class VotesViewModel { params: GetVotes.QueryParams hasMore = true loadMoreCursor?: string - private _loadMorePromise: Promise | undefined // data uri: string = '' @@ -54,17 +56,31 @@ export class VotesViewModel { return this.loadMore(true) } - async loadMore(isRefreshing = false) { - if (this._loadMorePromise) { - return this._loadMorePromise + loadMore = bundleAsync(async (replace: boolean = false) => { + if (!replace && !this.hasMore) { + return } - if (!this.resolvedUri) { - await this._resolveUri() + this._xLoading(replace) + try { + if (!this.resolvedUri) { + await this._resolveUri() + } + const params = Object.assign({}, this.params, { + uri: this.resolvedUri, + limit: PAGE_SIZE, + before: replace ? undefined : this.loadMoreCursor, + }) + const res = await this.rootStore.api.app.bsky.feed.getVotes(params) + if (replace) { + this._replaceAll(res) + } else { + this._appendAll(res) + } + this._xIdle() + } catch (e: any) { + this._xIdle(e) } - this._loadMorePromise = this._loadMore(isRefreshing) - await this._loadMorePromise - this._loadMorePromise = undefined - } + }) // state transitions // = @@ -79,20 +95,20 @@ export class VotesViewModel { this.isLoading = false this.isRefreshing = false this.hasLoaded = true - this.error = err ? err.toString() : '' + this.error = cleanError(err) if (err) { this.rootStore.log.error('Failed to fetch votes', err) } } - // loader functions + // helper functions // = private async _resolveUri() { const urip = new AtUri(this.params.uri) if (!urip.host.startsWith('did:')) { try { - urip.host = await this.rootStore.resolveName(urip.host) + urip.host = await apilib.resolveName(this.rootStore, urip.host) } catch (e: any) { this.error = e.toString() } @@ -102,23 +118,9 @@ export class VotesViewModel { }) } - private async _loadMore(isRefreshing = false) { - this._xLoading(isRefreshing) - try { - const params = Object.assign({}, this.params, { - uri: this.resolvedUri, - limit: PAGE_SIZE, - before: this.loadMoreCursor, - }) - if (this.isRefreshing) { - this.votes = [] - } - const res = await this.rootStore.api.app.bsky.feed.getVotes(params) - this._appendAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } + private _replaceAll(res: GetVotes.Response) { + this.votes = [] + this._appendAll(res) } private _appendAll(res: GetVotes.Response) { diff --git a/src/view/com/composer/Autocomplete.tsx b/src/view/com/composer/Autocomplete.tsx index 94381e64..c17e41b3 100644 --- a/src/view/com/composer/Autocomplete.tsx +++ b/src/view/com/composer/Autocomplete.tsx @@ -5,9 +5,9 @@ import { StyleSheet, useWindowDimensions, } from 'react-native' -import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {usePalette} from 'lib/hooks/usePalette' import {Text} from '../util/text/Text' -import {usePalette} from '../../lib/hooks/usePalette' interface AutocompleteItem { handle: string diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx index 1144b5e4..6431a11a 100644 --- a/src/view/com/composer/ComposePost.tsx +++ b/src/view/com/composer/ComposePost.tsx @@ -3,10 +3,12 @@ import {observer} from 'mobx-react-lite' import { ActivityIndicator, KeyboardAvoidingView, + NativeSyntheticEvent, Platform, SafeAreaView, ScrollView, StyleSheet, + TextInputSelectionChangeEventData, TouchableOpacity, TouchableWithoutFeedback, View, @@ -16,8 +18,9 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -// import {useAnalytics} from '@segment/analytics-react-native' TODO -import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view' +import {useAnalytics} from 'lib/analytics' +import _isEqual from 'lodash.isequal' +import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view' import {Autocomplete} from './Autocomplete' import {ExternalEmbed} from './ExternalEmbed' import {Text} from '../util/text/Text' @@ -26,24 +29,27 @@ import {TextInput, TextInputRef} from './text-input/TextInput' import {CharProgress} from './char-progress/CharProgress' import {TextLink} from '../util/Link' import {UserAvatar} from '../util/UserAvatar' -import {useStores} from '../../../state' -import * as apilib from '../../../state/lib/api' -import {ComposerOpts} from '../../../state/models/shell-ui' -import {s, colors, gradients} from '../../lib/styles' -import { - detectLinkables, - extractEntities, - cleanError, -} from '../../../lib/strings' -import {getLinkMeta} from '../../../lib/link-meta' -import {downloadAndResize} from '../../../lib/images' +import {useStores} from 'state/index' +import * as apilib from 'lib/api/index' +import {ComposerOpts} from 'state/models/shell-ui' +import {s, colors, gradients} from 'lib/styles' +import {cleanError} from 'lib/strings/errors' +import {detectLinkables, extractEntities} from 'lib/strings/rich-text-detection' +import {getLinkMeta} from 'lib/link-meta/link-meta' +import {downloadAndResize} from 'lib/images' import {PhotoCarouselPicker, cropPhoto} from './photos/PhotoCarouselPicker' +import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip' import {SelectedPhoto} from './SelectedPhoto' -import {usePalette} from '../../lib/hooks/usePalette' +import {usePalette} from 'lib/hooks/usePalette' const MAX_TEXT_LENGTH = 256 const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} +interface Selection { + start: number + end: number +} + export const ComposePost = observer(function ComposePost({ replyTo, imagesOpen, @@ -55,10 +61,11 @@ export const ComposePost = observer(function ComposePost({ onPost?: ComposerOpts['onPost'] onClose: () => void }) { - // const {track} = useAnalytics() TODO + const {track, screen} = useAnalytics() const pal = usePalette('default') const store = useStores() const textInput = useRef(null) + const textInputSelection = useRef({start: 0, end: 0}) const [isProcessing, setIsProcessing] = useState(false) const [processingState, setProcessingState] = useState('') const [error, setError] = useState('') @@ -66,7 +73,9 @@ export const ComposePost = observer(function ComposePost({ const [extLink, setExtLink] = useState( undefined, ) - const [attemptedExtLinks, setAttemptedExtLinks] = useState([]) + const [suggestedExtLinks, setSuggestedExtLinks] = useState>( + new Set(), + ) const [isSelectingPhotos, setIsSelectingPhotos] = useState( imagesOpen || false, ) @@ -117,10 +126,10 @@ export const ComposePost = observer(function ComposePost({ if (extLink.isLoading && extLink.meta?.image && !extLink.localThumb) { downloadAndResize({ uri: extLink.meta.image, - width: 250, - height: 250, + width: 2000, + height: 2000, mode: 'contain', - maxSize: 100000, + maxSize: 1000000, timeout: 15e3, }) .catch(() => undefined) @@ -166,6 +175,7 @@ export const ComposePost = observer(function ComposePost({ textInput.current?.focus() } const onPressSelectPhotos = () => { + track('ComposePost:SelectPhotos') if (isSelectingPhotos) { setIsSelectingPhotos(false) } else if (selectedPhotos.length < 4) { @@ -173,35 +183,31 @@ export const ComposePost = observer(function ComposePost({ } } const onSelectPhotos = (photos: string[]) => { + track('ComposePost:SelectPhotos:Done') setSelectedPhotos(photos) if (photos.length >= 4) { setIsSelectingPhotos(false) } } + const onPressAddLinkCard = (uri: string) => { + setExtLink({uri, isLoading: true}) + } const onChangeText = (newText: string) => { setText(newText) - const prefix = extractTextAutocompletePrefix(newText) - if (typeof prefix === 'string') { + const prefix = getMentionAt(newText, textInputSelection.current?.start || 0) + if (prefix) { autocompleteView.setActive(true) - autocompleteView.setPrefix(prefix) + autocompleteView.setPrefix(prefix.value) } else { autocompleteView.setActive(false) } - if (!extLink && /\s$/.test(newText)) { - const ents = extractEntities(newText) - const entLink = ents - ?.filter( - ent => ent.type === 'link' && !attemptedExtLinks.includes(ent.value), - ) - .pop() // use last - if (entLink) { - setExtLink({ - uri: entLink.value, - isLoading: true, - }) - setAttemptedExtLinks([...attemptedExtLinks, entLink.value]) + if (!extLink) { + const ents = extractEntities(newText)?.filter(ent => ent.type === 'link') + const set = new Set(ents ? ents.map(e => e.value) : []) + if (!_isEqual(set, suggestedExtLinks)) { + setSuggestedExtLinks(set) } } } @@ -218,6 +224,16 @@ export const ComposePost = observer(function ComposePost({ onSelectPhotos([...selectedPhotos, finalImgPath]) } } + const onSelectionChange = ( + evt: NativeSyntheticEvent, + ) => { + // NOTE we track the input selection using a ref to avoid excessive renders -prf + textInputSelection.current = evt.nativeEvent.selection + } + const onSelectAutocompleteItem = (item: string) => { + setText(insertMentionAt(text, textInputSelection.current?.start || 0, item)) + autocompleteView.setActive(false) + } const onPressCancel = () => hackfixOnClose() const onPressPublish = async () => { if (isProcessing) { @@ -242,11 +258,15 @@ export const ComposePost = observer(function ComposePost({ autocompleteView.knownHandles, setProcessingState, ) - // TODO - // track('Create Post', { - // imageCount: selectedPhotos.length, - // }) + track('Create Post', { + imageCount: selectedPhotos.length, + }) } catch (e: any) { + setExtLink({ + ...extLink, + isLoading: true, + localThumb: undefined, + } as apilib.ExternalEmbedDraft) setError(cleanError(e.message)) setIsProcessing(false) return @@ -256,10 +276,6 @@ export const ComposePost = observer(function ComposePost({ hackfixOnClose() Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) } - const onSelectAutocompleteItem = (item: string) => { - setText(replaceTextAutocompletePrefix(text, item)) - autocompleteView.setActive(false) - } const canPost = text.length <= MAX_TEXT_LENGTH @@ -386,6 +402,7 @@ export const ComposePost = observer(function ComposePost({ innerRef={textInput} onChangeText={(str: string) => onChangeText(str)} onPaste={onPaste} + onSelectionChange={onSelectionChange} placeholder={selectTextInputPlaceholder} style={[ pal.text, @@ -406,12 +423,27 @@ export const ComposePost = observer(function ComposePost({ /> )} - {isSelectingPhotos && selectedPhotos.length < 4 && ( + {isSelectingPhotos && selectedPhotos.length < 4 ? ( - )} + ) : !extLink && + selectedPhotos.length === 0 && + suggestedExtLinks.size > 0 ? ( + + {Array.from(suggestedExtLinks).map(url => ( + onPressAddLinkCard(url)}> + + Add link card: {url} + + + ))} + + ) : null} {link.isLoading ? ( + style={[styles.image, {backgroundColor: pal.colors.backgroundLight}]}> ) : link.localThumb ? ( - - ) : ( - - )} - - - - - + + ) : undefined} {!!link.meta?.title && ( @@ -81,6 +61,11 @@ export const ExternalEmbed = ({ )} + + + + + ) } @@ -98,10 +83,7 @@ const styles = StyleSheet.create({ borderTopLeftRadius: 6, borderTopRightRadius: 6, width: '100%', - height: 200, - }, - imageFallback: { - height: 160, + maxHeight: 200, }, removeBtn: { position: 'absolute', diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx index d0c023e2..46a0cec6 100644 --- a/src/view/com/composer/Prompt.tsx +++ b/src/view/com/composer/Prompt.tsx @@ -2,7 +2,7 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../util/text/Text' -import {usePalette} from '../../lib/hooks/usePalette' +import {usePalette} from 'lib/hooks/usePalette' export function ComposePrompt({ text = "What's up?", diff --git a/src/view/com/composer/SelectedPhoto.tsx b/src/view/com/composer/SelectedPhoto.tsx index dd508fe1..6aeda33c 100644 --- a/src/view/com/composer/SelectedPhoto.tsx +++ b/src/view/com/composer/SelectedPhoto.tsx @@ -1,7 +1,8 @@ import React, {useCallback} from 'react' -import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' +import {StyleSheet, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {colors} from '../../lib/styles' +import Image from 'view/com/util/images/Image' +import {colors} from 'lib/styles' export const SelectedPhoto = ({ selectedPhotos, diff --git a/src/view/com/composer/char-progress/CharProgress.tsx b/src/view/com/composer/char-progress/CharProgress.tsx index d4093064..cd7cb2c4 100644 --- a/src/view/com/composer/char-progress/CharProgress.tsx +++ b/src/view/com/composer/char-progress/CharProgress.tsx @@ -5,7 +5,7 @@ import {Text} from '../../util/text/Text' import ProgressCircle from 'react-native-progress/Circle' // @ts-ignore no type definition -prf import ProgressPie from 'react-native-progress/Pie' -import {s, colors} from '../../../lib/styles' +import {s, colors} from 'lib/styles' const MAX_TEXT_LENGTH = 256 const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH diff --git a/src/view/com/composer/char-progress/CharProgress.web.tsx b/src/view/com/composer/char-progress/CharProgress.web.tsx index dfb2fad5..d32d7a72 100644 --- a/src/view/com/composer/char-progress/CharProgress.web.tsx +++ b/src/view/com/composer/char-progress/CharProgress.web.tsx @@ -1,7 +1,7 @@ import React from 'react' import {View} from 'react-native' import {Text} from '../../util/text/Text' -import {s} from '../../../lib/styles' +import {s} from 'lib/styles' const MAX_TEXT_LENGTH = 256 const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH diff --git a/src/view/com/composer/photos/PhotoCarouselPicker.tsx b/src/view/com/composer/photos/PhotoCarouselPicker.tsx index 7a5c9f65..406f8b04 100644 --- a/src/view/com/composer/photos/PhotoCarouselPicker.tsx +++ b/src/view/com/composer/photos/PhotoCarouselPicker.tsx @@ -4,6 +4,7 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' +import {useAnalytics} from 'lib/analytics' import { openPicker, openCamera, @@ -12,18 +13,26 @@ import { import { UserLocalPhotosModel, PhotoIdentifier, -} from '../../../../state/models/user-local-photos' -import {compressIfNeeded, scaleDownDimensions} from '../../../../lib/images' -import {usePalette} from '../../../lib/hooks/usePalette' -import {useStores, RootStoreModel} from '../../../../state' +} from 'state/models/user-local-photos' +import { + compressIfNeeded, + moveToPremanantPath, + scaleDownDimensions, +} from 'lib/images' +import {usePalette} from 'lib/hooks/usePalette' +import {useStores, RootStoreModel} from 'state/index' +import { + requestPhotoAccessIfNeeded, + requestCameraAccessIfNeeded, +} from 'lib/permissions' -const MAX_WIDTH = 1000 -const MAX_HEIGHT = 1000 -const MAX_SIZE = 300000 +const MAX_WIDTH = 2000 +const MAX_HEIGHT = 2000 +const MAX_SIZE = 1000000 const IMAGE_PARAMS = { - width: 1000, - height: 1000, + width: 2000, + height: 2000, freeStyleCropEnabled: true, } @@ -46,8 +55,10 @@ export async function cropPhoto( width, height, }) + const img = await compressIfNeeded(cropperRes, MAX_SIZE) - return img.path + const permanentPath = await moveToPremanantPath(img.path) + return permanentPath } export const PhotoCarouselPicker = ({ @@ -57,24 +68,28 @@ export const PhotoCarouselPicker = ({ selectedPhotos: string[] onSelectPhotos: (v: string[]) => void }) => { + const {track} = useAnalytics() const pal = usePalette('default') const store = useStores() - const [localPhotos, setLocalPhotos] = React.useState< - UserLocalPhotosModel | undefined - >(undefined) + const [isSetup, setIsSetup] = React.useState(false) + + const localPhotos = React.useMemo( + () => new UserLocalPhotosModel(store), + [store], + ) - // initial setup React.useEffect(() => { - const photos = new UserLocalPhotosModel(store) - photos.setup().then(() => { - if (photos.photos) { - setLocalPhotos(photos) - } + // initial setup + localPhotos.setup().then(() => { + setIsSetup(true) }) - }, [store]) + }, [localPhotos]) const handleOpenCamera = useCallback(async () => { try { + if (!(await requestCameraAccessIfNeeded())) { + return + } const cameraRes = await openCamera(store, { mediaType: 'photo', ...IMAGE_PARAMS, @@ -89,6 +104,7 @@ export const PhotoCarouselPicker = ({ const handleSelectPhoto = useCallback( async (item: PhotoIdentifier) => { + track('PhotoCarouselPicker:PhotoSelected') try { const imgPath = await cropPhoto( store, @@ -102,37 +118,41 @@ export const PhotoCarouselPicker = ({ store.log.warn('Error selecting photo', err) } }, - [store, selectedPhotos, onSelectPhotos], + [track, store, onSelectPhotos, selectedPhotos], ) - const handleOpenGallery = useCallback(() => { - openPicker(store, { + const handleOpenGallery = useCallback(async () => { + track('PhotoCarouselPicker:GalleryOpened') + if (!(await requestPhotoAccessIfNeeded())) { + return + } + const items = await openPicker(store, { multiple: true, maxFiles: 4 - selectedPhotos.length, mediaType: 'photo', - }).then(async items => { - const result = [] - - for (const image of items) { - // choose target dimensions based on the original - // this causes the photo cropper to start with the full image "selected" - const {width, height} = scaleDownDimensions( - {width: image.width, height: image.height}, - {width: MAX_WIDTH, height: MAX_HEIGHT}, - ) - const cropperRes = await openCropper(store, { - mediaType: 'photo', - path: image.path, - freeStyleCropEnabled: true, - width, - height, - }) - const finalImg = await compressIfNeeded(cropperRes, MAX_SIZE) - result.push(finalImg.path) - } - onSelectPhotos([...selectedPhotos, ...result]) }) - }, [store, selectedPhotos, onSelectPhotos]) + const result = [] + + for (const image of items) { + // choose target dimensions based on the original + // this causes the photo cropper to start with the full image "selected" + const {width, height} = scaleDownDimensions( + {width: image.width, height: image.height}, + {width: MAX_WIDTH, height: MAX_HEIGHT}, + ) + const cropperRes = await openCropper(store, { + mediaType: 'photo', + path: image.path, + ...IMAGE_PARAMS, + width, + height, + }) + const finalImg = await compressIfNeeded(cropperRes, MAX_SIZE) + const permanentPath = await moveToPremanantPath(finalImg.path) + result.push(permanentPath) + } + onSelectPhotos([...selectedPhotos, ...result]) + }, [track, store, selectedPhotos, onSelectPhotos]) return ( - {localPhotos != null && + {isSetup && localPhotos.photos.map((item: PhotoIdentifier, index: number) => ( onChangeText: (str: string) => void + onSelectionChange?: + | ((e: NativeSyntheticEvent) => void) + | undefined onPaste: (err: string | undefined, uris: string[]) => void } @@ -23,6 +31,7 @@ export function TextInput({ placeholder, style, onChangeText, + onSelectionChange, onPaste, children, }: React.PropsWithChildren) { @@ -44,6 +53,7 @@ export function TextInput({ multiline scrollEnabled onChangeText={(str: string) => onChangeText(str)} + onSelectionChange={onSelectionChange} onPaste={onPasteInner} placeholder={placeholder} placeholderTextColor={pal.colors.textLight} diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 395f8e5a..2b610850 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -1,12 +1,14 @@ import React from 'react' import { + NativeSyntheticEvent, StyleProp, StyleSheet, TextInput as RNTextInput, + TextInputSelectionChangeEventData, TextStyle, } from 'react-native' -import {usePalette} from '../../../lib/hooks/usePalette' -import {addStyle} from '../../../lib/addStyle' +import {usePalette} from 'lib/hooks/usePalette' +import {addStyle} from 'lib/styles' export type TextInputRef = RNTextInput @@ -16,6 +18,9 @@ interface TextInputProps { placeholder: string style: StyleProp onChangeText: (str: string) => void + onSelectionChange?: + | ((e: NativeSyntheticEvent) => void) + | undefined onPaste: (err: string | undefined, uris: string[]) => void } @@ -25,6 +30,7 @@ export function TextInput({ placeholder, style, onChangeText, + onSelectionChange, children, }: React.PropsWithChildren) { const pal = usePalette('default') @@ -36,6 +42,7 @@ export function TextInput({ multiline scrollEnabled onChangeText={(str: string) => onChangeText(str)} + onSelectionChange={onSelectionChange} placeholder={placeholder} placeholderTextColor={pal.colors.textLight} style={style}> diff --git a/src/view/com/discover/LiteSuggestedFollows.tsx b/src/view/com/discover/LiteSuggestedFollows.tsx index ce01af7c..5314e691 100644 --- a/src/view/com/discover/LiteSuggestedFollows.tsx +++ b/src/view/com/discover/LiteSuggestedFollows.tsx @@ -8,19 +8,18 @@ import { import LinearGradient from 'react-native-linear-gradient' import {observer} from 'mobx-react-lite' import _omit from 'lodash.omit' -import {ErrorMessage} from '../util/error/ErrorMessage' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {UserAvatar} from '../util/UserAvatar' import * as Toast from '../util/Toast' -import {useStores} from '../../../state' -import * as apilib from '../../../state/lib/api' +import {useStores} from 'state/index' +import * as apilib from 'lib/api/index' import { SuggestedActorsViewModel, SuggestedActor, -} from '../../../state/models/suggested-actors-view' -import {s, gradients} from '../../lib/styles' -import {usePalette} from '../../lib/hooks/usePalette' +} from 'state/models/suggested-actors-view' +import {s, gradients} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' export const LiteSuggestedFollows = observer(() => { const store = useStores() diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx index 9ed89314..1e40956c 100644 --- a/src/view/com/discover/SuggestedFollows.tsx +++ b/src/view/com/discover/SuggestedFollows.tsx @@ -1,51 +1,28 @@ -import React, {useEffect, useState} from 'react' -import { - ActivityIndicator, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' -import LinearGradient from 'react-native-linear-gradient' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {observer} from 'mobx-react-lite' -import _omit from 'lodash.omit' +import React from 'react' +import {ActivityIndicator, StyleSheet, View} from 'react-native' import {CenteredView, FlatList} from '../util/Views' +import {observer} from 'mobx-react-lite' import {ErrorScreen} from '../util/error/ErrorScreen' -import {Link} from '../util/Link' -import {Text} from '../util/text/Text' -import {UserAvatar} from '../util/UserAvatar' -import * as Toast from '../util/Toast' -import {useStores} from '../../../state' -import * as apilib from '../../../state/lib/api' +import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' +import {useStores} from 'state/index' import { SuggestedActorsViewModel, SuggestedActor, -} from '../../../state/models/suggested-actors-view' -import {s, gradients} from '../../lib/styles' -import {usePalette} from '../../lib/hooks/usePalette' +} from 'state/models/suggested-actors-view' +import {s} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' export const SuggestedFollows = observer( - ({ - onNoSuggestions, - asLinks, - }: { - onNoSuggestions?: () => void - asLinks?: boolean - }) => { + ({onNoSuggestions}: {onNoSuggestions?: () => void}) => { const pal = usePalette('default') const store = useStores() - const [follows, setFollows] = useState>({}) - // Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment const view = React.useMemo( () => new SuggestedActorsViewModel(store), [store], ) - useEffect(() => { + React.useEffect(() => { view .loadMore() .catch((err: any) => @@ -53,7 +30,7 @@ export const SuggestedFollows = observer( ) }, [view, store.log]) - useEffect(() => { + React.useEffect(() => { if (!view.isLoading && !view.hasError && !view.hasContent) { onNoSuggestions?.() } @@ -74,46 +51,16 @@ export const SuggestedFollows = observer( ) } - const onPressFollow = async (item: SuggestedActor) => { - try { - const res = await apilib.follow(store, item.did, item.declaration.cid) - setFollows({[item.did]: res.uri, ...follows}) - } catch (e: any) { - store.log.error('Failed fo create follow', e) - Toast.show('An issue occurred, please try again.') - } - } - const onPressUnfollow = async (item: SuggestedActor) => { - try { - await apilib.unfollow(store, follows[item.did]) - setFollows(_omit(follows, [item.did])) - } catch (e: any) { - store.log.error('Failed fo delete follow', e) - Toast.show('An issue occurred, please try again.') - } - } - const renderItem = ({item}: {item: SuggestedActor}) => { - if (asLinks) { - return ( - - - - ) - } return ( - ) } @@ -146,7 +93,6 @@ export const SuggestedFollows = observer( )} contentContainerStyle={s.contentContainer} - style={s.flex1} /> )} @@ -155,128 +101,16 @@ export const SuggestedFollows = observer( }, ) -const User = ({ - item, - follow, - onPressFollow, - onPressUnfollow, -}: { - item: SuggestedActor - follow: string | undefined - onPressFollow: (item: SuggestedActor) => void - onPressUnfollow: (item: SuggestedActor) => void -}) => { - const pal = usePalette('default') - return ( - - - - - - - - {item.displayName || item.handle} - - - @{item.handle} - - - - {follow ? ( - onPressUnfollow(item)}> - - - Unfollow - - - - ) : ( - onPressFollow(item)}> - - - Follow - - - )} - - - {item.description ? ( - - - {item.description} - - - ) : undefined} - - ) -} - const styles = StyleSheet.create({ container: { - flex: 1, + height: '100%', }, suggestionsContainer: { - flex: 1, + height: '100%', }, footer: { height: 200, paddingTop: 20, }, - - actor: { - borderTopWidth: 1, - }, - actorMeta: { - flexDirection: 'row', - }, - actorAvi: { - width: 60, - paddingLeft: 10, - paddingTop: 10, - paddingBottom: 10, - }, - actorContent: { - flex: 1, - paddingRight: 10, - paddingTop: 10, - }, - actorBtn: { - paddingRight: 10, - paddingTop: 10, - }, - actorDetails: { - paddingLeft: 60, - paddingRight: 10, - paddingBottom: 10, - }, - - gradientBtn: { - paddingHorizontal: 24, - paddingVertical: 6, - }, - secondaryBtn: { - paddingHorizontal: 14, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 7, - borderRadius: 50, - marginLeft: 6, - }, }) diff --git a/src/view/com/discover/SuggestedPosts.tsx b/src/view/com/discover/SuggestedPosts.tsx new file mode 100644 index 00000000..86a6bd39 --- /dev/null +++ b/src/view/com/discover/SuggestedPosts.tsx @@ -0,0 +1,66 @@ +import React from 'react' +import {ActivityIndicator, StyleSheet, View} from 'react-native' +import {observer} from 'mobx-react-lite' +import {useStores} from 'state/index' +import {SuggestedPostsView} from 'state/models/suggested-posts-view' +import {s} from 'lib/styles' +import {FeedItem as Post} from '../posts/FeedItem' +import {Text} from '../util/text/Text' +import {usePalette} from 'lib/hooks/usePalette' + +export const SuggestedPosts = observer(() => { + const pal = usePalette('default') + const store = useStores() + const suggestedPostsView = React.useMemo( + () => new SuggestedPostsView(store), + [store], + ) + + React.useEffect(() => { + if (!suggestedPostsView.hasLoaded) { + suggestedPostsView.setup() + } + }, [store, suggestedPostsView]) + + return ( + <> + {(suggestedPostsView.hasContent || suggestedPostsView.isLoading) && ( + + Recently, on Bluesky... + + )} + {suggestedPostsView.hasContent && ( + <> + + {suggestedPostsView.posts.map(item => ( + + ))} + + + )} + {suggestedPostsView.isLoading && ( + + + + )} + + ) +}) + +const styles = StyleSheet.create({ + heading: { + fontWeight: 'bold', + paddingHorizontal: 12, + paddingTop: 16, + paddingBottom: 8, + }, + + bottomBorder: { + borderBottomWidth: 1, + }, + + loadMore: { + paddingLeft: 12, + paddingVertical: 10, + }, +}) diff --git a/src/view/com/discover/WhoToFollow.tsx b/src/view/com/discover/WhoToFollow.tsx new file mode 100644 index 00000000..17c10ca7 --- /dev/null +++ b/src/view/com/discover/WhoToFollow.tsx @@ -0,0 +1,89 @@ +import React from 'react' +import { + ActivityIndicator, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native' +import {observer} from 'mobx-react-lite' +import {useStores} from 'state/index' +import {SuggestedActorsViewModel} from 'state/models/suggested-actors-view' +import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' +import {Text} from '../util/text/Text' +import {s} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' + +export const WhoToFollow = observer(() => { + const pal = usePalette('default') + const store = useStores() + const suggestedActorsView = React.useMemo( + () => new SuggestedActorsViewModel(store, {pageSize: 5}), + [store], + ) + + React.useEffect(() => { + suggestedActorsView.loadMore(true) + }, [store, suggestedActorsView]) + + const onPressLoadMoreSuggestedActors = () => { + suggestedActorsView.loadMore() + } + return ( + <> + {(suggestedActorsView.hasContent || suggestedActorsView.isLoading) && ( + + Who to follow + + )} + {suggestedActorsView.hasContent && ( + <> + + {suggestedActorsView.suggestions.map(item => ( + + ))} + + {!suggestedActorsView.isLoading && suggestedActorsView.hasMore && ( + + + Show more + + + )} + + )} + {suggestedActorsView.isLoading && ( + + + + )} + + ) +}) + +const styles = StyleSheet.create({ + heading: { + fontWeight: 'bold', + paddingHorizontal: 12, + paddingTop: 16, + paddingBottom: 8, + }, + + bottomBorder: { + borderBottomWidth: 1, + }, + + loadMore: { + paddingLeft: 16, + paddingVertical: 12, + }, +}) diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx index fdaafe73..83259330 100644 --- a/src/view/com/lightbox/ImageViewing/index.tsx +++ b/src/view/com/lightbox/ImageViewing/index.tsx @@ -86,12 +86,18 @@ function ImageViewing({ [toggleBarsVisible], ) + const onLayout = useCallback(() => { + if (imageIndex) { + imageList.current?.scrollToIndex({index: imageIndex, animated: false}) + } + }, [imageList, imageIndex]) + if (!visible) { return null } return ( - + @@ -108,12 +114,8 @@ function ImageViewing({ data={images} horizontal pagingEnabled - windowSize={2} - initialNumToRender={1} - maxToRenderPerBatch={1} showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} - initialScrollIndex={imageIndex} getItem={(_, index) => images[index]} getItemCount={() => images.length} getItemLayout={(_, index) => ({ diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index df583978..894c6b11 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -2,9 +2,9 @@ import React from 'react' import {View} from 'react-native' import {observer} from 'mobx-react-lite' import ImageView from './ImageViewing' -import {useStores} from '../../../state' -import * as models from '../../../state/models/shell-ui' -import {saveImageModal} from '../../../lib/images' +import {useStores} from 'state/index' +import * as models from 'state/models/shell-ui' +import {saveImageModal} from 'lib/images' import {ImageSource} from './ImageViewing/@types' export const Lightbox = observer(function Lightbox() { diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx index 4062ddef..dfe4f560 100644 --- a/src/view/com/lightbox/Lightbox.web.tsx +++ b/src/view/com/lightbox/Lightbox.web.tsx @@ -8,9 +8,9 @@ import { } from 'react-native' import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {useStores} from '../../../state' -import * as models from '../../../state/models/shell-ui' -import {colors} from '../../lib/styles' +import {useStores} from 'state/index' +import * as models from 'state/models/shell-ui' +import {colors} from 'lib/styles' interface Img { uri: string diff --git a/src/view/com/login/CreateAccount.tsx b/src/view/com/login/CreateAccount.tsx index dbb3ab52..6dc93f5e 100644 --- a/src/view/com/login/CreateAccount.tsx +++ b/src/view/com/login/CreateAccount.tsx @@ -15,24 +15,22 @@ import { } from '@fortawesome/react-native-fontawesome' import {ComAtprotoAccountCreate} from '@atproto/api' import * as EmailValidator from 'email-validator' -// import {useAnalytics} from '@segment/analytics-react-native' TODO +import {useAnalytics} from 'lib/analytics' import {LogoTextHero} from './Logo' import {Picker} from '../util/Picker' import {TextLink} from '../util/Link' import {Text} from '../util/text/Text' -import {s, colors} from '../../lib/styles' -import { - makeValidHandle, - createFullHandle, - toNiceDomain, -} from '../../../lib/strings' -import {useStores, DEFAULT_SERVICE} from '../../../state' -import {ServiceDescription} from '../../../state/models/session' -import {ServerInputModal} from '../../../state/models/shell-ui' -import {usePalette} from '../../lib/hooks/usePalette' +import {s, colors} from 'lib/styles' +import {makeValidHandle, createFullHandle} from 'lib/strings/handles' +import {toNiceDomain} from 'lib/strings/url-helpers' +import {useStores, DEFAULT_SERVICE} from 'state/index' +import {ServiceDescription} from 'state/models/session' +import {ServerInputModal} from 'state/models/shell-ui' +import {usePalette} from 'lib/hooks/usePalette' +import {cleanError} from 'lib/strings/errors' export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { - // const {track} = useAnalytics() TODO + const {track, screen} = useAnalytics() const pal = usePalette('default') const store = useStores() const [isProcessing, setIsProcessing] = useState(false) @@ -49,6 +47,10 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { const [handle, setHandle] = useState('') const [is13, setIs13] = useState(false) + useEffect(() => { + screen('CreateAccount') + }, [screen]) + useEffect(() => { let aborted = false setError('') @@ -109,7 +111,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { password, inviteCode, }) - // track('Create Account') TODO + track('Create Account') } catch (e: any) { let errMsg = e.toString() if (e instanceof ComAtprotoAccountCreate.InvalidInviteCodeError) { @@ -118,7 +120,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { } store.log.error('Failed to create account', e) setIsProcessing(false) - setError(errMsg.replace(/^Error:/, '')) + setError(cleanError(errMsg)) } } diff --git a/src/view/com/login/Logo.tsx b/src/view/com/login/Logo.tsx index ec53549b..7601ea31 100644 --- a/src/view/com/login/Logo.tsx +++ b/src/view/com/login/Logo.tsx @@ -1,7 +1,7 @@ import React from 'react' import {StyleSheet} from 'react-native' import LinearGradient from 'react-native-linear-gradient' -import {s, gradients} from '../../lib/styles' +import {s, gradients} from 'lib/styles' import {Text} from '../util/text/Text' export const LogoTextHero = () => { diff --git a/src/view/com/login/Signin.tsx b/src/view/com/login/Signin.tsx index 8a260a9b..eed17311 100644 --- a/src/view/com/login/Signin.tsx +++ b/src/view/com/login/Signin.tsx @@ -13,19 +13,21 @@ import { FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import * as EmailValidator from 'email-validator' -import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api' -// import {useAnalytics} from '@segment/analytics-react-native' TODO +import AtpAgent from '@atproto/api' +import {useAnalytics} from 'lib/analytics' import {LogoTextHero} from './Logo' import {Text} from '../util/text/Text' import {UserAvatar} from '../util/UserAvatar' -import {s, colors} from '../../lib/styles' -import {createFullHandle, toNiceDomain} from '../../../lib/strings' -import {useStores, RootStoreModel, DEFAULT_SERVICE} from '../../../state' -import {ServiceDescription} from '../../../state/models/session' -import {ServerInputModal} from '../../../state/models/shell-ui' -import {AccountData} from '../../../state/models/session' -import {isNetworkError} from '../../../lib/errors' -import {usePalette} from '../../lib/hooks/usePalette' +import {s, colors} from 'lib/styles' +import {createFullHandle} from 'lib/strings/handles' +import {toNiceDomain} from 'lib/strings/url-helpers' +import {useStores, RootStoreModel, DEFAULT_SERVICE} from 'state/index' +import {ServiceDescription} from 'state/models/session' +import {ServerInputModal} from 'state/models/shell-ui' +import {AccountData} from 'state/models/session' +import {isNetworkError} from 'lib/strings/errors' +import {usePalette} from 'lib/hooks/usePalette' +import {cleanError} from 'lib/strings/errors' enum Forms { Login, @@ -38,6 +40,7 @@ enum Forms { export const Signin = ({onPressBack}: {onPressBack: () => void}) => { const pal = usePalette('default') const store = useStores() + const {track} = useAnalytics() const [error, setError] = useState('') const [retryDescribeTrigger, setRetryDescribeTrigger] = useState({}) const [serviceUrl, setServiceUrl] = useState(DEFAULT_SERVICE) @@ -91,6 +94,10 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => { }, [store.session, store.log, serviceUrl, retryDescribeTrigger]) const onPressRetryConnect = () => setRetryDescribeTrigger({}) + const onPressForgotPassword = () => { + track('Signin:PressedForgotPassword') + setCurrentForm(Forms.ForgotPassword) + } return ( @@ -104,7 +111,7 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => { setError={setError} setServiceUrl={setServiceUrl} onPressBack={onPressBack} - onPressForgotPassword={gotoForm(Forms.ForgotPassword)} + onPressForgotPassword={onPressForgotPassword} onPressRetryConnect={onPressRetryConnect} /> ) : undefined} @@ -153,15 +160,19 @@ const ChooseAccountForm = ({ onSelectAccount: (account?: AccountData) => void onPressBack: () => void }) => { - // const {track} = useAnalytics() TODO + const {track, screen} = useAnalytics() const pal = usePalette('default') const [isProcessing, setIsProcessing] = React.useState(false) + // React.useEffect(() => { + screen('Choose Account') + // }, [screen]) + const onTryAccount = async (account: AccountData) => { if (account.accessJwt && account.refreshJwt) { setIsProcessing(true) if (await store.session.resumeSession(account)) { - // track('Sign In', {resumedSession: true}) TODO + track('Sign In', {resumedSession: true}) setIsProcessing(false) return } @@ -261,15 +272,16 @@ const LoginForm = ({ onPressBack: () => void onPressForgotPassword: () => void }) => { - // const {track} = useAnalytics() TODO + const {track} = useAnalytics() const pal = usePalette('default') const [isProcessing, setIsProcessing] = useState(false) - const [handle, setHandle] = useState(initialHandle) + const [identifier, setIdentifier] = useState(initialHandle) const [password, setPassword] = useState('') const onPressSelectService = () => { store.shell.openModal(new ServerInputModal(serviceUrl, setServiceUrl)) Keyboard.dismiss() + track('Signin:PressedSelectService') } const onPressNext = async () => { @@ -278,20 +290,21 @@ const LoginForm = ({ try { // try to guess the handle if the user just gave their own username - let fullHandle = handle + let fullIdent = identifier if ( + !identifier.includes('@') && // not an email serviceDescription && serviceDescription.availableUserDomains.length > 0 ) { let matched = false for (const domain of serviceDescription.availableUserDomains) { - if (fullHandle.endsWith(domain)) { + if (fullIdent.endsWith(domain)) { matched = true } } if (!matched) { - fullHandle = createFullHandle( - handle, + fullIdent = createFullHandle( + identifier, serviceDescription.availableUserDomains[0], ) } @@ -299,10 +312,10 @@ const LoginForm = ({ await store.session.login({ service: serviceUrl, - handle: fullHandle, + identifier: fullIdent, password, }) - // track('Sign In', {resumedSession: false}) TODO + track('Sign In', {resumedSession: false}) } catch (e: any) { const errMsg = e.toString() store.log.warn('Failed to login', e) @@ -314,12 +327,12 @@ const LoginForm = ({ 'Unable to contact your service. Please check your Internet connection.', ) } else { - setError(errMsg.replace(/^Error:/, '')) + setError(cleanError(errMsg)) } } } - const isReady = !!serviceDescription && !!handle && !!password + const isReady = !!serviceDescription && !!identifier && !!password return ( @@ -361,13 +374,13 @@ const LoginForm = ({ setHandle((str || '').toLowerCase())} + value={identifier} + onChangeText={str => setIdentifier((str || '').toLowerCase())} editable={!isProcessing} /> @@ -464,6 +477,11 @@ const ForgotPasswordForm = ({ const pal = usePalette('default') const [isProcessing, setIsProcessing] = useState(false) const [email, setEmail] = useState('') + const {screen} = useAnalytics() + + // useEffect(() => { + screen('Signin:ForgotPassword') + // }, [screen]) const onPressSelectService = () => { store.shell.openModal(new ServerInputModal(serviceUrl, setServiceUrl)) @@ -478,8 +496,8 @@ const ForgotPasswordForm = ({ setIsProcessing(true) try { - const api = AtpApi.service(serviceUrl) as SessionServiceClient - await api.com.atproto.account.requestPasswordReset({email}) + const agent = new AtpAgent({service: serviceUrl}) + await agent.api.com.atproto.account.requestPasswordReset({email}) onEmailSent() } catch (e: any) { const errMsg = e.toString() @@ -490,7 +508,7 @@ const ForgotPasswordForm = ({ 'Unable to contact your service. Please check your Internet connection.', ) } else { - setError(errMsg.replace(/^Error:/, '')) + setError(cleanError(errMsg)) } } } @@ -604,6 +622,12 @@ const SetNewPasswordForm = ({ onPasswordSet: () => void }) => { const pal = usePalette('default') + const {screen} = useAnalytics() + + // useEffect(() => { + screen('Signin:SetNewPasswordForm') + // }, [screen]) + const [isProcessing, setIsProcessing] = useState(false) const [resetCode, setResetCode] = useState('') const [password, setPassword] = useState('') @@ -613,8 +637,11 @@ const SetNewPasswordForm = ({ setIsProcessing(true) try { - const api = AtpApi.service(serviceUrl) as SessionServiceClient - await api.com.atproto.account.resetPassword({token: resetCode, password}) + const agent = new AtpAgent({service: serviceUrl}) + await agent.api.com.atproto.account.resetPassword({ + token: resetCode, + password, + }) onPasswordSet() } catch (e: any) { const errMsg = e.toString() @@ -625,7 +652,7 @@ const SetNewPasswordForm = ({ 'Unable to contact your service. Please check your Internet connection.', ) } else { - setError(errMsg.replace(/^Error:/, '')) + setError(cleanError(errMsg)) } } } @@ -726,6 +753,12 @@ const SetNewPasswordForm = ({ } const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => { + const {screen} = useAnalytics() + + // useEffect(() => { + screen('Signin:PasswordUpdatedForm') + // }, [screen]) + const pal = usePalette('default') return ( <> diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx index 3e2ad6ee..60c104f9 100644 --- a/src/view/com/modals/Confirm.tsx +++ b/src/view/com/modals/Confirm.tsx @@ -7,9 +7,10 @@ import { } from 'react-native' import LinearGradient from 'react-native-linear-gradient' import {Text} from '../util/text/Text' -import {useStores} from '../../../state' -import {s, colors, gradients} from '../../lib/styles' +import {useStores} from 'state/index' +import {s, colors, gradients} from 'lib/styles' import {ErrorMessage} from '../util/error/ErrorMessage' +import {cleanError} from 'lib/strings/errors' export const snapPoints = ['50%'] @@ -33,7 +34,7 @@ export function Component({ store.shell.closeModal() return } catch (e: any) { - setError(e.toString()) + setError(cleanError(e)) setIsProcessing(false) } } diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx new file mode 100644 index 00000000..de29e728 --- /dev/null +++ b/src/view/com/modals/DeleteAccount.tsx @@ -0,0 +1,210 @@ +import React from 'react' +import { + ActivityIndicator, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native' +import {BottomSheetTextInput} from '@gorhom/bottom-sheet' +import LinearGradient from 'react-native-linear-gradient' +import * as Toast from '../util/Toast' +import {Text} from '../util/text/Text' +import {useStores} from 'state/index' +import {s, colors, gradients} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {ErrorMessage} from '../util/error/ErrorMessage' +import {cleanError} from 'lib/strings/errors' + +export const snapPoints = ['60%'] + +export function Component({}: {}) { + const pal = usePalette('default') + const store = useStores() + const [isEmailSent, setIsEmailSent] = React.useState(false) + const [confirmCode, setConfirmCode] = React.useState('') + const [password, setPassword] = React.useState('') + const [isProcessing, setIsProcessing] = React.useState(false) + const [error, setError] = React.useState('') + const onPressSendEmail = async () => { + setError('') + setIsProcessing(true) + try { + await store.api.com.atproto.account.requestDelete() + setIsEmailSent(true) + } catch (e: any) { + setError(cleanError(e)) + } + setIsProcessing(false) + } + const onPressConfirmDelete = async () => { + setError('') + setIsProcessing(true) + try { + await store.api.com.atproto.account.delete({ + did: store.me.did, + password, + token: confirmCode, + }) + Toast.show('Your account has been deleted') + store.nav.tab.fixedTabReset() + store.session.clear() + store.shell.closeModal() + } catch (e: any) { + setError(cleanError(e)) + } + setIsProcessing(false) + } + const onCancel = () => { + store.shell.closeModal() + } + return ( + + + + Delete account + + {!isEmailSent ? ( + <> + + For security reasons, we'll need to send a confirmation code to + your email. + + {error ? ( + + + + ) : undefined} + {isProcessing ? ( + + + + ) : ( + <> + + + + Send email + + + + + + Cancel + + + + )} + + ) : ( + <> + + Check your inbox for an email with the confirmation code to enter + below: + + + + Please enter your password as well: + + + {error ? ( + + + + ) : undefined} + {isProcessing ? ( + + + + ) : ( + <> + + + Delete my account + + + + + Cancel + + + + )} + + )} + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + innerContainer: { + paddingBottom: 20, + }, + title: { + textAlign: 'center', + marginTop: 12, + marginBottom: 12, + }, + description: { + textAlign: 'center', + paddingHorizontal: 22, + marginBottom: 10, + }, + mt20: { + marginTop: 20, + }, + mb20: { + marginBottom: 20, + }, + textInput: { + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 16, + paddingVertical: 12, + fontSize: 20, + marginHorizontal: 20, + }, + btn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 32, + padding: 14, + marginHorizontal: 20, + }, + evilBtn: { + backgroundColor: colors.red4, + }, +}) diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index 12b72a39..add75e89 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -11,18 +11,17 @@ import {ScrollView, TextInput} from './util' import {PickedMedia} from '../util/images/image-crop-picker/ImageCropPicker' import {Text} from '../util/text/Text' import {ErrorMessage} from '../util/error/ErrorMessage' -import {useStores} from '../../../state' -import {ProfileViewModel} from '../../../state/models/profile-view' -import {s, colors, gradients} from '../../lib/styles' -import { - enforceLen, - MAX_DISPLAY_NAME, - MAX_DESCRIPTION, -} from '../../../lib/strings' -import {isNetworkError} from '../../../lib/errors' -import {compressIfNeeded} from '../../../lib/images' +import {useStores} from 'state/index' +import {ProfileViewModel} from 'state/models/profile-view' +import {s, colors, gradients} from 'lib/styles' +import {enforceLen} from 'lib/strings/helpers' +import {MAX_DISPLAY_NAME, MAX_DESCRIPTION} from 'lib/constants' +import {compressIfNeeded} from 'lib/images' import {UserBanner} from '../util/UserBanner' import {UserAvatar} from '../util/UserAvatar' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnalytics} from 'lib/analytics' +import {cleanError, isNetworkError} from 'lib/strings/errors' export const snapPoints = ['80%'] @@ -35,6 +34,9 @@ export function Component({ }) { const store = useStores() const [error, setError] = useState('') + const pal = usePalette('default') + const {track} = useAnalytics() + const [isProcessing, setProcessing] = useState(false) const [displayName, setDisplayName] = useState( profileView.displayName || '', @@ -54,24 +56,27 @@ export function Component({ store.shell.closeModal() } const onSelectNewAvatar = async (img: PickedMedia) => { + track('EditProfile:AvatarSelected') try { - const finalImg = await compressIfNeeded(img, 300000) - setNewUserAvatar(finalImg) + const finalImg = await compressIfNeeded(img, 1000000) + setNewUserAvatar({mediaType: 'photo', ...finalImg}) setUserAvatar(finalImg.path) } catch (e: any) { - setError(e.message || e.toString()) + setError(cleanError(e)) } } const onSelectNewBanner = async (img: PickedMedia) => { + track('EditProfile:BannerSelected') try { - const finalImg = await compressIfNeeded(img, 500000) - setNewUserBanner(finalImg) + const finalImg = await compressIfNeeded(img, 1000000) + setNewUserBanner({mediaType: 'photo', ...finalImg}) setUserBanner(finalImg.path) } catch (e: any) { - setError(e.message || e.toString()) + setError(cleanError(e)) } } const onPressSave = async () => { + track('EditProfile:Save') setProcessing(true) if (error) { setError('') @@ -94,7 +99,7 @@ export function Component({ 'Failed to save your profile. Check your internet connection and try again.', ) } else { - setError(e.message) + setError(cleanError(e)) } } setProcessing(false) @@ -103,13 +108,13 @@ export function Component({ return ( - Edit my profile + Edit my profile - + Display Name - Description + Description - Cancel + Cancel diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index d0b40d56..2529d0d5 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -2,23 +2,26 @@ import React, {useRef, useEffect} from 'react' import {View} from 'react-native' import {observer} from 'mobx-react-lite' import BottomSheet from '@gorhom/bottom-sheet' -import {useStores} from '../../../state' +import {useStores} from 'state/index' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' -import * as models from '../../../state/models/shell-ui' +import * as models from 'state/models/shell-ui' import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' import * as ServerInputModal from './ServerInput' import * as ReportPostModal from './ReportPost' import * as ReportAccountModal from './ReportAccount' +import * as DeleteAccountModal from './DeleteAccount' +import {usePalette} from 'lib/hooks/usePalette' +import {StyleSheet} from 'react-native' const CLOSED_SNAPPOINTS = ['10%'] export const Modal = observer(function Modal() { const store = useStores() const bottomSheetRef = useRef(null) - + const pal = usePalette('default') const onBottomSheetChange = (snapPoint: number) => { if (snapPoint === -1) { store.shell.closeModal() @@ -62,10 +65,21 @@ export const Modal = observer(function Modal() { ) } else if (store.shell.activeModal?.name === 'report-post') { snapPoints = ReportPostModal.snapPoints - element = + element = ( + + ) } else if (store.shell.activeModal?.name === 'report-account') { snapPoints = ReportAccountModal.snapPoints - element = + element = ( + + ) + } else if (store.shell.activeModal?.name === 'delete-account') { + snapPoints = DeleteAccountModal.snapPoints + element = } else { element = } @@ -80,8 +94,17 @@ export const Modal = observer(function Modal() { backdropComponent={ store.shell.isModalActive ? createCustomBackdrop(onClose) : undefined } + handleIndicatorStyle={{backgroundColor: pal.text.color}} + handleStyle={[styles.handle, pal.view]} onChange={onBottomSheetChange}> {element} ) }) + +const styles = StyleSheet.create({ + handle: { + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + }, +}) diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 44ea95f0..3c655109 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -1,10 +1,10 @@ import React from 'react' import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' -import {useStores} from '../../../state' -import {usePalette} from '../../lib/hooks/usePalette' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' -import * as models from '../../../state/models/shell-ui' +import * as models from 'state/models/shell-ui' import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' @@ -48,9 +48,17 @@ export const Modal = observer(function Modal() { /> ) } else if (store.shell.activeModal?.name === 'report-post') { - element = + element = ( + + ) } else if (store.shell.activeModal?.name === 'report-account') { - element = + element = ( + + ) } else if (store.shell.activeModal?.name === 'crop-image') { element = ( (false) const [error, setError] = useState('') @@ -28,13 +31,30 @@ export function Component() { const onSelectIssue = (v: string) => setIssue(v) const onPress = async () => { setError('') + if (!issue) { + return + } setIsProcessing(true) try { - // TODO + // NOTE: we should update the lexicon of reasontype to include more options -prf + let reasonType = ComAtprotoReportReasonType.OTHER + if (issue === 'spam') { + reasonType = ComAtprotoReportReasonType.SPAM + } + const reason = ITEMS.find(item => item.key === issue)?.label || '' + await store.api.com.atproto.report.create({ + reasonType, + reason, + subject: { + $type: 'com.atproto.repo.repoRef', + did, + }, + }) + Toast.show("Thank you for your report! We'll look into it promptly.") store.shell.closeModal() return } catch (e: any) { - setError(e.toString()) + setError(cleanError(e)) setIsProcessing(false) } } diff --git a/src/view/com/modals/ReportPost.tsx b/src/view/com/modals/ReportPost.tsx index 8a3a1f75..3d47e7ef 100644 --- a/src/view/com/modals/ReportPost.tsx +++ b/src/view/com/modals/ReportPost.tsx @@ -5,12 +5,15 @@ import { TouchableOpacity, View, } from 'react-native' +import {ComAtprotoReportReasonType} from '@atproto/api' import LinearGradient from 'react-native-linear-gradient' -import {useStores} from '../../../state' -import {s, colors, gradients} from '../../lib/styles' +import {useStores} from 'state/index' +import {s, colors, gradients} from 'lib/styles' import {RadioGroup, RadioGroupItem} from '../util/forms/RadioGroup' import {Text} from '../util/text/Text' +import * as Toast from '../util/Toast' import {ErrorMessage} from '../util/error/ErrorMessage' +import {cleanError} from 'lib/strings/errors' const ITEMS: RadioGroupItem[] = [ {key: 'spam', label: 'Spam or excessive repeat posts'}, @@ -21,7 +24,13 @@ const ITEMS: RadioGroupItem[] = [ export const snapPoints = ['50%'] -export function Component() { +export function Component({ + postUri, + postCid, +}: { + postUri: string + postCid: string +}) { const store = useStores() const [isProcessing, setIsProcessing] = useState(false) const [error, setError] = useState('') @@ -29,13 +38,31 @@ export function Component() { const onSelectIssue = (v: string) => setIssue(v) const onPress = async () => { setError('') + if (!issue) { + return + } setIsProcessing(true) try { - // TODO + // NOTE: we should update the lexicon of reasontype to include more options -prf + let reasonType = ComAtprotoReportReasonType.OTHER + if (issue === 'spam') { + reasonType = ComAtprotoReportReasonType.SPAM + } + const reason = ITEMS.find(item => item.key === issue)?.label || '' + await store.api.com.atproto.report.create({ + reasonType, + reason, + subject: { + $type: 'com.atproto.repo.recordRef', + uri: postUri, + cid: postCid, + }, + }) + Toast.show("Thank you for your report! We'll look into it promptly.") store.shell.closeModal() return } catch (e: any) { - setError(e.toString()) + setError(cleanError(e)) setIsProcessing(false) } } diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx index dde83671..5a9a4cfe 100644 --- a/src/view/com/modals/ServerInput.tsx +++ b/src/view/com/modals/ServerInput.tsx @@ -6,14 +6,10 @@ import { } from '@fortawesome/react-native-fontawesome' import {ScrollView, TextInput} from './util' import {Text} from '../util/text/Text' -import {useStores} from '../../../state' -import {s, colors} from '../../lib/styles' -import { - LOCAL_DEV_SERVICE, - STAGING_SERVICE, - PROD_SERVICE, -} from '../../../state/index' -import {LOGIN_INCLUDE_DEV_SERVERS} from '../../../build-flags' +import {useStores} from 'state/index' +import {s, colors} from 'lib/styles' +import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index' +import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags' export const snapPoints = ['80%'] @@ -37,6 +33,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) { {LOGIN_INCLUDE_DEV_SERVERS ? ( <> doSelect(LOCAL_DEV_SERVICE)}> Local dev server diff --git a/src/view/com/modals/crop-image/CropImage.web.tsx b/src/view/com/modals/crop-image/CropImage.web.tsx index 1f234c4a..e43f3739 100644 --- a/src/view/com/modals/crop-image/CropImage.web.tsx +++ b/src/view/com/modals/crop-image/CropImage.web.tsx @@ -5,10 +5,10 @@ import {Slider} from '@miblanchard/react-native-slider' import LinearGradient from 'react-native-linear-gradient' import {Text} from '../../util/text/Text' import {PickedMedia} from '../../util/images/image-crop-picker/types' -import {s, gradients} from '../../../lib/styles' -import {useStores} from '../../../../state' -import {usePalette} from '../../../lib/hooks/usePalette' -import {SquareIcon, RectWideIcon, RectTallIcon} from '../../../lib/icons' +import {s, gradients} from 'lib/styles' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons' enum AspectRatio { Square = 'square', diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index 55e958c2..5c1ee935 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -1,31 +1,61 @@ -import React from 'react' +import React, {MutableRefObject} from 'react' import {observer} from 'mobx-react-lite' -import {StyleSheet, View} from 'react-native' import {CenteredView, FlatList} from '../util/Views' -import {NotificationsViewModel} from '../../../state/models/notifications-view' +import {ActivityIndicator, StyleSheet, View} from 'react-native' +import {NotificationsViewModel} from 'state/models/notifications-view' import {FeedItem} from './FeedItem' import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {ErrorMessage} from '../util/error/ErrorMessage' import {EmptyState} from '../util/EmptyState' -import {OnScrollCb} from '../../lib/hooks/useOnMainScroll' -import {s} from '../../lib/styles' +import {OnScrollCb} from 'lib/hooks/useOnMainScroll' +import {s} from 'lib/styles' const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} export const Feed = observer(function Feed({ view, + scrollElRef, onPressTryAgain, onScroll, }: { view: NotificationsViewModel + scrollElRef?: MutableRefObject | null> onPressTryAgain?: () => void onScroll?: OnScrollCb }) { + const data = React.useMemo(() => { + let feedItems + if (view.hasLoaded) { + if (view.isEmpty) { + feedItems = [EMPTY_FEED_ITEM] + } else { + feedItems = view.notifications + } + } + return feedItems + }, [view.hasLoaded, view.isEmpty, view.notifications]) + + const onRefresh = React.useCallback(async () => { + try { + await view.refresh() + await view.markAllRead() + } catch (err) { + view.rootStore.log.error('Failed to refresh notifications feed', err) + } + }, [view]) + const onEndReached = React.useCallback(async () => { + try { + await view.loadMore() + } catch (err) { + view.rootStore.log.error('Failed to load more notifications', err) + } + }, [view]) + // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf // VirtualizedList: You have a large list that is slow to update - make sure your // renderItem function renders components that follow React performance best practices // like PureComponent, shouldComponentUpdate, etc - const renderItem = ({item}: {item: any}) => { + const renderItem = React.useCallback(({item}: {item: any}) => { if (item === EMPTY_FEED_ITEM) { return ( - } - const onRefresh = () => { - view - .refresh() - .catch(err => - view.rootStore.log.error('Failed to refresh notifications feed', err), - ) - } - const onEndReached = () => { - view - .loadMore() - .catch(err => - view.rootStore.log.error('Failed to load more notifications', err), - ) - } - let data - if (view.hasLoaded) { - if (view.isEmpty) { - data = [EMPTY_FEED_ITEM] - } else { - data = view.notifications - } - } + }, []) + + const FeedFooter = React.useCallback( + () => + view.isLoading ? ( + + + + ) : ( + + ), + [view], + ) + return ( @@ -72,9 +93,11 @@ export const Feed = observer(function Feed({ {data && ( item._reactKey} renderItem={renderItem} + ListFooterComponent={FeedFooter} refreshing={view.isRefreshing} onRefresh={onRefresh} onEndReached={onEndReached} @@ -87,5 +110,6 @@ export const Feed = observer(function Feed({ }) const styles = StyleSheet.create({ + feedFooter: {paddingTop: 20}, emptyState: {paddingVertical: 40}, }) diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index da6fc7d3..94bedc46 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -14,19 +14,19 @@ import { FontAwesomeIconStyle, Props, } from '@fortawesome/react-native-fontawesome' -import {NotificationsViewItemModel} from '../../../state/models/notifications-view' -import {PostThreadViewModel} from '../../../state/models/post-thread-view' -import {s, colors} from '../../lib/styles' -import {ago, pluralize} from '../../../lib/strings' -import {HeartIconSolid} from '../../lib/icons' +import {NotificationsViewItemModel} from 'state/models/notifications-view' +import {PostThreadViewModel} from 'state/models/post-thread-view' +import {s, colors} from 'lib/styles' +import {ago} from 'lib/strings/time' +import {pluralize} from 'lib/strings/helpers' +import {HeartIconSolid} from 'lib/icons' import {Text} from '../util/text/Text' import {UserAvatar} from '../util/UserAvatar' import {ImageHorzList} from '../util/images/ImageHorzList' -import {ErrorMessage} from '../util/error/ErrorMessage' import {Post} from '../post/Post' import {Link} from '../util/Link' -import {usePalette} from '../../lib/hooks/usePalette' -import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' const MAX_AUTHORS = 5 @@ -78,6 +78,10 @@ export const FeedItem = observer(function FeedItem({ } if (item.isReply || item.isMention) { + if (item.additionalPost?.error) { + // hide errors - it doesnt help the user to show them + return + } return ( } - if (additionalPost.error) { - return - } const text = additionalPost.thread?.postRecord.text const images = ( additionalPost.thread.post.embed as AppBskyEmbedImages.Presented diff --git a/src/view/com/onboard/FeatureExplainer.tsx b/src/view/com/onboard/FeatureExplainer.tsx index d8d502cf..323b1ba1 100644 --- a/src/view/com/onboard/FeatureExplainer.tsx +++ b/src/view/com/onboard/FeatureExplainer.tsx @@ -14,10 +14,10 @@ import { FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {Text} from '../util/text/Text' -import {useStores} from '../../../state' -import {s} from '../../lib/styles' -import {TABS_EXPLAINER} from '../../lib/assets' -import {TABS_ENABLED} from '../../../build-flags' +import {useStores} from 'state/index' +import {s} from 'lib/styles' +import {TABS_EXPLAINER} from 'lib/assets' +import {TABS_ENABLED} from 'lib/build-flags' const ROUTES = TABS_ENABLED ? [ @@ -127,11 +127,15 @@ export const FeatureExplainer = () => { )} - + Skip - + Next diff --git a/src/view/com/onboard/FeatureExplainer.web.tsx b/src/view/com/onboard/FeatureExplainer.web.tsx index 83d46080..177ac58d 100644 --- a/src/view/com/onboard/FeatureExplainer.web.tsx +++ b/src/view/com/onboard/FeatureExplainer.web.tsx @@ -2,7 +2,6 @@ import React, {useState} from 'react' import { Animated, Image, - SafeAreaView, StyleSheet, TouchableOpacity, useWindowDimensions, @@ -15,10 +14,10 @@ import { } from '@fortawesome/react-native-fontawesome' import {CenteredView} from '../util/Views.web' import {Text} from '../util/text/Text' -import {useStores} from '../../../state' -import {s, colors} from '../../lib/styles' -import {TABS_EXPLAINER} from '../../lib/assets' -import {TABS_ENABLED} from '../../../build-flags' +import {useStores} from 'state/index' +import {s, colors} from 'lib/styles' +import {TABS_EXPLAINER} from 'lib/assets' +import {TABS_ENABLED} from 'lib/build-flags' const ROUTES = TABS_ENABLED ? [ diff --git a/src/view/com/onboard/Follows.tsx b/src/view/com/onboard/Follows.tsx index 76eff3f4..e7de82b3 100644 --- a/src/view/com/onboard/Follows.tsx +++ b/src/view/com/onboard/Follows.tsx @@ -3,8 +3,8 @@ import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' import {SuggestedFollows} from '../discover/SuggestedFollows' import {Text} from '../util/text/Text' -import {useStores} from '../../../state' -import {s} from '../../lib/styles' +import {useStores} from 'state/index' +import {s} from 'lib/styles' export const Follows = observer(() => { const store = useStores() @@ -18,13 +18,15 @@ export const Follows = observer(() => { return ( Suggested follows - + + + - + Skip - + Next diff --git a/src/view/com/onboard/Follows.web.tsx b/src/view/com/onboard/Follows.web.tsx index 50e119e4..6b015bb0 100644 --- a/src/view/com/onboard/Follows.web.tsx +++ b/src/view/com/onboard/Follows.web.tsx @@ -1,11 +1,11 @@ import React from 'react' -import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native' +import {SafeAreaView, StyleSheet, TouchableOpacity} from 'react-native' import {observer} from 'mobx-react-lite' import {SuggestedFollows} from '../discover/SuggestedFollows' import {CenteredView} from '../util/Views.web' import {Text} from '../util/text/Text' -import {useStores} from '../../../state' -import {s, colors} from '../../lib/styles' +import {useStores} from 'state/index' +import {s} from 'lib/styles' export const Follows = observer(() => { const store = useStores() diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx index dacdfa50..a9fabac3 100644 --- a/src/view/com/post-thread/PostRepostedBy.tsx +++ b/src/view/com/post-thread/PostRepostedBy.tsx @@ -5,13 +5,10 @@ import {CenteredView, FlatList} from '../util/Views' import { RepostedByViewModel, RepostedByItem, -} from '../../../state/models/reposted-by-view' -import {UserAvatar} from '../util/UserAvatar' +} from 'state/models/reposted-by-view' +import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' import {ErrorMessage} from '../util/error/ErrorMessage' -import {Link} from '../util/Link' -import {Text} from '../util/text/Text' -import {useStores} from '../../../state' -import {s, colors} from '../../lib/styles' +import {useStores} from 'state/index' export const PostRepostedBy = observer(function PostRepostedBy({ uri, @@ -62,7 +59,15 @@ export const PostRepostedBy = observer(function PostRepostedBy({ // loaded // = const renderItem = ({item}: {item: RepostedByItem}) => ( - + ) return ( { - return ( - - - - - - - {item.displayName || item.handle} - @{item.handle} - - - - ) -} - const styles = StyleSheet.create({ - outer: { - marginTop: 1, - backgroundColor: colors.white, - }, - layout: { - flexDirection: 'row', - }, - layoutAvi: { - width: 60, - paddingLeft: 10, - paddingTop: 10, - paddingBottom: 10, - }, - avi: { - width: 40, - height: 40, - borderRadius: 20, - resizeMode: 'cover', - }, - layoutContent: { - flex: 1, - paddingRight: 10, - paddingTop: 10, - paddingBottom: 10, - }, footer: { height: 200, paddingTop: 20, diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 59dbf1e1..0a092c46 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -1,14 +1,14 @@ import React, {useRef} from 'react' import {observer} from 'mobx-react-lite' -import {ActivityIndicator, View} from 'react-native' +import {ActivityIndicator} from 'react-native' import {CenteredView, FlatList} from '../util/Views' import { PostThreadViewModel, PostThreadViewPostModel, -} from '../../../state/models/post-thread-view' +} from 'state/models/post-thread-view' import {PostThreadItem} from './PostThreadItem' import {ErrorMessage} from '../util/error/ErrorMessage' -import {s} from '../../lib/styles' +import {s} from 'lib/styles' export const PostThread = observer(function PostThread({ uri, @@ -18,15 +18,24 @@ export const PostThread = observer(function PostThread({ view: PostThreadViewModel }) { const ref = useRef(null) - const posts = view.thread ? Array.from(flattenThread(view.thread)) : [] - const onRefresh = () => { - view - ?.refresh() - .catch(err => - view.rootStore.log.error('Failed to refresh posts thread', err), - ) - } - const onLayout = () => { + const [isRefreshing, setIsRefreshing] = React.useState(false) + const posts = React.useMemo( + () => (view.thread ? Array.from(flattenThread(view.thread)) : []), + [view.thread], + ) + + // events + // = + const onRefresh = React.useCallback(async () => { + setIsRefreshing(true) + try { + view?.refresh() + } catch (err) { + view.rootStore.log.error('Failed to refresh posts thread', err) + } + setIsRefreshing(false) + }, [view, setIsRefreshing]) + const onLayout = React.useCallback(() => { const index = posts.findIndex(post => post._isHighlightedPost) if (index !== -1) { ref.current?.scrollToIndex({ @@ -35,17 +44,20 @@ export const PostThread = observer(function PostThread({ viewOffset: 40, }) } - } - const onScrollToIndexFailed = (info: { - index: number - highestMeasuredFrameIndex: number - averageItemLength: number - }) => { - ref.current?.scrollToOffset({ - animated: false, - offset: info.averageItemLength * info.index, - }) - } + }, [posts, ref]) + const onScrollToIndexFailed = React.useCallback( + (info: { + index: number + highestMeasuredFrameIndex: number + averageItemLength: number + }) => { + ref.current?.scrollToOffset({ + animated: false, + offset: info.averageItemLength * info.index, + }) + }, + [ref], + ) // loading // = @@ -76,9 +88,10 @@ export const PostThread = observer(function PostThread({ item._reactKey} renderItem={renderItem} - refreshing={view.isRefreshing} + refreshing={isRefreshing} onRefresh={onRefresh} onLayout={onLayout} onScrollToIndexFailed={onScrollToIndexFailed} diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index d3929628..cd3a49d6 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useState} from 'react' +import React from 'react' import {observer} from 'mobx-react-lite' import {StyleSheet, View} from 'react-native' import Clipboard from '@react-native-clipboard/clipboard' @@ -7,22 +7,23 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {PostThreadViewPostModel} from '../../../state/models/post-thread-view' +import {PostThreadViewPostModel} from 'state/models/post-thread-view' import {Link} from '../util/Link' import {RichText} from '../util/text/RichText' import {Text} from '../util/text/Text' import {PostDropdownBtn} from '../util/forms/DropdownButton' import * as Toast from '../util/Toast' import {UserAvatar} from '../util/UserAvatar' -import {s} from '../../lib/styles' -import {ago, pluralize} from '../../../lib/strings' -import {useStores} from '../../../state' +import {s} from 'lib/styles' +import {ago} from 'lib/strings/time' +import {pluralize} from 'lib/strings/helpers' +import {useStores} from 'state/index' import {PostMeta} from '../util/PostMeta' import {PostEmbeds} from '../util/PostEmbeds' import {PostCtrls} from '../util/PostCtrls' import {ErrorMessage} from '../util/error/ErrorMessage' import {ComposePrompt} from '../composer/Prompt' -import {usePalette} from '../../lib/hooks/usePalette' +import {usePalette} from 'lib/hooks/usePalette' const PARENT_REPLY_LINE_LENGTH = 8 @@ -35,29 +36,31 @@ export const PostThreadItem = observer(function PostThreadItem({ }) { const pal = usePalette('default') const store = useStores() - const [deleted, setDeleted] = useState(false) + const [deleted, setDeleted] = React.useState(false) const record = item.postRecord const hasEngagement = item.post.upvoteCount || item.post.repostCount - const itemHref = useMemo(() => { + const itemUri = item.post.uri + const itemCid = item.post.cid + const itemHref = React.useMemo(() => { const urip = new AtUri(item.post.uri) return `/profile/${item.post.author.handle}/post/${urip.rkey}` }, [item.post.uri, item.post.author.handle]) const itemTitle = `Post by ${item.post.author.handle}` const authorHref = `/profile/${item.post.author.handle}` const authorTitle = item.post.author.handle - const upvotesHref = useMemo(() => { + const upvotesHref = React.useMemo(() => { const urip = new AtUri(item.post.uri) return `/profile/${item.post.author.handle}/post/${urip.rkey}/upvoted-by` }, [item.post.uri, item.post.author.handle]) const upvotesTitle = 'Likes on this post' - const repostsHref = useMemo(() => { + const repostsHref = React.useMemo(() => { const urip = new AtUri(item.post.uri) return `/profile/${item.post.author.handle}/post/${urip.rkey}/reposted-by` }, [item.post.uri, item.post.author.handle]) const repostsTitle = 'Reposts of this post' - const onPressReply = () => { + const onPressReply = React.useCallback(() => { store.shell.openComposer({ replyTo: { uri: item.post.uri, @@ -71,22 +74,22 @@ export const PostThreadItem = observer(function PostThreadItem({ }, onPost: onPostReply, }) - } - const onPressToggleRepost = () => { - item + }, [store, item, record, onPostReply]) + const onPressToggleRepost = React.useCallback(() => { + return item .toggleRepost() .catch(e => store.log.error('Failed to toggle repost', e)) - } - const onPressToggleUpvote = () => { - item + }, [item, store]) + const onPressToggleUpvote = React.useCallback(() => { + return item .toggleUpvote() .catch(e => store.log.error('Failed to toggle upvote', e)) - } - const onCopyPostText = () => { + }, [item, store]) + const onCopyPostText = React.useCallback(() => { Clipboard.setString(record?.text || '') Toast.show('Copied to clipboard') - } - const onDeletePost = () => { + }, [record]) + const onDeletePost = React.useCallback(() => { item.delete().then( () => { setDeleted(true) @@ -97,7 +100,7 @@ export const PostThreadItem = observer(function PostThreadItem({ Toast.show('Failed to delete post, please try again') }, ) - } + }, [item, store]) if (!record) { return @@ -154,6 +157,8 @@ export const PostThreadItem = observer(function PostThreadItem({ - {record.text ? ( + {item.richText?.text ? ( @@ -233,6 +237,8 @@ export const PostThreadItem = observer(function PostThreadItem({ This post is by a muted account. - ) : record.text ? ( + ) : item.richText?.text ? ( @@ -314,6 +319,8 @@ export const PostThreadItem = observer(function PostThreadItem({ ) : undefined} - Load more + Continue thread... + ) : undefined} @@ -433,8 +445,12 @@ const styles = StyleSheet.create({ marginRight: 10, }, loadMore: { + flexDirection: 'row', + justifyContent: 'space-between', borderTopWidth: 1, - paddingLeft: 28, + paddingLeft: 80, + paddingRight: 20, paddingVertical: 10, + marginBottom: 8, }, }) diff --git a/src/view/com/post-thread/PostVotedBy.tsx b/src/view/com/post-thread/PostVotedBy.tsx index 680bbadf..2734aaea 100644 --- a/src/view/com/post-thread/PostVotedBy.tsx +++ b/src/view/com/post-thread/PostVotedBy.tsx @@ -2,14 +2,10 @@ import React, {useEffect} from 'react' import {observer} from 'mobx-react-lite' import {ActivityIndicator, StyleSheet, View} from 'react-native' import {CenteredView, FlatList} from '../util/Views' -import {VotesViewModel, VoteItem} from '../../../state/models/votes-view' -import {Link} from '../util/Link' -import {Text} from '../util/text/Text' +import {VotesViewModel, VoteItem} from 'state/models/votes-view' import {ErrorMessage} from '../util/error/ErrorMessage' -import {UserAvatar} from '../util/UserAvatar' -import {useStores} from '../../../state' -import {s} from '../../lib/styles' -import {usePalette} from '../../lib/hooks/usePalette' +import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' +import {useStores} from 'state/index' export const PostVotedBy = observer(function PostVotedBy({ uri, @@ -57,7 +53,17 @@ export const PostVotedBy = observer(function PostVotedBy({ // loaded // = - const renderItem = ({item}: {item: VoteItem}) => + const renderItem = ({item}: {item: VoteItem}) => ( + + ) return ( { - const pal = usePalette('default') - - return ( - - - - - - - - {item.actor.displayName || item.actor.handle} - - - @{item.actor.handle} - - - - - ) -} - const styles = StyleSheet.create({ - outer: { - marginTop: 1, - }, - layout: { - flexDirection: 'row', - }, - layoutAvi: { - width: 60, - paddingLeft: 10, - paddingTop: 10, - paddingBottom: 10, - }, - avi: { - width: 40, - height: 40, - borderRadius: 20, - resizeMode: 'cover', - }, - layoutContent: { - flex: 1, - paddingRight: 10, - paddingTop: 10, - paddingBottom: 10, - }, footer: { height: 200, paddingTop: 20, diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index d00cc83c..8e793ecc 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -10,7 +10,7 @@ import {observer} from 'mobx-react-lite' import Clipboard from '@react-native-clipboard/clipboard' import {AtUri} from '../../../third-party/uri' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {PostThreadViewModel} from '../../../state/models/post-thread-view' +import {PostThreadViewModel} from 'state/models/post-thread-view' import {Link} from '../util/Link' import {UserInfoText} from '../util/UserInfoText' import {PostMeta} from '../util/PostMeta' @@ -20,9 +20,9 @@ import {Text} from '../util/text/Text' import {RichText} from '../util/text/RichText' import * as Toast from '../util/Toast' import {UserAvatar} from '../util/UserAvatar' -import {useStores} from '../../../state' -import {s, colors} from '../../lib/styles' -import {usePalette} from '../../lib/hooks/usePalette' +import {useStores} from 'state/index' +import {s, colors} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' export const Post = observer(function Post({ uri, @@ -80,6 +80,8 @@ export const Post = observer(function Post({ const item = view.thread const record = view.thread.postRecord + const itemUri = item.post.uri + const itemCid = item.post.cid const itemUrip = new AtUri(item.post.uri) const itemHref = `/profile/${item.post.author.handle}/post/${itemUrip.rkey}` const itemTitle = `Post by ${item.post.author.handle}` @@ -105,12 +107,12 @@ export const Post = observer(function Post({ }) } const onPressToggleRepost = () => { - item + return item .toggleRepost() .catch(e => store.log.error('Failed to toggle repost', e)) } const onPressToggleUpvote = () => { - item + return item .toggleUpvote() .catch(e => store.log.error('Failed to toggle upvote', e)) } @@ -178,18 +180,19 @@ export const Post = observer(function Post({ This post is by a muted account. - ) : record.text ? ( + ) : item.richText?.text ? ( ) : undefined} void }) { - const pal = usePalette('default') - return ( - - onPressCompose(false)} - style={[styles.btn, {backgroundColor: pal.colors.backgroundLight}]}> - - New post - - - onPressCompose(true)} - style={[styles.btn, {backgroundColor: pal.colors.backgroundLight}]}> - - Share photo - - - - ) + return null } - -const styles = StyleSheet.create({ - container: { - paddingVertical: 12, - paddingHorizontal: 16, - flexDirection: 'row', - alignItems: 'center', - borderTopWidth: 1, - }, - btn: { - paddingVertical: 6, - paddingHorizontal: 14, - borderRadius: 30, - marginRight: 10, - }, -}) diff --git a/src/view/com/posts/ComposerPrompt.web.tsx b/src/view/com/posts/ComposerPrompt.web.tsx index 96c09f0b..a87653cf 100644 --- a/src/view/com/posts/ComposerPrompt.web.tsx +++ b/src/view/com/posts/ComposerPrompt.web.tsx @@ -1,8 +1,8 @@ import React from 'react' import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' import {Text} from '../util/text/Text' -import {usePalette} from '../../lib/hooks/usePalette' -import {s} from '../../lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {s} from 'lib/styles' export function ComposerPrompt({ onPressCompose, diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 409ce4af..57363ca5 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -11,103 +11,144 @@ import {CenteredView, FlatList} from '../util/Views' import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {EmptyState} from '../util/EmptyState' import {ErrorMessage} from '../util/error/ErrorMessage' -import {FeedModel} from '../../../state/models/feed-view' +import {FeedModel} from 'state/models/feed-view' import {FeedItem} from './FeedItem' import {ComposerPrompt} from './ComposerPrompt' -import {OnScrollCb} from '../../lib/hooks/useOnMainScroll' -import {s} from '../../lib/styles' +import {OnScrollCb} from 'lib/hooks/useOnMainScroll' +import {s} from 'lib/styles' +import {useAnalytics} from 'lib/analytics' const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} +const ERROR_FEED_ITEM = {_reactKey: '__error__'} export const Feed = observer(function Feed({ feed, style, scrollElRef, - onPressCompose, onPressTryAgain, + onPressCompose, onScroll, testID, + headerOffset = 0, }: { feed: FeedModel style?: StyleProp scrollElRef?: MutableRefObject | null> - onPressCompose: (imagesOpen?: boolean) => void onPressTryAgain?: () => void + onPressCompose: (imagesOpen?: boolean) => void onScroll?: OnScrollCb testID?: string + headerOffset?: number }) { + const {track} = useAnalytics() + const [isRefreshing, setIsRefreshing] = React.useState(false) + + const data = React.useMemo(() => { + let feedItems: any[] = [] + if (feed.hasLoaded) { + feedItems = feedItems.concat([COMPOSE_PROMPT_ITEM]) + if (feed.hasError) { + feedItems = feedItems.concat([ERROR_FEED_ITEM]) + } + if (feed.isEmpty) { + feedItems = feedItems.concat([EMPTY_FEED_ITEM]) + } else { + feedItems = feedItems.concat(feed.feed) + } + } + return feedItems + }, [feed.hasError, feed.hasLoaded, feed.isEmpty, feed.feed]) + + // events + // = + + const onRefresh = React.useCallback(async () => { + track('Feed:onRefresh') + setIsRefreshing(true) + try { + await feed.refresh() + } catch (err) { + feed.rootStore.log.error('Failed to refresh posts feed', err) + } + setIsRefreshing(false) + }, [feed, track, setIsRefreshing]) + const onEndReached = React.useCallback(async () => { + track('Feed:onEndReached') + try { + await feed.loadMore() + } catch (err) { + feed.rootStore.log.error('Failed to load more posts', err) + } + }, [feed, track]) + + // rendering + // = + // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf // VirtualizedList: You have a large list that is slow to update - make sure your // renderItem function renders components that follow React performance best practices // like PureComponent, shouldComponentUpdate, etc - const renderItem = ({item}: {item: any}) => { - if (item === COMPOSE_PROMPT_ITEM) { - return - } else if (item === EMPTY_FEED_ITEM) { - return ( - - ) - } else { - return - } - } - const onRefresh = () => { - feed - .refresh() - .catch(err => - feed.rootStore.log.error('Failed to refresh posts feed', err), - ) - } - const onEndReached = () => { - feed - .loadMore() - .catch(err => feed.rootStore.log.error('Failed to load more posts', err)) - } - let data - if (feed.hasLoaded) { - if (feed.isEmpty) { - data = [COMPOSE_PROMPT_ITEM, EMPTY_FEED_ITEM] - } else { - data = [COMPOSE_PROMPT_ITEM].concat(feed.feed) - } - } - const FeedFooter = () => - feed.isLoading ? ( - - - - ) : ( - - ) - return ( - - - {!data && } - {feed.isLoading && !data && } - {feed.hasError && ( + const renderItem = React.useCallback( + ({item}: {item: any}) => { + if (item === COMPOSE_PROMPT_ITEM) { + return + } else if (item === EMPTY_FEED_ITEM) { + return ( + + ) + } else if (item === ERROR_FEED_ITEM) { + return ( - )} - - {feed.hasLoaded && data && ( + ) + } + return + }, + [feed, onPressTryAgain, onPressCompose], + ) + + const FeedFooter = React.useCallback( + () => + feed.isLoading ? ( + + + + ) : ( + + ), + [feed], + ) + + return ( + + {feed.isLoading && data.length === 0 && ( + + + + )} + {data.length > 0 && ( item._reactKey} renderItem={renderItem} ListFooterComponent={FeedFooter} - refreshing={feed.isRefreshing} + refreshing={isRefreshing} contentContainerStyle={s.contentContainer} onScroll={onScroll} onRefresh={onRefresh} onEndReached={onEndReached} + removeClippedSubviews={true} + contentInset={{top: headerOffset}} + contentOffset={{x: 0, y: headerOffset * -1}} + progressViewOffset={headerOffset} /> )} diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index cda2ac0b..67807b14 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -8,7 +8,7 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {FeedItemModel} from '../../../state/models/feed-view' +import {FeedItemModel} from 'state/models/feed-view' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {UserInfoText} from '../util/UserInfoText' @@ -18,9 +18,10 @@ import {PostEmbeds} from '../util/PostEmbeds' import {RichText} from '../util/text/RichText' import * as Toast from '../util/Toast' import {UserAvatar} from '../util/UserAvatar' -import {s} from '../../lib/styles' -import {useStores} from '../../../state' -import {usePalette} from '../../lib/hooks/usePalette' +import {s} from 'lib/styles' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnalytics} from 'lib/analytics' export const FeedItem = observer(function ({ item, @@ -33,8 +34,11 @@ export const FeedItem = observer(function ({ }) { const store = useStores() const pal = usePalette('default') + const {track} = useAnalytics() const [deleted, setDeleted] = useState(false) const record = item.postRecord + const itemUri = item.post.uri + const itemCid = item.post.cid const itemHref = useMemo(() => { const urip = new AtUri(item.post.uri) return `/profile/${item.post.author.handle}/post/${urip.rkey}` @@ -50,6 +54,7 @@ export const FeedItem = observer(function ({ }, [record?.reply]) const onPressReply = () => { + track('FeedItem:PostReply') store.shell.openComposer({ replyTo: { uri: item.post.uri, @@ -64,12 +69,14 @@ export const FeedItem = observer(function ({ }) } const onPressToggleRepost = () => { - item + track('FeedItem:PostRepost') + return item .toggleRepost() .catch(e => store.log.error('Failed to toggle repost', e)) } const onPressToggleUpvote = () => { - item + track('FeedItem:PostLike') + return item .toggleUpvote() .catch(e => store.log.error('Failed to toggle upvote', e)) } @@ -78,6 +85,7 @@ export const FeedItem = observer(function ({ Toast.show('Copied to clipboard') } const onDeletePost = () => { + track('FeedItem:PostDelete') item.delete().then( () => { setDeleted(true) @@ -195,12 +203,11 @@ export const FeedItem = observer(function ({ This post is by a muted account. - ) : record.text ? ( + ) : item.richText?.text ? ( @@ -210,6 +217,8 @@ export const FeedItem = observer(function ({ ) : null} JSX.Element - onPressButton?: () => void }) { const pal = usePalette('default') return ( @@ -36,30 +42,117 @@ export function ProfileCard({ /> - + {displayName || handle} - + @{handle} + {isFollowedBy && ( + + + Follows You + + + )} {renderButton ? ( - - - {renderButton()} - - + {renderButton()} ) : undefined} + {description ? ( + + + {description} + + + ) : undefined} ) } +export const ProfileCardWithFollowBtn = observer( + ({ + did, + declarationCid, + handle, + displayName, + avatar, + description, + isFollowedBy, + }: { + did: string + declarationCid: string + handle: string + displayName?: string + avatar?: string + description?: string + isFollowedBy?: boolean + }) => { + const store = useStores() + const isMe = store.me.handle === handle + const isFollowing = store.me.follows.isFollowing(did) + const onToggleFollow = async () => { + if (store.me.follows.isFollowing(did)) { + try { + await apilib.unfollow(store, store.me.follows.getFollowUri(did)) + store.me.follows.removeFollow(did) + } catch (e: any) { + store.log.error('Failed fo delete follow', e) + Toast.show('An issue occurred, please try again.') + } + } else { + try { + const res = await apilib.follow(store, did, declarationCid) + store.me.follows.addFollow(did, res.uri) + } catch (e: any) { + store.log.error('Failed fo create follow', e) + Toast.show('An issue occurred, please try again.') + } + } + } + return ( + ( + + ) + } + /> + ) + }, +) + +function FollowBtn({ + isFollowing, + onPress, +}: { + isFollowing: boolean + onPress: () => void +}) { + const pal = usePalette('default') + return ( + + + + {isFollowing ? 'Unfollow' : 'Follow'} + + + + ) +} + const styles = StyleSheet.create({ outer: { borderTopWidth: 1, + paddingHorizontal: 6, }, layout: { flexDirection: 'row', @@ -68,7 +161,7 @@ const styles = StyleSheet.create({ layoutAvi: { width: 60, paddingLeft: 10, - paddingTop: 10, + paddingTop: 8, paddingBottom: 10, }, avi: { @@ -80,19 +173,26 @@ const styles = StyleSheet.create({ layoutContent: { flex: 1, paddingRight: 10, - paddingTop: 12, + paddingTop: 10, paddingBottom: 10, }, layoutButton: { paddingRight: 10, }, + details: { + paddingLeft: 60, + paddingRight: 10, + paddingBottom: 10, + }, + pill: { + borderRadius: 4, + paddingHorizontal: 6, + paddingVertical: 2, + }, btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', paddingVertical: 7, - paddingHorizontal: 14, borderRadius: 50, marginLeft: 6, + paddingHorizontal: 14, }, }) diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx index 13d134c3..7db770e4 100644 --- a/src/view/com/profile/ProfileFollowers.tsx +++ b/src/view/com/profile/ProfileFollowers.tsx @@ -4,15 +4,11 @@ import {ActivityIndicator, StyleSheet, View} from 'react-native' import { UserFollowersViewModel, FollowerItem, -} from '../../../state/models/user-followers-view' +} from 'state/models/user-followers-view' import {CenteredView, FlatList} from '../util/Views' -import {Link} from '../util/Link' -import {Text} from '../util/text/Text' import {ErrorMessage} from '../util/error/ErrorMessage' -import {UserAvatar} from '../util/UserAvatar' -import {useStores} from '../../../state' -import {s} from '../../lib/styles' -import {usePalette} from '../../lib/hooks/usePalette' +import {ProfileCardWithFollowBtn} from './ProfileCard' +import {useStores} from 'state/index' export const ProfileFollowers = observer(function ProfileFollowers({ name, @@ -63,7 +59,15 @@ export const ProfileFollowers = observer(function ProfileFollowers({ // loaded // = const renderItem = ({item}: {item: FollowerItem}) => ( - + ) return ( { - const pal = usePalette('default') - return ( - - - - - - - - {item.displayName || item.handle} - - - @{item.handle} - - - - - ) -} - const styles = StyleSheet.create({ - outer: { - borderTopWidth: 1, - }, - layout: { - flexDirection: 'row', - }, - layoutAvi: { - width: 60, - paddingLeft: 10, - paddingTop: 10, - paddingBottom: 10, - }, - layoutContent: { - flex: 1, - paddingRight: 10, - paddingTop: 10, - paddingBottom: 10, - }, footer: { height: 200, paddingTop: 20, diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx index de2fe332..fb7f08ed 100644 --- a/src/view/com/profile/ProfileFollows.tsx +++ b/src/view/com/profile/ProfileFollows.tsx @@ -2,17 +2,10 @@ import React, {useEffect} from 'react' import {observer} from 'mobx-react-lite' import {ActivityIndicator, StyleSheet, View} from 'react-native' import {CenteredView, FlatList} from '../util/Views' -import { - UserFollowsViewModel, - FollowItem, -} from '../../../state/models/user-follows-view' -import {Link} from '../util/Link' -import {Text} from '../util/text/Text' +import {UserFollowsViewModel, FollowItem} from 'state/models/user-follows-view' import {ErrorMessage} from '../util/error/ErrorMessage' -import {UserAvatar} from '../util/UserAvatar' -import {useStores} from '../../../state' -import {s} from '../../lib/styles' -import {usePalette} from '../../lib/hooks/usePalette' +import {ProfileCardWithFollowBtn} from './ProfileCard' +import {useStores} from 'state/index' export const ProfileFollows = observer(function ProfileFollows({ name, @@ -63,7 +56,15 @@ export const ProfileFollows = observer(function ProfileFollows({ // loaded // = const renderItem = ({item}: {item: FollowItem}) => ( - + ) return ( { - const pal = usePalette('default') - return ( - - - - - - - - {item.displayName || item.handle} - - - @{item.handle} - - - - - ) -} - const styles = StyleSheet.create({ - outer: { - borderTopWidth: 1, - }, - layout: { - flexDirection: 'row', - }, - layoutAvi: { - width: 60, - paddingLeft: 10, - paddingTop: 10, - paddingBottom: 10, - }, - layoutContent: { - flex: 1, - paddingRight: 10, - paddingTop: 10, - paddingBottom: 10, - }, footer: { height: 200, paddingTop: 20, diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index cebeea78..0ca6e1e7 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -13,15 +13,16 @@ import { FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {BlurView} from '../util/BlurView' -import {ProfileViewModel} from '../../../state/models/profile-view' -import {useStores} from '../../../state' +import {ProfileViewModel} from 'state/models/profile-view' +import {useStores} from 'state/index' import { EditProfileModal, ReportAccountModal, ProfileImageLightbox, -} from '../../../state/models/shell-ui' -import {pluralize, toShareUrl} from '../../../lib/strings' -import {s, gradients} from '../../lib/styles' +} from 'state/models/shell-ui' +import {pluralize} from 'lib/strings/helpers' +import {toShareUrl} from 'lib/strings/url-helpers' +import {s, gradients} from 'lib/styles' import {DropdownButton, DropdownItem} from '../util/forms/DropdownButton' import * as Toast from '../util/Toast' import {LoadingPlaceholder} from '../util/LoadingPlaceholder' @@ -29,7 +30,8 @@ import {Text} from '../util/text/Text' import {RichText} from '../util/text/RichText' import {UserAvatar} from '../util/UserAvatar' import {UserBanner} from '../util/UserBanner' -import {usePalette} from '../../lib/hooks/usePalette' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnalytics} from 'lib/analytics' export const ProfileHeader = observer(function ProfileHeader({ view, @@ -40,7 +42,7 @@ export const ProfileHeader = observer(function ProfileHeader({ }) { const pal = usePalette('default') const store = useStores() - + const {track} = useAnalytics() const onPressBack = () => { store.nav.tab.goBack() } @@ -53,7 +55,7 @@ export const ProfileHeader = observer(function ProfileHeader({ view?.toggleFollowing().then( () => { Toast.show( - `${view.myState.follow ? 'Following' : 'No longer following'} ${ + `${view.viewer.following ? 'Following' : 'No longer following'} ${ view.displayName || view.handle }`, ) @@ -62,18 +64,23 @@ export const ProfileHeader = observer(function ProfileHeader({ ) } const onPressEditProfile = () => { + track('ProfileHeader:EditProfileButtonClicked') store.shell.openModal(new EditProfileModal(view, onRefreshAll)) } const onPressFollowers = () => { + track('ProfileHeader:FollowersButtonClicked') store.nav.navigate(`/profile/${view.handle}/followers`) } const onPressFollows = () => { + track('ProfileHeader:FollowsButtonClicked') store.nav.navigate(`/profile/${view.handle}/follows`) } const onPressShare = () => { + track('ProfileHeader:ShareButtonClicked') Share.share({url: toShareUrl(`/profile/${view.handle}`)}) } const onPressMuteAccount = async () => { + track('ProfileHeader:MuteAccountButtonClicked') try { await view.muteAccount() Toast.show('Account muted') @@ -83,6 +90,7 @@ export const ProfileHeader = observer(function ProfileHeader({ } } const onPressUnmuteAccount = async () => { + track('ProfileHeader:UnmuteAccountButtonClicked') try { await view.unmuteAccount() Toast.show('Account unmuted') @@ -92,6 +100,7 @@ export const ProfileHeader = observer(function ProfileHeader({ } } const onPressReportAccount = () => { + track('ProfileHeader:ReportAccountButtonClicked') store.shell.openModal(new ReportAccountModal(view.did)) } @@ -110,7 +119,7 @@ export const ProfileHeader = observer(function ProfileHeader({ - + {view.displayName || view.handle} @@ -135,8 +144,8 @@ export const ProfileHeader = observer(function ProfileHeader({ let dropdownItems: DropdownItem[] = [{label: 'Share', onPress: onPressShare}] if (!isMe) { dropdownItems.push({ - label: view.myState.muted ? 'Unmute Account' : 'Mute Account', - onPress: view.myState.muted ? onPressUnmuteAccount : onPressMuteAccount, + label: view.viewer.muted ? 'Unmute Account' : 'Mute Account', + onPress: view.viewer.muted ? onPressUnmuteAccount : onPressMuteAccount, }) dropdownItems.push({ label: 'Report Account', @@ -159,7 +168,7 @@ export const ProfileHeader = observer(function ProfileHeader({ ) : ( <> - {view.myState.follow ? ( + {store.me.follows.isFollowing(view.did) ? ( @@ -206,11 +215,18 @@ export const ProfileHeader = observer(function ProfileHeader({ ) : undefined} - + {view.displayName || view.handle} + {view.viewer.followedBy ? ( + + + Follows you + + + ) : undefined} @{view.handle} @@ -247,22 +263,21 @@ export const ProfileHeader = observer(function ProfileHeader({ - {view.description ? ( + {view.descriptionRichText ? ( ) : undefined} - {view.myState.muted ? ( + {view.viewer.muted ? ( - Account muted. + Account muted ) : undefined} @@ -369,6 +384,12 @@ const styles = StyleSheet.create({ marginBottom: 5, }, + pill: { + borderRadius: 4, + paddingHorizontal: 6, + paddingVertical: 2, + }, + br40: {borderRadius: 40}, br50: {borderRadius: 50}, }) diff --git a/src/view/com/util/BlurView.web.tsx b/src/view/com/util/BlurView.web.tsx index 7e4300c7..efcf40b9 100644 --- a/src/view/com/util/BlurView.web.tsx +++ b/src/view/com/util/BlurView.web.tsx @@ -1,6 +1,6 @@ import React from 'react' import {StyleSheet, View, ViewProps} from 'react-native' -import {addStyle} from '../../lib/addStyle' +import {addStyle} from 'lib/styles' type BlurViewProps = ViewProps & { blurType?: 'dark' | 'light' diff --git a/src/view/com/util/EmptyState.tsx b/src/view/com/util/EmptyState.tsx index 6c5c3f34..2b2c4e65 100644 --- a/src/view/com/util/EmptyState.tsx +++ b/src/view/com/util/EmptyState.tsx @@ -6,8 +6,8 @@ import { FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {Text} from './text/Text' -import {UserGroupIcon} from '../../lib/icons' -import {usePalette} from '../../lib/hooks/usePalette' +import {UserGroupIcon} from 'lib/icons' +import {usePalette} from 'lib/hooks/usePalette' export function EmptyState({ icon, diff --git a/src/view/com/util/FAB.tsx b/src/view/com/util/FAB.tsx index 1129f336..007ca0ee 100644 --- a/src/view/com/util/FAB.tsx +++ b/src/view/com/util/FAB.tsx @@ -1,41 +1,53 @@ import React from 'react' import {observer} from 'mobx-react-lite' import { + Animated, GestureResponderEvent, StyleSheet, TouchableWithoutFeedback, - View, } from 'react-native' import LinearGradient from 'react-native-linear-gradient' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {IconProp} from '@fortawesome/fontawesome-svg-core' -import {colors, gradients} from '../../lib/styles' -import {useStores} from '../../../state' +import {colors, gradients} from 'lib/styles' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {useStores} from 'state/index' type OnPress = ((event: GestureResponderEvent) => void) | undefined export const FAB = observer( - ({icon, onPress}: {icon: IconProp; onPress: OnPress}) => { + ({ + testID, + icon, + onPress, + }: { + testID?: string + icon: IconProp + onPress: OnPress + }) => { const store = useStores() + const interp = useAnimatedValue(0) + React.useEffect(() => { + Animated.timing(interp, { + toValue: store.shell.minimalShellMode ? 1 : 0, + duration: 100, + useNativeDriver: true, + isInteraction: false, + }).start() + }, [interp, store.shell.minimalShellMode]) + const transform = { + transform: [{translateY: Animated.multiply(interp, 60)}], + } return ( - - + + - + - + ) }, @@ -46,16 +58,10 @@ const styles = StyleSheet.create({ position: 'absolute', zIndex: 1, right: 22, - bottom: 84, + bottom: 94, width: 60, height: 60, borderRadius: 30, - shadowColor: '#000', - shadowOpacity: 0.3, - shadowOffset: {width: 0, height: 1}, - }, - lower: { - bottom: 34, }, inner: { width: 60, @@ -64,5 +70,4 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', }, - icon: {}, }) diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 1cbb1af8..bdc44793 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -10,9 +10,9 @@ import { ViewStyle, } from 'react-native' import {Text} from './text/Text' -import {TypographyVariant} from '../../lib/ThemeContext' -import {useStores, RootStoreModel} from '../../../state' -import {convertBskyAppUrlIfNeeded} from '../../../lib/strings' +import {TypographyVariant} from 'lib/ThemeContext' +import {useStores, RootStoreModel} from 'state/index' +import {convertBskyAppUrlIfNeeded} from 'lib/strings/url-helpers' export const Link = observer(function Link({ style, @@ -22,17 +22,21 @@ export const Link = observer(function Link({ noFeedback, }: { style?: StyleProp - href: string + href?: string title?: string children?: React.ReactNode noFeedback?: boolean }) { const store = useStores() const onPress = () => { - handleLink(store, href, false) + if (href) { + handleLink(store, href, false) + } } const onLongPress = () => { - handleLink(store, href, true) + if (href) { + handleLink(store, href, true) + } } if (noFeedback) { return ( diff --git a/src/view/com/util/LoadLatestBtn.tsx b/src/view/com/util/LoadLatestBtn.tsx index 43fa97e6..fd05ecc9 100644 --- a/src/view/com/util/LoadLatestBtn.tsx +++ b/src/view/com/util/LoadLatestBtn.tsx @@ -4,9 +4,9 @@ import {observer} from 'mobx-react-lite' import LinearGradient from 'react-native-linear-gradient' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {Text} from './text/Text' -import {colors, gradients} from '../../lib/styles' +import {colors, gradients} from 'lib/styles' import {clamp} from 'lodash' -import {useStores} from '../../../state' +import {useStores} from 'state/index' const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} diff --git a/src/view/com/util/LoadLatestBtn.web.tsx b/src/view/com/util/LoadLatestBtn.web.tsx index 38892738..182c1ba5 100644 --- a/src/view/com/util/LoadLatestBtn.web.tsx +++ b/src/view/com/util/LoadLatestBtn.web.tsx @@ -1,7 +1,7 @@ import React from 'react' import {StyleSheet, TouchableOpacity} from 'react-native' import {Text} from './text/Text' -import {usePalette} from '../../lib/hooks/usePalette' +import {usePalette} from 'lib/hooks/usePalette' const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx index 9bb200d5..9e72640d 100644 --- a/src/view/com/util/LoadingPlaceholder.tsx +++ b/src/view/com/util/LoadingPlaceholder.tsx @@ -1,10 +1,10 @@ import React from 'react' import {StyleSheet, StyleProp, View, ViewStyle} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {HeartIcon} from '../../lib/icons' -import {s} from '../../lib/styles' -import {useTheme} from '../../lib/ThemeContext' -import {usePalette} from '../../lib/hooks/usePalette' +import {HeartIcon} from 'lib/icons' +import {s} from 'lib/styles' +import {useTheme} from 'lib/ThemeContext' +import {usePalette} from 'lib/hooks/usePalette' export function LoadingPlaceholder({ width, diff --git a/src/view/com/util/Picker.tsx b/src/view/com/util/Picker.tsx index 208ec049..9007cb1f 100644 --- a/src/view/com/util/Picker.tsx +++ b/src/view/com/util/Picker.tsx @@ -16,7 +16,7 @@ import { } from '@fortawesome/react-native-fontawesome' import RootSiblings from 'react-native-root-siblings' import {Text} from './text/Text' -import {colors} from '../../lib/styles' +import {colors} from 'lib/styles' interface PickerItem { value: string diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx index fca70b68..e42c5e63 100644 --- a/src/view/com/util/PostCtrls.tsx +++ b/src/view/com/util/PostCtrls.tsx @@ -1,6 +1,5 @@ import React from 'react' import { - Animated, StyleProp, StyleSheet, TouchableOpacity, @@ -12,6 +11,11 @@ import { FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import ReactNativeHapticFeedback from 'react-native-haptic-feedback' +// DISABLED see #135 +// import { +// TriggerableAnimated, +// TriggerableAnimatedRef, +// } from './anim/TriggerableAnimated' import {Text} from './text/Text' import {PostDropdownBtn} from './forms/DropdownButton' import { @@ -19,12 +23,13 @@ import { HeartIconSolid, RepostIcon, CommentBottomArrow, -} from '../../lib/icons' -import {s, colors} from '../../lib/styles' -import {useTheme} from '../../lib/ThemeContext' -import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' +} from 'lib/icons' +import {s, colors} from 'lib/styles' +import {useTheme} from 'lib/ThemeContext' interface PostCtrlsOpts { + itemUri: string + itemCid: string itemHref: string itemTitle: string isAuthor: boolean @@ -36,13 +41,49 @@ interface PostCtrlsOpts { isReposted: boolean isUpvoted: boolean onPressReply: () => void - onPressToggleRepost: () => void - onPressToggleUpvote: () => void + onPressToggleRepost: () => Promise + onPressToggleUpvote: () => Promise onCopyPostText: () => void onDeletePost: () => void } -const HITSLOP = {top: 2, left: 2, bottom: 2, right: 2} +const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} + +// DISABLED see #135 +/* +function ctrlAnimStart(interp: Animated.Value) { + return Animated.sequence([ + Animated.timing(interp, { + toValue: 1, + duration: 250, + useNativeDriver: true, + }), + Animated.delay(50), + Animated.timing(interp, { + toValue: 0, + duration: 20, + useNativeDriver: true, + }), + ]) +} + +function ctrlAnimStyle(interp: Animated.Value) { + return { + transform: [ + { + scale: interp.interpolate({ + inputRange: [0, 1.0], + outputRange: [1.0, 4.0], + }), + }, + ], + opacity: interp.interpolate({ + inputRange: [0, 1.0], + outputRange: [1.0, 0.0], + }), + } +} +*/ export function PostCtrls(opts: PostCtrlsOpts) { const theme = useTheme() @@ -51,76 +92,59 @@ export function PostCtrls(opts: PostCtrlsOpts) { color: theme.palette.default.postCtrl, }), [theme], - ) - const interp1 = useAnimatedValue(0) - const interp2 = useAnimatedValue(0) - - const anim1Style = { - transform: [ - { - scale: interp1.interpolate({ - inputRange: [0, 1.0], - outputRange: [1.0, 4.0], - }), - }, - ], - opacity: interp1.interpolate({ - inputRange: [0, 1.0], - outputRange: [1.0, 0.0], - }), - } - const anim2Style = { - transform: [ - { - scale: interp2.interpolate({ - inputRange: [0, 1.0], - outputRange: [1.0, 4.0], - }), - }, - ], - opacity: interp2.interpolate({ - inputRange: [0, 1.0], - outputRange: [1.0, 0.0], - }), - } - + ) as StyleProp + const [repostMod, setRepostMod] = React.useState(0) + const [likeMod, setLikeMod] = React.useState(0) + // DISABLED see #135 + // const repostRef = React.useRef(null) + // const likeRef = React.useRef(null) const onPressToggleRepostWrapper = () => { if (!opts.isReposted) { ReactNativeHapticFeedback.trigger('impactMedium') - Animated.sequence([ - Animated.timing(interp1, { - toValue: 1, - duration: 400, - useNativeDriver: true, - }), - Animated.delay(100), - Animated.timing(interp1, { - toValue: 0, - duration: 20, - useNativeDriver: true, - }), - ]).start() + setRepostMod(1) + opts + .onPressToggleRepost() + .catch(_e => undefined) + .then(() => setRepostMod(0)) + // DISABLED see #135 + // repostRef.current?.trigger( + // {start: ctrlAnimStart, style: ctrlAnimStyle}, + // async () => { + // await opts.onPressToggleRepost().catch(_e => undefined) + // setRepostMod(0) + // }, + // ) + } else { + setRepostMod(-1) + opts + .onPressToggleRepost() + .catch(_e => undefined) + .then(() => setRepostMod(0)) } - opts.onPressToggleRepost() } const onPressToggleUpvoteWrapper = () => { if (!opts.isUpvoted) { ReactNativeHapticFeedback.trigger('impactMedium') - Animated.sequence([ - Animated.timing(interp2, { - toValue: 1, - duration: 400, - useNativeDriver: true, - }), - Animated.delay(100), - Animated.timing(interp2, { - toValue: 0, - duration: 20, - useNativeDriver: true, - }), - ]).start() + setLikeMod(1) + opts + .onPressToggleUpvote() + .catch(_e => undefined) + .then(() => setLikeMod(0)) + // DISABLED see #135 + // likeRef.current?.trigger( + // {start: ctrlAnimStart, style: ctrlAnimStyle}, + // async () => { + // await opts.onPressToggleUpvote().catch(_e => undefined) + // setLikeMod(0) + // }, + // ) + } else { + setLikeMod(-1) + opts + .onPressToggleUpvote() + .catch(_e => undefined) + .then(() => setLikeMod(0)) } - opts.onPressToggleUpvote() } return ( @@ -147,7 +171,17 @@ export function PostCtrls(opts: PostCtrlsOpts) { hitSlop={HITSLOP} onPress={onPressToggleRepostWrapper} style={styles.ctrl}> - + 0 + ? (styles.ctrlIconReposted as StyleProp) + : defaultCtrlColor + } + strokeWidth={2.4} + size={opts.big ? 24 : 20} + /> + { + undefined /*DISABLED see #135 - + */ + } {typeof opts.repostCount !== 'undefined' ? ( 0 ? [s.bold, s.green3, s.f15, s.ml5] : [defaultCtrlColor, s.f15, s.ml5] }> - {opts.repostCount} + {opts.repostCount + repostMod} ) : undefined} @@ -175,8 +210,21 @@ export function PostCtrls(opts: PostCtrlsOpts) { style={styles.ctrl} hitSlop={HITSLOP} onPress={onPressToggleUpvoteWrapper}> - - {opts.isUpvoted ? ( + {opts.isUpvoted || likeMod > 0 ? ( + } + size={opts.big ? 22 : 16} + /> + ) : ( + + )} + { + undefined /*DISABLED see #135 + {opts.isUpvoted || likeMod > 0 ? ( )} - + */ + } {typeof opts.upvoteCount !== 'undefined' ? ( 0 ? [s.bold, s.red3, s.f15, s.ml5] : [defaultCtrlColor, s.f15, s.ml5] }> - {opts.upvoteCount} + {opts.upvoteCount + likeMod} ) : undefined} @@ -208,6 +257,8 @@ export function PostCtrls(opts: PostCtrlsOpts) { {opts.big ? undefined : ( void + imageChild?: React.ReactNode +}) => { + const pal = usePalette('default') + return ( + <> + {link.thumb ? ( + + {imageChild} + + ) : undefined} + + + {link.title || link.uri} + + + {link.uri} + + {link.description ? ( + + {link.description} + + ) : undefined} + + + ) +} + +const styles = StyleSheet.create({ + extInner: { + padding: 10, + }, + extImage: { + borderTopLeftRadius: 6, + borderTopRightRadius: 6, + width: '100%', + maxHeight: 200, + }, + extUri: { + marginTop: 2, + }, + extDescription: { + marginTop: 4, + }, +}) + +export default ExternalLinkEmbed diff --git a/src/view/com/util/PostEmbeds/YoutubeEmbed.tsx b/src/view/com/util/PostEmbeds/YoutubeEmbed.tsx new file mode 100644 index 00000000..d9425fe4 --- /dev/null +++ b/src/view/com/util/PostEmbeds/YoutubeEmbed.tsx @@ -0,0 +1,119 @@ +import React, {useEffect} from 'react' +import {useState} from 'react' +import { + View, + StyleSheet, + Pressable, + TouchableWithoutFeedback, + EmitterSubscription, +} from 'react-native' +import YoutubePlayer from 'react-native-youtube-iframe' +import {usePalette} from 'lib/hooks/usePalette' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import ExternalLinkEmbed from './ExternalLinkEmbed' +import {PresentedExternal} from '@atproto/api/dist/client/types/app/bsky/embed/external' +import {useStores} from 'state/index' + +const YoutubeEmbed = ({ + link, + videoId, +}: { + videoId: string + link: PresentedExternal +}) => { + const store = useStores() + const [displayVideoPlayer, setDisplayVideoPlayer] = useState(false) + const [playerDimensions, setPlayerDimensions] = useState({ + width: 0, + height: 0, + }) + const pal = usePalette('default') + const handlePlayButtonPressed = () => { + setDisplayVideoPlayer(true) + } + const handleOnLayout = (event: { + nativeEvent: {layout: {width: any; height: any}} + }) => { + setPlayerDimensions({ + width: event.nativeEvent.layout.width, + height: event.nativeEvent.layout.height, + }) + } + useEffect(() => { + let sub: EmitterSubscription + if (displayVideoPlayer) { + sub = store.onNavigation(() => { + setDisplayVideoPlayer(false) + }) + } + return () => sub && sub.remove() + }, [displayVideoPlayer, store]) + + const imageChild = ( + + + + ) + + if (!displayVideoPlayer) { + return ( + + + + ) + } + + const height = (playerDimensions.width / 16) * 9 + const noop = () => {} + + return ( + + + {/* Removing the outter View will make tap events propagate to parents */} + + + + ) +} + +const styles = StyleSheet.create({ + extOuter: { + borderWidth: 1, + borderRadius: 8, + marginTop: 4, + }, + playButton: { + position: 'absolute', + alignSelf: 'center', + alignItems: 'center', + top: '44%', + justifyContent: 'center', + backgroundColor: 'black', + padding: 10, + borderRadius: 50, + opacity: 0.8, + }, + webView: { + alignItems: 'center', + alignContent: 'center', + justifyContent: 'center', + }, +}) + +export default YoutubeEmbed diff --git a/src/view/com/util/PostEmbeds.tsx b/src/view/com/util/PostEmbeds/index.tsx similarity index 58% rename from src/view/com/util/PostEmbeds.tsx rename to src/view/com/util/PostEmbeds/index.tsx index 1d8df038..031f01e8 100644 --- a/src/view/com/util/PostEmbeds.tsx +++ b/src/view/com/util/PostEmbeds/index.tsx @@ -1,16 +1,22 @@ import React from 'react' -import {StyleSheet, StyleProp, View, ViewStyle} from 'react-native' +import { + StyleSheet, + StyleProp, + View, + ViewStyle, + Image as RNImage, +} from 'react-native' import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api' -import LinearGradient from 'react-native-linear-gradient' -import {Link} from '../util/Link' -import {Text} from './text/Text' -import {AutoSizedImage} from './images/AutoSizedImage' -import {ImageLayoutGrid} from './images/ImageLayoutGrid' -import {ImagesLightbox} from '../../../state/models/shell-ui' -import {useStores} from '../../../state' -import {usePalette} from '../../lib/hooks/usePalette' -import {gradients} from '../../lib/styles' -import {saveImageModal} from '../../../lib/images' +import {Link} from '../Link' +import {AutoSizedImage} from '../images/AutoSizedImage' +import {ImageLayoutGrid} from '../images/ImageLayoutGrid' +import {ImagesLightbox} from 'state/models/shell-ui' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {saveImageModal} from 'lib/images' +import YoutubeEmbed from './YoutubeEmbed' +import ExternalLinkEmbed from './ExternalLinkEmbed' +import {getYoutubeVideoId} from 'lib/strings/url-helpers' type Embed = | AppBskyEmbedImages.Presented @@ -35,6 +41,16 @@ export function PostEmbeds({ const onLongPress = (index: number) => { saveImageModal({uri: uris[index]}) } + const onPressIn = (index: number) => { + const firstImageToShow = uris[index] + RNImage.prefetch(firstImageToShow) + uris.forEach(uri => { + if (firstImageToShow !== uri) { + // First image already prefeched above + RNImage.prefetch(uri) + } + }) + } if (embed.images.length === 4) { return ( @@ -44,6 +60,7 @@ export function PostEmbeds({ uris={embed.images.map(img => img.thumb)} onPress={openLightbox} onLongPress={onLongPress} + onPressIn={onPressIn} /> ) @@ -55,6 +72,7 @@ export function PostEmbeds({ uris={embed.images.map(img => img.thumb)} onPress={openLightbox} onLongPress={onLongPress} + onPressIn={onPressIn} /> ) @@ -66,6 +84,7 @@ export function PostEmbeds({ uris={embed.images.map(img => img.thumb)} onPress={openLightbox} onLongPress={onLongPress} + onPressIn={onPressIn} /> ) @@ -76,7 +95,8 @@ export function PostEmbeds({ uri={embed.images[0].thumb} onPress={() => openLightbox(0)} onLongPress={() => onLongPress(0)} - containerStyle={styles.singleImage} + onPressIn={() => onPressIn(0)} + style={styles.singleImage} /> ) @@ -85,40 +105,18 @@ export function PostEmbeds({ } if (AppBskyEmbedExternal.isPresented(embed)) { const link = embed.external + const youtubeVideoId = getYoutubeVideoId(link.uri) + + if (youtubeVideoId) { + return + } + return ( - {link.thumb ? ( - - ) : ( - - )} - - - {link.title || link.uri} - - - {link.uri} - - {link.description ? ( - - {link.description} - - ) : undefined} - + ) } @@ -131,28 +129,11 @@ const styles = StyleSheet.create({ }, singleImage: { borderRadius: 8, + maxHeight: 500, }, extOuter: { borderWidth: 1, borderRadius: 8, marginTop: 4, }, - extInner: { - padding: 10, - }, - extImage: { - borderTopLeftRadius: 6, - borderTopRightRadius: 6, - width: '100%', - maxHeight: 200, - }, - extImageFallback: { - height: 160, - }, - extUri: { - marginTop: 2, - }, - extDescription: { - marginTop: 4, - }, }) diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 35edeafb..16b9535f 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -1,8 +1,8 @@ import React from 'react' import {Platform, StyleSheet, View} from 'react-native' import {Text} from './text/Text' -import {ago} from '../../../lib/strings' -import {usePalette} from '../../lib/hooks/usePalette' +import {ago} from 'lib/strings/time' +import {usePalette} from 'lib/hooks/usePalette' interface PostMetaOpts { authorHandle: string diff --git a/src/view/com/util/Selector.tsx b/src/view/com/util/Selector.tsx index 87540cf3..5b331dc8 100644 --- a/src/view/com/util/Selector.tsx +++ b/src/view/com/util/Selector.tsx @@ -6,7 +6,7 @@ import { View, } from 'react-native' import {Text} from './text/Text' -import {usePalette} from '../../lib/hooks/usePalette' +import {usePalette} from 'lib/hooks/usePalette' interface Layout { x: number diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 287d9441..9b8dd3de 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -1,15 +1,23 @@ -import React, {useCallback} from 'react' -import {Alert, Image, StyleSheet, TouchableOpacity, View} from 'react-native' +import React from 'react' +import {StyleSheet, View} from 'react-native' import Svg, {Circle, Text, Defs, LinearGradient, Stop} from 'react-native-svg' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {IconProp} from '@fortawesome/fontawesome-svg-core' +import {HighPriorityImage} from 'view/com/util/images/Image' import { openCamera, openCropper, openPicker, PickedMedia, } from './images/image-crop-picker/ImageCropPicker' -import {useStores} from '../../../state' -import {colors, gradients} from '../../lib/styles' +import { + requestPhotoAccessIfNeeded, + requestCameraAccessIfNeeded, +} from 'lib/permissions' +import {useStores} from 'state/index' +import {colors, gradients} from 'lib/styles' +import {DropdownButton} from './forms/DropdownButton' +import {usePalette} from 'lib/hooks/usePalette' export function UserAvatar({ size, @@ -25,40 +33,9 @@ export function UserAvatar({ onSelectNewAvatar?: (img: PickedMedia) => void }) { const store = useStores() + const pal = usePalette('default') const initials = getInitials(displayName || handle) - const handleEditAvatar = useCallback(() => { - Alert.alert('Select upload method', '', [ - { - text: 'Take a new photo', - onPress: () => { - openCamera(store, { - mediaType: 'photo', - width: 1000, - height: 1000, - cropperCircleOverlay: true, - }).then(onSelectNewAvatar) - }, - }, - { - text: 'Select from gallery', - onPress: () => { - openPicker(store, { - mediaType: 'photo', - }).then(async items => { - await openCropper(store, { - mediaType: 'photo', - path: items[0].path, - width: 1000, - height: 1000, - cropperCircleOverlay: true, - }).then(onSelectNewAvatar) - }) - }, - }, - ]) - }, [store, onSelectNewAvatar]) - const renderSvg = (svgSize: number, svgInitials: string) => ( @@ -80,11 +57,65 @@ export function UserAvatar({ ) + const dropdownItems = [ + { + label: 'Camera', + icon: 'camera' as IconProp, + onPress: async () => { + if (!(await requestCameraAccessIfNeeded())) { + return + } + onSelectNewAvatar?.( + await openCamera(store, { + mediaType: 'photo', + width: 1000, + height: 1000, + cropperCircleOverlay: true, + }), + ) + }, + }, + { + label: 'Library', + icon: 'image' as IconProp, + onPress: async () => { + if (!(await requestPhotoAccessIfNeeded())) { + return + } + const items = await openPicker(store, { + mediaType: 'photo', + }) + onSelectNewAvatar?.( + await openCropper(store, { + mediaType: 'photo', + path: items[0].path, + width: 1000, + height: 1000, + cropperCircleOverlay: true, + }), + ) + }, + }, + // TODO: Remove avatar https://github.com/bluesky-social/social-app/issues/122 + // { + // label: 'Remove', + // icon: ['far', 'trash-can'], + // onPress: () => { + // // Remove avatar API call + // }, + // }, + ] // onSelectNewAvatar is only passed as prop on the EditProfile component return onSelectNewAvatar ? ( - + {avatar ? ( - + - + ) : avatar ? ( - void }) { const store = useStores() - const handleEditBanner = useCallback(() => { - Alert.alert('Select upload method', '', [ - { - text: 'Take a new photo', - onPress: () => { - openCamera(store, { + const pal = usePalette('default') + const dropdownItems = [ + { + label: 'Camera', + icon: 'camera' as IconProp, + onPress: async () => { + if (!(await requestCameraAccessIfNeeded())) { + return + } + onSelectNewBanner?.( + await openCamera(store, { mediaType: 'photo', // compressImageMaxWidth: 3000, TODO needed? width: 3000, // compressImageMaxHeight: 1000, TODO needed? height: 1000, - }).then(onSelectNewBanner) - }, + }), + ) }, - { - text: 'Select from gallery', - onPress: () => { - openPicker(store, { + }, + { + label: 'Library', + icon: 'image' as IconProp, + onPress: async () => { + if (!(await requestPhotoAccessIfNeeded())) { + return + } + const items = await openPicker(store, { + mediaType: 'photo', + }) + onSelectNewBanner?.( + await openCropper(store, { mediaType: 'photo', - }).then(async items => { - await openCropper(store, { - mediaType: 'photo', - path: items[0].path, - // compressImageMaxWidth: 3000, TODO needed? - width: 3000, - // compressImageMaxHeight: 1000, TODO needed? - height: 1000, - }).then(onSelectNewBanner) - }) - }, + path: items[0].path, + // compressImageMaxWidth: 3000, TODO needed? + width: 3000, + // compressImageMaxHeight: 1000, TODO needed? + height: 1000, + }), + ) }, - ]) - }, [store, onSelectNewBanner]) + }, + // TODO: Remove banner https://github.com/bluesky-social/social-app/issues/122 + // { + // label: 'Remove', + // icon: ['far', 'trash-can'], + // onPress: () => { + // // Remove banner api call + // }, + // }, + ] const renderSvg = () => ( @@ -72,20 +98,27 @@ export function UserBanner({ // setUserBanner is only passed as prop on the EditProfile component return onSelectNewBanner ? ( - + {banner ? ( ) : ( renderSvg() )} - + - + ) : banner ? ( { store.nav.tab.goBack() } const onPressMenu = () => { + track('ViewHeader:MenuButtonClicked') store.shell.setMainMenuOpen(true) } - const onPressSearch = () => { - store.nav.navigate('/search') - } - const onPressReconnect = () => { - store.session.connect().catch(e => { - store.log.warn('Failed to reconnect to server', e) - }) - } if (typeof canGoBack === 'undefined') { canGoBack = store.nav.tab.canGoBack } return ( - + {title} - {subtitle ? ( - - {subtitle} - - ) : undefined} - - - - {!store.session.online ? ( - - {store.session.attemptingConnect ? ( - - ) : ( - <> - - - - )} - - ) : undefined} - + + ) }) +const Container = observer( + ({ + children, + hideOnScroll, + }: { + children: React.ReactNode + hideOnScroll: boolean + }) => { + const store = useStores() + const pal = usePalette('default') + const interp = useAnimatedValue(0) + + React.useEffect(() => { + if (store.shell.minimalShellMode) { + Animated.timing(interp, { + toValue: 1, + duration: 100, + useNativeDriver: true, + isInteraction: false, + }).start() + } else { + Animated.timing(interp, { + toValue: 0, + duration: 100, + useNativeDriver: true, + isInteraction: false, + }).start() + } + }, [interp, store.shell.minimalShellMode]) + const transform = { + transform: [{translateY: Animated.multiply(interp, -100)}], + } + + if (!hideOnScroll) { + return {children} + } + return ( + + {children} + + ) + }, +) + const styles = StyleSheet.create({ header: { flexDirection: 'row', @@ -125,20 +118,20 @@ const styles = StyleSheet.create({ paddingTop: 6, paddingBottom: 6, }, + headerFloating: { + position: 'absolute', + top: 0, + width: '100%', + }, titleContainer: { - flexDirection: 'row', - alignItems: 'baseline', + marginLeft: 'auto', marginRight: 'auto', + paddingRight: 10, }, title: { fontWeight: 'bold', }, - subtitle: { - marginLeft: 4, - maxWidth: 200, - fontWeight: 'normal', - }, backBtn: { width: 30, @@ -152,19 +145,4 @@ const styles = StyleSheet.create({ backIcon: { marginTop: 6, }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - width: 36, - height: 36, - borderRadius: 20, - marginLeft: 4, - }, - littleXIcon: { - color: colors.red3, - position: 'absolute', - right: 7, - bottom: 7, - }, }) diff --git a/src/view/com/util/ViewHeader.web.tsx b/src/view/com/util/ViewHeader.web.tsx index 0d5c99aa..5c0869e8 100644 --- a/src/view/com/util/ViewHeader.web.tsx +++ b/src/view/com/util/ViewHeader.web.tsx @@ -1,20 +1,12 @@ import React from 'react' import {observer} from 'mobx-react-lite' -import { - ActivityIndicator, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' +import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {CenteredView} from './Views' import {Text} from './text/Text' -import {useStores} from '../../../state' -import {usePalette} from '../../lib/hooks/usePalette' -import {colors} from '../../lib/styles' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {colors} from 'lib/styles' const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10} @@ -32,11 +24,6 @@ export const ViewHeader = observer(function ViewHeader({ const onPressBack = () => { store.nav.tab.goBack() } - const onPressReconnect = () => { - store.session.connect().catch(e => { - store.log.warn('Failed to reconnect to server', e) - }) - } if (typeof canGoBack === 'undefined') { canGoBack = store.nav.tab.canGoBack } @@ -76,29 +63,6 @@ export const ViewHeader = observer(function ViewHeader({ )} - {!store.session.online ? ( - - {store.session.attemptingConnect ? ( - - ) : ( - <> - - - - )} - - ) : undefined} ) }) diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index ff5115c5..b786c229 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -3,10 +3,10 @@ import {View} from 'react-native' import {Selector} from './Selector' import {HorzSwipe} from './gestures/HorzSwipe' import {FlatList} from './Views' -import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' -import {OnScrollCb} from '../../lib/hooks/useOnMainScroll' -import {clamp} from '../../../lib/numbers' -import {s} from '../../lib/styles' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {OnScrollCb} from 'lib/hooks/useOnMainScroll' +import {clamp} from 'lib/numbers' +import {s} from 'lib/styles' const HEADER_ITEM = {_reactKey: '__header__'} const SELECTOR_ITEM = {_reactKey: '__selector__'} @@ -101,6 +101,7 @@ export function ViewSelector({ onRefresh={onRefresh} onEndReached={onEndReached} contentContainerStyle={s.contentContainer} + removeClippedSubviews={true} /> ) diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx index 2df53414..c16070b2 100644 --- a/src/view/com/util/Views.web.tsx +++ b/src/view/com/util/Views.web.tsx @@ -22,9 +22,8 @@ import { View, ViewProps, } from 'react-native' -import {useTheme} from '../../lib/ThemeContext' -import {addStyle} from '../../lib/addStyle' -import {colors} from '../../lib/styles' +import {useTheme} from 'lib/ThemeContext' +import {addStyle, colors} from 'lib/styles' export function CenteredView({ style, diff --git a/src/view/com/util/anim/TriggerableAnimated.tsx b/src/view/com/util/anim/TriggerableAnimated.tsx new file mode 100644 index 00000000..2a3cbb95 --- /dev/null +++ b/src/view/com/util/anim/TriggerableAnimated.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import {Animated, StyleProp, View, ViewStyle} from 'react-native' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' + +type CreateAnimFn = (interp: Animated.Value) => Animated.CompositeAnimation +type FinishCb = () => void + +interface TriggeredAnimation { + start: CreateAnimFn + style: ( + interp: Animated.Value, + ) => Animated.WithAnimatedValue> +} + +export interface TriggerableAnimatedRef { + trigger: (anim: TriggeredAnimation, onFinish?: FinishCb) => void +} + +type TriggerableAnimatedProps = React.PropsWithChildren<{}> + +type PropsInner = TriggerableAnimatedProps & { + anim: TriggeredAnimation + onFinish: () => void +} + +export const TriggerableAnimated = React.forwardRef< + TriggerableAnimatedRef, + TriggerableAnimatedProps +>(({children, ...props}, ref) => { + const [anim, setAnim] = React.useState( + undefined, + ) + const [finishCb, setFinishCb] = React.useState( + undefined, + ) + React.useImperativeHandle(ref, () => ({ + trigger(v: TriggeredAnimation, cb?: FinishCb) { + setFinishCb(() => cb) // note- wrap in function due to react behaviors around setstate + setAnim(v) + }, + })) + const onFinish = () => { + finishCb?.() + setAnim(undefined) + setFinishCb(undefined) + } + return ( + + {anim ? ( + + {children} + + ) : ( + children + )} + + ) +}) + +function AnimatingView({ + anim, + onFinish, + children, +}: React.PropsWithChildren) { + const interp = useAnimatedValue(0) + React.useEffect(() => { + anim?.start(interp).start(() => { + onFinish() + }) + }) + const animStyle = anim?.style(interp) + return {children} +} diff --git a/src/view/com/util/error/ErrorMessage.tsx b/src/view/com/util/error/ErrorMessage.tsx index a6d77326..e6e27fac 100644 --- a/src/view/com/util/error/ErrorMessage.tsx +++ b/src/view/com/util/error/ErrorMessage.tsx @@ -11,8 +11,8 @@ import { FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {Text} from '../text/Text' -import {useTheme} from '../../../lib/ThemeContext' -import {usePalette} from '../../../lib/hooks/usePalette' +import {useTheme} from 'lib/ThemeContext' +import {usePalette} from 'lib/hooks/usePalette' export function ErrorMessage({ message, diff --git a/src/view/com/util/error/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx index f316dbcc..0221ea15 100644 --- a/src/view/com/util/error/ErrorScreen.tsx +++ b/src/view/com/util/error/ErrorScreen.tsx @@ -5,9 +5,9 @@ import { FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {Text} from '../text/Text' -import {colors} from '../../../lib/styles' -import {useTheme} from '../../../lib/ThemeContext' -import {usePalette} from '../../../lib/hooks/usePalette' +import {colors} from 'lib/styles' +import {useTheme} from 'lib/ThemeContext' +import {usePalette} from 'lib/hooks/usePalette' export function ErrorScreen({ title, diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx index b5c4da19..a070d2f0 100644 --- a/src/view/com/util/forms/Button.tsx +++ b/src/view/com/util/forms/Button.tsx @@ -7,8 +7,8 @@ import { ViewStyle, } from 'react-native' import {Text} from '../text/Text' -import {useTheme} from '../../../lib/ThemeContext' -import {choose} from '../../../../lib/functions' +import {useTheme} from 'lib/ThemeContext' +import {choose} from 'lib/functions' export type ButtonType = | 'primary' diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx index f911529d..8fddd594 100644 --- a/src/view/com/util/forms/DropdownButton.tsx +++ b/src/view/com/util/forms/DropdownButton.tsx @@ -13,11 +13,13 @@ import RootSiblings from 'react-native-root-siblings' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../text/Text' import {Button, ButtonType} from './Button' -import {colors} from '../../../lib/styles' -import {toShareUrl} from '../../../../lib/strings' -import {useStores} from '../../../../state' -import {ReportPostModal, ConfirmModal} from '../../../../state/models/shell-ui' -import {TABS_ENABLED} from '../../../../build-flags' +import {colors} from 'lib/styles' +import {toShareUrl} from 'lib/strings/url-helpers' +import {useStores} from 'state/index' +import {ReportPostModal, ConfirmModal} from 'state/models/shell-ui' +import {TABS_ENABLED} from 'lib/build-flags' +import {usePalette} from 'lib/hooks/usePalette' +import {useTheme} from 'lib/ThemeContext' const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} @@ -36,6 +38,9 @@ export function DropdownButton({ label, menuWidth, children, + openToRight = false, + rightOffset = 0, + bottomOffset = 0, }: { type?: DropdownButtonType style?: StyleProp @@ -43,6 +48,9 @@ export function DropdownButton({ label?: string menuWidth?: number children?: React.ReactNode + openToRight?: boolean + rightOffset?: number + bottomOffset?: number }) { const ref = useRef(null) @@ -59,12 +67,11 @@ export function DropdownButton({ if (!menuWidth) { menuWidth = 200 } - createDropdownMenu( - pageX + width - menuWidth, - pageY + height, - menuWidth, - items, - ) + const newX = openToRight + ? pageX + width + rightOffset + : pageX + width - menuWidth + const newY = pageY + height + bottomOffset + createDropdownMenu(newX, newY, menuWidth, items) }, ) } @@ -97,6 +104,8 @@ export function DropdownButton({ export function PostDropdownBtn({ style, children, + itemUri, + itemCid, itemHref, isAuthor, onCopyPostText, @@ -104,6 +113,8 @@ export function PostDropdownBtn({ }: { style?: StyleProp children?: React.ReactNode + itemUri: string + itemCid: string itemHref: string itemTitle: string isAuthor: boolean @@ -140,7 +151,7 @@ export function PostDropdownBtn({ icon: 'circle-exclamation', label: 'Report post', onPress() { - store.shell.openModal(new ReportPostModal(itemHref)) + store.shell.openModal(new ReportPostModal(itemUri, itemCid)) }, }, isAuthor @@ -180,24 +191,14 @@ function createDropdownMenu( const onOuterPress = () => sibling.destroy() const sibling = new RootSiblings( ( - <> - - - - - {items.map((item, index) => ( - onPressItem(index)}> - {item.icon && ( - - )} - {item.label} - - ))} - - + ), ) return sibling @@ -241,3 +242,55 @@ const styles = StyleSheet.create({ fontSize: 18, }, }) +type DropDownItemProps = { + onOuterPress: () => void + x: number + y: number + width: number + items: DropdownItem[] + onPressItem: (index: number) => void +} + +const DropdownItems = ({ + onOuterPress, + x, + y, + width, + items, + onPressItem, +}: DropDownItemProps) => { + const pal = usePalette('default') + const theme = useTheme() + const dropDownBackgroundColor = + theme.colorScheme === 'dark' ? pal.btn : pal.view + + return ( + <> + + + + + {items.map((item, index) => ( + onPressItem(index)}> + {item.icon && ( + + )} + {item.label} + + ))} + + + ) +} diff --git a/src/view/com/util/forms/RadioButton.tsx b/src/view/com/util/forms/RadioButton.tsx index 81489c44..57a875cd 100644 --- a/src/view/com/util/forms/RadioButton.tsx +++ b/src/view/com/util/forms/RadioButton.tsx @@ -2,8 +2,8 @@ import React from 'react' import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' import {Text} from '../text/Text' import {Button, ButtonType} from './Button' -import {useTheme} from '../../../lib/ThemeContext' -import {choose} from '../../../../lib/functions' +import {useTheme} from 'lib/ThemeContext' +import {choose} from 'lib/functions' export function RadioButton({ type = 'default-light', diff --git a/src/view/com/util/forms/RadioGroup.tsx b/src/view/com/util/forms/RadioGroup.tsx index b33cd983..901b0cdd 100644 --- a/src/view/com/util/forms/RadioGroup.tsx +++ b/src/view/com/util/forms/RadioGroup.tsx @@ -2,7 +2,7 @@ import React, {useState} from 'react' import {View} from 'react-native' import {RadioButton} from './RadioButton' import {ButtonType} from './Button' -import {s} from '../../../lib/styles' +import {s} from 'lib/styles' export interface RadioGroupItem { label: string diff --git a/src/view/com/util/forms/ToggleButton.tsx b/src/view/com/util/forms/ToggleButton.tsx index 77e8fa20..005d1165 100644 --- a/src/view/com/util/forms/ToggleButton.tsx +++ b/src/view/com/util/forms/ToggleButton.tsx @@ -2,9 +2,9 @@ import React from 'react' import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' import {Text} from '../text/Text' import {Button, ButtonType} from './Button' -import {useTheme} from '../../../lib/ThemeContext' -import {choose} from '../../../../lib/functions' -import {colors} from '../../../lib/styles' +import {useTheme} from 'lib/ThemeContext' +import {choose} from 'lib/functions' +import {colors} from 'lib/styles' export function ToggleButton({ type = 'default-light', diff --git a/src/view/com/util/gestures/HorzSwipe.tsx b/src/view/com/util/gestures/HorzSwipe.tsx index 22b15afe..09f6c345 100644 --- a/src/view/com/util/gestures/HorzSwipe.tsx +++ b/src/view/com/util/gestures/HorzSwipe.tsx @@ -9,7 +9,7 @@ import { View, } from 'react-native' import {clamp} from 'lodash' -import {s} from '../../../lib/styles' +import {s} from 'lib/styles' interface Props { panX: Animated.Value @@ -90,6 +90,7 @@ export function HorzSwipe({ // swiping right (diffX < 0 && !canSwipeRight) ) { + panX.setValue(0) return } @@ -119,6 +120,7 @@ export function HorzSwipe({ toValue: final, duration: 100, useNativeDriver, + isInteraction: false, }).start(() => { onSwipeEnd?.(final) panX.flattenOffset() @@ -130,6 +132,7 @@ export function HorzSwipe({ toValue: 0, duration: 100, useNativeDriver, + isInteraction: false, }).start(() => { panX.flattenOffset() panX.setValue(0) diff --git a/src/view/com/util/gestures/SwipeAndZoom.tsx b/src/view/com/util/gestures/SwipeAndZoom.tsx index ee00edab..75c67901 100644 --- a/src/view/com/util/gestures/SwipeAndZoom.tsx +++ b/src/view/com/util/gestures/SwipeAndZoom.tsx @@ -9,7 +9,7 @@ import { View, } from 'react-native' import {clamp} from 'lodash' -import {s} from '../../../lib/styles' +import {s} from 'lib/styles' export enum Dir { None, diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index cdefc712..0443c7be 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -1,125 +1,59 @@ -import React, {useState, useEffect} from 'react' -import { - Image, - ImageStyle, - LayoutChangeEvent, - StyleProp, - StyleSheet, - TouchableOpacity, - View, - ViewStyle, -} from 'react-native' -import {Text} from '../text/Text' -import {useTheme} from '../../../lib/ThemeContext' -import {usePalette} from '../../../lib/hooks/usePalette' -import {DELAY_PRESS_IN} from './constants' +import React from 'react' +import {StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native' +import Image, {OnLoadEvent} from 'view/com/util/images/Image' +import {clamp} from 'lib/numbers' -const MAX_HEIGHT = 300 - -interface Dim { - width: number - height: number -} +export const DELAY_PRESS_IN = 500 +const MIN_ASPECT_RATIO = 0.33 // 1/3 +const MAX_ASPECT_RATIO = 5 // 5/1 export function AutoSizedImage({ uri, onPress, onLongPress, + onPressIn, style, - containerStyle, + children = null, }: { uri: string onPress?: () => void onLongPress?: () => void - style?: StyleProp - containerStyle?: StyleProp + onPressIn?: () => void + style?: StyleProp + children?: React.ReactNode }) { - const theme = useTheme() - const errPal = usePalette('error') - const [error, setError] = useState('') - const [imgInfo, setImgInfo] = useState() - const [containerInfo, setContainerInfo] = useState() - - useEffect(() => { - let aborted = false - if (!imgInfo) { - Image.getSize( - uri, - (width: number, height: number) => { - if (!aborted) { - setImgInfo({width, height}) - } - }, - (err: any) => { - if (!aborted) { - setError(String(err)) - } - }, - ) - } - return () => { - aborted = true - } - }, [uri, imgInfo]) - - const onLayout = (evt: LayoutChangeEvent) => { - setContainerInfo({ - width: evt.nativeEvent.layout.width, - height: evt.nativeEvent.layout.height, - }) - } - - let calculatedStyle: StyleProp | undefined - if (imgInfo && containerInfo) { - // imgInfo.height / imgInfo.width = x / containerInfo.width - // x = imgInfo.height / imgInfo.width * containerInfo.width - calculatedStyle = { - height: Math.min( - MAX_HEIGHT, - (imgInfo.height / imgInfo.width) * containerInfo.width, + const [aspectRatio, setAspectRatio] = React.useState(1) + const onLoad = (e: OnLoadEvent) => { + setAspectRatio( + clamp( + e.nativeEvent.width / e.nativeEvent.height, + MIN_ASPECT_RATIO, + MAX_ASPECT_RATIO, ), - } + ) } - return ( - - - {error ? ( - - {error} - - ) : calculatedStyle ? ( - - - - ) : ( - - )} - - + + + {children} + ) } const styles = StyleSheet.create({ - placeholder: { - width: '100%', - aspectRatio: 1, - }, - errorContainer: { - paddingHorizontal: 12, - paddingVertical: 8, - }, container: { overflow: 'hidden', }, + image: { + width: '100%', + }, }) diff --git a/src/view/com/util/images/Image.tsx b/src/view/com/util/images/Image.tsx new file mode 100644 index 00000000..8c95a581 --- /dev/null +++ b/src/view/com/util/images/Image.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import FastImage, {FastImageProps, Source} from 'react-native-fast-image' +export default FastImage +export type {OnLoadEvent, ImageStyle, Source} from 'react-native-fast-image' + +export function HighPriorityImage({source, ...props}: FastImageProps) { + const updatedSource = { + uri: typeof source === 'object' && source ? source.uri : '', + priority: FastImage.priority.high, + } as Source + return +} diff --git a/src/view/com/util/images/Image.web.tsx b/src/view/com/util/images/Image.web.tsx new file mode 100644 index 00000000..ecd9d730 --- /dev/null +++ b/src/view/com/util/images/Image.web.tsx @@ -0,0 +1,11 @@ +import { + Image, + NativeSyntheticEvent, + ImageLoadEventData, + ImageSourcePropType, +} from 'react-native' +export default Image +export const HighPriorityImage = Image +export type OnLoadEvent = NativeSyntheticEvent +export type Source = ImageSourcePropType +export type {ImageStyle} from 'react-native' diff --git a/src/view/com/util/images/ImageHorzList.tsx b/src/view/com/util/images/ImageHorzList.tsx index 36642430..bed13406 100644 --- a/src/view/com/util/images/ImageHorzList.tsx +++ b/src/view/com/util/images/ImageHorzList.tsx @@ -1,12 +1,12 @@ import React from 'react' import { - Image, StyleProp, StyleSheet, TouchableWithoutFeedback, View, ViewStyle, } from 'react-native' +import Image from 'view/com/util/images/Image' export function ImageHorzList({ uris, diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index 97ad9d70..a1c73264 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -1,7 +1,5 @@ import React from 'react' import { - Image, - ImageStyle, LayoutChangeEvent, StyleProp, StyleSheet, @@ -9,7 +7,9 @@ import { View, ViewStyle, } from 'react-native' -import {DELAY_PRESS_IN} from './constants' +import Image, {ImageStyle} from 'view/com/util/images/Image' + +export const DELAY_PRESS_IN = 500 interface Dim { width: number @@ -23,12 +23,14 @@ export function ImageLayoutGrid({ uris, onPress, onLongPress, + onPressIn, style, }: { type: ImageLayoutGridType uris: string[] onPress?: (index: number) => void onLongPress?: (index: number) => void + onPressIn?: (index: number) => void style?: StyleProp }) { const [containerInfo, setContainerInfo] = React.useState() @@ -47,6 +49,7 @@ export function ImageLayoutGrid({ type={type} uris={uris} onPress={onPress} + onPressIn={onPressIn} onLongPress={onLongPress} containerInfo={containerInfo} /> @@ -60,15 +63,17 @@ function ImageLayoutGridInner({ uris, onPress, onLongPress, + onPressIn, containerInfo, }: { type: ImageLayoutGridType uris: string[] onPress?: (index: number) => void onLongPress?: (index: number) => void + onPressIn?: (index: number) => void containerInfo: Dim }) { - const size1 = React.useMemo(() => { + const size1 = React.useMemo>(() => { if (type === 'three') { const size = (containerInfo.width - 10) / 3 return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} @@ -77,7 +82,7 @@ function ImageLayoutGridInner({ return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} } }, [type, containerInfo]) - const size2 = React.useMemo(() => { + const size2 = React.useMemo>(() => { if (type === 'three') { const size = ((containerInfo.width - 10) / 3) * 2 + 5 return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} @@ -93,6 +98,7 @@ function ImageLayoutGridInner({ onPress?.(0)} + onPressIn={() => onPressIn?.(0)} onLongPress={() => onLongPress?.(0)}> @@ -100,6 +106,7 @@ function ImageLayoutGridInner({ onPress?.(1)} + onPressIn={() => onPressIn?.(1)} onLongPress={() => onLongPress?.(1)}> @@ -112,6 +119,7 @@ function ImageLayoutGridInner({ onPress?.(0)} + onPressIn={() => onPressIn?.(0)} onLongPress={() => onLongPress?.(0)}> @@ -120,6 +128,7 @@ function ImageLayoutGridInner({ onPress?.(1)} + onPressIn={() => onPressIn?.(1)} onLongPress={() => onLongPress?.(1)}> @@ -127,6 +136,7 @@ function ImageLayoutGridInner({ onPress?.(2)} + onPressIn={() => onPressIn?.(2)} onLongPress={() => onLongPress?.(2)}> @@ -141,29 +151,33 @@ function ImageLayoutGridInner({ onPress?.(0)} + onPressIn={() => onPressIn?.(0)} onLongPress={() => onLongPress?.(0)}> onPress?.(1)} - onLongPress={() => onLongPress?.(1)}> - + onPress={() => onPress?.(2)} + onPressIn={() => onPressIn?.(2)} + onLongPress={() => onLongPress?.(2)}> + onPress?.(2)} - onLongPress={() => onLongPress?.(2)}> - + onPress={() => onPress?.(1)} + onPressIn={() => onPressIn?.(1)} + onLongPress={() => onLongPress?.(1)}> + onPress?.(3)} + onPressIn={() => onPressIn?.(3)} onLongPress={() => onLongPress?.(3)}> diff --git a/src/view/com/util/images/constants.ts b/src/view/com/util/images/constants.ts deleted file mode 100644 index cb2c26ce..00000000 --- a/src/view/com/util/images/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const DELAY_PRESS_IN = 500 diff --git a/src/view/com/util/images/image-crop-picker/ImageCropPicker.tsx b/src/view/com/util/images/image-crop-picker/ImageCropPicker.tsx index ddc9e87f..d723fef9 100644 --- a/src/view/com/util/images/image-crop-picker/ImageCropPicker.tsx +++ b/src/view/com/util/images/image-crop-picker/ImageCropPicker.tsx @@ -4,7 +4,7 @@ import { openCropper as openCropperFn, ImageOrVideo, } from 'react-native-image-crop-picker' -import {RootStoreModel} from '../../../../../state' +import {RootStoreModel} from 'state/index' import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types' export type {PickedMedia} from './types' diff --git a/src/view/com/util/images/image-crop-picker/ImageCropPicker.web.tsx b/src/view/com/util/images/image-crop-picker/ImageCropPicker.web.tsx index a7037f3a..d632590d 100644 --- a/src/view/com/util/images/image-crop-picker/ImageCropPicker.web.tsx +++ b/src/view/com/util/images/image-crop-picker/ImageCropPicker.web.tsx @@ -1,9 +1,9 @@ /// -import {CropImageModal} from '../../../../../state/models/shell-ui' +import {CropImageModal} from 'state/models/shell-ui' import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types' export type {PickedMedia} from './types' -import {RootStoreModel} from '../../../../../state' +import {RootStoreModel} from 'state/index' interface PickedFile { uri: string @@ -31,17 +31,17 @@ export async function openPicker( export async function openCamera( _store: RootStoreModel, - opts: CameraOpts, + _opts: CameraOpts, ): Promise { - const mediaType = opts.mediaType || 'photo' + // const mediaType = opts.mediaType || 'photo' TODO throw new Error('TODO') } export async function openCropper( _store: RootStoreModel, - opts: CropperOpts, + _opts: CropperOpts, ): Promise { - const mediaType = opts.mediaType || 'photo' + // const mediaType = opts.mediaType || 'photo' TODO throw new Error('TODO') } diff --git a/src/view/com/util/text/RichText.tsx b/src/view/com/util/text/RichText.tsx index f04f4566..d4cf1917 100644 --- a/src/view/com/util/text/RichText.tsx +++ b/src/view/com/util/text/RichText.tsx @@ -2,29 +2,21 @@ import React from 'react' import {TextStyle, StyleProp} from 'react-native' import {TextLink} from '../Link' import {Text} from './Text' -import {lh} from '../../../lib/styles' -import {toShortUrl} from '../../../../lib/strings' -import {useTheme, TypographyVariant} from '../../../lib/ThemeContext' -import {usePalette} from '../../../lib/hooks/usePalette' - -type TextSlice = {start: number; end: number} -type Entity = { - index: TextSlice - type: string - value: string -} +import {lh} from 'lib/styles' +import {toShortUrl} from 'lib/strings/url-helpers' +import {RichText as RichTextObj, Entity} from 'lib/strings/rich-text' +import {useTheme, TypographyVariant} from 'lib/ThemeContext' +import {usePalette} from 'lib/hooks/usePalette' export function RichText({ type = 'md', - text, - entities, + richText, lineHeight = 1.2, style, numberOfLines, }: { type?: TypographyVariant - text: string - entities?: Entity[] + richText?: RichTextObj lineHeight?: number style?: StyleProp numberOfLines?: number @@ -32,6 +24,12 @@ export function RichText({ const theme = useTheme() const pal = usePalette('default') const lineHeightStyle = lh(theme, type, lineHeight) + + if (!richText) { + return null + } + + const {text, entities} = richText if (!entities?.length) { if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) { style = { diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx index c3a8a219..14c57cf4 100644 --- a/src/view/com/util/text/Text.tsx +++ b/src/view/com/util/text/Text.tsx @@ -1,7 +1,7 @@ import React from 'react' import {Text as RNText, TextProps} from 'react-native' -import {s} from '../../../lib/styles' -import {useTheme, TypographyVariant} from '../../../lib/ThemeContext' +import {s} from 'lib/styles' +import {useTheme, TypographyVariant} from 'lib/ThemeContext' export type CustomTextProps = TextProps & { type?: TypographyVariant diff --git a/src/view/index.ts b/src/view/index.ts index 8ffd5957..7cd2c1df 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -65,6 +65,8 @@ import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket' import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan' import {faX} from '@fortawesome/free-solid-svg-icons/faX' import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark' +import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay' +import {faPause} from '@fortawesome/free-solid-svg-icons/faPause' export function setup() { library.add( @@ -133,5 +135,7 @@ export function setup() { faTrashCan, faX, faXmark, + faPlay, + faPause, ) } diff --git a/src/view/lib/addStyle.ts b/src/view/lib/addStyle.ts deleted file mode 100644 index 93a5390a..00000000 --- a/src/view/lib/addStyle.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {StyleProp} from 'react-native' - -export function addStyle( - base: StyleProp, - addedStyle: StyleProp, -): StyleProp { - if (Array.isArray(base)) { - return base.concat([addedStyle]) - } - return [base, addedStyle] -} diff --git a/src/view/lib/assets.native.ts b/src/view/lib/assets.native.ts deleted file mode 100644 index a2db553e..00000000 --- a/src/view/lib/assets.native.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {ImageSourcePropType} from 'react-native' - -export const DEF_AVATAR: ImageSourcePropType = require('../../../public/img/default-avatar.jpg') -export const TABS_EXPLAINER: ImageSourcePropType = require('../../../public/img/tabs-explainer.jpg') -export const CLOUD_SPLASH: ImageSourcePropType = require('../../../public/img/cloud-splash.png') diff --git a/src/view/lib/assets.ts b/src/view/lib/assets.ts deleted file mode 100644 index 948f3a22..00000000 --- a/src/view/lib/assets.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {ImageSourcePropType} from 'react-native' - -export const DEF_AVATAR: ImageSourcePropType = {uri: '/img/default-avatar.jpg'} -export const TABS_EXPLAINER: ImageSourcePropType = { - uri: '/img/tabs-explainer.jpg', -} -export const CLOUD_SPLASH: ImageSourcePropType = {uri: '/img/cloud-splash.png'} diff --git a/src/view/routes.ts b/src/view/routes.ts index 98e8111b..1cd9ef8e 100644 --- a/src/view/routes.ts +++ b/src/view/routes.ts @@ -1,5 +1,4 @@ -import React, {MutableRefObject} from 'react' -import {FlatList} from 'react-native' +import React from 'react' import {IconProp} from '@fortawesome/fontawesome-svg-core' import {Home} from './screens/Home' import {Contacts} from './screens/Contacts' @@ -21,7 +20,6 @@ export type ScreenParams = { navIdx: string params: Record visible: boolean - scrollElRef?: MutableRefObject | null> } export type Route = [React.FC, string, IconProp, RegExp] export type MatchResult = { diff --git a/src/view/screens/Contacts.tsx b/src/view/screens/Contacts.tsx index cba17f28..21943a10 100644 --- a/src/view/screens/Contacts.tsx +++ b/src/view/screens/Contacts.tsx @@ -4,10 +4,10 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows' import {Selector} from '../com/util/Selector' import {Text} from '../com/util/text/Text' -import {colors} from '../lib/styles' +import {colors} from 'lib/styles' import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import {useAnimatedValue} from '../lib/hooks/useAnimatedValue' +import {useStores} from 'state/index' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' export const Contacts = ({navIdx, visible}: ScreenParams) => { const store = useStores() diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx index 0223e631..09e3dd46 100644 --- a/src/view/screens/Debug.tsx +++ b/src/view/screens/Debug.tsx @@ -1,11 +1,10 @@ import React from 'react' import {ScrollView, View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' -import {ThemeProvider} from '../lib/ThemeContext' -import {PaletteColorName} from '../lib/ThemeContext' -import {usePalette} from '../lib/hooks/usePalette' -import {s} from '../lib/styles' -import {displayNotification} from '../lib/notifee' +import {ThemeProvider, PaletteColorName} from 'lib/ThemeContext' +import {usePalette} from 'lib/hooks/usePalette' +import {s} from 'lib/styles' +import {displayNotification} from 'lib/notifee' import {Text} from '../com/util/text/Text' import {ViewSelector} from '../com/util/ViewSelector' @@ -284,6 +283,9 @@ function TypographyView() { 'xs-heavy' lorem ipsum dolor + + 'title-2xl' lorem ipsum dolor + 'title-xl' lorem ipsum dolor @@ -296,6 +298,9 @@ function TypographyView() { Button + + Button-lg + ) } diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 4222c751..de7e61ba 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -1,23 +1,24 @@ import React, {useEffect} from 'react' -import {View} from 'react-native' +import {FlatList, View} from 'react-native' import {observer} from 'mobx-react-lite' import useAppState from 'react-native-appstate-hook' import {ViewHeader} from '../com/util/ViewHeader' import {Feed} from '../com/posts/Feed' import {FAB} from '../com/util/FAB' import {LoadLatestBtn} from '../com/util/LoadLatestBtn' -import {useStores} from '../../state' +import {useStores} from 'state/index' import {ScreenParams} from '../routes' -import {s} from '../lib/styles' -import {useOnMainScroll} from '../lib/hooks/useOnMainScroll' +import {s} from 'lib/styles' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {useAnalytics} from 'lib/analytics' -export const Home = observer(function Home({ - navIdx, - visible, - scrollElRef, -}: ScreenParams) { +const HEADER_HEIGHT = 42 + +export const Home = observer(function Home({navIdx, visible}: ScreenParams) { const store = useStores() const onMainScroll = useOnMainScroll(store) + const {screen, track} = useAnalytics() + const scrollElRef = React.useRef(null) const [wasVisible, setWasVisible] = React.useState(false) const {appState} = useAppState({ onForeground: () => doPoll(true), @@ -31,22 +32,31 @@ export const Home = observer(function Home({ if (store.me.mainFeed.isLoading) { return } - store.log.debug('Polling home feed') - store.me.mainFeed.checkForLatest().catch(e => { - store.log.error('Failed to poll feed', e) - }) + store.log.debug('HomeScreen: Polling for new posts') + store.me.mainFeed.checkForLatest() }, [appState, visible, store], ) + const scrollToTop = React.useCallback(() => { + // NOTE: the feed is offset by the height of the collapsing header, + // so we scroll to the negative of that height -prf + scrollElRef.current?.scrollToOffset({offset: -HEADER_HEIGHT}) + }, [scrollElRef]) + useEffect(() => { + const softResetSub = store.onScreenSoftReset(scrollToTop) const feedCleanup = store.me.mainFeed.registerListeners() - const pollInterval = setInterval(() => doPoll(), 15e3) + const pollInterval = setInterval(doPoll, 15e3) const cleanup = () => { clearInterval(pollInterval) + softResetSub.remove() feedCleanup() } + // guard to only continue when transitioning from !visible -> visible + // TODO is this 100% needed? depends on if useEffect() is getting refired + // for reasons other than `visible` changing -prf if (!visible) { setWasVisible(false) return cleanup @@ -55,17 +65,20 @@ export const Home = observer(function Home({ } setWasVisible(true) + // just became visible + screen('Feed') store.nav.setTitle(navIdx, 'Home') - store.log.debug('Updating home feed') + store.log.debug('HomeScreen: Updating feed') if (store.me.mainFeed.hasContent) { store.me.mainFeed.update() } else { store.me.mainFeed.setup() } return cleanup - }, [visible, store, navIdx, doPoll, wasVisible]) + }, [visible, store, store.me.mainFeed, navIdx, doPoll, wasVisible, scrollToTop, screen]) const onPressCompose = (imagesOpen?: boolean) => { + track('Home:ComposeButtonPressed') store.shell.openComposer({imagesOpen}) } const onPressTryAgain = () => { @@ -73,26 +86,31 @@ export const Home = observer(function Home({ } const onPressLoadLatest = () => { store.me.mainFeed.refresh() - scrollElRef?.current?.scrollToOffset({offset: 0}) + scrollToTop() } return ( - + {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && ( )} - onPressCompose(false)} /> + onPressCompose(false)} + /> ) }) diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx index c3e156dc..c067d350 100644 --- a/src/view/screens/Log.tsx +++ b/src/view/screens/Log.tsx @@ -3,13 +3,13 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {ScrollView} from '../com/util/Views' -import {useStores} from '../../state' +import {useStores} from 'state/index' import {ScreenParams} from '../routes' -import {s} from '../lib/styles' +import {s} from 'lib/styles' import {ViewHeader} from '../com/util/ViewHeader' import {Text} from '../com/util/text/Text' -import {usePalette} from '../lib/hooks/usePalette' -import {ago} from '../../lib/strings' +import {usePalette} from 'lib/hooks/usePalette' +import {ago} from 'lib/strings/time' export const Log = observer(function Log({navIdx, visible}: ScreenParams) { const pal = usePalette('default') diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx index 81a2c9e6..50b2a34c 100644 --- a/src/view/screens/Login.tsx +++ b/src/view/screens/Login.tsx @@ -1,19 +1,16 @@ -import React, {useState} from 'react' -import { - Image, - SafeAreaView, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' +import React, {useEffect, useState} from 'react' +import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native' +import Image, {Source as ImageSource} from 'view/com/util/images/Image' import {observer} from 'mobx-react-lite' import {Signin} from '../com/login/Signin' import {CreateAccount} from '../com/login/CreateAccount' import {Text} from '../com/util/text/Text' import {ErrorBoundary} from '../com/util/ErrorBoundary' -import {colors} from '../lib/styles' -import {usePalette} from '../lib/hooks/usePalette' -import {CLOUD_SPLASH} from '../lib/assets' +import {colors} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {useStores} from 'state/index' +import {CLOUD_SPLASH} from 'lib/assets' +import {useAnalytics} from 'lib/analytics' enum ScreenState { S_SigninOrCreateAccount, @@ -28,6 +25,12 @@ const SigninOrCreateAccount = ({ onPressSignin: () => void onPressCreateAccount: () => void }) => { + const {screen} = useAnalytics() + + useEffect(() => { + screen('Login') + }, [screen]) + const pal = usePalette('default') return ( <> @@ -57,22 +60,28 @@ const SigninOrCreateAccount = ({ export const Login = observer(() => { const pal = usePalette('default') + const store = useStores() const [screenState, setScreenState] = useState( ScreenState.S_SigninOrCreateAccount, ) - if (screenState === ScreenState.S_SigninOrCreateAccount) { + if ( + store.session.isResumingSession || + screenState === ScreenState.S_SigninOrCreateAccount + ) { return ( - + - setScreenState(ScreenState.S_Signin)} - onPressCreateAccount={() => - setScreenState(ScreenState.S_CreateAccount) - } - /> + {!store.session.isResumingSession && ( + setScreenState(ScreenState.S_Signin)} + onPressCreateAccount={() => + setScreenState(ScreenState.S_CreateAccount) + } + /> + )} diff --git a/src/view/screens/Login.web.tsx b/src/view/screens/Login.web.tsx index 77149090..90effc5d 100644 --- a/src/view/screens/Login.web.tsx +++ b/src/view/screens/Login.web.tsx @@ -1,20 +1,13 @@ import React, {useState} from 'react' -import { - Image, - SafeAreaView, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' +import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' import {CenteredView} from '../com/util/Views' import {Signin} from '../com/login/Signin' import {CreateAccount} from '../com/login/CreateAccount' import {Text} from '../com/util/text/Text' import {ErrorBoundary} from '../com/util/ErrorBoundary' -import {colors} from '../lib/styles' -import {usePalette} from '../lib/hooks/usePalette' -import {CLOUD_SPLASH} from '../lib/assets' +import {colors} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' enum ScreenState { S_SigninOrCreateAccount, @@ -125,6 +118,7 @@ const styles = StyleSheet.create({ width: '100%', height: '100%', }, + hero: {}, heroText: { backgroundColor: colors.white, paddingTop: 10, diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx index c5c5ff00..77bbdd2a 100644 --- a/src/view/screens/NotFound.tsx +++ b/src/view/screens/NotFound.tsx @@ -2,7 +2,7 @@ import React from 'react' import {Button, StyleSheet, View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {Text} from '../com/util/text/Text' -import {useStores} from '../../state' +import {useStores} from 'state/index' export const NotFound = () => { const stores = useStores() diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 9b5dc597..548b0d56 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -1,35 +1,79 @@ import React, {useEffect} from 'react' -import {View} from 'react-native' +import {FlatList, View} from 'react-native' +import useAppState from 'react-native-appstate-hook' import {ViewHeader} from '../com/util/ViewHeader' import {Feed} from '../com/notifications/Feed' -import {useStores} from '../../state' +import {useStores} from 'state/index' import {ScreenParams} from '../routes' -import {useOnMainScroll} from '../lib/hooks/useOnMainScroll' -import {s} from '../lib/styles' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {s} from 'lib/styles' +import {useAnalytics} from 'lib/analytics' + +const NOTIFICATIONS_POLL_INTERVAL = 15e3 export const Notifications = ({navIdx, visible}: ScreenParams) => { const store = useStores() const onMainScroll = useOnMainScroll(store) + const scrollElRef = React.useRef(null) + const {screen} = useAnalytics() + const {appState} = useAppState({ + onForeground: () => doPoll(true), + }) + // event handlers + // = + const onPressTryAgain = () => { + store.me.notifications.refresh() + } + const scrollToTop = React.useCallback(() => { + scrollElRef.current?.scrollToOffset({offset: 0}) + }, [scrollElRef]) + + // periodic polling + // = + const doPoll = React.useCallback( + async (isForegrounding = false) => { + if (isForegrounding) { + // app is foregrounding, refresh optimistically + store.log.debug('NotificationsScreen: Refreshing on app foreground') + await Promise.all([ + store.me.notifications.loadUnreadCount(), + store.me.notifications.refresh(), + ]) + } else if (appState === 'active') { + // periodic poll, refresh if there are new notifs + store.log.debug('NotificationsScreen: Polling for new notifications') + const didChange = await store.me.notifications.loadUnreadCount() + if (didChange) { + store.log.debug('NotificationsScreen: Loading new notifications') + await store.me.notifications.loadLatest() + } + } + }, + [appState, store], + ) + useEffect(() => { + const pollInterval = setInterval(doPoll, NOTIFICATIONS_POLL_INTERVAL) + return () => clearInterval(pollInterval) + }, [doPoll]) + + // on-visible setup + // = useEffect(() => { if (!visible) { return } - store.log.debug('Updating notifications feed') - store.me.notifications - .update() - .catch(e => { - store.log.error('Error while updating notifications feed', e) - }) - .then(() => { - store.me.notifications.updateReadState() - }) + store.log.debug('NotificationsScreen: Updating feed') + const softResetSub = store.onScreenSoftReset(scrollToTop) + store.me.notifications.update().then(() => { + store.me.notifications.markAllRead() + }) + screen('Notifications') store.nav.setTitle(navIdx, 'Notifications') - }, [visible, store, navIdx]) - - const onPressTryAgain = () => { - store.me.notifications.refresh() - } + return () => { + softResetSub.remove() + } + }, [visible, store, navIdx, screen, scrollToTop]) return ( @@ -38,6 +82,7 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => { view={store.me.notifications} onPressTryAgain={onPressTryAgain} onScroll={onMainScroll} + scrollElRef={scrollElRef} /> ) diff --git a/src/view/screens/Onboard.tsx b/src/view/screens/Onboard.tsx index e31b42ad..1485670e 100644 --- a/src/view/screens/Onboard.tsx +++ b/src/view/screens/Onboard.tsx @@ -3,8 +3,8 @@ import {StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' import {FeatureExplainer} from '../com/onboard/FeatureExplainer' import {Follows} from '../com/onboard/Follows' -import {OnboardStage, OnboardStageOrder} from '../../state/models/onboard' -import {useStores} from '../../state' +import {OnboardStage, OnboardStageOrder} from 'state/models/onboard' +import {useStores} from 'state/index' export const Onboard = observer(() => { const store = useStores() diff --git a/src/view/screens/PostDownvotedBy.tsx b/src/view/screens/PostDownvotedBy.tsx index 1401868d..57048259 100644 --- a/src/view/screens/PostDownvotedBy.tsx +++ b/src/view/screens/PostDownvotedBy.tsx @@ -3,8 +3,8 @@ import {View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedBy' import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import {makeRecordUri} from '../../lib/strings' +import {useStores} from 'state/index' +import {makeRecordUri} from 'lib/strings/url-helpers' export const PostDownvotedBy = ({navIdx, visible, params}: ScreenParams) => { const store = useStores() diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx index bf4d6ec9..4be4b4b4 100644 --- a/src/view/screens/PostRepostedBy.tsx +++ b/src/view/screens/PostRepostedBy.tsx @@ -3,8 +3,8 @@ import {View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy' import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import {makeRecordUri} from '../../lib/strings' +import {useStores} from 'state/index' +import {makeRecordUri} from 'lib/strings/url-helpers' export const PostRepostedBy = ({navIdx, visible, params}: ScreenParams) => { const store = useStores() diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index febaddc0..4b799468 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -1,17 +1,16 @@ -import React, {useEffect, useMemo, useState} from 'react' +import React, {useEffect, useMemo} from 'react' import {View} from 'react-native' -import {makeRecordUri} from '../../lib/strings' +import {makeRecordUri} from 'lib/strings/url-helpers' import {ViewHeader} from '../com/util/ViewHeader' import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread' -import {PostThreadViewModel} from '../../state/models/post-thread-view' +import {PostThreadViewModel} from 'state/models/post-thread-view' import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import {s} from '../lib/styles' +import {useStores} from 'state/index' +import {s} from 'lib/styles' export const PostThread = ({navIdx, visible, params}: ScreenParams) => { const store = useStores() const {name, rkey} = params - const [viewSubtitle, setViewSubtitle] = useState(`by ${name}`) const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const view = useMemo( () => new PostThreadViewModel(store, {uri}), @@ -24,7 +23,6 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => { const setTitle = () => { const author = view.thread?.post.author const niceName = author?.handle || name - setViewSubtitle(`by ${niceName}`) store.nav.setTitle(navIdx, `Post by ${niceName}`) } if (!visible) { @@ -52,7 +50,7 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => { return ( - + diff --git a/src/view/screens/PostUpvotedBy.tsx b/src/view/screens/PostUpvotedBy.tsx index 4bba222a..4d6ad411 100644 --- a/src/view/screens/PostUpvotedBy.tsx +++ b/src/view/screens/PostUpvotedBy.tsx @@ -3,8 +3,8 @@ import {View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedBy' import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import {makeRecordUri} from '../../lib/strings' +import {useStores} from 'state/index' +import {makeRecordUri} from 'lib/strings/url-helpers' export const PostUpvotedBy = ({navIdx, visible, params}: ScreenParams) => { const store = useStores() diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 5c661698..03d973b9 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -4,8 +4,8 @@ import {observer} from 'mobx-react-lite' import {ViewSelector} from '../com/util/ViewSelector' import {CenteredView} from '../com/util/Views' import {ScreenParams} from '../routes' -import {ProfileUiModel, Sections} from '../../state/models/profile-ui' -import {useStores} from '../../state' +import {ProfileUiModel, Sections} from 'state/models/profile-ui' +import {useStores} from 'state/index' import {ProfileHeader} from '../com/profile/ProfileHeader' import {FeedItem} from '../com/posts/FeedItem' import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder' @@ -14,8 +14,9 @@ import {ErrorMessage} from '../com/util/error/ErrorMessage' import {EmptyState} from '../com/util/EmptyState' import {Text} from '../com/util/text/Text' import {FAB} from '../com/util/FAB' -import {s, colors} from '../lib/styles' -import {useOnMainScroll} from '../lib/hooks/useOnMainScroll' +import {s, colors} from 'lib/styles' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {useAnalytics} from 'lib/analytics' const LOADING_ITEM = {_reactKey: '__loading__'} const END_ITEM = {_reactKey: '__end__'} @@ -23,6 +24,12 @@ const EMPTY_ITEM = {_reactKey: '__empty__'} export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { const store = useStores() + const {screen} = useAnalytics() + + useEffect(() => { + screen('Profile') + }, [screen]) + const onMainScroll = useOnMainScroll(store) const [hasSetup, setHasSetup] = useState(false) const uiState = React.useMemo( @@ -128,7 +135,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { } if (!uiState.feed.hasMore) { items = items.concat([END_ITEM]) - } else { + } else if (uiState.feed.isLoading) { Footer = LoadingMoreFooter } renderItem = (item: any) => { @@ -184,7 +191,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { ) : ( {renderHeader()} )} - + ) }) diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx index f7520549..9f1a9c74 100644 --- a/src/view/screens/ProfileFollowers.tsx +++ b/src/view/screens/ProfileFollowers.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers' import {ScreenParams} from '../routes' -import {useStores} from '../../state' +import {useStores} from 'state/index' export const ProfileFollowers = ({navIdx, visible, params}: ScreenParams) => { const store = useStores() @@ -18,7 +18,7 @@ export const ProfileFollowers = ({navIdx, visible, params}: ScreenParams) => { return ( - + ) diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx index 65e4004e..1cdb5bcc 100644 --- a/src/view/screens/ProfileFollows.tsx +++ b/src/view/screens/ProfileFollows.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows' import {ScreenParams} from '../routes' -import {useStores} from '../../state' +import {useStores} from 'state/index' export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => { const store = useStores() @@ -18,7 +18,7 @@ export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => { return ( - + ) diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx index 2a1caab8..2e176d98 100644 --- a/src/view/screens/Search.tsx +++ b/src/view/screens/Search.tsx @@ -1,41 +1,73 @@ -import React, {useEffect, useState, useMemo, useRef} from 'react' +import React from 'react' import { Keyboard, StyleSheet, TextInput, TouchableOpacity, + TouchableWithoutFeedback, View, } from 'react-native' -import {ViewHeader} from '../com/util/ViewHeader' -import {CenteredView, ScrollView} from '../com/util/Views' -import {SuggestedFollows} from '../com/discover/SuggestedFollows' +import {ScrollView} from '../com/util/Views' +import {observer} from 'mobx-react-lite' import {UserAvatar} from '../com/util/UserAvatar' import {Text} from '../com/util/text/Text' import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import {UserAutocompleteViewModel} from '../../state/models/user-autocomplete-view' -import {s} from '../lib/styles' -import {MagnifyingGlassIcon} from '../lib/icons' -import {usePalette} from '../lib/hooks/usePalette' +import {useStores} from 'state/index' +import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view' +import {s} from 'lib/styles' +import {MagnifyingGlassIcon} from 'lib/icons' +import {WhoToFollow} from '../com/discover/WhoToFollow' +import {SuggestedPosts} from '../com/discover/SuggestedPosts' +import {ProfileCard} from '../com/profile/ProfileCard' +import {usePalette} from 'lib/hooks/usePalette' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {useAnalytics} from 'lib/analytics' -export const Search = ({navIdx, visible, params}: ScreenParams) => { +const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10} +const FIVE_MIN = 5 * 60 * 1e3 + +export const Search = observer(({navIdx, visible, params}: ScreenParams) => { const pal = usePalette('default') const store = useStores() - const textInput = useRef(null) - const [query, setQuery] = useState('') - const autocompleteView = useMemo( + const {track} = useAnalytics() + const scrollElRef = React.useRef(null) + const onMainScroll = useOnMainScroll(store) + const textInput = React.useRef(null) + const [lastRenderTime, setRenderTime] = React.useState(Date.now()) // used to trigger reloads + const [isInputFocused, setIsInputFocused] = React.useState(false) + const [query, setQuery] = React.useState('') + const autocompleteView = React.useMemo( () => new UserAutocompleteViewModel(store), [store], ) const {name} = params - useEffect(() => { + const onSoftReset = () => { + scrollElRef.current?.scrollTo({x: 0, y: 0}) + } + + React.useEffect(() => { + const softResetSub = store.onScreenSoftReset(onSoftReset) + const cleanup = () => { + softResetSub.remove() + } + if (visible) { + const now = Date.now() + if (now - lastRenderTime > FIVE_MIN) { + setRenderTime(Date.now()) // trigger reload of suggestions + } store.shell.setMinimalShellMode(false) autocompleteView.setup() store.nav.setTitle(navIdx, 'Search') } - }, [store, visible, name, navIdx, autocompleteView]) + return cleanup + }, [store, visible, name, navIdx, autocompleteView, lastRenderTime]) + + const onPressMenu = () => { + track('ViewHeader:MenuButtonClicked') + store.shell.setMainMenuOpen(true) + } const onChangeQuery = (text: string) => { setQuery(text) @@ -46,87 +78,139 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => { autocompleteView.setActive(false) } } - const onSelect = (handle: string) => { - textInput.current?.blur() - store.nav.navigate(`/profile/${handle}`) + const onPressCancelSearch = () => { + setQuery('') + autocompleteView.setActive(false) } return ( - - - - - - - - {query ? ( - - {autocompleteView.searchRes.map((item, i) => ( - onSelect(item.handle)}> - - - - {item.displayName || item.handle} - - @{item.handle} - + + + + + + + + + setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + onChangeText={onChangeQuery} + /> + + {query ? ( + + + Cancel + + ) : undefined} + + {query && autocompleteView.searchRes.length ? ( + <> + {autocompleteView.searchRes.map(item => ( + ))} + + ) : query && !autocompleteView.searchRes.length ? ( + + + No results found for {autocompleteView.prefix} + + + ) : isInputFocused ? ( + + + Search for users on the network + + + ) : ( + + + - ) : ( - )} - - + + + ) -} +}) const styles = StyleSheet.create({ container: { flex: 1, }, - inputContainer: { + header: { flexDirection: 'row', - paddingVertical: 16, - paddingHorizontal: 16, - borderTopWidth: 1, + alignItems: 'center', + paddingHorizontal: 12, + paddingTop: 4, + marginBottom: 14, }, - inputIcon: { - marginRight: 10, + headerMenuBtn: { + width: 40, + height: 30, + marginLeft: 6, + }, + headerSearchContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + borderRadius: 30, + paddingHorizontal: 12, + paddingVertical: 8, + }, + headerSearchIcon: { + marginRight: 6, alignSelf: 'center', }, - input: { + headerSearchInput: { flex: 1, - fontSize: 16, + fontSize: 17, + }, + headerCancelBtn: { + width: 60, + paddingLeft: 10, }, - outputContainer: { - flex: 1, - }, - - searchResult: { - flexDirection: 'row', - borderTopWidth: 1, - paddingVertical: 12, - paddingHorizontal: 16, + searchPrompt: { + textAlign: 'center', + paddingTop: 10, }, }) diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index c2953b59..94f5acd9 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -7,17 +7,20 @@ import { } from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {observer} from 'mobx-react-lite' -import {useStores} from '../../state' +import * as AppInfo from 'lib/app-info' +import {useStores} from 'state/index' import {ScreenParams} from '../routes' -import {s} from '../lib/styles' +import {s} from 'lib/styles' import {ScrollView} from '../com/util/Views' import {ViewHeader} from '../com/util/ViewHeader' import {Link} from '../com/util/Link' import {Text} from '../com/util/text/Text' import * as Toast from '../com/util/Toast' import {UserAvatar} from '../com/util/UserAvatar' -import {usePalette} from '../lib/hooks/usePalette' -import {AccountData} from '../../state/models/session' +import {usePalette} from 'lib/hooks/usePalette' +import {AccountData} from 'state/models/session' +import {useAnalytics} from 'lib/analytics' +import {DeleteAccountModal} from 'state/models/shell-ui' export const Settings = observer(function Settings({ navIdx, @@ -25,8 +28,13 @@ export const Settings = observer(function Settings({ }: ScreenParams) { const pal = usePalette('default') const store = useStores() + const {screen, track} = useAnalytics() const [isSwitching, setIsSwitching] = React.useState(false) + useEffect(() => { + screen('Settings') + }, [screen]) + useEffect(() => { if (!visible) { return @@ -36,22 +44,30 @@ export const Settings = observer(function Settings({ }, [visible, store, navIdx]) const onPressSwitchAccount = async (acct: AccountData) => { + track('Settings:SwitchAccountButtonClicked') setIsSwitching(true) if (await store.session.resumeSession(acct)) { setIsSwitching(false) + store.nav.tab.fixedTabReset() Toast.show(`Signed in as ${acct.displayName || acct.handle}`) return } setIsSwitching(false) Toast.show('Sorry! We need you to enter your password.') + store.nav.tab.fixedTabReset() store.session.clear() } const onPressAddAccount = () => { + track('Settings:AddAccountButtonClicked') store.session.clear() } const onPressSignout = () => { + track('Settings:SignOutButtonClicked') store.session.logout() } + const onPressDeleteAccount = () => { + store.shell.openModal(new DeleteAccountModal()) + } return ( @@ -143,22 +159,34 @@ export const Settings = observer(function Settings({ + + Danger zone + + + Delete my account + + Developer tools - System log + System log - Storybook + Storybook + + Build version {AppInfo.appVersion} ({AppInfo.buildVersion}) + diff --git a/src/view/shell/mobile/Composer.tsx b/src/view/shell/mobile/Composer.tsx index c93931ab..304c1772 100644 --- a/src/view/shell/mobile/Composer.tsx +++ b/src/view/shell/mobile/Composer.tsx @@ -2,9 +2,9 @@ import React, {useEffect} from 'react' import {observer} from 'mobx-react-lite' import {Animated, Easing, Platform, StyleSheet, View} from 'react-native' import {ComposePost} from '../../com/composer/ComposePost' -import {ComposerOpts} from '../../../state/models/shell-ui' -import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' -import {usePalette} from '../../lib/hooks/usePalette' +import {ComposerOpts} from 'state/models/shell-ui' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {usePalette} from 'lib/hooks/usePalette' export const Composer = observer( ({ diff --git a/src/view/shell/mobile/Menu.tsx b/src/view/shell/mobile/Menu.tsx index a7d3e214..ceeda8c5 100644 --- a/src/view/shell/mobile/Menu.tsx +++ b/src/view/shell/mobile/Menu.tsx @@ -1,6 +1,6 @@ import React from 'react' import { - ScrollView, + Linking, StyleProp, StyleSheet, TouchableOpacity, @@ -8,40 +8,56 @@ import { ViewStyle, } from 'react-native' import {observer} from 'mobx-react-lite' -import VersionNumber from 'react-native-version-number' -import {s, colors} from '../../lib/styles' -import {useStores} from '../../../state' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {s, colors} from 'lib/styles' +import {FEEDBACK_FORM_URL} from 'lib/constants' +import {useStores} from 'state/index' import { HomeIcon, BellIcon, UserIcon, CogIcon, MagnifyingGlassIcon, -} from '../../lib/icons' +} from 'lib/icons' +import {TabPurpose, TabPurposeMainPath} from 'state/models/navigation' import {UserAvatar} from '../../com/util/UserAvatar' import {Text} from '../../com/util/text/Text' import {ToggleButton} from '../../com/util/forms/ToggleButton' -import {usePalette} from '../../lib/hooks/usePalette' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnalytics} from 'lib/analytics' export const Menu = observer(({onClose}: {onClose: () => void}) => { const pal = usePalette('default') const store = useStores() + const {track} = useAnalytics() // events // = const onNavigate = (url: string) => { + track('Menu:ItemClicked', {url}) + onClose() - if (url === '/notifications') { - store.nav.switchTo(1, true) + if (url === TabPurposeMainPath[TabPurpose.Notifs]) { + store.nav.switchTo(TabPurpose.Notifs, true) + } else if (url === TabPurposeMainPath[TabPurpose.Search]) { + store.nav.switchTo(TabPurpose.Search, true) } else { - store.nav.switchTo(0, true) + store.nav.switchTo(TabPurpose.Default, true) if (url !== '/') { store.nav.navigate(url) } } } + const onPressFeedback = () => { + track('Menu:FeedbackClicked') + Linking.openURL(FEEDBACK_FORM_URL) + } + // rendering // = @@ -84,8 +100,19 @@ export const Menu = observer(({onClose}: {onClose: () => void}) => { ) + const onDarkmodePress = () => { + track('Menu:ItemClicked', {url: '/darkmode'}) + store.shell.setDarkMode(!store.shell.darkMode) + } + return ( - + onNavigate(`/profile/${store.me.handle}`)} @@ -132,7 +159,7 @@ export const Menu = observer(({onClose}: {onClose: () => void}) => { icon={} size="28" />} label="Notifications" url="/notifications" - count={store.me.notificationCount} + count={store.me.notifications.unreadCount} /> void}) => { store.shell.setDarkMode(!store.shell.darkMode)} + onPress={onDarkmodePress} /> + - - Build version {VersionNumber.appVersion} ({VersionNumber.buildVersion} - ) - + + } + label="Feedback" + onPress={onPressFeedback} + /> - - + ) }) const styles = StyleSheet.create({ view: { flex: 1, + paddingBottom: 90, + }, + viewMinimalShell: { + paddingBottom: 50, }, section: { paddingHorizontal: 10, @@ -254,7 +292,6 @@ const styles = StyleSheet.create({ }, footer: { - paddingHorizontal: 14, - paddingVertical: 18, + paddingHorizontal: 10, }, }) diff --git a/src/view/shell/mobile/TabsSelector.tsx b/src/view/shell/mobile/TabsSelector.tsx index 921a0c85..92854e3b 100644 --- a/src/view/shell/mobile/TabsSelector.tsx +++ b/src/view/shell/mobile/TabsSelector.tsx @@ -12,11 +12,11 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../../com/util/text/Text' import Swipeable from 'react-native-gesture-handler/Swipeable' -import {useStores} from '../../../state' -import {s, colors} from '../../lib/styles' -import {toShareUrl} from '../../../lib/strings' +import {useStores} from 'state/index' +import {s, colors} from 'lib/styles' +import {toShareUrl} from 'lib/strings/url-helpers' import {match} from '../../routes' -import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' const TAB_HEIGHT = 42 diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx index b0b83b12..0b3921b7 100644 --- a/src/view/shell/mobile/index.tsx +++ b/src/view/shell/mobile/index.tsx @@ -1,9 +1,8 @@ -import React, {useState, useEffect, useRef} from 'react' +import React, {useState, useEffect} from 'react' import {observer} from 'mobx-react-lite' import { Animated, Easing, - FlatList, GestureResponderEvent, StatusBar, StyleSheet, @@ -12,15 +11,18 @@ import { useColorScheme, useWindowDimensions, View, - ViewStyle, } from 'react-native' import {ScreenContainer, Screen} from 'react-native-screens' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {IconProp} from '@fortawesome/fontawesome-svg-core' -import {TABS_ENABLED} from '../../../build-flags' -import {useStores} from '../../../state' -import {NavigationModel} from '../../../state/models/navigation' +import {TABS_ENABLED} from 'lib/build-flags' +import {useStores} from 'state/index' +import { + NavigationModel, + TabPurpose, + TabPurposeMainPath, +} from 'state/models/navigation' import {match, MatchResult} from '../../routes' import {Login} from '../../screens/Login' import {Menu} from './Menu' @@ -32,19 +34,21 @@ import {Text} from '../../com/util/text/Text' import {ErrorBoundary} from '../../com/util/ErrorBoundary' import {TabsSelector} from './TabsSelector' import {Composer} from './Composer' -import {s, colors} from '../../lib/styles' -import {clamp} from '../../../lib/numbers' +import {s, colors} from 'lib/styles' +import {clamp} from 'lib/numbers' import { GridIcon, GridIconSolid, HomeIcon, HomeIconSolid, + MagnifyingGlassIcon, BellIcon, BellIconSolid, -} from '../../lib/icons' -import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' -import {useTheme} from '../../lib/ThemeContext' -import {usePalette} from '../../lib/hooks/usePalette' +} from 'lib/icons' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {useTheme} from 'lib/ThemeContext' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnalytics} from 'lib/analytics' const Btn = ({ icon, @@ -59,6 +63,8 @@ const Btn = ({ | 'menu-solid' | 'home' | 'home-solid' + | 'search' + | 'search-solid' | 'bell' | 'bell-solid' notificationCount?: number @@ -76,24 +82,40 @@ const Btn = ({ iconEl = } else if (icon === 'home-solid') { iconEl = - } else if (icon === 'bell') { - const addedStyles = {position: 'relative', top: -1} as ViewStyle + } else if (icon === 'search') { iconEl = ( - + + ) + } else if (icon === 'search-solid') { + iconEl = ( + + ) + } else if (icon === 'bell') { + iconEl = ( + ) } else if (icon === 'bell-solid') { - const addedStyles = {position: 'relative', top: -1} as ViewStyle iconEl = ( ) } else { iconEl = ( ) @@ -125,7 +147,6 @@ export const MobileShell: React.FC = observer(() => { const pal = usePalette('default') const store = useStores() const [isTabsSelectorActive, setTabsSelectorActive] = useState(false) - const scrollElRef = useRef(null) const winDim = useWindowDimensions() const [menuSwipingDirection, setMenuSwipingDirection] = useState(0) const swipeGestureInterp = useAnimatedValue(0) @@ -136,32 +157,48 @@ export const MobileShell: React.FC = observer(() => { const colorScheme = useColorScheme() const safeAreaInsets = useSafeAreaInsets() const screenRenderDesc = constructScreenRenderDesc(store.nav) + const {track} = useAnalytics() const onPressHome = () => { - if (store.shell.isMainMenuOpen) { - store.shell.setMainMenuOpen(false) - } - if (store.nav.tab.fixedTabPurpose === 0) { - if (store.nav.tab.current.url === '/') { - scrollElRef.current?.scrollToOffset({offset: 0}) + track('MobileShell:HomeButtonPressed') + if (store.nav.tab.fixedTabPurpose === TabPurpose.Default) { + if (!store.nav.tab.canGoBack) { + store.emitScreenSoftReset() } else { store.nav.tab.fixedTabReset() } } else { - store.nav.switchTo(0, false) + store.nav.switchTo(TabPurpose.Default, false) + if (store.nav.tab.index === 0) { + store.nav.tab.fixedTabReset() + } + } + } + const onPressSearch = () => { + track('MobileShell:SearchButtonPressed') + if (store.nav.tab.fixedTabPurpose === TabPurpose.Search) { + if (!store.nav.tab.canGoBack) { + store.emitScreenSoftReset() + } else { + store.nav.tab.fixedTabReset() + } + } else { + store.nav.switchTo(TabPurpose.Search, false) if (store.nav.tab.index === 0) { store.nav.tab.fixedTabReset() } } } const onPressNotifications = () => { - if (store.shell.isMainMenuOpen) { - store.shell.setMainMenuOpen(false) - } - if (store.nav.tab.fixedTabPurpose === 1) { - store.nav.tab.fixedTabReset() + track('MobileShell:NotificationsButtonPressed') + if (store.nav.tab.fixedTabPurpose === TabPurpose.Notifs) { + if (!store.nav.tab.canGoBack) { + store.emitScreenSoftReset() + } else { + store.nav.tab.fixedTabReset() + } } else { - store.nav.switchTo(1, false) + store.nav.switchTo(TabPurpose.Notifs, false) if (store.nav.tab.index === 0) { store.nav.tab.fixedTabReset() } @@ -178,12 +215,14 @@ export const MobileShell: React.FC = observer(() => { toValue: 1, duration: 100, useNativeDriver: true, + isInteraction: false, }).start() } else { Animated.timing(minimalShellInterp, { toValue: 0, duration: 100, useNativeDriver: true, + isInteraction: false, }).start() } }, [minimalShellInterp, store.shell.minimalShellMode]) @@ -343,8 +382,12 @@ export const MobileShell: React.FC = observer(() => { ) } - const isAtHome = store.nav.tab.current.url === '/' - const isAtNotifications = store.nav.tab.current.url === '/notifications' + const isAtHome = + store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Default] + const isAtSearch = + store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Search] + const isAtNotifications = + store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Notifs] const screenBg = { backgroundColor: theme.colorScheme === 'dark' ? colors.gray7 : colors.gray1, @@ -358,7 +401,7 @@ export const MobileShell: React.FC = observer(() => { /> { params={params} navIdx={navIdx} visible={current} - scrollElRef={current ? scrollElRef : undefined} /> @@ -454,6 +496,11 @@ export const MobileShell: React.FC = observer(() => { onPress={onPressHome} onLongPress={TABS_ENABLED ? doNewTab('/') : undefined} /> + {TABS_ENABLED ? ( { icon={isAtNotifications ? 'bell-solid' : 'bell'} onPress={onPressNotifications} onLongPress={TABS_ENABLED ? doNewTab('/notifications') : undefined} - notificationCount={store.me.notificationCount} + notificationCount={store.me.notifications.unreadCount} /> @@ -576,7 +623,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', borderTopWidth: 1, paddingLeft: 5, - paddingRight: 15, + paddingRight: 25, }, ctrl: { flex: 1, @@ -614,4 +661,8 @@ const styles = StyleSheet.create({ inactive: { color: colors.gray3, }, + bumpUpOnePixel: { + position: 'relative', + top: -1, + }, }) diff --git a/src/view/shell/web/Composer.tsx b/src/view/shell/web/Composer.tsx index 63904009..5006b312 100644 --- a/src/view/shell/web/Composer.tsx +++ b/src/view/shell/web/Composer.tsx @@ -2,8 +2,8 @@ import React from 'react' import {observer} from 'mobx-react-lite' import {StyleSheet, View} from 'react-native' import {ComposePost} from '../../com/composer/ComposePost' -import {ComposerOpts} from '../../../state/models/shell-ui' -import {usePalette} from '../../lib/hooks/usePalette' +import {ComposerOpts} from 'state/models/shell-ui' +import {usePalette} from 'lib/hooks/usePalette' export const Composer = observer( ({ diff --git a/src/view/shell/web/DesktopLeftColumn.tsx b/src/view/shell/web/DesktopLeftColumn.tsx index 44559b6a..d5fe45e8 100644 --- a/src/view/shell/web/DesktopLeftColumn.tsx +++ b/src/view/shell/web/DesktopLeftColumn.tsx @@ -5,9 +5,9 @@ import LinearGradient from 'react-native-linear-gradient' import {Link} from '../../com/util/Link' import {Text} from '../../com/util/text/Text' import {UserAvatar} from '../../com/util/UserAvatar' -import {s, colors, gradients} from '../../lib/styles' -import {useStores} from '../../../state' -import {usePalette} from '../../lib/hooks/usePalette' +import {s, colors, gradients} from 'lib/styles' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' import { HomeIcon, HomeIconSolid, @@ -15,7 +15,7 @@ import { BellIconSolid, MagnifyingGlassIcon, CogIcon, -} from '../../lib/icons' +} from 'lib/icons' interface NavItemProps { label: string @@ -97,7 +97,7 @@ export const DesktopLeftColumn = observer(() => { } iconFilled={} /> diff --git a/src/view/shell/web/DesktopRightColumn.tsx b/src/view/shell/web/DesktopRightColumn.tsx index 2daa16f6..22acac38 100644 --- a/src/view/shell/web/DesktopRightColumn.tsx +++ b/src/view/shell/web/DesktopRightColumn.tsx @@ -2,10 +2,10 @@ import React from 'react' import {View, StyleSheet} from 'react-native' import {Link} from '../../com/util/Link' import {Text} from '../../com/util/text/Text' -import {usePalette} from '../../lib/hooks/usePalette' -import {MagnifyingGlassIcon} from '../../lib/icons' +import {usePalette} from 'lib/hooks/usePalette' +import {MagnifyingGlassIcon} from 'lib/icons' import {LiteSuggestedFollows} from '../../com/discover/LiteSuggestedFollows' -import {s} from '../../lib/styles' +import {s} from 'lib/styles' export const DesktopRightColumn: React.FC = () => { const pal = usePalette('default') diff --git a/src/view/shell/web/index.tsx b/src/view/shell/web/index.tsx index 0eb5cf75..7d76c01d 100644 --- a/src/view/shell/web/index.tsx +++ b/src/view/shell/web/index.tsx @@ -1,7 +1,9 @@ import React from 'react' import {observer} from 'mobx-react-lite' import {View, StyleSheet} from 'react-native' -import {useStores} from '../../../state' +import {IconProp} from '@fortawesome/fontawesome-svg-core' +import {useStores} from 'state/index' +import {NavigationModel} from 'state/models/navigation' import {match, MatchResult} from '../../routes' import {DesktopLeftColumn} from './DesktopLeftColumn' import {DesktopRightColumn} from './DesktopRightColumn' @@ -11,8 +13,8 @@ import {ErrorBoundary} from '../../com/util/ErrorBoundary' import {Lightbox} from '../../com/lightbox/Lightbox' import {Modal} from '../../com/modals/Modal' import {Composer} from './Composer' -import {usePalette} from '../../lib/hooks/usePalette' -import {s} from '../../lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {s} from 'lib/styles' export const WebShell: React.FC = observer(() => { const pal = usePalette('default') diff --git a/tsconfig.json b/tsconfig.json index 35d9492a..2b93dd0f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,3 +1,10 @@ { - "extends": "@tsconfig/react-native/tsconfig.json" + "extends": "@tsconfig/react-native/tsconfig.json", + "compilerOptions": { + "paths": { + "lib/*": ["./src/lib/*"], + "state/*": ["./src/state/*"], + "view/*": ["./src/view/*"] + } + } } \ No newline at end of file diff --git a/web/webpack.config.js b/web/webpack.config.js index c7b248d8..e41f0a14 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -44,6 +44,16 @@ const imageLoaderConfiguration = { }, } +const reactNativeWebWebviewConfiguration = { + test: /postMock.html$/, + use: { + loader: 'file-loader', + options: { + name: '[name].[ext]', + }, + }, +} + module.exports = { mode: webpackEnv, @@ -64,13 +74,18 @@ module.exports = { ], module: { - rules: [babelLoaderConfiguration, imageLoaderConfiguration], + rules: [ + babelLoaderConfiguration, + imageLoaderConfiguration, + reactNativeWebWebviewConfiguration, + ], }, resolve: { alias: { 'react-native$': 'react-native-web', 'react-native-linear-gradient': 'react-native-web-linear-gradient', + 'react-native-webview': 'react-native-web-webview', }, extensions: [ '.web.tsx', diff --git a/yarn.lock b/yarn.lock index f2f4b957..66997864 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@ampproject/remapping@^2.1.0": +"@ampproject/remapping@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== @@ -19,10 +19,10 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@atproto/api@^0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.0.6.tgz#4b4f0d46549e75ef8425bf3fdfb2b5f742395b5e" - integrity sha512-yG5S+Mz+Z6K7qUTRVc3oAFWaoW92IeiB8yDT5rIxkrON/r3gez1LVnBOAU0dgeLFZ1mdRsF5pycX+zcleTYInQ== +"@atproto/api@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.1.2.tgz#66102f9203ba499432bc5aeb30cd19313ab2e4fc" + integrity sha512-lDcFGkrk0J7rkIPSie18xS7sO3IL6DsosX8GgoeqCNeVaDuphBRaFCcpBUWf0q4fHrpmdgghGo4ulefyKHTIFQ== dependencies: "@atproto/xrpc" "*" typed-emitter "^2.1.0" @@ -90,12 +90,11 @@ resolved "https://registry.yarnpkg.com/@atproto/nsid/-/nsid-0.0.1.tgz#0cdc00cefe8f0b1385f352b9f57b3ad37fff09a4" integrity sha512-t5M6/CzWBVYoBbIvfKDpqPj/+ZmyoK9ydZSStcTXosJ27XXwOPhz0VDUGKK2SM9G5Y7TPes8S5KTAU0UdVYFCw== -"@atproto/pds@^0.0.1": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.0.1.tgz#4492595cd0796a3dc4c10836cd121bb3b132f858" - integrity sha512-8CaAIBzri05CN2gkMtXnUlNxCdUwaWp1qz4b9RXpMQ83Pk1+j3nw09fsml0kjlMCPcWokTF+iGJXrrTL3XVjOw== +"@atproto/pds@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.0.3.tgz#118a1d51687664f085f8e1c19ae3ac1646dc69b2" + integrity sha512-l5iGJNyQs73V/mQWkcg4NXNGbnfXfv+Yg3g8nqwtun409iAAeYZimA1Tt0JV/hyq+Oz3antG0VvLQQEtNVUpVQ== dependencies: - "@atproto/auth" "*" "@atproto/common" "*" "@atproto/crypto" "*" "@atproto/did-resolver" "*" @@ -125,6 +124,7 @@ pino "^8.6.1" pino-http "^8.2.1" sharp "^0.31.2" + typed-emitter "^2.1.0" uint8arrays "3.0.0" "@atproto/plc@*": @@ -181,10 +181,10 @@ mime-types "^2.1.35" zod "^3.14.2" -"@atproto/xrpc@*", "@atproto/xrpc@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.0.3.tgz#510028753d51dffd754ee4f96897b74bcba50bda" - integrity sha512-dOxtUqUfXOalPhtc1jyIvzCasd+iXwt/Sp+QIAy5qOTCxF9IvXuJ9bNFY49NUPEWlagaRtL8glQhbR307GHcuA== +"@atproto/xrpc@*", "@atproto/xrpc@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.0.4.tgz#d7dd45cdb21e29b9715ca30eb18320548f293413" + integrity sha512-Hxh+GgZx21Zvlb2RMlSlJDd3r3GR0vAS6OOZPW2xzWiVHsetb9ZlFB6D0AeAPj2R+U2UUkmdUR8G3U/nkgnQFA== dependencies: "@atproto/lexicon" "*" zod "^3.14.2" @@ -197,46 +197,25 @@ "@babel/highlight" "^7.18.6" "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5": - version "7.20.10" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.10.tgz#9d92fa81b87542fff50e848ed585b4212c1d34ec" - integrity sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.0.tgz#c241dc454e5b5917e40d37e525e2f4530c399298" + integrity sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g== -"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.12.3", "@babel/core@^7.12.9", "@babel/core@^7.13.16", "@babel/core@^7.14.0", "@babel/core@^7.16.0", "@babel/core@^7.7.2", "@babel/core@^7.8.0": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.7.tgz#37072f951bd4d28315445f66e0ec9f6ae0c8c35f" - integrity sha512-t1ZjCluspe5DW24bn2Rr1CDb2v9rn/hROtg9a2tmd0+QYf4bsloYfLQzjG4qHPNMhWtKdGC33R5AxGR2Af2cBw== +"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.14.0", "@babel/core@^7.16.0", "@babel/core@^7.20.0", "@babel/core@^7.7.2", "@babel/core@^7.8.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.0.tgz#1341aefdcc14ccc7553fcc688dd8986a2daffc13" + integrity sha512-PuxUbxcW6ZYe656yL3EAhpy7qXKq0DmYsrJLpbB8XrsCP9Nm+XCg9XFMb5vIDliPD7+U/+M+QJlH17XOcB7eXA== dependencies: - "@ampproject/remapping" "^2.1.0" + "@ampproject/remapping" "^2.2.0" "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" + "@babel/generator" "^7.21.0" "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-module-transforms" "^7.20.7" - "@babel/helpers" "^7.20.7" - "@babel/parser" "^7.20.7" + "@babel/helper-module-transforms" "^7.21.0" + "@babel/helpers" "^7.21.0" + "@babel/parser" "^7.21.0" "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.7" - "@babel/types" "^7.20.7" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.1" - semver "^6.3.0" - -"@babel/core@^7.11.6", "@babel/core@^7.20.0": - version "7.20.12" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" - integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helpers" "^7.20.7" - "@babel/parser" "^7.20.7" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.12" - "@babel/types" "^7.20.7" + "@babel/traverse" "^7.21.0" + "@babel/types" "^7.21.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -252,13 +231,14 @@ eslint-visitor-keys "^2.1.0" semver "^6.3.0" -"@babel/generator@^7.20.0", "@babel/generator@^7.20.7", "@babel/generator@^7.7.2": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.7.tgz#f8ef57c8242665c5929fe2e8d82ba75460187b4a" - integrity sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw== +"@babel/generator@^7.20.0", "@babel/generator@^7.21.0", "@babel/generator@^7.7.2": + version "7.21.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.1.tgz#951cc626057bc0af2c35cd23e9c64d384dea83dd" + integrity sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA== dependencies: - "@babel/types" "^7.20.7" + "@babel/types" "^7.21.0" "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" "@babel/helper-annotate-as-pure@^7.18.6": @@ -287,26 +267,27 @@ lru-cache "^5.1.1" semver "^6.3.0" -"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.20.5", "@babel/helper-create-class-features-plugin@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.7.tgz#d0e1f8d7e4ed5dac0389364d9c0c191d948ade6f" - integrity sha512-LtoWbDXOaidEf50hmdDqn9g8VEzsorMexoWMQdQODbvmqYmaF23pBP5VNPAGIFHsFQCIeKokDiz3CH5Y2jlY6w== +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.0.tgz#64f49ecb0020532f19b1d014b03bccaa1ab85fb9" + integrity sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-member-expression-to-functions" "^7.20.7" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-member-expression-to-functions" "^7.21.0" "@babel/helper-optimise-call-expression" "^7.18.6" "@babel/helper-replace-supers" "^7.20.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" "@babel/helper-split-export-declaration" "^7.18.6" "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz#5ea79b59962a09ec2acf20a963a01ab4d076ccca" - integrity sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.0.tgz#53ff78472e5ce10a52664272a239787107603ebb" + integrity sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" - regexpu-core "^5.2.1" + regexpu-core "^5.3.1" "@babel/helper-define-polyfill-provider@^0.3.3": version "0.3.3" @@ -332,13 +313,13 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" - integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== +"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4" + integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg== dependencies: - "@babel/template" "^7.18.10" - "@babel/types" "^7.19.0" + "@babel/template" "^7.20.7" + "@babel/types" "^7.21.0" "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" @@ -347,12 +328,12 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-member-expression-to-functions@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz#a6f26e919582275a93c3aa6594756d71b0bb7f05" - integrity sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw== +"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz#319c6a940431a133897148515877d2f3269c3ba5" + integrity sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q== dependencies: - "@babel/types" "^7.20.7" + "@babel/types" "^7.21.0" "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.18.6": version "7.18.6" @@ -361,10 +342,10 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.20.7": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz#df4c7af713c557938c50ea3ad0117a7944b2f1b0" - integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg== +"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.0.tgz#89a8f86ad748870e3d024e470b2e8405e869db67" + integrity sha512-eD/JQ21IG2i1FraJnTMbUarAUkA7G988ofehG5MDCRXaUU91rEBJuCeSoou2Sk1y4RbLYXzqEg1QLwEmRU4qcQ== dependencies: "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-module-imports" "^7.18.6" @@ -372,8 +353,8 @@ "@babel/helper-split-export-declaration" "^7.18.6" "@babel/helper-validator-identifier" "^7.19.1" "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.10" - "@babel/types" "^7.20.7" + "@babel/traverse" "^7.21.0" + "@babel/types" "^7.21.0" "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" @@ -440,10 +421,10 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== -"@babel/helper-validator-option@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" - integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== +"@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" + integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== "@babel/helper-wrap-function@^7.18.9": version "7.20.5" @@ -455,14 +436,14 @@ "@babel/traverse" "^7.20.5" "@babel/types" "^7.20.5" -"@babel/helpers@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.7.tgz#04502ff0feecc9f20ecfaad120a18f011a8e6dce" - integrity sha512-PBPjs5BppzsGaxHQCDKnZ6Gd9s6xl8bBCluz3vEInLGRJmnZan4F6BYCeqtyXqkk4W5IlPmjK4JlOuZkpJ3xZA== +"@babel/helpers@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.0.tgz#9dd184fb5599862037917cdc9eecb84577dc4e7e" + integrity sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA== dependencies: "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.7" - "@babel/types" "^7.20.7" + "@babel/traverse" "^7.21.0" + "@babel/types" "^7.21.0" "@babel/highlight@^7.18.6": version "7.18.6" @@ -473,10 +454,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.7.tgz#66fe23b3c8569220817d5feb8b9dcdc95bb4f71b" - integrity sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg== +"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7", "@babel/parser@^7.21.0": + version "7.21.1" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.1.tgz#a8f81ee2fe872af23faea4b17a08fcc869de7bcc" + integrity sha512-JzhBFpkuhBNYUY7qs+wTzNmyCWUHEaAFpQQD2YfU1rPL38/L43Wvid0fFkiOCnHvsGncRZgEPyGnltABLcVDTg== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -513,24 +494,24 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-proposal-class-static-block@^7.18.6": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.20.7.tgz#92592e9029b13b15be0f7ce6a7aedc2879ca45a7" - integrity sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz#77bdd66fb7b605f3a61302d224bdfacf5547977d" + integrity sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.20.7" + "@babel/helper-create-class-features-plugin" "^7.21.0" "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-class-static-block" "^7.14.5" "@babel/plugin-proposal-decorators@^7.16.4": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.20.7.tgz#05d37453c2ce818f3e47bbeda9468c8de947eecc" - integrity sha512-JB45hbUweYpwAGjkiM7uCyXMENH2lG+9r3G2E+ttc2PRXAoEkpfd/KW5jDg4j8RS6tLtTG1jZi9LbHZVSfs1/A== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.21.0.tgz#70e0c89fdcd7465c97593edb8f628ba6e4199d63" + integrity sha512-MfgX49uRrFUTL/HvWtmx3zmpyzMMr4MTj3d527MLlr/4RTT9G/ytFFP7qet2uM2Ve03b+BkpWUpK+lRXnQ+v9w== dependencies: - "@babel/helper-create-class-features-plugin" "^7.20.7" + "@babel/helper-create-class-features-plugin" "^7.21.0" "@babel/helper-plugin-utils" "^7.20.2" "@babel/helper-replace-supers" "^7.20.7" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/plugin-syntax-decorators" "^7.19.0" + "@babel/plugin-syntax-decorators" "^7.21.0" "@babel/plugin-proposal-dynamic-import@^7.18.6": version "7.18.6" @@ -608,9 +589,9 @@ "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-proposal-optional-chaining@^7.0.0", "@babel/plugin-proposal-optional-chaining@^7.13.12", "@babel/plugin-proposal-optional-chaining@^7.16.0", "@babel/plugin-proposal-optional-chaining@^7.18.9", "@babel/plugin-proposal-optional-chaining@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz#49f2b372519ab31728cc14115bb0998b15bfda55" - integrity sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz#886f5c8978deb7d30f678b2e24346b287234d3ea" + integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA== dependencies: "@babel/helper-plugin-utils" "^7.20.2" "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" @@ -625,12 +606,12 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-proposal-private-property-in-object@^7.18.6": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz#309c7668f2263f1c711aa399b5a9a6291eef6135" - integrity sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz#19496bd9883dd83c23c7d7fc45dcd9ad02dfa1dc" + integrity sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-create-class-features-plugin" "^7.20.5" + "@babel/helper-create-class-features-plugin" "^7.21.0" "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" @@ -670,12 +651,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-decorators@^7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.19.0.tgz#5f13d1d8fce96951bea01a10424463c9a5b3a599" - integrity sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ== +"@babel/plugin-syntax-decorators@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.21.0.tgz#d2b3f31c3e86fa86e16bb540b7660c55bd7d0e78" + integrity sha512-tIoPpGBR8UuM4++ccWN3gifhVvQu7ZizuR1fklhRJrd5ewgbkUS+0KVFeWWxELtn18NTLoW32XV7zyOgIAiz+w== dependencies: - "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-dynamic-import@^7.0.0", "@babel/plugin-syntax-dynamic-import@^7.8.3": version "7.8.3" @@ -698,7 +679,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.18.0", "@babel/plugin-syntax-flow@^7.18.6", "@babel/plugin-syntax-flow@^7.2.0": +"@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.18.0", "@babel/plugin-syntax-flow@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz#774d825256f2379d06139be0c723c4dd444f3ca1" integrity sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A== @@ -820,21 +801,21 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.20.2": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.11.tgz#9f5a3424bd112a3f32fe0cf9364fbb155cff262a" - integrity sha512-tA4N427a7fjf1P0/2I4ScsHGc5jcHPbb30xMbaTke2gxDuWpUfXDuX1FEymJwKk4tuGUvGcejAR6HdZVqmmPyw== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz#e737b91037e5186ee16b76e7ae093358a5634f02" + integrity sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ== dependencies: "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-transform-classes@^7.0.0", "@babel/plugin-transform-classes@^7.20.2": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.7.tgz#f438216f094f6bb31dc266ebfab8ff05aecad073" - integrity sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz#f469d0b07a4c5a7dbb21afad9e27e57b47031665" + integrity sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/helper-compilation-targets" "^7.20.7" "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" + "@babel/helper-function-name" "^7.21.0" "@babel/helper-optimise-call-expression" "^7.18.6" "@babel/helper-plugin-utils" "^7.20.2" "@babel/helper-replace-supers" "^7.20.7" @@ -880,19 +861,19 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-flow-strip-types@^7.0.0", "@babel/plugin-transform-flow-strip-types@^7.16.0", "@babel/plugin-transform-flow-strip-types@^7.18.6": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.19.0.tgz#e9e8606633287488216028719638cbbb2f2dde8f" - integrity sha512-sgeMlNaQVbCSpgLSKP4ZZKfsJVnFnNQlUSk6gPYzR/q7tzCgQF2t8RBKAP6cKJeZdveei7Q7Jm527xepI8lNLg== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.21.0.tgz#6aeca0adcb81dc627c8986e770bfaa4d9812aff5" + integrity sha512-FlFA2Mj87a6sDkW4gfGrQQqwY/dLlBAyJa2dJEZ+FHXUVHBflO2wyKvg+OOEzXfrKYIa4HWl0mgmbCzt0cMb7w== dependencies: - "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-flow" "^7.18.6" "@babel/plugin-transform-for-of@^7.0.0", "@babel/plugin-transform-for-of@^7.18.8": - version "7.18.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz#6ef8a50b244eb6a0bdbad0c7c61877e4e30097c1" - integrity sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz#964108c9988de1a60b4be2354a7d7e245f36e86e" + integrity sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-transform-function-name@^7.0.0", "@babel/plugin-transform-function-name@^7.18.9": version "7.18.9" @@ -1018,11 +999,11 @@ "@babel/plugin-transform-react-jsx" "^7.18.6" "@babel/plugin-transform-react-jsx-self@^7.0.0": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.18.6.tgz#3849401bab7ae8ffa1e3e5687c94a753fc75bda7" - integrity sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.21.0.tgz#ec98d4a9baafc5a1eb398da4cf94afbb40254a54" + integrity sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-transform-react-jsx-source@^7.0.0": version "7.19.6" @@ -1032,15 +1013,15 @@ "@babel/helper-plugin-utils" "^7.19.0" "@babel/plugin-transform-react-jsx@^7.0.0", "@babel/plugin-transform-react-jsx@^7.18.6": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.20.7.tgz#025d85a1935fd7e19dfdcb1b1d4df34d4da484f7" - integrity sha512-Tfq7qqD+tRj3EoDhY00nn2uP2hsRxgYGi5mLQ5TimKav0a9Lrpd4deE+fcLXU8zFYRjlKPHZhpCvfEA6qnBxqQ== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz#656b42c2fdea0a6d8762075d58ef9d4e3c4ab8a2" + integrity sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/helper-module-imports" "^7.18.6" "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-jsx" "^7.18.6" - "@babel/types" "^7.20.7" + "@babel/types" "^7.21.0" "@babel/plugin-transform-react-pure-annotations@^7.18.6": version "7.18.6" @@ -1066,12 +1047,12 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-runtime@^7.0.0", "@babel/plugin-transform-runtime@^7.12.1", "@babel/plugin-transform-runtime@^7.16.4": - version "7.19.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz#9d2a9dbf4e12644d6f46e5e75bfbf02b5d6e9194" - integrity sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.21.0.tgz#2a884f29556d0a68cd3d152dcc9e6c71dfb6eee8" + integrity sha512-ReY6pxwSzEU0b3r2/T/VhqMKg/AkceBT19X0UptA3/tYi5Pe2eXgEUH+NNMC5nok6c6XQz5tyVTUpuezRfSMSg== dependencies: "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-plugin-utils" "^7.20.2" babel-plugin-polyfill-corejs2 "^0.3.3" babel-plugin-polyfill-corejs3 "^0.6.0" babel-plugin-polyfill-regenerator "^0.4.1" @@ -1113,12 +1094,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.9" -"@babel/plugin-transform-typescript@^7.18.6", "@babel/plugin-transform-typescript@^7.5.0": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.20.7.tgz#673f49499cd810ae32a1ea5f3f8fab370987e055" - integrity sha512-m3wVKEvf6SoszD8pu4NZz3PvfKRCMgk6D6d0Qi9hNnlM5M6CFS92EgF4EiHVLKbU0r/r7ty1hg7NPZwE7WRbYw== +"@babel/plugin-transform-typescript@^7.21.0", "@babel/plugin-transform-typescript@^7.5.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.0.tgz#f0956a153679e3b377ae5b7f0143427151e4c848" + integrity sha512-xo///XTPp3mDzTtrqXoBlK9eiAYW3wv9JXglcn/u1bi60RW11dEUxIgA8cbnDhutS1zacjMRmAwxE0gMklLnZg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.20.7" + "@babel/helper-create-class-features-plugin" "^7.21.0" "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-typescript" "^7.20.0" @@ -1137,7 +1118,7 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.14.0", "@babel/preset-env@^7.16.4": +"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.16.4", "@babel/preset-env@^7.20.0": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.20.2.tgz#9b1642aa47bb9f43a86f9630011780dab7f86506" integrity sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg== @@ -1251,18 +1232,18 @@ "@babel/plugin-transform-react-pure-annotations" "^7.18.6" "@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.16.0", "@babel/preset-typescript@^7.16.7": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz#ce64be3e63eddc44240c6358daefac17b3186399" - integrity sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.0.tgz#bcbbca513e8213691fe5d4b23d9251e01f00ebff" + integrity sha512-myc9mpoVA5m1rF8K8DgLEatOYFDpwC+RkMkjZ0Du6uI62YvDe8uxIEYVs/VCdSJ097nlALiU/yBC7//3nI+hNg== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-validator-option" "^7.18.6" - "@babel/plugin-transform-typescript" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-validator-option" "^7.21.0" + "@babel/plugin-transform-typescript" "^7.21.0" "@babel/register@^7.13.16": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.18.9.tgz#1888b24bc28d5cc41c412feb015e9ff6b96e439c" - integrity sha512-ZlbnXDcNYHMR25ITwwNKT88JiaukkdVj/nG7r3wnuXkOTHc60Uy05PwMCPre0hSkY68E6zK3xz+vUJSP2jWmcw== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.21.0.tgz#c97bf56c2472e063774f31d344c592ebdcefa132" + integrity sha512-9nKsPmYDi5DidAqJaQooxIhsLJiNMkGr8ypQ8Uic7cIox7UCDsM7HuUGxdGT7mSDTYbqzIdsOWzfBton/YJrMw== dependencies: clone-deep "^4.0.1" find-cache-dir "^2.0.0" @@ -1270,25 +1251,15 @@ pirates "^4.0.5" source-map-support "^0.5.16" -"@babel/runtime-corejs3@^7.10.2": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.20.7.tgz#a1e5ea3d758ba6beb715210142912e3f29981d84" - integrity sha512-jr9lCZ4RbRQmCR28Q8U8Fu49zvFqLxTY9AMOUz+iyMohMoAgpEcVxY+wJNay99oXOpOcCTODkk70NDN2aaJEeg== - dependencies: - core-js-pure "^3.25.1" - regenerator-runtime "^0.13.11" +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.8.4": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" - integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ== - dependencies: - regenerator-runtime "^0.13.11" - -"@babel/runtime@^7.18.6": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b" - integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.8.4": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" + integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== dependencies: regenerator-runtime "^0.13.11" @@ -1301,51 +1272,35 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" -"@babel/traverse@^7.14.0", "@babel/traverse@^7.20.10", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.7.2", "@babel/traverse@^7.7.4": - version "7.20.10" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.10.tgz#2bf98239597fcec12f842756f186a9dde6d09230" - integrity sha512-oSf1juCgymrSez8NI4A2sr4+uB/mFd9MXplYGPEBnfAuWmmyeVcHa6xLPiaRBcXkcb/28bgxmQLTVwFKE1yfsg== +"@babel/traverse@^7.20.0", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.7.2", "@babel/traverse@^7.7.4": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.0.tgz#0e1807abd5db98e6a19c204b80ed1e3f5bca0edc" + integrity sha512-Xdt2P1H4LKTO8ApPfnO1KmzYMFpp7D/EinoXzLYN/cHcBNrVCAkAtGUcXnHXrl/VGktureU6fkQrHSBE2URfoA== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" + "@babel/generator" "^7.21.0" "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" + "@babel/helper-function-name" "^7.21.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" + "@babel/parser" "^7.21.0" + "@babel/types" "^7.21.0" debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.20.0", "@babel/traverse@^7.20.12": - version "7.20.12" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.12.tgz#7f0f787b3a67ca4475adef1f56cb94f6abd4a4b5" - integrity sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" - integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== +"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.0.tgz#1da00d89c2f18b226c9207d96edbeb79316a1819" + integrity sha512-uR7NWq2VNFnDi7EYqiRz2Jv/VQIu38tu64Zy8TX2nQFQ6etJ9V/Rr2msW8BS132mum2rL645qpDrLtAJtVpuow== dependencies: "@babel/helper-string-parser" "^7.19.4" "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" "@bam.tech/react-native-image-resizer@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@bam.tech/react-native-image-resizer/-/react-native-image-resizer-3.0.4.tgz#3e127818e88df89e5c51a07e2556bf6ef872df26" - integrity sha512-p2Ecs469bntk4Ke4CEYyEoVBKBT+8JyaXXPz4Fm55s9G+jEg99p1V9MHkza2k7OAq1iUF8oAtZ/wZ9zKn6b43Q== + version "3.0.5" + resolved "https://registry.yarnpkg.com/@bam.tech/react-native-image-resizer/-/react-native-image-resizer-3.0.5.tgz#6661ba020de156268f73bdc92fbb93ef86f88a13" + integrity sha512-u5QGUQGGVZiVCJ786k9/kd7pPRZ6eYfJCYO18myVCH8FbVI7J8b5GT2Svjj2x808DlWeqfaZOOzxPqo27XYvrQ== "@bcoe/v8-coverage@^0.2.3": version "0.2.3" @@ -1459,9 +1414,9 @@ integrity sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g== "@csstools/selector-specificity@^2.0.0", "@csstools/selector-specificity@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36" - integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== + version "2.1.1" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz#c9c61d9fe5ca5ac664e1153bb0aa0eba1c6d6308" + integrity sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw== "@discoveryjs/json-ext@^0.5.0": version "0.5.7" @@ -1490,31 +1445,31 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@fortawesome/fontawesome-common-types@6.2.1": - version "6.2.1" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.1.tgz#411e02a820744d3f7e0d8d9df9d82b471beaa073" - integrity sha512-Sz07mnQrTekFWLz5BMjOzHl/+NooTdW8F8kDQxjWwbpOJcnoSg4vUDng8d/WR1wOxM0O+CY9Zw0nR054riNYtQ== +"@fortawesome/fontawesome-common-types@6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz#51f734e64511dbc3674cd347044d02f4dd26e86b" + integrity sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg== "@fortawesome/fontawesome-svg-core@^6.1.1": - version "6.2.1" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.2.1.tgz#e87e905e444b5e7b715af09b64d27b53d4c8f9d9" - integrity sha512-HELwwbCz6C1XEcjzyT1Jugmz2NNklMrSPjZOWMlc+ZsHIVk+XOvOXLGGQtFBwSyqfJDNgRq4xBCwWOaZ/d9DEA== + version "6.3.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.3.0.tgz#b6a17d48d231ac1fad93e43fca7271676bf316cf" + integrity sha512-uz9YifyKlixV6AcKlOX8WNdtF7l6nakGyLYxYaCa823bEBqyj/U2ssqtctO38itNEwXb8/lMzjdoJ+aaJuOdrw== dependencies: - "@fortawesome/fontawesome-common-types" "6.2.1" + "@fortawesome/fontawesome-common-types" "6.3.0" "@fortawesome/free-regular-svg-icons@^6.1.1": - version "6.2.1" - resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.2.1.tgz#650e56d937755a8341f2eef258ecb6f95458820f" - integrity sha512-wiqcNDNom75x+pe88FclpKz7aOSqS2lOivZeicMV5KRwOAeypxEYWAK/0v+7r+LrEY30+qzh8r2XDaEHvoLsMA== + version "6.3.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.3.0.tgz#286f87f777e6c96af59151e86647c81083029ee2" + integrity sha512-cZnwiVHZ51SVzWHOaNCIA+u9wevZjCuAGSvSYpNlm6A4H4Vhwh8481Bf/5rwheIC3fFKlgXxLKaw8Xeroz8Ntg== dependencies: - "@fortawesome/fontawesome-common-types" "6.2.1" + "@fortawesome/fontawesome-common-types" "6.3.0" "@fortawesome/free-solid-svg-icons@^6.1.1": - version "6.2.1" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.2.1.tgz#2290ea5adcf1537cbd0c43de6feb38af02141d27" - integrity sha512-oKuqrP5jbfEPJWTij4sM+/RvgX+RMFwx3QZCZcK9PrBDgxC35zuc7AOFsyMjMd/PIFPeB2JxyqDr5zs/DZFPPw== + version "6.3.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.3.0.tgz#d3bd33ae18bb15fdfc3ca136e2fea05f32768a65" + integrity sha512-x5tMwzF2lTH8pyv8yeZRodItP2IVlzzmBuD1M7BjawWgg9XAvktqJJ91Qjgoaf8qJpHQ8FEU9VxRfOkLhh86QA== dependencies: - "@fortawesome/fontawesome-common-types" "6.2.1" + "@fortawesome/fontawesome-common-types" "6.3.0" "@fortawesome/react-native-fontawesome@^0.3.0": version "0.3.0" @@ -1632,16 +1587,16 @@ jest-util "^28.1.3" slash "^3.0.0" -"@jest/console@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.3.1.tgz#3e3f876e4e47616ea3b1464b9fbda981872e9583" - integrity sha512-IRE6GD47KwcqA09RIWrabKdHPiKDGgtAL31xDxbi/RjQMsr+lY+ppxmHwY0dUEV3qvvxZzoe5Hl0RXZJOjQNUg== +"@jest/console@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.4.3.tgz#1f25a99f7f860e4c46423b5b1038262466fadde1" + integrity sha512-W/o/34+wQuXlgqlPYTansOSiBnuxrTv61dEVkA6HNmpcgHLUjfaUbdqt6oVvOzaawwo9IdW9QOtMgQ1ScSZC4A== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.3" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^29.3.1" - jest-util "^29.3.1" + jest-message-util "^29.4.3" + jest-util "^29.4.3" slash "^3.0.0" "@jest/core@^27.5.1": @@ -1678,46 +1633,46 @@ slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/core@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.3.1.tgz#bff00f413ff0128f4debec1099ba7dcd649774a1" - integrity sha512-0ohVjjRex985w5MmO5L3u5GR1O30DexhBSpuwx2P+9ftyqHdJXnk7IUWiP80oHMvt7ubHCJHxV0a0vlKVuZirw== +"@jest/core@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.4.3.tgz#829dd65bffdb490de5b0f69e97de8e3b5eadd94b" + integrity sha512-56QvBq60fS4SPZCuM7T+7scNrkGIe7Mr6PVIXUpu48ouvRaWOFqRPV91eifvFM0ay2HmfswXiGf97NGUN5KofQ== dependencies: - "@jest/console" "^29.3.1" - "@jest/reporters" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/console" "^29.4.3" + "@jest/reporters" "^29.4.3" + "@jest/test-result" "^29.4.3" + "@jest/transform" "^29.4.3" + "@jest/types" "^29.4.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" ci-info "^3.2.0" exit "^0.1.2" graceful-fs "^4.2.9" - jest-changed-files "^29.2.0" - jest-config "^29.3.1" - jest-haste-map "^29.3.1" - jest-message-util "^29.3.1" - jest-regex-util "^29.2.0" - jest-resolve "^29.3.1" - jest-resolve-dependencies "^29.3.1" - jest-runner "^29.3.1" - jest-runtime "^29.3.1" - jest-snapshot "^29.3.1" - jest-util "^29.3.1" - jest-validate "^29.3.1" - jest-watcher "^29.3.1" + jest-changed-files "^29.4.3" + jest-config "^29.4.3" + jest-haste-map "^29.4.3" + jest-message-util "^29.4.3" + jest-regex-util "^29.4.3" + jest-resolve "^29.4.3" + jest-resolve-dependencies "^29.4.3" + jest-runner "^29.4.3" + jest-runtime "^29.4.3" + jest-snapshot "^29.4.3" + jest-util "^29.4.3" + jest-validate "^29.4.3" + jest-watcher "^29.4.3" micromatch "^4.0.4" - pretty-format "^29.3.1" + pretty-format "^29.4.3" slash "^3.0.0" strip-ansi "^6.0.0" "@jest/create-cache-key-function@^29.2.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/create-cache-key-function/-/create-cache-key-function-29.3.1.tgz#3a0970ea595ab3d9507244edbcef14d6b016cdc9" - integrity sha512-4i+E+E40gK13K78ffD/8cy4lSSqeWwyXeTZoq16tndiCP12hC8uQsPJdIu5C6Kf22fD8UbBk71so7s/6VwpUOQ== + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/create-cache-key-function/-/create-cache-key-function-29.4.3.tgz#ea37769f69523019d81ee089a25a62550f209eb7" + integrity sha512-AJVFQTTy6jnZAQiAZrdOaTAPzJUrvAE/4IMe+Foav6WPhypFszqg7a4lOTyuzYQEEiT5RSrGYg9IY+/ivxiyXw== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.3" "@jest/environment@^27.5.1": version "27.5.1" @@ -1729,30 +1684,30 @@ "@types/node" "*" jest-mock "^27.5.1" -"@jest/environment@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.3.1.tgz#eb039f726d5fcd14698acd072ac6576d41cfcaa6" - integrity sha512-pMmvfOPmoa1c1QpfFW0nXYtNLpofqo4BrCIk6f2kW4JFeNlHV2t3vd+3iDLf31e2ot2Mec0uqZfmI+U0K2CFag== +"@jest/environment@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.4.3.tgz#9fe2f3169c3b33815dc4bd3960a064a83eba6548" + integrity sha512-dq5S6408IxIa+lr54zeqce+QgI+CJT4nmmA+1yzFgtcsGK8c/EyiUb9XQOgz3BMKrRDfKseeOaxj2eO8LlD3lA== dependencies: - "@jest/fake-timers" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/fake-timers" "^29.4.3" + "@jest/types" "^29.4.3" "@types/node" "*" - jest-mock "^29.3.1" + jest-mock "^29.4.3" -"@jest/expect-utils@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.3.1.tgz#531f737039e9b9e27c42449798acb5bba01935b6" - integrity sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g== +"@jest/expect-utils@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.4.3.tgz#95ce4df62952f071bcd618225ac7c47eaa81431e" + integrity sha512-/6JWbkxHOP8EoS8jeeTd9dTfc9Uawi+43oLKHfp6zzux3U2hqOOVnV3ai4RpDYHOccL6g+5nrxpoc8DmJxtXVQ== dependencies: - jest-get-type "^29.2.0" + jest-get-type "^29.4.3" -"@jest/expect@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.3.1.tgz#456385b62894349c1d196f2d183e3716d4c6a6cd" - integrity sha512-QivM7GlSHSsIAWzgfyP8dgeExPRZ9BIe2LsdPyEhCGkZkoyA+kGsoIzbKAfZCvvRzfZioKwPtCZIt5SaoxYCvg== +"@jest/expect@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.4.3.tgz#d31a28492e45a6bcd0f204a81f783fe717045c6e" + integrity sha512-iktRU/YsxEtumI9zsPctYUk7ptpC+AVLLk1Ax3AsA4g1C+8OOnKDkIQBDHtD5hA/+VtgMd5AWI5gNlcAlt2vxQ== dependencies: - expect "^29.3.1" - jest-snapshot "^29.3.1" + expect "^29.4.3" + jest-snapshot "^29.4.3" "@jest/fake-timers@^27.5.1": version "27.5.1" @@ -1766,17 +1721,17 @@ jest-mock "^27.5.1" jest-util "^27.5.1" -"@jest/fake-timers@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.3.1.tgz#b140625095b60a44de820876d4c14da1aa963f67" - integrity sha512-iHTL/XpnDlFki9Tq0Q1GGuVeQ8BHZGIYsvCO5eN/O/oJaRzofG9Xndd9HuSDBI/0ZS79pg0iwn07OMTQ7ngF2A== +"@jest/fake-timers@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.4.3.tgz#31e982638c60fa657d310d4b9d24e023064027b0" + integrity sha512-4Hote2MGcCTWSD2gwl0dwbCpBRHhE6olYEuTj8FMowdg3oQWNKr2YuxenPQYZ7+PfqPY1k98wKDU4Z+Hvd4Tiw== dependencies: - "@jest/types" "^29.3.1" - "@sinonjs/fake-timers" "^9.1.2" + "@jest/types" "^29.4.3" + "@sinonjs/fake-timers" "^10.0.2" "@types/node" "*" - jest-message-util "^29.3.1" - jest-mock "^29.3.1" - jest-util "^29.3.1" + jest-message-util "^29.4.3" + jest-mock "^29.4.3" + jest-util "^29.4.3" "@jest/globals@^27.5.1": version "27.5.1" @@ -1787,15 +1742,15 @@ "@jest/types" "^27.5.1" expect "^27.5.1" -"@jest/globals@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.3.1.tgz#92be078228e82d629df40c3656d45328f134a0c6" - integrity sha512-cTicd134vOcwO59OPaB6AmdHQMCtWOe+/DitpTZVxWgMJ+YvXL1HNAmPyiGbSHmF/mXVBkvlm8YYtQhyHPnV6Q== +"@jest/globals@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.4.3.tgz#63a2c4200d11bc6d46f12bbe25b07f771fce9279" + integrity sha512-8BQ/5EzfOLG7AaMcDh7yFCbfRLtsc+09E1RQmRBI4D6QQk4m6NSK/MXo+3bJrBN0yU8A2/VIcqhvsOLFmziioA== dependencies: - "@jest/environment" "^29.3.1" - "@jest/expect" "^29.3.1" - "@jest/types" "^29.3.1" - jest-mock "^29.3.1" + "@jest/environment" "^29.4.3" + "@jest/expect" "^29.4.3" + "@jest/types" "^29.4.3" + jest-mock "^29.4.3" "@jest/reporters@^27.5.1": version "27.5.1" @@ -1828,16 +1783,16 @@ terminal-link "^2.0.0" v8-to-istanbul "^8.1.0" -"@jest/reporters@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.3.1.tgz#9a6d78c109608e677c25ddb34f907b90e07b4310" - integrity sha512-GhBu3YFuDrcAYW/UESz1JphEAbvUjaY2vShRZRoRY1mxpCMB3yGSJ4j9n0GxVlEOdCf7qjvUfBCrTUUqhVfbRA== +"@jest/reporters@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.4.3.tgz#0a68a0c0f20554760cc2e5443177a0018969e353" + integrity sha512-sr2I7BmOjJhyqj9ANC6CTLsL4emMoka7HkQpcoMRlhCbQJjz2zsRzw0BDPiPyEFDXAbxKgGFYuQZiSJ1Y6YoTg== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/console" "^29.4.3" + "@jest/test-result" "^29.4.3" + "@jest/transform" "^29.4.3" + "@jest/types" "^29.4.3" "@jridgewell/trace-mapping" "^0.3.15" "@types/node" "*" chalk "^4.0.0" @@ -1850,9 +1805,9 @@ istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.1.3" - jest-message-util "^29.3.1" - jest-util "^29.3.1" - jest-worker "^29.3.1" + jest-message-util "^29.4.3" + jest-util "^29.4.3" + jest-worker "^29.4.3" slash "^3.0.0" string-length "^4.0.1" strip-ansi "^6.0.0" @@ -1865,12 +1820,12 @@ dependencies: "@sinclair/typebox" "^0.24.1" -"@jest/schemas@^29.0.0": - version "29.0.0" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.0.0.tgz#5f47f5994dd4ef067fb7b4188ceac45f77fe952a" - integrity sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA== +"@jest/schemas@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788" + integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg== dependencies: - "@sinclair/typebox" "^0.24.1" + "@sinclair/typebox" "^0.25.16" "@jest/source-map@^27.5.1": version "27.5.1" @@ -1881,10 +1836,10 @@ graceful-fs "^4.2.9" source-map "^0.6.0" -"@jest/source-map@^29.2.0": - version "29.2.0" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.2.0.tgz#ab3420c46d42508dcc3dc1c6deee0b613c235744" - integrity sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ== +"@jest/source-map@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.4.3.tgz#ff8d05cbfff875d4a791ab679b4333df47951d20" + integrity sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w== dependencies: "@jridgewell/trace-mapping" "^0.3.15" callsites "^3.0.0" @@ -1910,13 +1865,13 @@ "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-result@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.3.1.tgz#92cd5099aa94be947560a24610aa76606de78f50" - integrity sha512-qeLa6qc0ddB0kuOZyZIhfN5q0e2htngokyTWsGriedsDhItisW7SDYZ7ceOe57Ii03sL988/03wAcBh3TChMGw== +"@jest/test-result@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.4.3.tgz#e13d973d16c8c7cc0c597082d5f3b9e7f796ccb8" + integrity sha512-Oi4u9NfBolMq9MASPwuWTlC5WvmNRwI4S8YrQg5R5Gi47DYlBe3sh7ILTqi/LGrK1XUE4XY9KZcQJTH1WJCLLA== dependencies: - "@jest/console" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/console" "^29.4.3" + "@jest/types" "^29.4.3" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" @@ -1930,14 +1885,14 @@ jest-haste-map "^27.5.1" jest-runtime "^27.5.1" -"@jest/test-sequencer@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.3.1.tgz#fa24b3b050f7a59d48f7ef9e0b782ab65123090d" - integrity sha512-IqYvLbieTv20ArgKoAMyhLHNrVHJfzO6ARZAbQRlY4UGWfdDnLlZEF0BvKOMd77uIiIjSZRwq3Jb3Fa3I8+2UA== +"@jest/test-sequencer@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.4.3.tgz#0862e876a22993385a0f3e7ea1cc126f208a2898" + integrity sha512-yi/t2nES4GB4G0mjLc0RInCq/cNr9dNwJxcGg8sslajua5Kb4kmozAc+qPLzplhBgfw1vLItbjyHzUN92UXicw== dependencies: - "@jest/test-result" "^29.3.1" + "@jest/test-result" "^29.4.3" graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" + jest-haste-map "^29.4.3" slash "^3.0.0" "@jest/transform@^27.5.1": @@ -1961,26 +1916,26 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/transform@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.3.1.tgz#1e6bd3da4af50b5c82a539b7b1f3770568d6e36d" - integrity sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug== +"@jest/transform@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.4.3.tgz#f7d17eac9cb5bb2e1222ea199c7c7e0835e0c037" + integrity sha512-8u0+fBGWolDshsFgPQJESkDa72da/EVwvL+II0trN2DR66wMwiQ9/CihaGfHdlLGFzbBZwMykFtxuwFdZqlKwg== dependencies: "@babel/core" "^7.11.6" - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.3" "@jridgewell/trace-mapping" "^0.3.15" babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" convert-source-map "^2.0.0" fast-json-stable-stringify "^2.1.0" graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" - jest-regex-util "^29.2.0" - jest-util "^29.3.1" + jest-haste-map "^29.4.3" + jest-regex-util "^29.4.3" + jest-util "^29.4.3" micromatch "^4.0.4" pirates "^4.0.4" slash "^3.0.0" - write-file-atomic "^4.0.1" + write-file-atomic "^4.0.2" "@jest/types@^26.6.2": version "26.6.2" @@ -2016,12 +1971,12 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jest/types@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.3.1.tgz#7c5a80777cb13e703aeec6788d044150341147e3" - integrity sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA== +"@jest/types@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.4.3.tgz#9069145f4ef09adf10cec1b2901b2d390031431f" + integrity sha512-bPYfw8V65v17m2Od1cv44FH+SiKW7w2Xu7trhcdTLUmSv85rfKsP+qXSjO4KGJr4dtPSzl/gvslZBXctf1qGEA== dependencies: - "@jest/schemas" "^29.0.0" + "@jest/schemas" "^29.4.3" "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" "@types/node" "*" @@ -2068,7 +2023,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.9": +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== @@ -2082,9 +2037,9 @@ integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== "@mattermost/react-native-paste-input@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@mattermost/react-native-paste-input/-/react-native-paste-input-0.6.0.tgz#bc920c8f1b442266a6bc58f9122df137b73bc9fa" - integrity sha512-Hy4w8RaiiXl2AKcLXT0FjJJsh4FXtLiWCxfh6zaBtCkx7jsr4d9xwJ/zqrnjv0jkG7XbRUCp40dgNBpWYZ1pyQ== + version "0.6.2" + resolved "https://registry.yarnpkg.com/@mattermost/react-native-paste-input/-/react-native-paste-input-0.6.2.tgz#783779bc60758927781270293f35d6b4f937cd07" + integrity sha512-+92KQfHytTdVzKzydkJQetyLy8H91uwCoDxKswJ4+fK4f4zNJpF/10LrHL+/aijcy0vJG+6gUC8zXvyLzv6wCw== dependencies: semver "7.3.8" @@ -2127,9 +2082,9 @@ fastq "^1.6.0" "@notifee/react-native@^7.4.0": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.4.0.tgz#0f20744307bf3b800f7b56eb2d0bbdd474748d09" - integrity sha512-c8pkxDQFRbw0JlUmTb07OTG/4LQHRj8MBodMLwEcO+SvqIxK8ya8zSUEzfdcdWsSVqdoym0v3zpSNroR3Quj/w== + version "7.5.0" + resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.5.0.tgz#3d846b6a607a6352e798a3440cd3880c005a7d56" + integrity sha512-FR1xzQI2KpMyLuM7YZyHHjCcOXbjAcBWMeRlarro8peujr6gJZSg4j2Aw4o8O4ifMHDopFrovwyK1uYBaB9MUg== "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": version "0.5.10" @@ -2154,9 +2109,9 @@ merge-options "^3.0.4" "@react-native-camera-roll/camera-roll@^5.2.2": - version "5.2.2" - resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.2.2.tgz#dbdfa4ffb126b4d7efa01f3c5fc030ce3bfcdf2d" - integrity sha512-LVzUX1KdKvOXJGiV/9tlkDyDSOEjvAzuiV8OkSUD13TXN/Tk5u2KVHTYRYJz5pmXanLN2dmEamctJcqKCeXYxg== + version "5.2.4" + resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.2.4.tgz#216d0ea4656b6538c10b60f057118c6f5e704c0d" + integrity sha512-pEQDartgO8Nqy6QDm1efvIPpv4q5W+AtTgS05JGK/9x8VzSj8fJ/cvHmMZBlm/4sFpJyvJZ+1KYxKsvFJLbGuQ== "@react-native-clipboard/clipboard@^1.10.0": version "1.11.1" @@ -2168,22 +2123,22 @@ resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.3.0.tgz#e5018b3b0bd6de9632ac6cf34e9f8e0f1a9a28ec" integrity sha512-d6phh39kKcbZ4IluDftiVWqfeFOgjl1AbQWzN47x+hLKQ5GvQJ6QhRvgAuDZ+xbJksrbXgNpMjVYkjsbcVehxg== -"@react-native-community/cli-clean@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-10.0.0.tgz#09cc4c63116e81d3765ffedecc38387bcc7b4483" - integrity sha512-9uHRicQXycqu55rSplQh2/o/nDdA5qDXiU09/s7/fJbUlCNUySy5rXw5FtbQv+Bj+bD9tXFoDRKN1ZnNHtT4QQ== +"@react-native-community/cli-clean@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-10.1.1.tgz#4c73ce93a63a24d70c0089d4025daac8184ff504" + integrity sha512-iNsrjzjIRv9yb5y309SWJ8NDHdwYtnCpmxZouQDyOljUdC9MwdZ4ChbtA4rwQyAwgOVfS9F/j56ML3Cslmvrxg== dependencies: - "@react-native-community/cli-tools" "^10.0.0" + "@react-native-community/cli-tools" "^10.1.1" chalk "^4.1.2" execa "^1.0.0" prompts "^2.4.0" -"@react-native-community/cli-config@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-config/-/cli-config-10.0.0.tgz#25b87760153ffc3b5bad3018c485f17ce982fc74" - integrity sha512-cbJfncqFtONfPPFnfL4bgdYYZU+Muo6jQMgTnR+rbp6gNxTPUYioctHgXcvyJAubl886mr3lirfU31V+a96AqA== +"@react-native-community/cli-config@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-config/-/cli-config-10.1.1.tgz#08dcc5d7ca1915647dc06507ed853fe0c1488395" + integrity sha512-p4mHrjC+s/ayiNVG6T35GdEGdP6TuyBUg5plVGRJfTl8WT6LBfLYLk+fz/iETrEZ/YkhQIsQcEUQC47MqLNHog== dependencies: - "@react-native-community/cli-tools" "^10.0.0" + "@react-native-community/cli-tools" "^10.1.1" chalk "^4.1.2" cosmiconfig "^5.1.0" deepmerge "^3.2.0" @@ -2197,14 +2152,14 @@ dependencies: serve-static "^1.13.1" -"@react-native-community/cli-doctor@^10.0.0": - version "10.1.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-doctor/-/cli-doctor-10.1.0.tgz#582f33fb149c515820605ee1cecf148b96d34080" - integrity sha512-3TMZX44QJ7njaimtmbHY2Q5Hb/R2YAYjx2ICi0TiHMLvofBlyXlxkJDs4nl7KDxLmZV9NpRw8fpkpCiHqimAhQ== +"@react-native-community/cli-doctor@^10.1.1": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-doctor/-/cli-doctor-10.2.0.tgz#6050030eea9200ce3c35de360cf8455e126b4d45" + integrity sha512-yLxJazUmNSPslHxeeev0gLvsK0nQan8BmGWbtqPz2WwbIbD89vbytC7G96OxiQXr46iWEWAwEJiTTdgA7jlA5Q== dependencies: - "@react-native-community/cli-config" "^10.0.0" - "@react-native-community/cli-platform-ios" "^10.1.0" - "@react-native-community/cli-tools" "^10.0.0" + "@react-native-community/cli-config" "^10.1.1" + "@react-native-community/cli-platform-ios" "^10.2.0" + "@react-native-community/cli-tools" "^10.1.1" chalk "^4.1.2" command-exists "^1.2.8" envinfo "^7.7.2" @@ -2219,85 +2174,86 @@ sudo-prompt "^9.0.0" wcwidth "^1.0.1" -"@react-native-community/cli-hermes@^10.0.0": - version "10.1.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-hermes/-/cli-hermes-10.1.0.tgz#c28ad9452657f8dea5ffc514bbbb019809e1e0fc" - integrity sha512-A79Z5lm44xRbF0JDyT4i1Cq06hXskl5l/iAWZFxCkpbgMqTyghTG1gFeoCZudIv7Ez+nwQbX5edcSerotbzcxg== +"@react-native-community/cli-hermes@^10.1.3": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-hermes/-/cli-hermes-10.2.0.tgz#cc252f435b149f74260bc918ce22fdf58033a87e" + integrity sha512-urfmvNeR8IiO/Sd92UU3xPO+/qI2lwCWQnxOkWaU/i2EITFekE47MD6MZrfVulRVYRi5cuaFqKZO/ccOdOB/vQ== dependencies: - "@react-native-community/cli-platform-android" "^10.1.0" - "@react-native-community/cli-tools" "^10.0.0" + "@react-native-community/cli-platform-android" "^10.2.0" + "@react-native-community/cli-tools" "^10.1.1" chalk "^4.1.2" hermes-profile-transformer "^0.0.6" ip "^1.1.5" -"@react-native-community/cli-platform-android@10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-10.0.0.tgz#9894a0b54de94da4d01f3b9db4e6b51ba112fa72" - integrity sha512-wUXq+//PagXVjG6ZedO+zIbNPkCsAiP+uiE45llFTsCtI6vFBwa6oJFHH6fhfeib4mOd7DvIh2Kktrpgyb6nBg== +"@react-native-community/cli-platform-android@10.1.3": + version "10.1.3" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-10.1.3.tgz#8380799cd4d3f9a0ca568b0f5b4ae9e462ce3669" + integrity sha512-8YZEpBL6yd9l4CIoFcLOgrV8x2GDujdqrdWrNsNERDAbsiFwqAQvfjyyb57GAZVuEPEJCoqUlGlMCwOh3XQb9A== dependencies: - "@react-native-community/cli-tools" "^10.0.0" + "@react-native-community/cli-tools" "^10.1.1" chalk "^4.1.2" execa "^1.0.0" glob "^7.1.3" logkitty "^0.7.1" -"@react-native-community/cli-platform-android@^10.1.0": - version "10.1.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-10.1.0.tgz#0c80b4ef4253990cc49e4cccf2344716f1316c16" - integrity sha512-Mr5eBuhHDdib1hUeh+s3N/eztPVtUOiuh/soZd8QT9fEufayqOnpm++gP8D993DaI0R3knzfAisz8jTMZSRMow== +"@react-native-community/cli-platform-android@^10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-10.2.0.tgz#0bc689270a5f1d9aaf9e723181d43ca4dbfffdef" + integrity sha512-CBenYwGxwFdObZTn1lgxWtMGA5ms2G/ALQhkS+XTAD7KHDrCxFF9yT/fnAjFZKM6vX/1TqGI1RflruXih3kAhw== dependencies: - "@react-native-community/cli-tools" "^10.0.0" + "@react-native-community/cli-tools" "^10.1.1" chalk "^4.1.2" execa "^1.0.0" glob "^7.1.3" logkitty "^0.7.1" -"@react-native-community/cli-platform-ios@10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-10.0.0.tgz#91ee301620e509b44db5fd7c93eaf7ee992d097a" - integrity sha512-WLpXzZQ53zb1RhkpSDNHyBR3SIN3WObDRTEaR0TMXsXDeTj8/Eu2DPFpT+uEnD10ly/Y6/DqJsAt4Ku2X76klA== +"@react-native-community/cli-platform-ios@10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-10.1.1.tgz#39ed6810117d8e7330d3aa4d85818fb6ae358785" + integrity sha512-EB9/L8j1LqrqyfJtLRixU+d8FIP6Pr83rEgUgXgya/u8wk3h/bvX70w+Ff2skwjdPLr5dLUQ/n5KFX4r3bsNmA== dependencies: - "@react-native-community/cli-tools" "^10.0.0" + "@react-native-community/cli-tools" "^10.1.1" chalk "^4.1.2" execa "^1.0.0" glob "^7.1.3" ora "^5.4.1" -"@react-native-community/cli-platform-ios@^10.1.0": - version "10.1.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-10.1.0.tgz#503932e0655b95973c393dedbda1190501564d95" - integrity sha512-w5bhqwxvW9RmZQfLWNr3eLB2cjV0mrwLfWVN2XUFAlXS5B8H0ee9sydkBSEApcKYaHjA3EUvq3sewM+/m7u6Hw== +"@react-native-community/cli-platform-ios@^10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-10.2.0.tgz#be21c0e3bbf17358d540cc23e5556bf679f6322e" + integrity sha512-hIPK3iL/mL+0ChXmQ9uqqzNOKA48H+TAzg+hrxQLll/6dNMxDeK9/wZpktcsh8w+CyhqzKqVernGcQs7tPeKGw== dependencies: - "@react-native-community/cli-tools" "^10.0.0" + "@react-native-community/cli-tools" "^10.1.1" chalk "^4.1.2" execa "^1.0.0" + fast-xml-parser "^4.0.12" glob "^7.1.3" ora "^5.4.1" -"@react-native-community/cli-plugin-metro@^10.0.0": - version "10.1.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-10.1.0.tgz#11dff6a83fa0441a44eae0ce3ff6f57cc742960a" - integrity sha512-dRlUjD6F2EsR5lqfb3O9dMY26E/OLIXpastWJgdqLtoCYDlk38aGtiRM365CYFpO77vvn38OhE0TujygkeLpLg== +"@react-native-community/cli-plugin-metro@^10.1.1": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-10.2.0.tgz#83cabbc04c80f7e94f88ed998b72c7d572c6f094" + integrity sha512-9eiJrKYuauEDkQLCrjJUh7tS9T0oaMQqVUSSSuyDG6du7HQcfaR4mSf21wK75jvhKiwcQLpsFmMdctAb+0v+Cg== dependencies: - "@react-native-community/cli-server-api" "^10.0.0" - "@react-native-community/cli-tools" "^10.0.0" + "@react-native-community/cli-server-api" "^10.1.1" + "@react-native-community/cli-tools" "^10.1.1" chalk "^4.1.2" execa "^1.0.0" - metro "0.73.7" - metro-config "0.73.7" - metro-core "0.73.7" - metro-react-native-babel-transformer "0.73.7" - metro-resolver "0.73.7" - metro-runtime "0.73.7" + metro "0.73.8" + metro-config "0.73.8" + metro-core "0.73.8" + metro-react-native-babel-transformer "0.73.8" + metro-resolver "0.73.8" + metro-runtime "0.73.8" readline "^1.3.0" -"@react-native-community/cli-server-api@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-server-api/-/cli-server-api-10.0.0.tgz#b3f69f30285bed2019d4ee22879abb6b5c85b609" - integrity sha512-UXOYno0NMisMm8F61q1bG/HzVWkgvJvfuL5C9W036vo83y6oQGjjZBpIRWi/QF94BULz0hrdiPXFNXworLmAcQ== +"@react-native-community/cli-server-api@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-server-api/-/cli-server-api-10.1.1.tgz#e382269de281bb380c2e685431364fbbb8c1cb3a" + integrity sha512-NZDo/wh4zlm8as31UEBno2bui8+ufzsZV+KN7QjEJWEM0levzBtxaD+4je0OpfhRIIkhaRm2gl/vVf7OYAzg4g== dependencies: "@react-native-community/cli-debugger-ui" "^10.0.0" - "@react-native-community/cli-tools" "^10.0.0" + "@react-native-community/cli-tools" "^10.1.1" compression "^1.7.1" connect "^3.6.5" errorhandler "^1.5.0" @@ -2306,10 +2262,10 @@ serve-static "^1.13.1" ws "^7.5.1" -"@react-native-community/cli-tools@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-tools/-/cli-tools-10.0.0.tgz#51ec1775f699951837091cf84dc765e290377a53" - integrity sha512-cPUaOrahRcMJvJpBaoc/zpYPHoPqj91qV5KmvA9cJvKktY4rl/PFfUi1A0gTqqFhdH7qW1zkeyKo80lWq7NvxA== +"@react-native-community/cli-tools@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-tools/-/cli-tools-10.1.1.tgz#fa66e509c0d3faa31f7bb87ed7d42ad63f368ddd" + integrity sha512-+FlwOnZBV+ailEzXjcD8afY2ogFEBeHOw/8+XXzMgPaquU2Zly9B+8W089tnnohO3yfiQiZqkQlElP423MY74g== dependencies: appdirsjs "^1.2.4" chalk "^4.1.2" @@ -2328,19 +2284,19 @@ dependencies: joi "^17.2.1" -"@react-native-community/cli@10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-10.0.0.tgz#46f68a1184014956dc237e12e6c2ca9b318a04db" - integrity sha512-KHV9/AbPeIK87jHP7iY07/HQG00J5AYF/dHz2rzqAZGB2WYFAbc5uoLRw90u/U2AcSeO7ep+4kawm+/B9LJreg== +"@react-native-community/cli@10.1.3": + version "10.1.3" + resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-10.1.3.tgz#ad610c46da9fc7c717272024ec757dc646726506" + integrity sha512-kzh6bYLGN1q1q0IiczKSP1LTrovFeVzppYRTKohPI9VdyZwp7b5JOgaQMB/Ijtwm3MxBDrZgV9AveH/eUmUcKQ== dependencies: - "@react-native-community/cli-clean" "^10.0.0" - "@react-native-community/cli-config" "^10.0.0" + "@react-native-community/cli-clean" "^10.1.1" + "@react-native-community/cli-config" "^10.1.1" "@react-native-community/cli-debugger-ui" "^10.0.0" - "@react-native-community/cli-doctor" "^10.0.0" - "@react-native-community/cli-hermes" "^10.0.0" - "@react-native-community/cli-plugin-metro" "^10.0.0" - "@react-native-community/cli-server-api" "^10.0.0" - "@react-native-community/cli-tools" "^10.0.0" + "@react-native-community/cli-doctor" "^10.1.1" + "@react-native-community/cli-hermes" "^10.1.3" + "@react-native-community/cli-plugin-metro" "^10.1.1" + "@react-native-community/cli-server-api" "^10.1.1" + "@react-native-community/cli-tools" "^10.1.1" "@react-native-community/cli-types" "^10.0.0" chalk "^4.1.2" commander "^9.4.1" @@ -2433,15 +2389,14 @@ integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== "@segment/analytics-react-native@^2.10.1": - version "2.10.1" - resolved "https://registry.yarnpkg.com/@segment/analytics-react-native/-/analytics-react-native-2.10.1.tgz#cfc6c2186eaffc31a73b2ea1f229581dbad1f928" - integrity sha512-9CzqVijoQK86K/piItYfg88sFSHOg9G2RmZ0uKQyrplHo0Cm3ngzzyhf7Ry/A7XNT6ebIIgMrh8UGIRR7YwY3g== + version "2.13.0" + resolved "https://registry.yarnpkg.com/@segment/analytics-react-native/-/analytics-react-native-2.13.0.tgz#2a612253219cd91ea3716f46d4fa837231d968d8" + integrity sha512-Yt2/t7jNcEaMaYRgeT8KqMsemzjIBaxx5jk01+WI/TXPf0P1OBizMjoxYH3Bpv7CzyEVbVSFlUSrJmD5ll9xtQ== dependencies: "@react-native-async-storage/async-storage" "^1.15.17" "@segment/sovran-react-native" "^0.4.5" deepmerge "^4.2.2" js-base64 "^3.7.2" - promise.allsettled "^1.0.5" react-native-uuid "^2.0.1" "@segment/sovran-react-native@^0.4.5": @@ -2468,7 +2423,7 @@ dependencies: "@hapi/hoek" "^10.0.0" -"@sideway/formula@^3.0.0": +"@sideway/formula@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== @@ -2483,6 +2438,11 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA== +"@sinclair/typebox@^0.25.16": + version "0.25.23" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.23.tgz#1c15b0d2b872d89cc0f47c7243eacb447df8b8bd" + integrity sha512-VEB8ygeP42CFLWyAJhN5OklpxUliqdNEUcXb4xZ/CINqtYGTjL5ukluKdKzQ0iWdUxyQ7B0539PAUhHKrCNWSQ== + "@sinonjs/commons@^1.7.0": version "1.8.6" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" @@ -2490,6 +2450,20 @@ dependencies: type-detect "4.0.8" +"@sinonjs/commons@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" + integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c" + integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw== + dependencies: + "@sinonjs/commons" "^2.0.0" + "@sinonjs/fake-timers@^8.0.1": version "8.1.0" resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" @@ -2497,13 +2471,6 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@sinonjs/fake-timers@^9.1.2": - version "9.1.2" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" - integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== - dependencies: - "@sinonjs/commons" "^1.7.0" - "@surma/rollup-plugin-off-main-thread@^2.2.3": version "2.2.3" resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" @@ -2618,9 +2585,9 @@ loader-utils "^2.0.0" "@testing-library/jest-native@^5.3.3": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@testing-library/jest-native/-/jest-native-5.4.0.tgz#d294d82f9780acb0be248a4cf261280997d6b213" - integrity sha512-6zoWxj1aDV/eRiGcrKX4bgxc7cIbK6iWyWWT6zzGCEacpcCfiIdhbQs+qbL+GzSmMufltzojCvka7X76JhooEg== + version "5.4.2" + resolved "https://registry.yarnpkg.com/@testing-library/jest-native/-/jest-native-5.4.2.tgz#6b0c987cc57f8d900763e763025d00d26ccbc85f" + integrity sha512-Vo/CE1uvCVH1H8YPoOEXLXVsm+BjzSQTq35+wkri1fr0O5D+A2WZ+m3ni5g6f1OCzNKNGIAHmisBEWkDs1P1mw== dependencies: chalk "^4.1.2" jest-diff "^29.0.1" @@ -2629,11 +2596,11 @@ redent "^3.0.0" "@testing-library/react-native@^11.5.0": - version "11.5.0" - resolved "https://registry.yarnpkg.com/@testing-library/react-native/-/react-native-11.5.0.tgz#b043c5db7b15eca42a65e95d3f3ae196fab9493b" - integrity sha512-seV+qebsbX4E5CWk/wizU1+2wVLsPyqEzG7sTgrhJ81cgAawg7ay06fIZR9IS75pDeWn2KZVd4mGk1pjJ3i3Zw== + version "11.5.2" + resolved "https://registry.yarnpkg.com/@testing-library/react-native/-/react-native-11.5.2.tgz#64238431de1ce4e128810bf8277a7cc118f1c812" + integrity sha512-haa/82Sy4ngAbXF6VTw5U306ZQHWfv7OKxALA4AkMKnX+6frhSd9aAe9rp50CyI2XJsNna0Rh6G+CSw+SPztYg== dependencies: - pretty-format "^29.0.0" + pretty-format "^29.4.0" "@tokenizer/token@^0.3.0": version "0.3.0" @@ -2656,12 +2623,12 @@ integrity sha512-jE58snEKBd9DXfyR4+ssZmYJ/W2mOSnNrvljR0aLyQJL9JKX6vlWELHkRjb3HBbcM9Uy0hZGijXbqEAjOERW2A== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": - version "7.1.20" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.20.tgz#e168cdd612c92a2d335029ed62ac94c95b362359" - integrity sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ== + version "7.20.0" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" + integrity sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ== dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" "@types/babel__generator" "*" "@types/babel__template" "*" "@types/babel__traverse" "*" @@ -2727,9 +2694,9 @@ "@types/estree" "*" "@types/eslint@*", "@types/eslint@^7.29.0 || ^8.4.1": - version "8.4.10" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.10.tgz#19731b9685c19ed1552da7052b6f668ed7eb64bb" - integrity sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw== + version "8.21.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.21.1.tgz#110b441a210d53ab47795124dbc3e9bb993d1e7c" + integrity sha512-rc9K8ZpVjNcLs8Fp0dkozd5Pt2Apk1glO4Vgz8ix1u6yFByxfqo5Yavpy65o+93TAe24jr7v+eSBtFLvOQtCRQ== dependencies: "@types/estree" "*" "@types/json-schema" "*" @@ -2749,33 +2716,26 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.31": - version "4.17.32" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.32.tgz#93dda387f5516af616d8d3f05f2c4c79d81e1b82" - integrity sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA== +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": + version "4.17.33" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" + integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" "@types/express@*", "@types/express@^4.17.13": - version "4.17.15" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.15.tgz#9290e983ec8b054b65a5abccb610411953d417ff" - integrity sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ== + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== dependencies: "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.31" + "@types/express-serve-static-core" "^4.17.33" "@types/qs" "*" "@types/serve-static" "*" -"@types/graceful-fs@^4.1.2": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" - integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== - dependencies: - "@types/node" "*" - -"@types/graceful-fs@^4.1.3": +"@types/graceful-fs@^4.1.2", "@types/graceful-fs@^4.1.3": version "4.1.6" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" integrity sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw== @@ -2788,9 +2748,9 @@ integrity sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA== "@types/he@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@types/he/-/he-1.1.2.tgz#0c8b275f36d2b8b651104638e4d45693349c3953" - integrity sha512-kSJPcLO1x+oolc0R89pUl2kozldQ/fVQ1C1p5mp8fPoLdF/ZcBvckaTC2M8xXh3GYendXvCpy5m/a2eSbfgNgw== + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/he/-/he-1.2.0.tgz#3845193e597d943bab4e61ca5d7f3d8fc3d572a3" + integrity sha512-uH2smqTN4uGReAiKedIVzoLUAXIYLBTbSofhx3hbNqj74Ua6KqFsLYszduTrLCMEAEAozF73DbGi/SC1bzQq4g== "@types/html-minifier-terser@^6.0.0": version "6.1.0" @@ -2798,17 +2758,12 @@ integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== "@types/http-proxy@^1.17.8": - version "1.17.9" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.9.tgz#7f0e7931343761efde1e2bf48c40f02f3f75705a" - integrity sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw== + version "1.17.10" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.10.tgz#e576c8e4a0cc5c6a138819025a88e167ebb38d6c" + integrity sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g== dependencies: "@types/node" "*" -"@types/invariant@^2.2.35": - version "2.2.35" - resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be" - integrity sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg== - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -2853,6 +2808,20 @@ dependencies: "@types/lodash" "*" +"@types/lodash.clonedeep@^4.5.7": + version "4.5.7" + resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.7.tgz#0e119f582ed6f9e6b373c04a644651763214f197" + integrity sha512-ccNqkPptFIXrpVqUECi60/DFxjNKsfoQxSQsgcBJCX/fuX1wgyQieojkcWH/KpE3xzLoWN/2k+ZeGqIN3paSvw== + dependencies: + "@types/lodash" "*" + +"@types/lodash.isequal@^4.5.6": + version "4.5.6" + resolved "https://registry.yarnpkg.com/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz#ff42a1b8e20caa59a97e446a77dc57db923bc02b" + integrity sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg== + dependencies: + "@types/lodash" "*" + "@types/lodash.omit@^4.5.7": version "4.5.7" resolved "https://registry.yarnpkg.com/@types/lodash.omit/-/lodash.omit-4.5.7.tgz#2357ed2412b4164344e8ee41f85bb0b2920304ba" @@ -2860,6 +2829,13 @@ dependencies: "@types/lodash" "*" +"@types/lodash.shuffle@^4.2.7": + version "4.2.7" + resolved "https://registry.yarnpkg.com/@types/lodash.shuffle/-/lodash.shuffle-4.2.7.tgz#b714d829af948a266b0df1477d629c70de2f4c72" + integrity sha512-b+K0NBpB4WcNoQTfifuTmi5nm5mJXRw9DBdbFfBr1q1+EVoTKkClDxq/7r1sq2GZcRelMFRsFcGGHrHQgxRySg== + dependencies: + "@types/lodash" "*" + "@types/lodash@*": version "4.14.191" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" @@ -2871,9 +2847,9 @@ integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== "@types/node@*": - version "18.11.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" - integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== + version "18.14.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.0.tgz#94c47b9217bbac49d4a67a967fdcdeed89ebb7d0" + integrity sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A== "@types/parse-json@^4.0.0": version "4.0.0" @@ -2913,9 +2889,9 @@ "@types/react" "*" "@types/react-native@^0.67.3": - version "0.67.17" - resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.67.17.tgz#afebc3fff1d6314840c13b7936e17fa350eb7aae" - integrity sha512-HYo9XbYPwyf5L7FWhRjawbDQ4ma5k/bEzxrpDjG2olwLbXR/IR5ZghCmUUIcEtHB9KDg0QIAs+WriddtOKVmFg== + version "0.67.19" + resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.67.19.tgz#8f2fb257bd9f7b56b07a98be488aab0d79f087fe" + integrity sha512-tk3D4HtJ4KBmnoOMiPWY5og0m34cwavCPSlV75hMqut2WgcDF9SXvkqZU0RP6qddHwvEstYIJSvSfLMPOak5vQ== dependencies: "@types/react" "^17" @@ -2927,9 +2903,9 @@ "@types/react" "^17" "@types/react@*", "@types/react@^17": - version "17.0.52" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.52.tgz#10d8b907b5c563ac014a541f289ae8eaa9bf2e9b" - integrity sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A== + version "17.0.53" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.53.tgz#10d4d5999b8af3d6bc6a9369d7eb953da82442ab" + integrity sha512-1yIpQR2zdYu1Z/dc1OxC+MA6GR240u3gcnP4l6mvj/PJiVaqHsQPmWttsvHsfnhfPbU2FuGmo0wSITPygjBmsw== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -2985,9 +2961,9 @@ integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== "@types/trusted-types@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" - integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311" + integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g== "@types/ws@^8.5.1": version "8.5.4" @@ -3002,50 +2978,36 @@ integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^15.0.0": - version "15.0.14" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06" - integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ== + version "15.0.15" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.15.tgz#e609a2b1ef9e05d90489c2f5f45bbfb2be092158" + integrity sha512-IziEYMU9XoVj8hWg7k+UJrXALkGFjWJhn5QFEv9q4p+v40oZhSuC135M38st8XPjICL7Ey4TV64ferBGUoJhBg== dependencies: "@types/yargs-parser" "*" "@types/yargs@^16.0.0": - version "16.0.4" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" - integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== + version "16.0.5" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.5.tgz#12cc86393985735a283e387936398c2f9e5f88e3" + integrity sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ== dependencies: "@types/yargs-parser" "*" "@types/yargs@^17.0.8": - version "17.0.18" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.18.tgz#466225ab4fbabb9aa711f5b406796daf1374a5b7" - integrity sha512-eIJR1UER6ur3EpKM3d+2Pgd+ET+k6Kn9B4ZItX0oPjjVI5PrfaRjKyLT5UYendDpLuoiJMNJvovLQbEXqhsPaw== + version "17.0.22" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.22.tgz#7dd37697691b5f17d020f3c63e7a45971ff71e9a" + integrity sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g== dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.30.5", "@typescript-eslint/eslint-plugin@^5.48.2": - version "5.48.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.2.tgz#112e6ae1e23a1dc8333ce82bb9c65c2608b4d8a3" - integrity sha512-sR0Gja9Ky1teIq4qJOl0nC+Tk64/uYdX+mi+5iB//MH8gwyx8e3SOyhEzeLZEFEEfCaLf8KJq+Bd/6je1t+CAg== +"@typescript-eslint/eslint-plugin@^5.30.5", "@typescript-eslint/eslint-plugin@^5.48.2", "@typescript-eslint/eslint-plugin@^5.5.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.53.0.tgz#24b8b4a952f3c615fe070e3c461dd852b5056734" + integrity sha512-alFpFWNucPLdUOySmXCJpzr6HKC3bu7XooShWM+3w/EL6J2HIoB2PFxpLnq4JauWVk6DiVeNKzQlFEaE+X9sGw== dependencies: - "@typescript-eslint/scope-manager" "5.48.2" - "@typescript-eslint/type-utils" "5.48.2" - "@typescript-eslint/utils" "5.48.2" - debug "^4.3.4" - ignore "^5.2.0" - natural-compare-lite "^1.4.0" - regexpp "^3.2.0" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/eslint-plugin@^5.5.0": - version "5.48.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz#54f8368d080eb384a455f60c2ee044e948a8ce67" - integrity sha512-SVLafp0NXpoJY7ut6VFVUU9I+YeFsDzeQwtK0WZ+xbRN3mtxJ08je+6Oi2N89qDn087COdO0u3blKZNv9VetRQ== - dependencies: - "@typescript-eslint/scope-manager" "5.48.0" - "@typescript-eslint/type-utils" "5.48.0" - "@typescript-eslint/utils" "5.48.0" + "@typescript-eslint/scope-manager" "5.53.0" + "@typescript-eslint/type-utils" "5.53.0" + "@typescript-eslint/utils" "5.53.0" debug "^4.3.4" + grapheme-splitter "^1.0.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" regexpp "^3.2.0" @@ -3053,146 +3015,78 @@ tsutils "^3.21.0" "@typescript-eslint/experimental-utils@^5.0.0": - version "5.48.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.48.0.tgz#10c0871a1dfee734fbcd49399b6a07ce38bdb61c" - integrity sha512-ehoJFf67UViwnYuz6JUneZ8qxgDk0qEWKiTLmpE8WpPEr15e2cSLtp0E6Zicx2DaYdwctUA0uLRTbLckxQpurg== + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.53.0.tgz#e249e3a47ace290ea3d83a5a08c8d90cd7fe2a53" + integrity sha512-4SklZEwRn0jqkhtW+pPZpbKFXprwGneBndRM0TGzJu/LWdb9QV2hBgFIVU9AREo02BzqFvyG/ypd+xAW5YGhXw== dependencies: - "@typescript-eslint/utils" "5.48.0" + "@typescript-eslint/utils" "5.53.0" -"@typescript-eslint/parser@^5.30.5", "@typescript-eslint/parser@^5.48.2": - version "5.48.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.48.2.tgz#c9edef2a0922d26a37dba03be20c5fff378313b3" - integrity sha512-38zMsKsG2sIuM5Oi/olurGwYJXzmtdsHhn5mI/pQogP+BjYVkK5iRazCQ8RGS0V+YLk282uWElN70zAAUmaYHw== +"@typescript-eslint/parser@^5.30.5", "@typescript-eslint/parser@^5.48.2", "@typescript-eslint/parser@^5.5.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.53.0.tgz#a1f2b9ae73b83181098747e96683f1b249ecab52" + integrity sha512-MKBw9i0DLYlmdOb3Oq/526+al20AJZpANdT6Ct9ffxcV8nKCHz63t/S0IhlTFNsBIHJv+GY5SFJ0XfqVeydQrQ== dependencies: - "@typescript-eslint/scope-manager" "5.48.2" - "@typescript-eslint/types" "5.48.2" - "@typescript-eslint/typescript-estree" "5.48.2" + "@typescript-eslint/scope-manager" "5.53.0" + "@typescript-eslint/types" "5.53.0" + "@typescript-eslint/typescript-estree" "5.53.0" debug "^4.3.4" -"@typescript-eslint/parser@^5.5.0": - version "5.48.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.48.0.tgz#02803355b23884a83e543755349809a50b7ed9ba" - integrity sha512-1mxNA8qfgxX8kBvRDIHEzrRGrKHQfQlbW6iHyfHYS0Q4X1af+S6mkLNtgCOsGVl8+/LUPrqdHMssAemkrQ01qg== +"@typescript-eslint/scope-manager@5.53.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.53.0.tgz#42b54f280e33c82939275a42649701024f3fafef" + integrity sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w== dependencies: - "@typescript-eslint/scope-manager" "5.48.0" - "@typescript-eslint/types" "5.48.0" - "@typescript-eslint/typescript-estree" "5.48.0" - debug "^4.3.4" + "@typescript-eslint/types" "5.53.0" + "@typescript-eslint/visitor-keys" "5.53.0" -"@typescript-eslint/scope-manager@5.48.0": - version "5.48.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.48.0.tgz#607731cb0957fbc52fd754fd79507d1b6659cecf" - integrity sha512-0AA4LviDtVtZqlyUQnZMVHydDATpD9SAX/RC5qh6cBd3xmyWvmXYF+WT1oOmxkeMnWDlUVTwdODeucUnjz3gow== +"@typescript-eslint/type-utils@5.53.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.53.0.tgz#41665449935ba9b4e6a1ba6e2a3f4b2c31d6cf97" + integrity sha512-HO2hh0fmtqNLzTAme/KnND5uFNwbsdYhCZghK2SoxGp3Ifn2emv+hi0PBUjzzSh0dstUIFqOj3bp0AwQlK4OWw== dependencies: - "@typescript-eslint/types" "5.48.0" - "@typescript-eslint/visitor-keys" "5.48.0" - -"@typescript-eslint/scope-manager@5.48.2": - version "5.48.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.48.2.tgz#bb7676cb78f1e94921eaab637a4b5d596f838abc" - integrity sha512-zEUFfonQid5KRDKoI3O+uP1GnrFd4tIHlvs+sTJXiWuypUWMuDaottkJuR612wQfOkjYbsaskSIURV9xo4f+Fw== - dependencies: - "@typescript-eslint/types" "5.48.2" - "@typescript-eslint/visitor-keys" "5.48.2" - -"@typescript-eslint/type-utils@5.48.0": - version "5.48.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.48.0.tgz#40496dccfdc2daa14a565f8be80ad1ae3882d6d6" - integrity sha512-vbtPO5sJyFjtHkGlGK4Sthmta0Bbls4Onv0bEqOGm7hP9h8UpRsHJwsrCiWtCUndTRNQO/qe6Ijz9rnT/DB+7g== - dependencies: - "@typescript-eslint/typescript-estree" "5.48.0" - "@typescript-eslint/utils" "5.48.0" + "@typescript-eslint/typescript-estree" "5.53.0" + "@typescript-eslint/utils" "5.53.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/type-utils@5.48.2": - version "5.48.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.48.2.tgz#7d3aeca9fa37a7ab7e3d9056a99b42f342c48ad7" - integrity sha512-QVWx7J5sPMRiOMJp5dYshPxABRoZV1xbRirqSk8yuIIsu0nvMTZesKErEA3Oix1k+uvsk8Cs8TGJ6kQ0ndAcew== +"@typescript-eslint/types@5.53.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.53.0.tgz#f79eca62b97e518ee124086a21a24f3be267026f" + integrity sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A== + +"@typescript-eslint/typescript-estree@5.53.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz#bc651dc28cf18ab248ecd18a4c886c744aebd690" + integrity sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w== dependencies: - "@typescript-eslint/typescript-estree" "5.48.2" - "@typescript-eslint/utils" "5.48.2" - debug "^4.3.4" - tsutils "^3.21.0" - -"@typescript-eslint/types@5.48.0": - version "5.48.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.48.0.tgz#d725da8dfcff320aab2ac6f65c97b0df30058449" - integrity sha512-UTe67B0Ypius0fnEE518NB2N8gGutIlTojeTg4nt0GQvikReVkurqxd2LvYa9q9M5MQ6rtpNyWTBxdscw40Xhw== - -"@typescript-eslint/types@5.48.2": - version "5.48.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.48.2.tgz#635706abb1ec164137f92148f06f794438c97b8e" - integrity sha512-hE7dA77xxu7ByBc6KCzikgfRyBCTst6dZQpwaTy25iMYOnbNljDT4hjhrGEJJ0QoMjrfqrx+j1l1B9/LtKeuqA== - -"@typescript-eslint/typescript-estree@5.48.0": - version "5.48.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.0.tgz#a7f04bccb001003405bb5452d43953a382c2fac2" - integrity sha512-7pjd94vvIjI1zTz6aq/5wwE/YrfIyEPLtGJmRfyNR9NYIW+rOvzzUv3Cmq2hRKpvt6e9vpvPUQ7puzX7VSmsEw== - dependencies: - "@typescript-eslint/types" "5.48.0" - "@typescript-eslint/visitor-keys" "5.48.0" + "@typescript-eslint/types" "5.53.0" + "@typescript-eslint/visitor-keys" "5.53.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.48.2": - version "5.48.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.2.tgz#6e206b462942b32383582a6c9251c05021cc21b0" - integrity sha512-bibvD3z6ilnoVxUBFEgkO0k0aFvUc4Cttt0dAreEr+nrAHhWzkO83PEVVuieK3DqcgL6VAK5dkzK8XUVja5Zcg== - dependencies: - "@typescript-eslint/types" "5.48.2" - "@typescript-eslint/visitor-keys" "5.48.2" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/utils@5.48.0", "@typescript-eslint/utils@^5.13.0": - version "5.48.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.48.0.tgz#eee926af2733f7156ad8d15e51791e42ce300273" - integrity sha512-x2jrMcPaMfsHRRIkL+x96++xdzvrdBCnYRd5QiW5Wgo1OB4kDYPbC1XjWP/TNqlfK93K/lUL92erq5zPLgFScQ== +"@typescript-eslint/utils@5.53.0", "@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.43.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.53.0.tgz#e55eaad9d6fffa120575ffaa530c7e802f13bce8" + integrity sha512-VUOOtPv27UNWLxFwQK/8+7kvxVC+hPHNsJjzlJyotlaHjLSIgOCKj9I0DBUjwOOA64qjBwx5afAPjksqOxMO0g== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.48.0" - "@typescript-eslint/types" "5.48.0" - "@typescript-eslint/typescript-estree" "5.48.0" + "@typescript-eslint/scope-manager" "5.53.0" + "@typescript-eslint/types" "5.53.0" + "@typescript-eslint/typescript-estree" "5.53.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/utils@5.48.2", "@typescript-eslint/utils@^5.10.0": - version "5.48.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.48.2.tgz#3777a91dcb22b8499a25519e06eef2e9569295a3" - integrity sha512-2h18c0d7jgkw6tdKTlNaM7wyopbLRBiit8oAxoP89YnuBOzCZ8g8aBCaCqq7h208qUTroL7Whgzam7UY3HVLow== +"@typescript-eslint/visitor-keys@5.53.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz#8a5126623937cdd909c30d8fa72f79fa56cc1a9f" + integrity sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w== dependencies: - "@types/json-schema" "^7.0.9" - "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.48.2" - "@typescript-eslint/types" "5.48.2" - "@typescript-eslint/typescript-estree" "5.48.2" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - semver "^7.3.7" - -"@typescript-eslint/visitor-keys@5.48.0": - version "5.48.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.0.tgz#4446d5e7f6cadde7140390c0e284c8702d944904" - integrity sha512-5motVPz5EgxQ0bHjut3chzBkJ3Z3sheYVcSwS5BpHZpLqSptSmELNtGixmgj65+rIfhvtQTz5i9OP2vtzdDH7Q== - dependencies: - "@typescript-eslint/types" "5.48.0" - eslint-visitor-keys "^3.3.0" - -"@typescript-eslint/visitor-keys@5.48.2": - version "5.48.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.2.tgz#c247582a0bcce467461d7b696513bf9455000060" - integrity sha512-z9njZLSkwmjFWUelGEwEbdf4NwKvfHxvGC0OcGN1Hp/XNDIcJ7D5DpPNPv6x6/mFvc1tQHsaWmpD/a4gOvvCJQ== - dependencies: - "@typescript-eslint/types" "5.48.2" + "@typescript-eslint/types" "5.53.0" eslint-visitor-keys "^3.3.0" "@ucans/core@0.11.0": @@ -3416,9 +3310,9 @@ acorn@^7.0.0, acorn@^7.1.1: integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: - version "8.8.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" - integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== address@^1.0.1, address@^1.1.2: version "1.2.2" @@ -3469,7 +3363,7 @@ ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.6.0, ajv@^8.8.0: +ajv@^8.0.0, ajv@^8.6.0, ajv@^8.6.3, ajv@^8.8.0: version "8.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -3569,13 +3463,12 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" - integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== +aria-query@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" + integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== dependencies: - "@babel/runtime" "^7.10.2" - "@babel/runtime-corejs3" "^7.10.2" + deep-equal "^2.0.5" arr-diff@^4.0.0: version "4.0.0" @@ -3602,7 +3495,7 @@ array-flatten@^2.1.2: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== -array-includes@^3.1.4, array-includes@^3.1.5, array-includes@^3.1.6: +array-includes@^3.1.5, array-includes@^3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== @@ -3623,7 +3516,7 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== -array.prototype.flat@^1.2.5: +array.prototype.flat@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== @@ -3643,17 +3536,6 @@ array.prototype.flatmap@^1.3.1: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" -array.prototype.map@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.5.tgz#6e43c2fee6c0fb5e4806da2dc92eb00970809e55" - integrity sha512-gfaKntvwqYIuC7mLLyv2wzZIJqrRhn5PZ9EfFejSx6a78sV7iDsGpG9P+3oUPtm1Rerqm6nrKS4FYuTIvWfo3g== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-array-method-boxes-properly "^1.0.0" - is-string "^1.0.7" - array.prototype.reduce@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz#6b20b0daa9d9734dd6bc7ea66b5bbce395471eac" @@ -3752,10 +3634,20 @@ autoprefixer@^10.4.13: picocolors "^1.0.0" postcss-value-parser "^4.2.0" -axe-core@^4.4.3: - version "4.6.1" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.1.tgz#79cccdee3e3ab61a8f42c458d4123a6768e6fbce" - integrity sha512-lCZN5XRuOnpG4bpMq8v0khrWtUOn+i8lZSb6wHZH56ZfbIEv6XwJV84AAueh9/zi7qPVJ/E4yz6fmsiyOmXR4w== +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + +await-lock@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef" + integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw== + +axe-core@^4.6.2: + version "4.6.3" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece" + integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg== axios@^0.24.0: version "0.24.0" @@ -3772,10 +3664,12 @@ axios@^0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" -axobject-query@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" - integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== +axobject-query@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" + integrity sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg== + dependencies: + deep-equal "^2.0.5" babel-core@^7.0.0-bridge.0: version "7.0.0-bridge.0" @@ -3796,15 +3690,15 @@ babel-jest@^27.4.2, babel-jest@^27.5.1: graceful-fs "^4.2.9" slash "^3.0.0" -babel-jest@^29.2.1, babel-jest@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.3.1.tgz#05c83e0d128cd48c453eea851482a38782249f44" - integrity sha512-aard+xnMoxgjwV70t0L6wkW/3HQQtV+O0PEimxKgzNqCJnbYmroPojdP2tqKSOAt8QAKV/uSZU8851M7B5+fcA== +babel-jest@^29.2.1, babel-jest@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.4.3.tgz#478b84d430972b277ad67dd631be94abea676792" + integrity sha512-o45Wyn32svZE+LnMVWv/Z4x0SwtLbh4FyGcYtR20kIWd+rdrDZ9Fzq8Ml3MYLD+mZvEdzCjZsCnYZ2jpJyQ+Nw== dependencies: - "@jest/transform" "^29.3.1" + "@jest/transform" "^29.4.3" "@types/babel__core" "^7.1.14" babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^29.2.0" + babel-preset-jest "^29.4.3" chalk "^4.0.0" graceful-fs "^4.2.9" slash "^3.0.0" @@ -3848,10 +3742,10 @@ babel-plugin-jest-hoist@^27.5.1: "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" -babel-plugin-jest-hoist@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.2.0.tgz#23ee99c37390a98cfddf3ef4a78674180d823094" - integrity sha512-TnspP2WNiR3GLfCsUNHqeXw0RoQ2f9U5hQ5L3XFpwuO8htQmSrhh8qsB6vi5Yi8+kuynN1yjDjQsPfkebmB6ZA== +babel-plugin-jest-hoist@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.4.3.tgz#ad1dfb5d31940957e00410ef7d9b2aa94b216101" + integrity sha512-mB6q2q3oahKphy5V7CpnNqZOCkxxZ9aokf1eh82Dy3jQmg4xvM1tGrh5y6BQUJh4a3Pj9+eLfwvAZ7VNKg7H8Q== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" @@ -3981,12 +3875,12 @@ babel-preset-jest@^27.5.1: babel-plugin-jest-hoist "^27.5.1" babel-preset-current-node-syntax "^1.0.0" -babel-preset-jest@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.2.0.tgz#3048bea3a1af222e3505e4a767a974c95a7620dc" - integrity sha512-z9JmMJppMxNv8N7fNRHvhMg9cvIkMxQBXgFkane3yKVEvEOP+kB50lk8DFRvF9PGqbyXxlmebKWhuDORO8RgdA== +babel-preset-jest@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.4.3.tgz#bb926b66ae253b69c6e3ef87511b8bb5c53c5b52" + integrity sha512-gWx6COtSuma6n9bw+8/F+2PCXrIgxV/D1TJFnp6OyBK2cxPWg0K9p/sriNYeifKjpUkMViWQ09DSWtzJQRETsw== dependencies: - babel-plugin-jest-hoist "^29.2.0" + babel-plugin-jest-hoist "^29.4.3" babel-preset-current-node-syntax "^1.0.0" babel-preset-react-app@^10.0.1: @@ -4093,7 +3987,7 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" -bluebird@^3.5.5: +bluebird@^3.5.4, bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -4117,9 +4011,9 @@ body-parser@1.20.1: unpipe "1.0.0" bonjour-service@^1.0.11: - version "1.0.14" - resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.0.14.tgz#c346f5bc84e87802d08f8d5a60b93f758e514ee7" - integrity sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ== + version "1.1.0" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.1.0.tgz#424170268d68af26ff83a5c640b95def01803a13" + integrity sha512-LVRinRB3k1/K0XzZ2p58COnWvkQknIY6sf0zF2rpErvcJXpMBttEPQSxK+HEXSS9VmpZlDoDnQWv8ftJT20B0Q== dependencies: array-flatten "^2.1.2" dns-equal "^1.0.0" @@ -4179,15 +4073,15 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.18.1, browserslist@^4.21.3, browserslist@^4.21.4: - version "4.21.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" - integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.21.5: + version "4.21.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" + integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== dependencies: - caniuse-lite "^1.0.30001400" - electron-to-chromium "^1.4.251" - node-releases "^2.0.6" - update-browserslist-db "^1.0.9" + caniuse-lite "^1.0.30001449" + electron-to-chromium "^1.4.284" + node-releases "^2.0.8" + update-browserslist-db "^1.0.10" bser@2.1.1: version "2.1.1" @@ -4232,6 +4126,23 @@ builtin-modules@^3.1.0: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== +bunyan-debug-stream@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bunyan-debug-stream/-/bunyan-debug-stream-3.1.0.tgz#78309c67ad85cfb8f011155334152c49209dcda8" + integrity sha512-VaFYbDVdiSn3ZpdozrjZ8mFpxHXl26t11C1DKRQtbo0EgffqeFNrRLOGIESKVeGEvVu4qMxMSSxzNlSw7oTj7w== + dependencies: + chalk "^4.1.2" + +bunyan@^1.8.12: + version "1.8.15" + resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.15.tgz#8ce34ca908a17d0776576ca1b2f6cbd916e93b46" + integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig== + optionalDependencies: + dtrace-provider "~0.8" + moment "^2.19.3" + mv "~2" + safe-json-stringify "~1" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -4257,6 +4168,11 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +caf@^15.0.1: + version "15.0.1" + resolved "https://registry.yarnpkg.com/caf/-/caf-15.0.1.tgz#28f1f17bd93dc4b5d95207ad07066eddf4768160" + integrity sha512-Xp/IK6vMwujxWZXra7djdYzPdPnEQKa7Mudu2wZgDQ3TJry1I0TgtjEgwZHpoBcMp68j4fb0/FZ1SJyMEgJrXQ== + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -4322,10 +4238,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426: - version "1.0.30001441" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz#987437b266260b640a23cd18fbddb509d7f69f3e" - integrity sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001449: + version "1.0.30001457" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001457.tgz#6af34bb5d720074e2099432aa522c21555a18301" + integrity sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA== case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" @@ -4337,7 +4253,7 @@ cborg@^1.6.0: resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.0.tgz#0fe157961dd47b537ccb84dc9ba681de8b699013" integrity sha512-/eM0JCaL99HDHxjySNQJLaolZFVdl6VA0/hEKIoiQPcQzE5LrG5QHdml0HaBt31brgB9dNe1zMr3f8IVrpotRQ== -chalk@^2.0.0, chalk@^2.4.1: +chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -4369,6 +4285,15 @@ check-types@^11.1.1: resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.2.2.tgz#7afc0b6a860d686885062f2dba888ba5710335b4" integrity sha512-HBiYvXvn9Z70Z88XKjz3AEKd4HJhBXsa3j7xFnITAzoS8+q6eIGi8qDB8FKPBAjtuxjI/zFpwuiCb8oDtKOYrA== +child-process-promise@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/child-process-promise/-/child-process-promise-2.2.1.tgz#4730a11ef610fad450b8f223c79d31d7bdad8074" + integrity sha512-Fi4aNdqBsr0mv+jgWxcZ/7rAIC2mgihrptyVI4foh/rrjY/3BNjfP9+oaiFx/fzim+1ZyCNBae0DlyfQhSugog== + dependencies: + cross-spawn "^4.0.2" + node-version "^1.0.0" + promise-polyfill "^6.0.1" + chokidar@^3.4.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -4400,9 +4325,9 @@ ci-info@^2.0.0: integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== ci-info@^3.2.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f" - integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w== + version "3.8.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" + integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== cjs-module-lexer@^1.0.0: version "1.2.2" @@ -4420,9 +4345,9 @@ class-utils@^0.3.5: static-extend "^0.1.1" clean-css@^5.2.2: - version "5.3.1" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.1.tgz#d0610b0b90d125196a2894d35366f734e5d7aa32" - integrity sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg== + version "5.3.2" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.2.tgz#70ecc7d4d4114921f5d298349ff86a31a9975224" + integrity sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww== dependencies: source-map "~0.6.0" @@ -4671,9 +4596,9 @@ content-disposition@0.5.4: safe-buffer "5.2.1" content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.9.0" @@ -4701,21 +4626,21 @@ copy-descriptor@^0.1.0: integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== core-js-compat@^3.25.1: - version "3.27.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.27.1.tgz#b5695eb25c602d72b1d30cbfba3cb7e5e4cf0a67" - integrity sha512-Dg91JFeCDA17FKnneN7oCMz4BkQ4TcffkgHP4OWwp9yx3pi7ubqMDXXSacfNak1PQqjc95skyt+YBLHQJnkJwA== + version "3.28.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.28.0.tgz#c08456d854608a7264530a2afa281fadf20ecee6" + integrity sha512-myzPgE7QodMg4nnd3K1TDoES/nADRStM8Gpz0D6nhkwbmwEnE0ZGJgoWsvQ722FR8D7xS0n0LV556RcEicjTyg== dependencies: - browserslist "^4.21.4" + browserslist "^4.21.5" -core-js-pure@^3.23.3, core-js-pure@^3.25.1: - version "3.27.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.27.1.tgz#ede4a6b8440585c7190062757069c01d37a19dca" - integrity sha512-BS2NHgwwUppfeoqOXqi08mUqS5FiZpuRuJJpKsaME7kJz0xxuk0xkhDdfMIlP/zLa80krBqss1LtD7f889heAw== +core-js-pure@^3.23.3: + version "3.28.0" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.28.0.tgz#4ef2888475b6c856ef6f5aeef8b4f618b76ad048" + integrity sha512-DSOVleA9/v3LNj/vFxAPfUHttKTzrB2RXhAPvR5TPXn4vrra3Z2ssytvRyt8eruJwAfwAiFADEbrjcRdcvPLQQ== core-js@^3.19.2: - version "3.27.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.27.1.tgz#23cc909b315a6bb4e418bf40a52758af2103ba46" - integrity sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww== + version "3.28.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.28.0.tgz#ed8b9e99c273879fdfff0edfc77ee709a5800e4a" + integrity sha512-GiZn9D4Z/rSYvTeg1ljAIsEqFm0LaN9gVtwDCrKL80zHtS31p9BAjmTxVqTQDMpwlMolJZOFntUG2uwyj7DAqw== core-util-is@~1.0.0: version "1.0.3" @@ -4777,6 +4702,14 @@ cross-fetch@^3.1.5: dependencies: node-fetch "2.6.7" +cross-spawn@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" + integrity sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA== + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -4923,31 +4856,31 @@ css-what@^6.0.1, css-what@^6.1.0: integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== cssdb@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.2.0.tgz#f44bd4abc430f0ff7f4c64b8a1fb857a753f77a8" - integrity sha512-JYlIsE7eKHSi0UNuCyo96YuIDFqvhGgHw4Ck6lsN+DP0Tp8M64UTDT2trGbkMDqnCoEjks7CkS0XcjU0rkvBdg== + version "7.4.1" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.4.1.tgz#61d55c0173126689922a219e15e131e4b5caf422" + integrity sha512-0Q8NOMpXJ3iTDDbUv9grcmQAfdDx4qz+fN/+Md2FGbevT+6+bJNQ2LjB2YIUlLbpBTM32idU1Sb+tb/uGt6/XQ== cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -cssnano-preset-default@^5.2.13: - version "5.2.13" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.13.tgz#e7353b0c57975d1bdd97ac96e68e5c1b8c68e990" - integrity sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ== +cssnano-preset-default@^5.2.14: + version "5.2.14" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz#309def4f7b7e16d71ab2438052093330d9ab45d8" + integrity sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A== dependencies: css-declaration-sorter "^6.3.1" cssnano-utils "^3.1.0" postcss-calc "^8.2.3" - postcss-colormin "^5.3.0" + postcss-colormin "^5.3.1" postcss-convert-values "^5.1.3" postcss-discard-comments "^5.1.2" postcss-discard-duplicates "^5.1.0" postcss-discard-empty "^5.1.1" postcss-discard-overridden "^5.1.0" postcss-merge-longhand "^5.1.7" - postcss-merge-rules "^5.1.3" + postcss-merge-rules "^5.1.4" postcss-minify-font-values "^5.1.0" postcss-minify-gradients "^5.1.1" postcss-minify-params "^5.1.4" @@ -4962,7 +4895,7 @@ cssnano-preset-default@^5.2.13: postcss-normalize-url "^5.1.0" postcss-normalize-whitespace "^5.1.1" postcss-ordered-values "^5.1.3" - postcss-reduce-initial "^5.1.1" + postcss-reduce-initial "^5.1.2" postcss-reduce-transforms "^5.1.0" postcss-svgo "^5.1.0" postcss-unique-selectors "^5.1.1" @@ -4973,11 +4906,11 @@ cssnano-utils@^3.1.0: integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== cssnano@^5.0.6: - version "5.1.14" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.14.tgz#07b0af6da73641276fe5a6d45757702ebae2eb05" - integrity sha512-Oou7ihiTocbKqi0J1bB+TRJIQX5RMR3JghA8hcWSw9mjBLQ5Y3RWqEDoYG3sRNlAbCIXpqMoZGbq5KDR3vdzgw== + version "5.1.15" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.15.tgz#ded66b5480d5127fcb44dac12ea5a983755136bf" + integrity sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw== dependencies: - cssnano-preset-default "^5.2.13" + cssnano-preset-default "^5.2.14" lilconfig "^2.0.3" yaml "^1.10.2" @@ -5029,7 +4962,7 @@ dayjs@^1.8.15: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== -debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -5055,6 +4988,11 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + decimal.js@^10.2.1: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" @@ -5077,6 +5015,29 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== +deep-equal@^2.0.5: + version "2.2.0" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6" + integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw== + dependencies: + call-bind "^1.0.2" + es-get-iterator "^1.1.2" + get-intrinsic "^1.1.3" + is-arguments "^1.1.1" + is-array-buffer "^3.0.1" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -5093,9 +5054,9 @@ deepmerge@^3.2.0: integrity sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA== deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + version "4.3.0" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.0.tgz#65491893ec47756d44719ae520e0e2609233b59b" + integrity sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og== default-gateway@^6.0.3: version "6.0.3" @@ -5117,9 +5078,9 @@ define-lazy-prop@^2.0.0: integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== define-properties@^1.1.3, define-properties@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" - integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + version "1.2.0" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" + integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== dependencies: has-property-descriptors "^1.0.0" object-keys "^1.1.1" @@ -5231,6 +5192,45 @@ detective@^5.2.1: defined "^1.0.0" minimist "^1.2.6" +detox@^20.1.2: + version "20.2.0" + resolved "https://registry.yarnpkg.com/detox/-/detox-20.2.0.tgz#b0662ace6a1bc1f61cbf6c08ce9f748a68831add" + integrity sha512-br2af9iFkHqpgPe1+ChwQu+/rc/3VPMK31LlQNbdK+x/yeRfR0Rww3BFsmehJF6hD9oFpojtm4siJsX32X3Esw== + dependencies: + ajv "^8.6.3" + bunyan "^1.8.12" + bunyan-debug-stream "^3.1.0" + caf "^15.0.1" + chalk "^2.4.2" + child-process-promise "^2.2.0" + find-up "^4.1.0" + fs-extra "^4.0.2" + funpermaproxy "^1.1.0" + glob "^8.0.3" + ini "^1.3.4" + json-cycle "^1.3.0" + lodash "^4.17.11" + multi-sort-stream "^1.0.3" + multipipe "^4.0.0" + node-ipc "^9.2.1" + proper-lockfile "^3.0.2" + resolve-from "^5.0.0" + sanitize-filename "^1.6.1" + semver "^7.0.0" + serialize-error "^8.0.1" + shell-quote "^1.7.2" + signal-exit "^3.0.3" + stream-json "^1.7.4" + strip-ansi "^6.0.1" + telnet-client "1.2.8" + tempfile "^2.0.0" + trace-event-lib "^1.3.1" + which "^1.3.1" + ws "^7.0.0" + yargs "^16.0.3" + yargs-parser "^20.2.9" + yargs-unparser "^2.0.0" + did-resolver@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/did-resolver/-/did-resolver-4.0.1.tgz#11bb3f19ed1c8f53f4af4702912fa9f7852fc305" @@ -5251,10 +5251,10 @@ diff-sequences@^27.5.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== -diff-sequences@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e" - integrity sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ== +diff-sequences@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" + integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== dir-glob@^3.0.1: version "3.0.1" @@ -5407,11 +5407,30 @@ dotenv@^16.0.0, dotenv@^16.0.2, dotenv@^16.0.3: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== +dtrace-provider@~0.8: + version "0.8.8" + resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" + integrity sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg== + dependencies: + nan "^2.14.0" + +duplexer2@^0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== + dependencies: + readable-stream "^2.0.2" + duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== +easy-stack@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/easy-stack/-/easy-stack-1.0.1.tgz#8afe4264626988cabb11f3c704ccd0c835411066" + integrity sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w== + ecdsa-sig-formatter@1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" @@ -5431,10 +5450,10 @@ ejs@^3.1.6: dependencies: jake "^10.8.5" -electron-to-chromium@^1.4.251: - version "1.4.284" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" - integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== +electron-to-chromium@^1.4.284: + version "1.4.306" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.306.tgz#3f16bc14e150ad79803145fffeaf0bee15d3fca7" + integrity sha512-1zGmLFfpcs2v7ELt/1HgLZF6Gm2CCHaAdNKxd9Ge4INSU/HDYWjs7fcWU6eVMmhkpwmh+52ZrGCUU+Ji9OJihA== email-validator@^2.0.4: version "2.0.4" @@ -5529,26 +5548,32 @@ errorhandler@^1.5.0: escape-html "~1.0.3" es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.20.4: - version "1.20.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.5.tgz#e6dc99177be37cacda5988e692c3fa8b218e95d2" - integrity sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ== + version "1.21.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.1.tgz#e6105a099967c08377830a0c9cb589d570dd86c6" + integrity sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg== dependencies: + available-typed-arrays "^1.0.5" call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" es-to-primitive "^1.2.1" function-bind "^1.1.1" function.prototype.name "^1.1.5" get-intrinsic "^1.1.3" get-symbol-description "^1.0.0" + globalthis "^1.0.3" gopd "^1.0.1" has "^1.0.3" has-property-descriptors "^1.0.0" + has-proto "^1.0.1" has-symbols "^1.0.3" - internal-slot "^1.0.3" + internal-slot "^1.0.4" + is-array-buffer "^3.0.1" is-callable "^1.2.7" is-negative-zero "^2.0.2" is-regex "^1.1.4" is-shared-array-buffer "^1.0.2" is-string "^1.0.7" + is-typed-array "^1.1.10" is-weakref "^1.0.2" object-inspect "^1.12.2" object-keys "^1.1.1" @@ -5557,14 +5582,16 @@ es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.20.4: safe-regex-test "^1.0.0" string.prototype.trimend "^1.0.6" string.prototype.trimstart "^1.0.6" + typed-array-length "^1.0.4" unbox-primitive "^1.0.2" + which-typed-array "^1.1.9" es-array-method-boxes-properly@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== -es-get-iterator@^1.0.2: +es-get-iterator@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== @@ -5584,6 +5611,15 @@ es-module-lexer@^0.9.0: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" + es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" @@ -5610,16 +5646,16 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== +escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -5662,21 +5698,29 @@ eslint-config-react-app@^7.0.1: eslint-plugin-react-hooks "^4.3.0" eslint-plugin-testing-library "^5.0.1" -eslint-import-resolver-node@^0.3.6: - version "0.3.6" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" - integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== +eslint-import-resolver-node@^0.3.7: + version "0.3.7" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7" + integrity sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA== dependencies: debug "^3.2.7" - resolve "^1.20.0" + is-core-module "^2.11.0" + resolve "^1.22.1" -eslint-module-utils@^2.7.3: +eslint-module-utils@^2.7.4: version "2.7.4" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== dependencies: debug "^3.2.7" +eslint-plugin-detox@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-detox/-/eslint-plugin-detox-1.0.0.tgz#2d9c0130e8ebc4ced56efb6eeaf0d0f5c163398d" + integrity sha512-Dd+Cwyap5IO9DBKXOKrQTE1RQk9hvSSi+qsS1cMVPZY37mojz2PvriEOfGhKj5XN1G14lJ8TArf+6Y+Np2ZsoQ== + dependencies: + requireindex "~1.1.0" + eslint-plugin-eslint-comments@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz#9e1cd7b4413526abb313933071d7aba05ca12ffa" @@ -5693,7 +5737,7 @@ eslint-plugin-flowtype@^8.0.3: lodash "^4.17.21" string-natural-compare "^3.0.1" -eslint-plugin-ft-flow@^2.0.1: +eslint-plugin-ft-flow@^2.0.1, eslint-plugin-ft-flow@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/eslint-plugin-ft-flow/-/eslint-plugin-ft-flow-2.0.3.tgz#3b3c113c41902bcbacf0e22b536debcfc3c819e8" integrity sha512-Vbsd/b+LYA99jUbsL6viEUWShFaYQt2YQs3QN3f+aeszOhh2sgdcU0mjzDyD4yyBvMc8qy2uwvBBWfMzEX06tg== @@ -5702,22 +5746,24 @@ eslint-plugin-ft-flow@^2.0.1: string-natural-compare "^3.0.1" eslint-plugin-import@^2.25.3: - version "2.26.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b" - integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA== + version "2.27.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" + integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== dependencies: - array-includes "^3.1.4" - array.prototype.flat "^1.2.5" - debug "^2.6.9" + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + array.prototype.flatmap "^1.3.1" + debug "^3.2.7" doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.7.3" + eslint-import-resolver-node "^0.3.7" + eslint-module-utils "^2.7.4" has "^1.0.3" - is-core-module "^2.8.1" + is-core-module "^2.11.0" is-glob "^4.0.3" minimatch "^3.1.2" - object.values "^1.1.5" - resolve "^1.22.0" + object.values "^1.1.6" + resolve "^1.22.1" + semver "^6.3.0" tsconfig-paths "^3.14.1" eslint-plugin-jest@^25.3.0: @@ -5735,22 +5781,25 @@ eslint-plugin-jest@^26.5.3: "@typescript-eslint/utils" "^5.10.0" eslint-plugin-jsx-a11y@^6.5.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz#93736fc91b83fdc38cc8d115deedfc3091aef1ff" - integrity sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q== + version "6.7.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz#fca5e02d115f48c9a597a6894d5bcec2f7a76976" + integrity sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA== dependencies: - "@babel/runtime" "^7.18.9" - aria-query "^4.2.2" - array-includes "^3.1.5" + "@babel/runtime" "^7.20.7" + aria-query "^5.1.3" + array-includes "^3.1.6" + array.prototype.flatmap "^1.3.1" ast-types-flow "^0.0.7" - axe-core "^4.4.3" - axobject-query "^2.2.0" + axe-core "^4.6.2" + axobject-query "^3.1.1" damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" has "^1.0.3" - jsx-ast-utils "^3.3.2" - language-tags "^1.0.5" + jsx-ast-utils "^3.3.3" + language-tags "=1.0.5" minimatch "^3.1.2" + object.entries "^1.1.6" + object.fromentries "^2.0.6" semver "^6.3.0" eslint-plugin-prettier@^4.2.1: @@ -5778,31 +5827,10 @@ eslint-plugin-react-native@^4.0.0: "@babel/traverse" "^7.7.4" eslint-plugin-react-native-globals "^0.1.1" -eslint-plugin-react@^7.27.1: - version "7.31.11" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.11.tgz#011521d2b16dcf95795df688a4770b4eaab364c8" - integrity sha512-TTvq5JsT5v56wPa9OYHzsrOlHzKZKjV+aLgS+55NJP/cuzdiQPC7PfYoUjMoxlffKtvijpk7vA/jmuqRb9nohw== - dependencies: - array-includes "^3.1.6" - array.prototype.flatmap "^1.3.1" - array.prototype.tosorted "^1.1.1" - doctrine "^2.1.0" - estraverse "^5.3.0" - jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.1.2" - object.entries "^1.1.6" - object.fromentries "^2.0.6" - object.hasown "^1.1.2" - object.values "^1.1.6" - prop-types "^15.8.1" - resolve "^2.0.0-next.3" - semver "^6.3.0" - string.prototype.matchall "^4.0.8" - -eslint-plugin-react@^7.30.1: - version "7.32.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.32.1.tgz#88cdeb4065da8ca0b64e1274404f53a0f9890200" - integrity sha512-vOjdgyd0ZHBXNsmvU+785xY8Bfe57EFbTYYk8XrROzWpr9QBvpjITvAXt9xqcE6+8cjR/g1+mfumPToxsl1www== +eslint-plugin-react@^7.27.1, eslint-plugin-react@^7.30.1: + version "7.32.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz#e71f21c7c265ebce01bcbc9d0955170c55571f10" + integrity sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg== dependencies: array-includes "^3.1.6" array.prototype.flatmap "^1.3.1" @@ -5821,11 +5849,11 @@ eslint-plugin-react@^7.30.1: string.prototype.matchall "^4.0.8" eslint-plugin-testing-library@^5.0.1: - version "5.9.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.9.1.tgz#12e4bd34c48683ee98af4df2e3318ec9f51dcf8a" - integrity sha512-6BQp3tmb79jLLasPHJmy8DnxREe+2Pgf7L+7o09TSWPfdqqtQfRZmZNetr5mOs3yqZk/MRNxpN3RUpJe0wB4LQ== + version "5.10.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.10.2.tgz#12f231ad9b52b6aef45c801fd00aa129a932e0c2" + integrity sha512-f1DmDWcz5SDM+IpCkEX0lbFqrrTs8HRsEElzDEqN/EBI0hpRj8Cns5+IVANXswE8/LeybIJqPAOQIFu2j5Y5sw== dependencies: - "@typescript-eslint/utils" "^5.13.0" + "@typescript-eslint/utils" "^5.43.0" eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" @@ -5871,55 +5899,10 @@ eslint-webpack-plugin@^3.1.1: normalize-path "^3.0.0" schema-utils "^4.0.0" -eslint@^8.19.0: - version "8.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.32.0.tgz#d9690056bb6f1a302bd991e7090f5b68fbaea861" - integrity sha512-nETVXpnthqKPFyuY2FNjz/bEd6nbosRgKbkgS/y1C7LJop96gYHWpiguLecMHQ2XCPxn77DS0P+68WzG6vkZSQ== - dependencies: - "@eslint/eslintrc" "^1.4.1" - "@humanwhocodes/config-array" "^0.11.8" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.4.0" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-sdsl "^4.1.4" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - -eslint@^8.3.0: - version "8.31.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.31.0.tgz#75028e77cbcff102a9feae1d718135931532d524" - integrity sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA== +eslint@^8.19.0, eslint@^8.3.0: + version "8.34.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.34.0.tgz#fe0ab0ef478104c1f9ebc5537e303d25a8fb22d6" + integrity sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg== dependencies: "@eslint/eslintrc" "^1.4.1" "@humanwhocodes/config-array" "^0.11.8" @@ -5976,9 +5959,9 @@ esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + version "1.4.2" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.2.tgz#c6d3fee05dd665808e2ad870631f221f5617b1d1" + integrity sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng== dependencies: estraverse "^5.1.0" @@ -6014,6 +5997,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +event-pubsub@4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/event-pubsub/-/event-pubsub-4.3.0.tgz#f68d816bc29f1ec02c539dc58c8dd40ce72cb36e" + integrity sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ== + event-target-shim@^5.0.0, event-target-shim@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -6090,16 +6078,16 @@ expect@^27.5.1: jest-matcher-utils "^27.5.1" jest-message-util "^27.5.1" -expect@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.3.1.tgz#92877aad3f7deefc2e3f6430dd195b92295554a6" - integrity sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA== +expect@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.4.3.tgz#5e47757316df744fe3b8926c3ae8a3ebdafff7fe" + integrity sha512-uC05+Q7eXECFpgDrHdXA4k2rpMyStAYPItEDLyQDo5Ta7fVkJnNA/4zh/OIVkVVNZ1oOK1PipQoyNjuZ6sz6Dg== dependencies: - "@jest/expect-utils" "^29.3.1" - jest-get-type "^29.2.0" - jest-matcher-utils "^29.3.1" - jest-message-util "^29.3.1" - jest-util "^29.3.1" + "@jest/expect-utils" "^29.4.3" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.4.3" + jest-message-util "^29.4.3" + jest-util "^29.4.3" express-async-errors@^3.1.1: version "3.1.1" @@ -6230,12 +6218,12 @@ fast-redact@^3.1.1: resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa" integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw== -fast-url-parser@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" - integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ== +fast-xml-parser@^4.0.12: + version "4.1.2" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.1.2.tgz#5a98c18238d28a57bbdfa9fe4cda01211fff8f4a" + integrity sha512-CDYeykkle1LiA/uqQyNwYpFbyF6Axec6YapmpUP+/RHWIoR1zKjocdvNaTsxCxZzQ6v9MLXaSYm9Qq0thv0DHg== dependencies: - punycode "^1.3.2" + strnum "^1.0.5" fastest-levenshtein@^1.0.12: version "1.0.16" @@ -6422,15 +6410,20 @@ flat-cache@^3.0.4: flatted "^3.1.0" rimraf "^3.0.2" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + flatted@^3.1.0: version "3.2.7" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== flow-parser@0.*: - version "0.196.3" - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.196.3.tgz#dd923f29a6c194770a4f999f8026ef1da79d428b" - integrity sha512-R8wj12eHW6og+IBWeRS6aihkdac1Prh4zw1bfxtt/aeu8r5OFmQEZjnmINcjO/5Q+OKvI4Eg367ygz2SHvtH+w== + version "0.200.0" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.200.0.tgz#7cbdd4cace2843e2fd6098df68247b3876f55917" + integrity sha512-01eYYVlCEO2p6JexhTbMbDNPeDKQZV++SJbaHE/HzypTkV83oJmsJQVjVCGD8FYWiQ3ha9ALD/AHwsFhPwnZJA== flow-parser@^0.185.0: version "0.185.2" @@ -6442,6 +6435,13 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.4, follow-redirects@^1.14.9: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -6520,6 +6520,15 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" + integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -6574,6 +6583,11 @@ functions-have-names@^1.2.2: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +funpermaproxy@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/funpermaproxy/-/funpermaproxy-1.1.0.tgz#39cb0b8bea908051e4608d8a414f1d87b55bf557" + integrity sha512-2Sp1hWuO8m5fqeFDusyhKqYPT+7rGLw34N3qonDcdRP8+n7M7Gl/yKp/q7oCxnnJ6pWCectOmLFJpsMU/++KrQ== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -6584,10 +6598,10 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" - integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" + integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== dependencies: function-bind "^1.1.1" has "^1.0.3" @@ -6664,6 +6678,17 @@ glob@7.0.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + integrity sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A== + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -6709,13 +6734,13 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.19.0: - version "13.19.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.19.0.tgz#7a42de8e6ad4f7242fbcca27ea5b23aca367b5c8" - integrity sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ== + version "13.20.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" + integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== dependencies: type-fest "^0.20.2" -globalthis@^1.0.2: +globalthis@^1.0.2, globalthis@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== @@ -6802,6 +6827,11 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -7099,9 +7129,9 @@ image-size@^0.6.0: integrity sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA== immer@^9.0.7: - version "9.0.17" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.17.tgz#7cfe8fbb8b461096444e9da7a5ec4a67c6c4adf4" - integrity sha512-+hBruaLSQvkPfxRiTLK/mi4vLH+/VQS6z2KJahdoxlleFOI8ARqzOF17uy12eFDlqWmPoygwc5evgwcp+dlHhg== + version "9.0.19" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.19.tgz#67fb97310555690b5f9cd8380d38fc0aabb6b38b" + integrity sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ== import-fresh@^2.0.0: version "2.0.0" @@ -7155,7 +7185,7 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== -ini@^1.3.5, ini@~1.3.0: +ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -7169,11 +7199,11 @@ inline-style-prefixer@^6.0.1: fast-loops "^1.1.3" internal-slot@^1.0.3, internal-slot@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" - integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== + version "1.0.5" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" + integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== dependencies: - get-intrinsic "^1.1.3" + get-intrinsic "^1.2.0" has "^1.0.3" side-channel "^1.0.4" @@ -7182,7 +7212,7 @@ interpret@^3.1.1: resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== -invariant@*, invariant@^2.2.4: +invariant@*, invariant@2.2.4, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -7226,6 +7256,15 @@ is-arguments@^1.1.1: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-array-buffer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a" + integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-typed-array "^1.1.10" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -7263,12 +7302,12 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.2.7: +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.8.1, is-core-module@^2.9.0: +is-core-module@^2.11.0, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== @@ -7289,7 +7328,7 @@ is-data-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-date-object@^1.0.1: +is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== @@ -7368,7 +7407,7 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== -is-map@^2.0.2: +is-map@^2.0.1, is-map@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== @@ -7452,7 +7491,7 @@ is-root@^2.1.0: resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== -is-set@^2.0.2: +is-set@^2.0.1, is-set@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== @@ -7488,6 +7527,17 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" +is-typed-array@^1.1.10, is-typed-array@^1.1.9: + version "1.1.10" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" + integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -7498,6 +7548,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" @@ -7505,6 +7560,14 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +is-weakset@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" + integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -7596,19 +7659,6 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -iterate-iterator@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.2.tgz#551b804c9eaa15b847ea6a7cdc2f5bf1ec150f91" - integrity sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw== - -iterate-value@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57" - integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ== - dependencies: - es-get-iterator "^1.0.2" - iterate-iterator "^1.0.1" - jake@^10.8.5: version "10.8.5" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" @@ -7628,10 +7678,10 @@ jest-changed-files@^27.5.1: execa "^5.0.0" throat "^6.0.1" -jest-changed-files@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.2.0.tgz#b6598daa9803ea6a4dce7968e20ab380ddbee289" - integrity sha512-qPVmLLyBmvF5HJrY7krDisx6Voi8DmlV3GZYX0aFNbaQsZeoz1hfxcCMbqDGuQCxU1dJy9eYc2xscE8QrCCYaA== +jest-changed-files@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.4.3.tgz#7961fe32536b9b6d5c28dfa0abcfab31abcf50a7" + integrity sha512-Vn5cLuWuwmi2GNNbokPOEcvrXGSGrqVnPEZV7rC6P7ck07Dyw9RFnvWglnupSh+hGys0ajGtw/bc2ZgweljQoQ== dependencies: execa "^5.0.0" p-limit "^3.1.0" @@ -7661,28 +7711,28 @@ jest-circus@^27.5.1: stack-utils "^2.0.3" throat "^6.0.1" -jest-circus@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.3.1.tgz#177d07c5c0beae8ef2937a67de68f1e17bbf1b4a" - integrity sha512-wpr26sEvwb3qQQbdlmei+gzp6yoSSoSL6GsLPxnuayZSMrSd5Ka7IjAvatpIernBvT2+Ic6RLTg+jSebScmasg== +jest-circus@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.4.3.tgz#fff7be1cf5f06224dd36a857d52a9efeb005ba04" + integrity sha512-Vw/bVvcexmdJ7MLmgdT3ZjkJ3LKu8IlpefYokxiqoZy6OCQ2VAm6Vk3t/qHiAGUXbdbJKJWnc8gH3ypTbB/OBw== dependencies: - "@jest/environment" "^29.3.1" - "@jest/expect" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/environment" "^29.4.3" + "@jest/expect" "^29.4.3" + "@jest/test-result" "^29.4.3" + "@jest/types" "^29.4.3" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" dedent "^0.7.0" is-generator-fn "^2.0.0" - jest-each "^29.3.1" - jest-matcher-utils "^29.3.1" - jest-message-util "^29.3.1" - jest-runtime "^29.3.1" - jest-snapshot "^29.3.1" - jest-util "^29.3.1" + jest-each "^29.4.3" + jest-matcher-utils "^29.4.3" + jest-message-util "^29.4.3" + jest-runtime "^29.4.3" + jest-snapshot "^29.4.3" + jest-util "^29.4.3" p-limit "^3.1.0" - pretty-format "^29.3.1" + pretty-format "^29.4.3" slash "^3.0.0" stack-utils "^2.0.3" @@ -7704,21 +7754,21 @@ jest-cli@^27.5.1: prompts "^2.0.1" yargs "^16.2.0" -jest-cli@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.3.1.tgz#e89dff427db3b1df50cea9a393ebd8640790416d" - integrity sha512-TO/ewvwyvPOiBBuWZ0gm04z3WWP8TIK8acgPzE4IxgsLKQgb377NYGrQLc3Wl/7ndWzIH2CDNNsUjGxwLL43VQ== +jest-cli@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.4.3.tgz#fe31fdd0c90c765f392b8b7c97e4845071cd2163" + integrity sha512-PiiAPuFNfWWolCE6t3ZrDXQc6OsAuM3/tVW0u27UWc1KE+n/HSn5dSE6B2juqN7WP+PP0jAcnKtGmI4u8GMYCg== dependencies: - "@jest/core" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/core" "^29.4.3" + "@jest/test-result" "^29.4.3" + "@jest/types" "^29.4.3" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.9" import-local "^3.0.2" - jest-config "^29.3.1" - jest-util "^29.3.1" - jest-validate "^29.3.1" + jest-config "^29.4.3" + jest-util "^29.4.3" + jest-validate "^29.4.3" prompts "^2.0.1" yargs "^17.3.1" @@ -7752,31 +7802,31 @@ jest-config@^27.5.1: slash "^3.0.0" strip-json-comments "^3.1.1" -jest-config@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.3.1.tgz#0bc3dcb0959ff8662957f1259947aedaefb7f3c6" - integrity sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg== +jest-config@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.4.3.tgz#fca9cdfe6298ae6d04beef1624064d455347c978" + integrity sha512-eCIpqhGnIjdUCXGtLhz4gdDoxKSWXKjzNcc5r+0S1GKOp2fwOipx5mRcwa9GB/ArsxJ1jlj2lmlD9bZAsBxaWQ== dependencies: "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.3.1" - "@jest/types" "^29.3.1" - babel-jest "^29.3.1" + "@jest/test-sequencer" "^29.4.3" + "@jest/types" "^29.4.3" + babel-jest "^29.4.3" chalk "^4.0.0" ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.3" graceful-fs "^4.2.9" - jest-circus "^29.3.1" - jest-environment-node "^29.3.1" - jest-get-type "^29.2.0" - jest-regex-util "^29.2.0" - jest-resolve "^29.3.1" - jest-runner "^29.3.1" - jest-util "^29.3.1" - jest-validate "^29.3.1" + jest-circus "^29.4.3" + jest-environment-node "^29.4.3" + jest-get-type "^29.4.3" + jest-regex-util "^29.4.3" + jest-resolve "^29.4.3" + jest-runner "^29.4.3" + jest-util "^29.4.3" + jest-validate "^29.4.3" micromatch "^4.0.4" parse-json "^5.2.0" - pretty-format "^29.3.1" + pretty-format "^29.4.3" slash "^3.0.0" strip-json-comments "^3.1.1" @@ -7800,15 +7850,15 @@ jest-diff@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" -jest-diff@^29.0.1, jest-diff@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.3.1.tgz#d8215b72fed8f1e647aed2cae6c752a89e757527" - integrity sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw== +jest-diff@^29.0.1, jest-diff@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.4.3.tgz#42f4eb34d0bf8c0fb08b0501069b87e8e84df347" + integrity sha512-YB+ocenx7FZ3T5O9lMVMeLYV4265socJKtkwgk/6YUz/VsEzYDkiMuMhWzZmxm3wDRQvayJu/PjkjjSkjoHsCA== dependencies: chalk "^4.0.0" - diff-sequences "^29.3.1" - jest-get-type "^29.2.0" - pretty-format "^29.3.1" + diff-sequences "^29.4.3" + jest-get-type "^29.4.3" + pretty-format "^29.4.3" jest-docblock@^27.5.1: version "27.5.1" @@ -7817,10 +7867,10 @@ jest-docblock@^27.5.1: dependencies: detect-newline "^3.0.0" -jest-docblock@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.2.0.tgz#307203e20b637d97cee04809efc1d43afc641e82" - integrity sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A== +jest-docblock@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8" + integrity sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg== dependencies: detect-newline "^3.0.0" @@ -7835,16 +7885,16 @@ jest-each@^27.5.1: jest-util "^27.5.1" pretty-format "^27.5.1" -jest-each@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.3.1.tgz#bc375c8734f1bb96625d83d1ca03ef508379e132" - integrity sha512-qrZH7PmFB9rEzCSl00BWjZYuS1BSOH8lLuC0azQE9lQrAx3PWGKHTDudQiOSwIy5dGAJh7KA0ScYlCP7JxvFYA== +jest-each@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.4.3.tgz#a434c199a2f6151c5e3dc80b2d54586bdaa72819" + integrity sha512-1ElHNAnKcbJb/b+L+7j0/w7bDvljw4gTv1wL9fYOczeJrbTbkMGQ5iQPFJ3eFQH19VPTx1IyfePdqSpePKss7Q== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.3" chalk "^4.0.0" - jest-get-type "^29.2.0" - jest-util "^29.3.1" - pretty-format "^29.3.1" + jest-get-type "^29.4.3" + jest-util "^29.4.3" + pretty-format "^29.4.3" jest-environment-jsdom@^27.5.1: version "27.5.1" @@ -7871,17 +7921,17 @@ jest-environment-node@^27.5.1: jest-mock "^27.5.1" jest-util "^27.5.1" -jest-environment-node@^29.2.1, jest-environment-node@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.3.1.tgz#5023b32472b3fba91db5c799a0d5624ad4803e74" - integrity sha512-xm2THL18Xf5sIHoU7OThBPtuH6Lerd+Y1NLYiZJlkE3hbE+7N7r8uvHIl/FkZ5ymKXJe/11SQuf3fv4v6rUMag== +jest-environment-node@^29.2.1, jest-environment-node@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.4.3.tgz#579c4132af478befc1889ddc43c2413a9cdbe014" + integrity sha512-gAiEnSKF104fsGDXNkwk49jD/0N0Bqu2K9+aMQXA6avzsA9H3Fiv1PW2D+gzbOSR705bWd2wJZRFEFpV0tXISg== dependencies: - "@jest/environment" "^29.3.1" - "@jest/fake-timers" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/environment" "^29.4.3" + "@jest/fake-timers" "^29.4.3" + "@jest/types" "^29.4.3" "@types/node" "*" - jest-mock "^29.3.1" - jest-util "^29.3.1" + jest-mock "^29.4.3" + jest-util "^29.4.3" jest-get-type@^26.3.0: version "26.3.0" @@ -7893,10 +7943,10 @@ jest-get-type@^27.5.1: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== -jest-get-type@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.2.0.tgz#726646f927ef61d583a3b3adb1ab13f3a5036408" - integrity sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA== +jest-get-type@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" + integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== jest-haste-map@^27.5.1: version "27.5.1" @@ -7918,20 +7968,20 @@ jest-haste-map@^27.5.1: optionalDependencies: fsevents "^2.3.2" -jest-haste-map@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.3.1.tgz#af83b4347f1dae5ee8c2fb57368dc0bb3e5af843" - integrity sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A== +jest-haste-map@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.4.3.tgz#085a44283269e7ace0645c63a57af0d2af6942e2" + integrity sha512-eZIgAS8tvm5IZMtKlR8Y+feEOMfo2pSQkmNbufdbMzMSn9nitgGxF1waM/+LbryO3OkMcKS98SUb+j/cQxp/vQ== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.3" "@types/graceful-fs" "^4.1.3" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.9" - jest-regex-util "^29.2.0" - jest-util "^29.3.1" - jest-worker "^29.3.1" + jest-regex-util "^29.4.3" + jest-util "^29.4.3" + jest-worker "^29.4.3" micromatch "^4.0.4" walker "^1.0.8" optionalDependencies: @@ -7978,13 +8028,13 @@ jest-leak-detector@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" -jest-leak-detector@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.3.1.tgz#95336d020170671db0ee166b75cd8ef647265518" - integrity sha512-3DA/VVXj4zFOPagGkuqHnSQf1GZBmmlagpguxEERO6Pla2g84Q1MaVIB3YMxgUaFIaYag8ZnTyQgiZ35YEqAQA== +jest-leak-detector@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.4.3.tgz#2b35191d6b35aa0256e63a9b79b0f949249cf23a" + integrity sha512-9yw4VC1v2NspMMeV3daQ1yXPNxMgCzwq9BocCwYrRgXe4uaEJPAN0ZK37nFBhcy3cUwEVstFecFLaTHpF7NiGA== dependencies: - jest-get-type "^29.2.0" - pretty-format "^29.3.1" + jest-get-type "^29.4.3" + pretty-format "^29.4.3" jest-matcher-utils@^27.5.1: version "27.5.1" @@ -7996,15 +8046,15 @@ jest-matcher-utils@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" -jest-matcher-utils@^29.0.1, jest-matcher-utils@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.3.1.tgz#6e7f53512f80e817dfa148672bd2d5d04914a572" - integrity sha512-fkRMZUAScup3txIKfMe3AIZZmPEjWEdsPJFK3AIy5qRohWqQFg1qrmKfYXR9qEkNc7OdAu2N4KPHibEmy4HPeQ== +jest-matcher-utils@^29.0.1, jest-matcher-utils@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.4.3.tgz#ea68ebc0568aebea4c4213b99f169ff786df96a0" + integrity sha512-TTciiXEONycZ03h6R6pYiZlSkvYgT0l8aa49z/DLSGYjex4orMUcafuLXYyyEDWB1RKglq00jzwY00Ei7yFNVg== dependencies: chalk "^4.0.0" - jest-diff "^29.3.1" - jest-get-type "^29.2.0" - pretty-format "^29.3.1" + jest-diff "^29.4.3" + jest-get-type "^29.4.3" + pretty-format "^29.4.3" jest-message-util@^27.5.1: version "27.5.1" @@ -8036,18 +8086,18 @@ jest-message-util@^28.1.3: slash "^3.0.0" stack-utils "^2.0.3" -jest-message-util@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.3.1.tgz#37bc5c468dfe5120712053dd03faf0f053bd6adb" - integrity sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA== +jest-message-util@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.4.3.tgz#65b5280c0fdc9419503b49d4f48d4999d481cb5b" + integrity sha512-1Y8Zd4ZCN7o/QnWdMmT76If8LuDv23Z1DRovBj/vcSFNlGCJGoO8D1nJDw1AdyAGUk0myDLFGN5RbNeJyCRGCw== dependencies: "@babel/code-frame" "^7.12.13" - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.3" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.9" micromatch "^4.0.4" - pretty-format "^29.3.1" + pretty-format "^29.4.3" slash "^3.0.0" stack-utils "^2.0.3" @@ -8059,14 +8109,14 @@ jest-mock@^27.5.1: "@jest/types" "^27.5.1" "@types/node" "*" -jest-mock@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.3.1.tgz#60287d92e5010979d01f218c6b215b688e0f313e" - integrity sha512-H8/qFDtDVMFvFP4X8NuOT3XRDzOUTz+FeACjufHzsOIBAxivLqkB1PoLCaJx9iPPQ8dZThHPp/G3WRWyMgA3JA== +jest-mock@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.4.3.tgz#23d84a20a74cdfff0510fdbeefb841ed57b0fe7e" + integrity sha512-LjFgMg+xed9BdkPMyIJh+r3KeHt1klXPJYBULXVVAkbTaaKjPX1o1uVCAZADMEp/kOxGTwy/Ot8XbvgItOrHEg== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.3" "@types/node" "*" - jest-util "^29.3.1" + jest-util "^29.4.3" jest-pnp-resolver@^1.2.2: version "1.2.3" @@ -8083,10 +8133,10 @@ jest-regex-util@^28.0.0: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-28.0.2.tgz#afdc377a3b25fb6e80825adcf76c854e5bf47ead" integrity sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw== -jest-regex-util@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.2.0.tgz#82ef3b587e8c303357728d0322d48bbfd2971f7b" - integrity sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA== +jest-regex-util@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8" + integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg== jest-resolve-dependencies@^27.5.1: version "27.5.1" @@ -8097,13 +8147,13 @@ jest-resolve-dependencies@^27.5.1: jest-regex-util "^27.5.1" jest-snapshot "^27.5.1" -jest-resolve-dependencies@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.3.1.tgz#a6a329708a128e68d67c49f38678a4a4a914c3bf" - integrity sha512-Vk0cYq0byRw2WluNmNWGqPeRnZ3p3hHmjJMp2dyyZeYIfiBskwq4rpiuGFR6QGAdbj58WC7HN4hQHjf2mpvrLA== +jest-resolve-dependencies@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.4.3.tgz#9ad7f23839a6d88cef91416bda9393a6e9fd1da5" + integrity sha512-uvKMZAQ3nmXLH7O8WAOhS5l0iWyT3WmnJBdmIHiV5tBbdaDZ1wqtNX04FONGoaFvSOSHBJxnwAVnSn1WHdGVaw== dependencies: - jest-regex-util "^29.2.0" - jest-snapshot "^29.3.1" + jest-regex-util "^29.4.3" + jest-snapshot "^29.4.3" jest-resolve@^27.4.2, jest-resolve@^27.5.1: version "27.5.1" @@ -8121,19 +8171,19 @@ jest-resolve@^27.4.2, jest-resolve@^27.5.1: resolve.exports "^1.1.0" slash "^3.0.0" -jest-resolve@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.3.1.tgz#9a4b6b65387a3141e4a40815535c7f196f1a68a7" - integrity sha512-amXJgH/Ng712w3Uz5gqzFBBjxV8WFLSmNjoreBGMqxgCz5cH7swmBZzgBaCIOsvb0NbpJ0vgaSFdJqMdT+rADw== +jest-resolve@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.4.3.tgz#3c5b5c984fa8a763edf9b3639700e1c7900538e2" + integrity sha512-GPokE1tzguRyT7dkxBim4wSx6E45S3bOQ7ZdKEG+Qj0Oac9+6AwJPCk0TZh5Vu0xzeX4afpb+eDmgbmZFFwpOw== dependencies: chalk "^4.0.0" graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" + jest-haste-map "^29.4.3" jest-pnp-resolver "^1.2.2" - jest-util "^29.3.1" - jest-validate "^29.3.1" + jest-util "^29.4.3" + jest-validate "^29.4.3" resolve "^1.20.0" - resolve.exports "^1.1.0" + resolve.exports "^2.0.0" slash "^3.0.0" jest-runner@^27.5.1: @@ -8163,30 +8213,30 @@ jest-runner@^27.5.1: source-map-support "^0.5.6" throat "^6.0.1" -jest-runner@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.3.1.tgz#a92a879a47dd096fea46bb1517b0a99418ee9e2d" - integrity sha512-oFvcwRNrKMtE6u9+AQPMATxFcTySyKfLhvso7Sdk/rNpbhg4g2GAGCopiInk1OP4q6gz3n6MajW4+fnHWlU3bA== +jest-runner@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.4.3.tgz#68dc82c68645eda12bea42b5beece6527d7c1e5e" + integrity sha512-GWPTEiGmtHZv1KKeWlTX9SIFuK19uLXlRQU43ceOQ2hIfA5yPEJC7AMkvFKpdCHx6pNEdOD+2+8zbniEi3v3gA== dependencies: - "@jest/console" "^29.3.1" - "@jest/environment" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/console" "^29.4.3" + "@jest/environment" "^29.4.3" + "@jest/test-result" "^29.4.3" + "@jest/transform" "^29.4.3" + "@jest/types" "^29.4.3" "@types/node" "*" chalk "^4.0.0" emittery "^0.13.1" graceful-fs "^4.2.9" - jest-docblock "^29.2.0" - jest-environment-node "^29.3.1" - jest-haste-map "^29.3.1" - jest-leak-detector "^29.3.1" - jest-message-util "^29.3.1" - jest-resolve "^29.3.1" - jest-runtime "^29.3.1" - jest-util "^29.3.1" - jest-watcher "^29.3.1" - jest-worker "^29.3.1" + jest-docblock "^29.4.3" + jest-environment-node "^29.4.3" + jest-haste-map "^29.4.3" + jest-leak-detector "^29.4.3" + jest-message-util "^29.4.3" + jest-resolve "^29.4.3" + jest-runtime "^29.4.3" + jest-util "^29.4.3" + jest-watcher "^29.4.3" + jest-worker "^29.4.3" p-limit "^3.1.0" source-map-support "0.5.13" @@ -8218,31 +8268,31 @@ jest-runtime@^27.5.1: slash "^3.0.0" strip-bom "^4.0.0" -jest-runtime@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.3.1.tgz#21efccb1a66911d6d8591276a6182f520b86737a" - integrity sha512-jLzkIxIqXwBEOZx7wx9OO9sxoZmgT2NhmQKzHQm1xwR1kNW/dn0OjxR424VwHHf1SPN6Qwlb5pp1oGCeFTQ62A== +jest-runtime@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.4.3.tgz#f25db9874dcf35a3ab27fdaabca426666cc745bf" + integrity sha512-F5bHvxSH+LvLV24vVB3L8K467dt3y3dio6V3W89dUz9nzvTpqd/HcT9zfYKL2aZPvD63vQFgLvaUX/UpUhrP6Q== dependencies: - "@jest/environment" "^29.3.1" - "@jest/fake-timers" "^29.3.1" - "@jest/globals" "^29.3.1" - "@jest/source-map" "^29.2.0" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/environment" "^29.4.3" + "@jest/fake-timers" "^29.4.3" + "@jest/globals" "^29.4.3" + "@jest/source-map" "^29.4.3" + "@jest/test-result" "^29.4.3" + "@jest/transform" "^29.4.3" + "@jest/types" "^29.4.3" "@types/node" "*" chalk "^4.0.0" cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" glob "^7.1.3" graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" - jest-message-util "^29.3.1" - jest-mock "^29.3.1" - jest-regex-util "^29.2.0" - jest-resolve "^29.3.1" - jest-snapshot "^29.3.1" - jest-util "^29.3.1" + jest-haste-map "^29.4.3" + jest-message-util "^29.4.3" + jest-mock "^29.4.3" + jest-regex-util "^29.4.3" + jest-resolve "^29.4.3" + jest-snapshot "^29.4.3" + jest-util "^29.4.3" slash "^3.0.0" strip-bom "^4.0.0" @@ -8282,10 +8332,10 @@ jest-snapshot@^27.5.1: pretty-format "^27.5.1" semver "^7.3.2" -jest-snapshot@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.3.1.tgz#17bcef71a453adc059a18a32ccbd594b8cc4e45e" - integrity sha512-+3JOc+s28upYLI2OJM4PWRGK9AgpsMs/ekNryUV0yMBClT9B1DF2u2qay8YxcQd338PPYSFNb0lsar1B49sLDA== +jest-snapshot@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.4.3.tgz#183d309371450d9c4a3de7567ed2151eb0e91145" + integrity sha512-NGlsqL0jLPDW91dz304QTM/SNO99lpcSYYAjNiX0Ou+sSGgkanKBcSjCfp/pqmiiO1nQaOyLp6XQddAzRcx3Xw== dependencies: "@babel/core" "^7.11.6" "@babel/generator" "^7.7.2" @@ -8293,23 +8343,23 @@ jest-snapshot@^29.3.1: "@babel/plugin-syntax-typescript" "^7.7.2" "@babel/traverse" "^7.7.2" "@babel/types" "^7.3.3" - "@jest/expect-utils" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/expect-utils" "^29.4.3" + "@jest/transform" "^29.4.3" + "@jest/types" "^29.4.3" "@types/babel__traverse" "^7.0.6" "@types/prettier" "^2.1.5" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^29.3.1" + expect "^29.4.3" graceful-fs "^4.2.9" - jest-diff "^29.3.1" - jest-get-type "^29.2.0" - jest-haste-map "^29.3.1" - jest-matcher-utils "^29.3.1" - jest-message-util "^29.3.1" - jest-util "^29.3.1" + jest-diff "^29.4.3" + jest-get-type "^29.4.3" + jest-haste-map "^29.4.3" + jest-matcher-utils "^29.4.3" + jest-message-util "^29.4.3" + jest-util "^29.4.3" natural-compare "^1.4.0" - pretty-format "^29.3.1" + pretty-format "^29.4.3" semver "^7.3.5" jest-util@^27.2.0, jest-util@^27.5.1: @@ -8336,12 +8386,12 @@ jest-util@^28.1.3: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-util@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.3.1.tgz#1dda51e378bbcb7e3bc9d8ab651445591ed373e1" - integrity sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ== +jest-util@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.4.3.tgz#851a148e23fc2b633c55f6dad2e45d7f4579f496" + integrity sha512-ToSGORAz4SSSoqxDSylWX8JzkOQR7zoBtNRsA7e+1WUX5F8jrOwaNpuh1YfJHJKDHXLHmObv5eOjejUd+/Ws+Q== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.3" "@types/node" "*" chalk "^4.0.0" ci-info "^3.2.0" @@ -8372,17 +8422,17 @@ jest-validate@^27.5.1: leven "^3.1.0" pretty-format "^27.5.1" -jest-validate@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.3.1.tgz#d56fefaa2e7d1fde3ecdc973c7f7f8f25eea704a" - integrity sha512-N9Lr3oYR2Mpzuelp1F8negJR3YE+L1ebk1rYA5qYo9TTY3f9OWdptLoNSPP9itOCBIRBqjt/S5XHlzYglLN67g== +jest-validate@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.4.3.tgz#a13849dec4f9e95446a7080ad5758f58fa88642f" + integrity sha512-J3u5v7aPQoXPzaar6GndAVhdQcZr/3osWSgTeKg5v574I9ybX/dTyH0AJFb5XgXIB7faVhf+rS7t4p3lL9qFaw== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.3" camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^29.2.0" + jest-get-type "^29.4.3" leven "^3.1.0" - pretty-format "^29.3.1" + pretty-format "^29.4.3" jest-watch-typeahead@^1.0.0: version "1.1.0" @@ -8424,18 +8474,18 @@ jest-watcher@^28.0.0: jest-util "^28.1.3" string-length "^4.0.1" -jest-watcher@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.3.1.tgz#3341547e14fe3c0f79f9c3a4c62dbc3fc977fd4a" - integrity sha512-RspXG2BQFDsZSRKGCT/NiNa8RkQ1iKAjrO0//soTMWx/QUt+OcxMqMSBxz23PYGqUuWm2+m2mNNsmj0eIoOaFg== +jest-watcher@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.4.3.tgz#e503baa774f0c2f8f3c8db98a22ebf885f19c384" + integrity sha512-zwlXH3DN3iksoIZNk73etl1HzKyi5FuQdYLnkQKm5BW4n8HpoG59xSwpVdFrnh60iRRaRBGw0gcymIxjJENPcA== dependencies: - "@jest/test-result" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/test-result" "^29.4.3" + "@jest/types" "^29.4.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" emittery "^0.13.1" - jest-util "^29.3.1" + jest-util "^29.4.3" string-length "^4.0.1" jest-worker@^26.2.1: @@ -8465,13 +8515,13 @@ jest-worker@^28.0.2: merge-stream "^2.0.0" supports-color "^8.0.0" -jest-worker@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.3.1.tgz#e9462161017a9bb176380d721cab022661da3d6b" - integrity sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw== +jest-worker@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.4.3.tgz#9a4023e1ea1d306034237c7133d7da4240e8934e" + integrity sha512-GLHN/GTAAMEy5BFdvpUfzr9Dr80zQqBrh0fz1mtRMe05hqP45+HfQltu7oTBfduD0UeZs09d+maFtFYAXFWvAA== dependencies: "@types/node" "*" - jest-util "^29.3.1" + jest-util "^29.4.3" merge-stream "^2.0.0" supports-color "^8.0.0" @@ -8485,35 +8535,47 @@ jest@^27.4.3: jest-cli "^27.5.1" jest@^29.2.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.3.1.tgz#c130c0d551ae6b5459b8963747fed392ddbde122" - integrity sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA== + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.4.3.tgz#1b8be541666c6feb99990fd98adac4737e6e6386" + integrity sha512-XvK65feuEFGZT8OO0fB/QAQS+LGHvQpaadkH5p47/j3Ocqq3xf2pK9R+G0GzgfuhXVxEv76qCOOcMb5efLk6PA== dependencies: - "@jest/core" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/core" "^29.4.3" + "@jest/types" "^29.4.3" import-local "^3.0.2" - jest-cli "^29.3.1" + jest-cli "^29.4.3" joi@^17.2.1: - version "17.7.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.7.0.tgz#591a33b1fe1aca2bc27f290bcad9b9c1c570a6b3" - integrity sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg== + version "17.8.3" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.8.3.tgz#d772fe27a87a5cda21aace5cf11eee8671ca7e6f" + integrity sha512-q5Fn6Tj/jR8PfrLrx4fpGH4v9qM6o+vDUfD4/3vxxyg34OmKcNqYZ1qn2mpLza96S8tL0p0rIw2gOZX+/cTg9w== dependencies: "@hapi/hoek" "^9.0.0" "@hapi/topo" "^5.0.0" "@sideway/address" "^4.1.3" - "@sideway/formula" "^3.0.0" + "@sideway/formula" "^3.0.1" "@sideway/pinpoint" "^2.0.0" js-base64@^3.7.2: - version "3.7.4" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.4.tgz#af95b20f23efc8034afd2d1cc5b9d0adf7419037" - integrity sha512-wpM/wi20Tl+3ifTyi0RdDckS4YTD4Lf953mBRrpG8547T7hInHNPEj8+ck4gB8VDcGyeAWFK++Wb/fU1BeavKQ== + version "3.7.5" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.5.tgz#21e24cf6b886f76d6f5f165bfcd69cc55b9e3fca" + integrity sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA== + +js-message@1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/js-message/-/js-message-1.0.7.tgz#fbddd053c7a47021871bb8b2c95397cc17c20e47" + integrity sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA== + +js-queue@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/js-queue/-/js-queue-2.0.2.tgz#0be590338f903b36c73d33c31883a821412cd482" + integrity sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA== + dependencies: + easy-stack "^1.0.1" js-sdsl@^4.1.4: - version "4.2.0" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0" - integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ== + version "4.3.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" + integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -8608,6 +8670,11 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +json-cycle@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/json-cycle/-/json-cycle-1.3.0.tgz#c4f6f7d926c2979012cba173b06f9cae9e866d3f" + integrity sha512-FD/SedD78LCdSvJaOUQAXseT8oQBb5z6IVYaQaCrVUlu9zOAr1BDdKyVYQaSD/GDsAMrXpKcOyBD4LIl8nfjHw== + json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -8645,7 +8712,7 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.1, json5@^2.1.2, json5@^2.2.0, json5@^2.2.1, json5@^2.2.2: +json5@^2.1.1, json5@^2.1.2, json5@^2.2.0, json5@^2.2.2: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -8687,7 +8754,7 @@ jsonwebtoken@^8.5.1: ms "^2.1.1" semver "^5.6.0" -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea" integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== @@ -8742,26 +8809,26 @@ kleur@^3.0.3: integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== klona@^2.0.4, klona@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" - integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== + version "2.0.6" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" + integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== kysely@^0.22.0: version "0.22.0" resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.22.0.tgz#8aac53942da3cadc604d7d154a746d983fe8f7b9" integrity sha512-ZE3qWtnqLOalodzfK5QUEcm7AEulhxsPNuKaGFsC3XiqO92vMLm+mAHk/NnbSIOtC4RmGm0nsv700i8KDp1gfQ== -language-subtag-registry@^0.3.20: +language-subtag-registry@~0.3.2: version "0.3.22" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== -language-tags@^1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.7.tgz#41cc248730f3f12a452c2e2efe32bc0bbce67967" - integrity sha512-bSytju1/657hFjgUzPAPqszxH62ouE8nQFoFaVlIQfne4wO/wXC9A4+m8jYve7YBBvi59eq0SUpcshvG8h5Usw== +language-tags@=1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" + integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== dependencies: - language-subtag-registry "^0.3.20" + language-subtag-registry "~0.3.2" leven@^3.1.0: version "3.1.0" @@ -8840,6 +8907,11 @@ lodash.chunk@^4.2.0: resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" integrity sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w== +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -8900,6 +8972,11 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== +lodash.shuffle@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.shuffle/-/lodash.shuffle-4.2.0.tgz#145b5053cf875f6f5c2a33f48b6e9948c6ec7b4b" + integrity sha512-V/rTAABKLFjoecTZjKSv+A1ZomG8hZg8hlgeG6wwQVD9AGv+10zqqSf6mFq2tVA703Zd5R0YhSuSlXA+E/Ei+Q== + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -8915,7 +8992,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash@^4.17.11, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -8951,6 +9028,14 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -9027,9 +9112,9 @@ media-typer@0.3.0: integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== memfs@^3.1.2, memfs@^3.4.3: - version "3.4.12" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.12.tgz#d00f8ad8dab132dc277c659dc85bfd14b07d03bd" - integrity sha512-BcjuQn6vfqP+k100e0E9m61Hyqa//Brp+I3f0OBmN0ATHlFA8vx3Lt8z57R3u2bPqe3WGDBC+nF72fTH7isyEw== + version "3.4.13" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.13.tgz#248a8bd239b3c240175cd5ec548de5227fc4f345" + integrity sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg== dependencies: fs-monkey "^1.0.3" @@ -9065,16 +9150,6 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -metro-babel-transformer@0.73.5: - version "0.73.5" - resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.73.5.tgz#e7ebe371cd8bf5df90b0e9153587b4d5089d2ce7" - integrity sha512-G3awAJ9of/R2jEg+MRokYcq/TNvMSxJipwybQ2NfwwSj5iLEmRH2YbwTx5w8f5qKgs2K4SS2pmBIs8qjdV6p3Q== - dependencies: - "@babel/core" "^7.14.0" - hermes-parser "0.8.0" - metro-source-map "0.73.5" - nullthrows "^1.1.1" - metro-babel-transformer@0.73.7: version "0.73.7" resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.73.7.tgz#561ffa0336eb6d7d112e7128e957114c729fdb71" @@ -9085,43 +9160,53 @@ metro-babel-transformer@0.73.7: metro-source-map "0.73.7" nullthrows "^1.1.1" -metro-cache-key@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.73.7.tgz#fa3b4ece5f3191ce238a623051a0d03bada2a153" - integrity sha512-GngYzrHwZU9U0Xl81H4aq9Tn5cjQyU12v9/flB0hzpeiYO5A89TIeilb4Kg8jtfC6JcmmsdK9nxYIGEq7odHhQ== - -metro-cache@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.73.7.tgz#dd2b6a791b2754eae9c0a86dcf714b98e025fd95" - integrity sha512-CPPgI+i9yVzOEDCdmEEZ67JgOvZyNDs8kStmGUFgDuLSjj3//HhkqT5XyfWjGeH6KmyGiS8ip3cgLOVn3IsOSA== +metro-babel-transformer@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.73.8.tgz#521374cb9234ba126f3f8d63588db5901308b4ed" + integrity sha512-GO6H/W2RjZ0/gm1pIvdO9EP34s3XN6kzoeyxqmfqKfYhJmYZf1SzXbyiIHyMbJNwJVrsKuHqu32+GopTlKscWw== dependencies: - metro-core "0.73.7" + "@babel/core" "^7.20.0" + hermes-parser "0.8.0" + metro-source-map "0.73.8" + nullthrows "^1.1.1" + +metro-cache-key@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.73.8.tgz#afc9f63454edbd9d207544445a66e8a4e119462d" + integrity sha512-VzFGu4kJGIkLjyDgVoM2ZxIHlMdCZWMqVIux9N+EeyMVMvGXTiXW8eGROgxzDhVjyR58IjfMsYpRCKz5dR+2ew== + +metro-cache@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.73.8.tgz#85e2d7f7c7c74d1f942b7ecd168f7aceb987d883" + integrity sha512-/uFbTIw813Rvb8kSAIHvax9gWl41dtgjY2SpJLNIBLdQ6oFZ3CVo3ahZIiEZOrCeHl9xfGn5tmvNb8CEFa/Q5w== + dependencies: + metro-core "0.73.8" rimraf "^3.0.2" -metro-config@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.73.7.tgz#8935054ece6155d214420c263272cd3a690a82e2" - integrity sha512-pD/F+vK3u37cbj1skYmI6cUsEEscqNRtW2KlDKu1m+n8nooDB2oGTOZatlS5WQa7Ga6jYQRydftlq4CLDexAfA== +metro-config@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.73.8.tgz#8f6c22c94528919635c6688ed8d2ad8a10c70b27" + integrity sha512-sAYq+llL6ZAfro64U99ske8HcKKswxX4wIZbll9niBKG7TkWm7tfMY1jO687XEmE4683rHncZeBRav9pLngIzg== dependencies: cosmiconfig "^5.0.5" jest-validate "^26.5.2" - metro "0.73.7" - metro-cache "0.73.7" - metro-core "0.73.7" - metro-runtime "0.73.7" + metro "0.73.8" + metro-cache "0.73.8" + metro-core "0.73.8" + metro-runtime "0.73.8" -metro-core@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.73.7.tgz#f5abe2448ea72a65f54db9bc90068f3308de1df2" - integrity sha512-H7j1Egj1VnNnsSYf9ZKv0SRwijgtRKIcaGNQq/T+er73vqqb4kR9H+2VIJYPXi6R8lT+QLIMfs6CWSUHAJUgtg== +metro-core@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.73.8.tgz#a31ba7d7bfe3f4c2ac2c7a2493aa4229ecad701e" + integrity sha512-Aew4dthbZf8bRRjlYGL3cnai3+LKYTf6mc7YS2xLQRWtgGZ1b/H8nQtBvXZpfRYFcS84UeEQ10vwIf5eR3qPdQ== dependencies: lodash.throttle "^4.1.1" - metro-resolver "0.73.7" + metro-resolver "0.73.8" -metro-file-map@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.73.7.tgz#709f33ac5ea6f87668d454c77973ab296b7a064b" - integrity sha512-BYaCo2e/4FMN4nOajeN+Za5cPfecfikzUYuFWWMyLAmHU6dj7B+PFkaJ4OEJO3vmRoeq5vMOmhpKXgysYbNXJg== +metro-file-map@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.73.8.tgz#88d666e7764e1b0adf5fd634d91e97e3135d2db7" + integrity sha512-CM552hUO9om02jJdLszOCIDADKNaaeVz8CjYXItndvgr5jmFlQYAR+UMvaDzeT8oYdAV1DXAljma2CS2UBymPg== dependencies: abort-controller "^3.0.0" anymatch "^3.0.3" @@ -9139,79 +9224,35 @@ metro-file-map@0.73.7: optionalDependencies: fsevents "^2.3.2" -metro-hermes-compiler@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-hermes-compiler/-/metro-hermes-compiler-0.73.7.tgz#d1b519c4040423240d89e7816340ca9635deeae8" - integrity sha512-F8PlJ8mWEEumGNH3eMRA3gjgP70ZvH4Ex5F1KY6ofD/gpn7w5HJHSPTeVw8gtUb1pYLN4nevptpyXGg04Jfcog== +metro-hermes-compiler@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-hermes-compiler/-/metro-hermes-compiler-0.73.8.tgz#c522e2c97afc8bdc249755d88146a75720bc2498" + integrity sha512-2d7t+TEoQLk+jyXgBykmAtPPJK2B46DB3qUYIMKDFDDaKzCljrojyVuGgQq6SM1f95fe6HDAQ3K9ihTjeB90yw== -metro-inspector-proxy@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-inspector-proxy/-/metro-inspector-proxy-0.73.7.tgz#edb966c1581a41a3302860d264f3228e1f57a220" - integrity sha512-TsAtQeKr9X7NaQHlpshu+ZkGWlPi5fFKNqieLkfqvT1oXN4PQF/4q38INyiZtWLPvoUzTR6PRnm4pcUbJ7+Nzg== +metro-inspector-proxy@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-inspector-proxy/-/metro-inspector-proxy-0.73.8.tgz#67d5aadfc33fe97f61c716eb168db4bd5d0e3c96" + integrity sha512-F0QxwDTox0TDeXVRN7ZmI7BknBjPDVKQ1ZeKznFBiMa0SXiD1kzoksfpDbZ6hTEKrhVM9Ep0YQmC7avwZouOnA== dependencies: connect "^3.6.5" debug "^2.2.0" ws "^7.5.1" yargs "^17.5.1" -metro-minify-terser@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.73.7.tgz#e45fc05eb2e3bc76c9b4fe4abccee0fffeedcf75" - integrity sha512-gbv1fmMOZm6gJ6dQoD+QktlCi2wk6nlTR8j8lQCjeeXGbs6O9e5XLWNPOexHqo7S69bdbohEnfZnLJFcxgHeNw== +metro-minify-terser@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.73.8.tgz#a0fe857d6aaf99cba3a2aef59ee06ac409682c6b" + integrity sha512-pnagyXAoMPhihWrHRIWqCxrP6EJ8Hfugv5RXBb6HbOANmwajn2uQuzeu18+dXaN1yPoDCMCgpg/UA4ibFN5jtQ== dependencies: terser "^5.15.0" -metro-minify-uglify@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-minify-uglify/-/metro-minify-uglify-0.73.7.tgz#3dfd397e8202905731e4a519a58fc334d9232a15" - integrity sha512-DmDCzfdbaPExQuQ7NQozCNOSOAgp5Ux9kWzmKAT8seQ38/3NtUepW+PTgxXIHmwNjJV4oHsHwlBlTwJmYihKXg== +metro-minify-uglify@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-minify-uglify/-/metro-minify-uglify-0.73.8.tgz#b2e2430014c340479db4fc393a2ea4c5bad75ecd" + integrity sha512-9wZqKfraVfmtMXdOzRyan+6r1woQXqqa4KeXfVh7+Mxl+5+J0Lmw6EvTrWawsaOEpvpn32q9MfoHC1d8plDJwA== dependencies: uglify-es "^3.1.9" -metro-react-native-babel-preset@0.73.5: - version "0.73.5" - resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.5.tgz#9b92f1ebc2b3d96f511c45a03f8e35e0fc46cc19" - integrity sha512-Ej6J8ozWSs6nrh0nwX7hgX4oPXUai40ckah37cSLu8qeED2XiEtfLV1YksTLafFE8fX0EieiP97U97dkOGHP4w== - dependencies: - "@babel/core" "^7.14.0" - "@babel/plugin-proposal-async-generator-functions" "^7.0.0" - "@babel/plugin-proposal-class-properties" "^7.0.0" - "@babel/plugin-proposal-export-default-from" "^7.0.0" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.0.0" - "@babel/plugin-proposal-object-rest-spread" "^7.0.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.0.0" - "@babel/plugin-proposal-optional-chaining" "^7.0.0" - "@babel/plugin-syntax-dynamic-import" "^7.0.0" - "@babel/plugin-syntax-export-default-from" "^7.0.0" - "@babel/plugin-syntax-flow" "^7.2.0" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.0.0" - "@babel/plugin-syntax-optional-chaining" "^7.0.0" - "@babel/plugin-transform-arrow-functions" "^7.0.0" - "@babel/plugin-transform-async-to-generator" "^7.0.0" - "@babel/plugin-transform-block-scoping" "^7.0.0" - "@babel/plugin-transform-classes" "^7.0.0" - "@babel/plugin-transform-computed-properties" "^7.0.0" - "@babel/plugin-transform-destructuring" "^7.0.0" - "@babel/plugin-transform-flow-strip-types" "^7.0.0" - "@babel/plugin-transform-function-name" "^7.0.0" - "@babel/plugin-transform-literals" "^7.0.0" - "@babel/plugin-transform-modules-commonjs" "^7.0.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.0.0" - "@babel/plugin-transform-parameters" "^7.0.0" - "@babel/plugin-transform-react-display-name" "^7.0.0" - "@babel/plugin-transform-react-jsx" "^7.0.0" - "@babel/plugin-transform-react-jsx-self" "^7.0.0" - "@babel/plugin-transform-react-jsx-source" "^7.0.0" - "@babel/plugin-transform-runtime" "^7.0.0" - "@babel/plugin-transform-shorthand-properties" "^7.0.0" - "@babel/plugin-transform-spread" "^7.0.0" - "@babel/plugin-transform-sticky-regex" "^7.0.0" - "@babel/plugin-transform-template-literals" "^7.0.0" - "@babel/plugin-transform-typescript" "^7.5.0" - "@babel/plugin-transform-unicode-regex" "^7.0.0" - "@babel/template" "^7.0.0" - react-refresh "^0.4.0" - metro-react-native-babel-preset@0.73.7: version "0.73.7" resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.7.tgz#78e1ce448aa9a5cf3651c0ebe73cb225465211b4" @@ -9256,18 +9297,49 @@ metro-react-native-babel-preset@0.73.7: "@babel/template" "^7.0.0" react-refresh "^0.4.0" -metro-react-native-babel-transformer@0.73.5: - version "0.73.5" - resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.73.5.tgz#fb1d48cc73ce26371cf2a115f702b7bf3bb5516b" - integrity sha512-CZYgUguqFTzV9vSOZb60p8qlp31aWz8aBB6OqoZ2gJday+n/1k+Y0yy6VPr/tfXJheuQYVIXKvG1gMmUDyxt+Q== +metro-react-native-babel-preset@0.73.8, metro-react-native-babel-preset@^0.73.7: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.8.tgz#04908f264f5d99c944ae20b5b11f659431328431" + integrity sha512-spNrcQJTbQntEIqJnCA6yL4S+dzV9fXCk7U+Rm7yJasZ4o4Frn7jP23isu7FlZIp1Azx1+6SbP7SgQM+IP5JgQ== dependencies: - "@babel/core" "^7.14.0" - babel-preset-fbjs "^3.4.0" - hermes-parser "0.8.0" - metro-babel-transformer "0.73.5" - metro-react-native-babel-preset "0.73.5" - metro-source-map "0.73.5" - nullthrows "^1.1.1" + "@babel/core" "^7.20.0" + "@babel/plugin-proposal-async-generator-functions" "^7.0.0" + "@babel/plugin-proposal-class-properties" "^7.0.0" + "@babel/plugin-proposal-export-default-from" "^7.0.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.0.0" + "@babel/plugin-proposal-object-rest-spread" "^7.0.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.0.0" + "@babel/plugin-proposal-optional-chaining" "^7.0.0" + "@babel/plugin-syntax-dynamic-import" "^7.0.0" + "@babel/plugin-syntax-export-default-from" "^7.0.0" + "@babel/plugin-syntax-flow" "^7.18.0" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.0.0" + "@babel/plugin-syntax-optional-chaining" "^7.0.0" + "@babel/plugin-transform-arrow-functions" "^7.0.0" + "@babel/plugin-transform-async-to-generator" "^7.0.0" + "@babel/plugin-transform-block-scoping" "^7.0.0" + "@babel/plugin-transform-classes" "^7.0.0" + "@babel/plugin-transform-computed-properties" "^7.0.0" + "@babel/plugin-transform-destructuring" "^7.0.0" + "@babel/plugin-transform-flow-strip-types" "^7.0.0" + "@babel/plugin-transform-function-name" "^7.0.0" + "@babel/plugin-transform-literals" "^7.0.0" + "@babel/plugin-transform-modules-commonjs" "^7.0.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.0.0" + "@babel/plugin-transform-parameters" "^7.0.0" + "@babel/plugin-transform-react-display-name" "^7.0.0" + "@babel/plugin-transform-react-jsx" "^7.0.0" + "@babel/plugin-transform-react-jsx-self" "^7.0.0" + "@babel/plugin-transform-react-jsx-source" "^7.0.0" + "@babel/plugin-transform-runtime" "^7.0.0" + "@babel/plugin-transform-shorthand-properties" "^7.0.0" + "@babel/plugin-transform-spread" "^7.0.0" + "@babel/plugin-transform-sticky-regex" "^7.0.0" + "@babel/plugin-transform-template-literals" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.5.0" + "@babel/plugin-transform-unicode-regex" "^7.0.0" + "@babel/template" "^7.0.0" + react-refresh "^0.4.0" metro-react-native-babel-transformer@0.73.7: version "0.73.7" @@ -9282,21 +9354,26 @@ metro-react-native-babel-transformer@0.73.7: metro-source-map "0.73.7" nullthrows "^1.1.1" -metro-resolver@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.73.7.tgz#1e174cf59eac84c0869172764316042b466daaa5" - integrity sha512-mGW3XPeKBCwZnkHcKo1dhFa9olcx7SyNzG1vb5kjzJYe4Qs3yx04r/qFXIJLcIgLItB69TIGvosznUhpeOOXzg== +metro-react-native-babel-transformer@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.73.8.tgz#cbcd4b243216878431dc4311ce46f02a928e3991" + integrity sha512-oH/LCCJPauteAE28c0KJAiSrkV+1VJbU0PwA9UwaWnle+qevs/clpKQ8LrIr33YbBj4CiI1kFoVRuNRt5h4NFg== + dependencies: + "@babel/core" "^7.20.0" + babel-preset-fbjs "^3.4.0" + hermes-parser "0.8.0" + metro-babel-transformer "0.73.8" + metro-react-native-babel-preset "0.73.8" + metro-source-map "0.73.8" + nullthrows "^1.1.1" + +metro-resolver@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.73.8.tgz#65cc158575d130363296f66a33257c7971228640" + integrity sha512-GiBWont7/OgAftkkj2TiEp+Gf1PYZUk8xV4MbtnQjIKyy3MlGY3GbpMQ1BHih9GUQqlF0n9jsUlC2K5P0almXQ== dependencies: absolute-path "^0.0.0" -metro-runtime@0.73.5: - version "0.73.5" - resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.73.5.tgz#8c92c3947e97a8dede6347ba6a9844bfb8be8258" - integrity sha512-8QJOS7bhJmR6r/Gkki/qY9oX/DdxnLhS8FpdG1Xmm2hDeUVAug12ekWTiCRMu7d1CDVv1F8WvUz09QckZ0dO0g== - dependencies: - "@babel/runtime" "^7.0.0" - react-refresh "^0.4.0" - metro-runtime@0.73.7: version "0.73.7" resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.73.7.tgz#9f3a7f3ff668c1a87370650e32b47d8f6329fd1e" @@ -9305,19 +9382,13 @@ metro-runtime@0.73.7: "@babel/runtime" "^7.0.0" react-refresh "^0.4.0" -metro-source-map@0.73.5: - version "0.73.5" - resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.73.5.tgz#67e14bd1fcc1074b9623640ca311cd99d07426fa" - integrity sha512-58p3zNWgUrqYYjFJb0KkZ+uJurTL4oz7i5T7577b3kvTYuJ0eK4y7rtYf8EwOfMYxRAn/m20aH1Y1fHTsLUwjQ== +metro-runtime@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.73.8.tgz#dadae7c154fbbde24390cf7f7e7d934a2768cd18" + integrity sha512-M+Bg9M4EN5AEpJ8NkiUsawD75ifYvYfHi05w6QzHXaqOrsTeaRbbeLuOGCYxU2f/tPg17wQV97/rqUQzs9qEtA== dependencies: - "@babel/traverse" "^7.14.0" - "@babel/types" "^7.20.0" - invariant "^2.2.4" - metro-symbolicate "0.73.5" - nullthrows "^1.1.1" - ob1 "0.73.5" - source-map "^0.5.6" - vlq "^1.0.0" + "@babel/runtime" "^7.0.0" + react-refresh "^0.4.0" metro-source-map@0.73.7: version "0.73.7" @@ -9333,16 +9404,18 @@ metro-source-map@0.73.7: source-map "^0.5.6" vlq "^1.0.0" -metro-symbolicate@0.73.5: - version "0.73.5" - resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.73.5.tgz#8de118be231decd55c8c70ed54deb308fdffceda" - integrity sha512-aIC8sDlaEdtn0dTt+64IFZFEATatFx3GtzRbJi0+jJx47RjDRiuCt9fzmTMLuadWwnbFK9ZfVMuWEXM9sdtQ7w== +metro-source-map@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.73.8.tgz#5134174e3d43de26ad331b95f637944c6547d441" + integrity sha512-wozFXuBYMAy7b8BCYwC+qoXsvayVJBHWtSTlSLva99t+CoUSG9JO9kg1umzbOz28YYPxKmvb/wbnLMkHdas2cA== dependencies: + "@babel/traverse" "^7.20.0" + "@babel/types" "^7.20.0" invariant "^2.2.4" - metro-source-map "0.73.5" + metro-symbolicate "0.73.8" nullthrows "^1.1.1" + ob1 "0.73.8" source-map "^0.5.6" - through2 "^2.0.1" vlq "^1.0.0" metro-symbolicate@0.73.7: @@ -9357,10 +9430,22 @@ metro-symbolicate@0.73.7: through2 "^2.0.1" vlq "^1.0.0" -metro-transform-plugins@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.73.7.tgz#49ff2571742d557f20301880f55b00054e468e52" - integrity sha512-M5isiWEau0jMudb5ezaNBZnYqXxcATMqnAYc+Cu25IahT1NHi5aWwLok9EBmBpN5641IZUZXScf+KnS7fPxPCQ== +metro-symbolicate@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.73.8.tgz#96920f607bce484283d822ee5fe18d932f69c03d" + integrity sha512-xkBAcceYYp0GGdCCuMzkCF1ejHsd0lYlbKBkjSRgM0Nlj80VapPaSwumYoAvSaDxcbkvS7/sCjURGp5DsSFgRQ== + dependencies: + invariant "^2.2.4" + metro-source-map "0.73.8" + nullthrows "^1.1.1" + source-map "^0.5.6" + through2 "^2.0.1" + vlq "^1.0.0" + +metro-transform-plugins@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.73.8.tgz#07be7fd94a448ea1b245ab02ce7d277d757f9a32" + integrity sha512-IxjlnB5eA49M0WfvPEzvRikK3Rr6bECUUfcZt/rWpSphq/mttgyLYcHQ+VTZZl0zHolC3cTLwgoDod4IIJBn1A== dependencies: "@babel/core" "^7.20.0" "@babel/generator" "^7.20.0" @@ -9368,29 +9453,29 @@ metro-transform-plugins@0.73.7: "@babel/traverse" "^7.20.0" nullthrows "^1.1.1" -metro-transform-worker@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.73.7.tgz#be111805e92ea48b7c76dd75830798f318e252e0" - integrity sha512-gZYIu9JAqEI9Rxi0xGMuMW6QsHGbMSptozlTOwOd7T7yXX3WwYS/I3yLPbLhbZTjOhwMHkTt8Nhm2qBo8nh14g== +metro-transform-worker@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.73.8.tgz#701a006c2b4d93f1bb24802f3f2834c963153db9" + integrity sha512-B8kR6lmcvyG4UFSF2QDfr/eEnWJvg0ZadooF8Dg6m/3JSm9OAqfSoC0YrWqAuvtWImNDnbeKWN7/+ns44Hv6tg== dependencies: "@babel/core" "^7.20.0" "@babel/generator" "^7.20.0" "@babel/parser" "^7.20.0" "@babel/types" "^7.20.0" babel-preset-fbjs "^3.4.0" - metro "0.73.7" - metro-babel-transformer "0.73.7" - metro-cache "0.73.7" - metro-cache-key "0.73.7" - metro-hermes-compiler "0.73.7" - metro-source-map "0.73.7" - metro-transform-plugins "0.73.7" + metro "0.73.8" + metro-babel-transformer "0.73.8" + metro-cache "0.73.8" + metro-cache-key "0.73.8" + metro-hermes-compiler "0.73.8" + metro-source-map "0.73.8" + metro-transform-plugins "0.73.8" nullthrows "^1.1.1" -metro@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro/-/metro-0.73.7.tgz#435081339ac209e4d8802c57ac522638140c802b" - integrity sha512-pkRqFhuGUvkiu8HxKPUQelbCuyy6te6okMssTyLzQwsKilNLK4YMI2uD6PHnypg5SiMJ58lwfqkp/t5w72jEvw== +metro@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro/-/metro-0.73.8.tgz#25f014e4064eb34a4833c316e0a9094528061a8c" + integrity sha512-2EMJME9w5x7Uzn+DnQ4hzWr33u/aASaOBGdpf4lxbrlk6/vl4UBfX1sru6KU535qc/0Z1BMt4Vq9qsP3ZGFmWg== dependencies: "@babel/code-frame" "^7.0.0" "@babel/core" "^7.20.0" @@ -9414,23 +9499,23 @@ metro@0.73.7: invariant "^2.2.4" jest-worker "^27.2.0" lodash.throttle "^4.1.1" - metro-babel-transformer "0.73.7" - metro-cache "0.73.7" - metro-cache-key "0.73.7" - metro-config "0.73.7" - metro-core "0.73.7" - metro-file-map "0.73.7" - metro-hermes-compiler "0.73.7" - metro-inspector-proxy "0.73.7" - metro-minify-terser "0.73.7" - metro-minify-uglify "0.73.7" - metro-react-native-babel-preset "0.73.7" - metro-resolver "0.73.7" - metro-runtime "0.73.7" - metro-source-map "0.73.7" - metro-symbolicate "0.73.7" - metro-transform-plugins "0.73.7" - metro-transform-worker "0.73.7" + metro-babel-transformer "0.73.8" + metro-cache "0.73.8" + metro-cache-key "0.73.8" + metro-config "0.73.8" + metro-core "0.73.8" + metro-file-map "0.73.8" + metro-hermes-compiler "0.73.8" + metro-inspector-proxy "0.73.8" + metro-minify-terser "0.73.8" + metro-minify-uglify "0.73.8" + metro-react-native-babel-preset "0.73.8" + metro-resolver "0.73.8" + metro-runtime "0.73.8" + metro-source-map "0.73.8" + metro-symbolicate "0.73.8" + metro-transform-plugins "0.73.8" + metro-transform-worker "0.73.8" mime-types "^2.1.27" node-fetch "^2.2.0" nullthrows "^1.1.1" @@ -9519,7 +9604,7 @@ minimalistic-assert@^1.0.0: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +"minimatch@2 || 3", minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -9527,16 +9612,16 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatc brace-expansion "^1.1.7" minimatch@^5.0.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.2.tgz#0939d7d6f0898acbd1508abe534d1929368a8fff" - integrity sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg== + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: brace-expansion "^2.0.1" minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== mixin-deep@^1.2.0: version "1.3.2" @@ -9569,9 +9654,14 @@ mobx-react-lite@^3.4.0: integrity sha512-bRuZp3C0itgLKHu/VNxi66DN/XVkQG7xtoBVWxpvC5FhAqbOCP21+nPhULjnzEqd7xBMybp6KwytdUpZKEgpIQ== mobx@^6.6.1: - version "6.7.0" - resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.7.0.tgz#2d805610fee1801fd015c54fd5400d2601aa3768" - integrity sha512-1kBLBdSNG2bA522HQdbsTvwAwYf9hq9FWxmlhX7wTsJUAI54907J+ozfGW+LoYUo06vjit748g6QH1AAGLNebw== + version "6.8.0" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.8.0.tgz#59051755fdb5c8a9f3f2e0a9b6abaf86bab7f843" + integrity sha512-+o/DrHa4zykFMSKfS8Z+CPSEg5LW9tSNGTuN8o6MF1GKxlfkSHSeJn5UtgxvPkGgaouplnrLXCF+duAsmm6FHQ== + +moment@^2.19.3: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== ms@2.0.0: version "2.0.0" @@ -9588,6 +9678,11 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multi-sort-stream@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/multi-sort-stream/-/multi-sort-stream-1.0.4.tgz#e4348edc9edc36e16333e531a90c0f166235cc99" + integrity sha512-hAZ8JOEQFbgdLe8HWZbb7gdZg0/yAIHF00Qfo3kd0rXFv96nXe+/bPTrKHZ2QMHugGX4FiAyET1Lt+jiB+7Qlg== + multicast-dns@^7.2.5: version "7.2.5" resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" @@ -9601,6 +9696,28 @@ multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.6.4: resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== +multipipe@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-4.0.0.tgz#d302554ae664c1157dbfd1e8f98f03c517b3948a" + integrity sha512-jzcEAzFXoWwWwUbvHCNPwBlTz3WCWe/jPcXSmTfbo/VjRwRTfvLZ/bdvtiTdqCe8d4otCSsPCbhGYcX+eggpKQ== + dependencies: + duplexer2 "^0.1.2" + object-assign "^4.1.0" + +mv@~2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" + integrity sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg== + dependencies: + mkdirp "~0.5.1" + ncp "~2.0.0" + rimraf "~2.4.0" + +nan@^2.14.0: + version "2.17.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" + integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== + nanoid@^3.3.1, nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" @@ -9638,6 +9755,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +ncp@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA== + negotiator@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" @@ -9667,9 +9789,9 @@ nocache@^3.0.1: integrity sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw== node-abi@^3.3.0: - version "3.31.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.31.0.tgz#dfb2ea3d01188eb80859f69bb4a4354090c1b355" - integrity sha512-eSKV6s+APenqVh8ubJyiu/YhZgxQpGP66ntzUb3lY1xB9ukSRaGnx0AIxI+IM+1+IVYC1oWobgG5L3Lt9ARykQ== + version "3.33.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.33.0.tgz#8b23a0cec84e1c5f5411836de6a9b84bccf26e7f" + integrity sha512-7GGVawqyHF4pfd0YFybhv/eM9JwTtPqx0mAanQ146O3FlSh3pA24zf9IRQTOsfTSqXTNzPSP5iagAJ94jjuVog== dependencies: semver "^7.3.5" @@ -9685,13 +9807,20 @@ node-dir@^0.1.17: dependencies: minimatch "^3.0.2" -node-fetch@2.6.7, node-fetch@^2.2.0, node-fetch@^2.6.0: +node-fetch@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" +node-fetch@^2.2.0, node-fetch@^2.6.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" + integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== + dependencies: + whatwg-url "^5.0.0" + node-forge@^1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -9702,16 +9831,30 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== -node-releases@^2.0.6: - version "2.0.8" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae" - integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A== +node-ipc@^9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/node-ipc/-/node-ipc-9.2.1.tgz#b32f66115f9d6ce841dc4ec2009d6a733f98bb6b" + integrity sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ== + dependencies: + event-pubsub "4.3.0" + js-message "1.0.7" + js-queue "2.0.2" + +node-releases@^2.0.8: + version "2.0.10" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" + integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== node-stream-zip@^1.9.1: version "1.15.0" resolved "https://registry.yarnpkg.com/node-stream-zip/-/node-stream-zip-1.15.0.tgz#158adb88ed8004c6c49a396b50a6a5de3bca33ea" integrity sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw== +node-version@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/node-version/-/node-version-1.2.0.tgz#34fde3ffa8e1149bd323983479dda620e1b5060d" + integrity sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ== + nodemailer-html-to-text@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/nodemailer-html-to-text/-/nodemailer-html-to-text-3.2.0.tgz#91b959491fef8f7d91796047abb728aa86d4a12b" @@ -9720,9 +9863,9 @@ nodemailer-html-to-text@^3.2.0: html-to-text "7.1.1" nodemailer@^6.8.0: - version "6.9.0" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.0.tgz#a17488ff470ff9edf1bb31d9ec23079bc94f7dd3" - integrity sha512-jFaCEGTeT3E/m/5R2MHWiyQH3pSARECRUDM+1hokOYc3lQAAG7ASuy+2jIsYVf+RVa9zePopSQwKNVFH8DKUpA== + version "6.9.1" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.1.tgz#8249d928a43ed85fec17b13d2870c8f758a126ed" + integrity sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA== normalize-css-color@^1.0.2: version "1.0.2" @@ -9744,6 +9887,11 @@ normalize-url@^6.0.1: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== +normalize-url@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.0.tgz#593dbd284f743e8dcf6a5ddf8fadff149c82701a" + integrity sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw== + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -9782,16 +9930,16 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== -ob1@0.73.5: - version "0.73.5" - resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.73.5.tgz#b80dc4a6f787044e3d8afde3c2d034ae23d05a86" - integrity sha512-MxQH/rCq9/COvgTQbjCldArmesGEidZVVQIn4vDUJvJJ8uMphXOTCBsgWTief2ugvb0WUimIaslKSA+qryFjjQ== - ob1@0.73.7: version "0.73.7" resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.73.7.tgz#14c9b6ddc26cf99144f59eb542d7ae956e6b3192" integrity sha512-DfelfvR843KADhSUATGGhuepVMRcf5VQX+6MQLy5AW0BKDLlO7Usj6YZeAAZP7P86QwsoTxB0RXCFiA7t6S1IQ== +ob1@0.73.8: + version "0.73.8" + resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.73.8.tgz#c569f1a15ce2d04da6fd70293ad44b5a93b11978" + integrity sha512-1F7j+jzD+edS6ohQP7Vg5f3yiIk5i3x1uLrNIHOmLHWzWK1t3zrDpjnoXghccdVlsU+UjbyURnDynm4p0GgXeA== + object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -9812,9 +9960,17 @@ object-hash@^3.0.0: integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== object-inspect@^1.12.2, object-inspect@^1.9.0: - version "1.12.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" - integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + +object-is@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" object-keys@^1.1.1: version "1.1.1" @@ -9881,7 +10037,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.0, object.values@^1.1.5, object.values@^1.1.6: +object.values@^1.1.0, object.values@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== @@ -9946,9 +10102,9 @@ open@^6.2.0: is-wsl "^1.1.0" open@^8.0.9, open@^8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" - integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== dependencies: define-lazy-prop "^2.0.0" is-docker "^2.1.1" @@ -10204,10 +10360,10 @@ pg-pool@^3.5.2: resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.5.2.tgz#ed1bed1fb8d79f1c6fd5fb1c99e990fbf9ddf178" integrity sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w== -pg-protocol@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.5.0.tgz#b5dd452257314565e2d54ab3c132adc46565a6a0" - integrity sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ== +pg-protocol@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.0.tgz#4c91613c0315349363af2084608db843502f8833" + integrity sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q== pg-types@^2.1.0: version "2.2.0" @@ -10221,15 +10377,15 @@ pg-types@^2.1.0: postgres-interval "^1.1.0" pg@^8.8.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.8.0.tgz#a77f41f9d9ede7009abfca54667c775a240da686" - integrity sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw== + version "8.9.0" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.9.0.tgz#73c5d77a854d36b0e185450dacb8b90c669e040b" + integrity sha512-ZJM+qkEbtOHRuXjmvBtOgNOXOtLSbxiMiUVMgE4rV6Zwocy03RicCVvDXgx8l4Biwo8/qORUnEqn2fdQzV7KCg== dependencies: buffer-writer "2.0.0" packet-reader "1.0.0" pg-connection-string "^2.5.0" pg-pool "^3.5.2" - pg-protocol "^1.5.0" + pg-protocol "^1.6.0" pg-types "^2.1.0" pgpass "1.x" @@ -10274,11 +10430,10 @@ pino-abstract-transport@v1.0.0: split2 "^4.0.0" pino-http@^8.2.1: - version "8.3.1" - resolved "https://registry.yarnpkg.com/pino-http/-/pino-http-8.3.1.tgz#d33ade22044ce6efa7b5ed512e1bd7afdf9904ba" - integrity sha512-wqQMH2H+Di4ur5gaCYvWOMYhGmmmygHsgJduKr2HASSoegNsxfxiCDGxYYV8muOB8jKDHPoBXKGp03af+JESqg== + version "8.3.3" + resolved "https://registry.yarnpkg.com/pino-http/-/pino-http-8.3.3.tgz#2b140e734bfc6babe0df272a43bb8f36f2b525c0" + integrity sha512-p4umsNIXXVu95HD2C8wie/vXH7db5iGRpc+yj1/ZQ3sRtTQLXNjoS6Be5+eI+rQbqCRxen/7k/KSN+qiZubGDw== dependencies: - fast-url-parser "^1.1.3" get-caller-file "^2.0.5" pino "^8.0.0" pino-std-serializers "^6.0.0" @@ -10290,9 +10445,9 @@ pino-std-serializers@^6.0.0: integrity sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g== pino@^8.0.0, pino@^8.6.1: - version "8.8.0" - resolved "https://registry.yarnpkg.com/pino/-/pino-8.8.0.tgz#1f0d6695a224aa06afc7ad60f2ccc4772d3b9233" - integrity sha512-cF8iGYeu2ODg2gIwgAHcPrtR63ILJz3f7gkogaHC/TXVVXxZgInmNYiIpDYEwgEkxZti2Se6P2W2DxlBIZe6eQ== + version "8.11.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-8.11.0.tgz#2a91f454106b13e708a66c74ebc1c2ab7ab38498" + integrity sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg== dependencies: atomic-sleep "^1.0.0" fast-redact "^3.1.1" @@ -10385,12 +10540,12 @@ postcss-color-rebeccapurple@^7.1.1: dependencies: postcss-value-parser "^4.2.0" -postcss-colormin@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.0.tgz#3cee9e5ca62b2c27e84fce63affc0cfb5901956a" - integrity sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg== +postcss-colormin@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz#86c27c26ed6ba00d96c79e08f3ffb418d1d1988f" + integrity sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" caniuse-api "^3.0.0" colord "^2.9.1" postcss-value-parser "^4.2.0" @@ -10517,9 +10672,9 @@ postcss-initial@^4.0.1: integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== postcss-js@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00" - integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ== + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" + integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== dependencies: camelcase-css "^2.0.1" @@ -10566,10 +10721,10 @@ postcss-merge-longhand@^5.1.7: postcss-value-parser "^4.2.0" stylehacks "^5.1.1" -postcss-merge-rules@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.3.tgz#8f97679e67cc8d08677a6519afca41edf2220894" - integrity sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA== +postcss-merge-rules@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz#2f26fa5cacb75b1402e213789f6766ae5e40313c" + integrity sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g== dependencies: browserslist "^4.21.4" caniuse-api "^3.0.0" @@ -10817,10 +10972,10 @@ postcss-pseudo-class-any-link@^7.1.6: dependencies: postcss-selector-parser "^6.0.10" -postcss-reduce-initial@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.1.tgz#c18b7dfb88aee24b1f8e4936541c29adbd35224e" - integrity sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w== +postcss-reduce-initial@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz#798cd77b3e033eae7105c18c9d371d989e1382d6" + integrity sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg== dependencies: browserslist "^4.21.4" caniuse-api "^3.0.0" @@ -10844,7 +10999,7 @@ postcss-selector-not@^6.0.1: dependencies: postcss-selector-parser "^6.0.10" -postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: +postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: version "6.0.11" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== @@ -10880,10 +11035,10 @@ postcss@^7.0.35: picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.3.5, postcss@^8.4.18, postcss@^8.4.19, postcss@^8.4.4: - version "8.4.20" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.20.tgz#64c52f509644cecad8567e949f4081d98349dc56" - integrity sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g== +postcss@^8.0.9, postcss@^8.3.5, postcss@^8.4.19, postcss@^8.4.4: + version "8.4.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" + integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== dependencies: nanoid "^3.3.4" picocolors "^1.0.0" @@ -10947,9 +11102,9 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier@^2.8.3: - version "2.8.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.3.tgz#ab697b1d3dd46fb4626fbe2f543afe0cc98d8632" - integrity sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw== + version "2.8.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" + integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" @@ -10993,12 +11148,12 @@ pretty-format@^28.1.3: ansi-styles "^5.0.0" react-is "^18.0.0" -pretty-format@^29.0.0, pretty-format@^29.0.3, pretty-format@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.3.1.tgz#1841cac822b02b4da8971dacb03e8a871b4722da" - integrity sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg== +pretty-format@^29.0.3, pretty-format@^29.4.0, pretty-format@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.3.tgz#25500ada21a53c9e8423205cf0337056b201244c" + integrity sha512-cvpcHTc42lcsvOOAzd3XuNWTcvk1Jmnzqeu+WsOuiPmxUJTnkbAcFNsRKvEpBEUFVUgy/GTZLulZDcDEi+CIlA== dependencies: - "@jest/schemas" "^29.0.0" + "@jest/schemas" "^29.4.3" ansi-styles "^5.0.0" react-is "^18.0.0" @@ -11017,17 +11172,10 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== -promise.allsettled@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.6.tgz#8dc8ba8edf429feb60f8e81335b920e109c94b6e" - integrity sha512-22wJUOD3zswWFqgwjNHa1965LvqTX87WPu/lreY2KSd7SVcERfuZ4GfUaOnJNnvtoIv2yXT/W00YIGMetXtFXg== - dependencies: - array.prototype.map "^1.0.5" - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - get-intrinsic "^1.1.3" - iterate-value "^1.0.2" +promise-polyfill@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057" + integrity sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ== promise@^7.1.1: version "7.3.1" @@ -11060,6 +11208,15 @@ prop-types@*, prop-types@^15.5.10, prop-types@^15.7.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +proper-lockfile@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-3.2.0.tgz#89ca420eea1d55d38ca552578851460067bcda66" + integrity sha512-iMghHHXv2bsxl6NchhEaFck8tvX3F9cknEEh1SUpguUOBjN7PAAW9BLzmbc1g/mCD1gY3EE2EABBHPJfFdHFmA== + dependencies: + graceful-fs "^4.1.11" + retry "^0.12.0" + signal-exit "^3.0.2" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -11068,6 +11225,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== + psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" @@ -11081,22 +11243,17 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^1.3.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== - punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== -qs@6.11.0: +qs@6.11.0, qs@^6.5.1: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== @@ -11219,9 +11376,9 @@ react-dev-utils@^12.0.1: text-table "^0.2.0" react-devtools-core@^4.26.1: - version "4.27.1" - resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.1.tgz#167aa174383c65786cbb7e965a5b39c702f0a2d3" - integrity sha512-qXhcxxDWiFmFAOq48jts9YQYe1+wVoUXzJTlY4jbaATzyio6dd6CUGu3dXBhREeVgpZ+y4kg6vFJzIOZh6vY2w== + version "4.27.2" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.2.tgz#d20fc57e258c656eedabafc2c851d38b33583148" + integrity sha512-8SzmIkpO87alD7Xr6gWIEa1jHkMjawOZ+6egjazlnjB4UUcbnzGDf/vBJ4BzGuWWEM+pzrxuzsPpcMqlQkYK2g== dependencies: shell-quote "^1.6.1" ws "^7" @@ -11270,9 +11427,9 @@ react-native-background-fetch@^4.1.8: integrity sha512-/qe86laa0n4AbD6mrLL8SCGR+K5693URX95e02/bTJh3UkdS3+sU1Jyc/XTlz4MQwlquI929/lm5EZh8AOUqzQ== react-native-codegen@^0.71.3: - version "0.71.3" - resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.71.3.tgz#75fbc591819050791319ebdb9fe341ee4df5c288" - integrity sha512-5AvdHVU1sAaXg05i0dG664ZTaCaIFaY1znV5vNsj+wUu6MGxNEUNbDKk9dxKUkkxOyk2KZOK5uhzWL0p5H5yZQ== + version "0.71.5" + resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.71.5.tgz#454a42a891cd4ca5fc436440d301044dc1349c14" + integrity sha512-rfsuc0zkuUuMjFnrT55I1mDZ+pBRp2zAiRwxck3m6qeGJBGK5OV5JH66eDQ4aa+3m0of316CqrJDRzVlYufzIg== dependencies: "@babel/parser" "^7.14.0" flow-parser "^0.185.0" @@ -11280,12 +11437,17 @@ react-native-codegen@^0.71.3: nullthrows "^1.1.1" react-native-dotenv@^3.3.1: - version "3.4.6" - resolved "https://registry.yarnpkg.com/react-native-dotenv/-/react-native-dotenv-3.4.6.tgz#7860f9b4543789f661765a0e7c3f40914133fa4e" - integrity sha512-OS+XdceNZmfFTIobQg8J5ZT6979Dr3j5GTgsIzu1JDdKV2IjIfYBTryTT3hJ8VMwdj5WiaredmZEVWwrRIwdtg== + version "3.4.7" + resolved "https://registry.yarnpkg.com/react-native-dotenv/-/react-native-dotenv-3.4.7.tgz#131374dd5c22b1173f7025b2a3b4e124b0142e03" + integrity sha512-GqUDdmrtKw7qYS9jjQLipEPctMd0YuSBNVZBNlaHtaHMJ63iaB4FABO4uX0iSUJpgMwozZki4fRv4dDHmVUoLg== dependencies: dotenv "^16.0.3" +react-native-fast-image@^8.6.3: + version "8.6.3" + resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz#6edc3f9190092a909d636d93eecbcc54a8822255" + integrity sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg== + react-native-fs@^2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6" @@ -11295,9 +11457,9 @@ react-native-fs@^2.20.0: utf8 "^3.0.0" react-native-gesture-handler@^2.5.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.8.0.tgz#ef9857871c10663c95a51546225b6e00cd4740cf" - integrity sha512-poOSfz/w0IyD6Qwq7aaIRRfEaVTl1ecQFoyiIbpOpfNTjm2B1niY2FLrdVQIOtIOe+K9nH55Qal04nr4jGkHdQ== + version "2.9.0" + resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.9.0.tgz#2f63812e523c646f25b9ad660fc6f75948e51241" + integrity sha512-a0BcH3Qb1tgVqUutc6d3VuWQkI1AM3+fJx8dkxzZs9t06qA27QgURYFoklpabuWpsUTzuKRpxleykp25E8m7tg== dependencies: "@egjs/hammerjs" "^2.0.17" hoist-non-react-statics "^3.3.0" @@ -11305,10 +11467,10 @@ react-native-gesture-handler@^2.5.0: lodash "^4.17.21" prop-types "^15.7.2" -react-native-gradle-plugin@^0.71.12: - version "0.71.12" - resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.12.tgz#7f0000b3d9593288183a13889fd6225d0ab506b8" - integrity sha512-ILujN0C+cX5QHmm22MXbHqZR619OzV/VThLHFhe7qnzZWpPh8O4KSvbtezoYMiBbmowAfy8SQpohwlN3nv6wZQ== +react-native-gradle-plugin@^0.71.13: + version "0.71.15" + resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.15.tgz#9e6b506f30729fe8eb086981702f4e3c891d2b13" + integrity sha512-7S3pAuPaQJlhax6EZ4JMsDNpj05TfuzX9gPgWLrFfAIWIFLuJ6aDQYAZy2TEI9QJALPoWrj8LWaqP/DGYh14pw== react-native-haptic-feedback@^1.14.0: version "1.14.0" @@ -11334,9 +11496,14 @@ react-native-linear-gradient@^2.6.2: integrity sha512-Z8Xxvupsex+9BBFoSYS87bilNPWcRfRsGC0cpJk72Nxb5p2nEkGSBv73xZbEHnW2mUFvP+huYxrVvjZkr/gRjQ== react-native-pager-view@^6.0.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.1.2.tgz#3522079b9a9d6634ca5e8d153bc0b4d660254552" - integrity sha512-qs2KSFc+7N7B+UZ6SG2sTvCkppagm5fVyRclv1KFKc7lDtrhXLzN59tXJw575LDP/dRJoXsNwqUAhZJdws6ABQ== + version "6.1.4" + resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.1.4.tgz#3a63ebd1b72f81991157ea552bb9c887e529bc8c" + integrity sha512-fmTwgGwPxGCBusKAq7gHzm+s1Yp0qh5rKPoQszaCuxrb+76KgK4Qe82jJNPUp2xTZOKSw+FbJU2QahF8ncTl+w== + +react-native-permissions@^3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/react-native-permissions/-/react-native-permissions-3.6.1.tgz#73adcc1cef8cd57a9ef167b4507405f4ff5749c4" + integrity sha512-fzPpPQXeD34inUccqtoResSwYubfrwUguP4qrVUUv8+KSMjYSaHGoS5HaIJLZHlN9gO+TvLJZ2L5ZljTsb6qnQ== react-native-progress@^5.0.0: version "5.0.0" @@ -11346,13 +11513,13 @@ react-native-progress@^5.0.0: prop-types "^15.7.2" react-native-reanimated@^2.9.1: - version "2.13.0" - resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.13.0.tgz#d64c1386626822d4dc22094b4efe028ff2c49cc9" - integrity sha512-yUHyYVIegWWIza4+nVyS3CSmI/Mc8kLFVHw2c6gnSHaYhYA4LeEjH/jBkoMzHk9Xd0Ra3cwtjYKAMG8OTp6JVg== + version "2.14.4" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.14.4.tgz#3fa3da4e7b99f5dfb28f86bcf24d9d1024d38836" + integrity sha512-DquSbl7P8j4SAmc+kRdd75Ianm8G+IYQ9T4AQ6lrpLVeDkhZmjWI0wkutKWnp6L7c5XNVUrFDUf69dwETLCItQ== dependencies: "@babel/plugin-transform-object-assign" "^7.16.7" "@babel/preset-typescript" "^7.16.7" - "@types/invariant" "^2.2.35" + convert-source-map "^1.7.0" invariant "^2.2.4" lodash.isequal "^4.5.0" setimmediate "^1.0.5" @@ -11364,23 +11531,23 @@ react-native-root-siblings@^4.0.0, react-native-root-siblings@^4.1.1: integrity sha512-sdmLElNs5PDWqmZmj4/aNH4anyxreaPm61c4ZkRiR8SO/GzLg6KjAbb0e17RmMdnBdD0AIQbS38h/l55YKN4ZA== react-native-root-toast@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/react-native-root-toast/-/react-native-root-toast-3.4.0.tgz#f5fedc06c80b0ed20a93be8832d32e9a67762187" - integrity sha512-2jv9XG9tK0rbXkD3IaO/clSJJQYXRnOCnGliZnYP/Z6I3u2QtdOp5xQ/ni8BbLqpPwSeKfCuO7R9H6iHuAN+Ig== + version "3.4.1" + resolved "https://registry.yarnpkg.com/react-native-root-toast/-/react-native-root-toast-3.4.1.tgz#8832dcf9d7f6296f689b2c02dd6cab125d9aa040" + integrity sha512-rZFoaVZWZiPCcwP9n5a+1KtVM22JNp6pkodluCoRIb5oE2ip8mhpdvlZUIWoM5nxfmL4TBThWDp7se6j4X7lPA== dependencies: deprecated-react-native-prop-types "^2.3.0" prop-types "^15.5.10" react-native-root-siblings "^4.0.0" react-native-safe-area-context@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.4.1.tgz#239c60b8a9a80eac70a38a822b04c0f1d15ffc01" - integrity sha512-N9XTjiuD73ZpVlejHrUWIFZc+6Z14co1K/p1IFMkImU7+avD69F3y+lhkqA2hN/+vljdZrBSiOwXPkuo43nFQA== + version "4.5.0" + resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.5.0.tgz#9208313236e8f49e1920ac1e2a2c975f03aed284" + integrity sha512-0WORnk9SkREGUg2V7jHZbuN5x4vcxj/1B0QOcXJjdYWrzZHgLcUzYWWIUecUPJh747Mwjt/42RZDOaFn3L8kPQ== react-native-screens@^3.13.1: - version "3.18.2" - resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-3.18.2.tgz#d7ab2d145258d3db9fa630fa5379dc4474117866" - integrity sha512-ANUEuvMUlsYJ1QKukEhzhfrvOUO9BVH9Nzg+6eWxpn3cfD/O83yPBOF8Mx6x5H/2+sMy+VS5x/chWOOo/U7QJw== + version "3.20.0" + resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-3.20.0.tgz#4d154177395e5541387d9a05bc2e12e54d2fb5b1" + integrity sha512-joWUKWAVHxymP3mL9gYApFHAsbd9L6ZcmpoZa6Sl3W/82bvvNVMqcfP7MeNqVCg73qZ8yL4fW+J/syusHleUgg== dependencies: react-freeze "^1.0.0" warn-once "^0.1.0" @@ -11391,17 +11558,17 @@ react-native-splash-screen@^3.3.0: integrity sha512-rGjt6HkoSXxMqH4SQUJ1gnPQlPJV8+J47+4yhgTIan4bVvAwJhEeJH7wWt9hXSdH4+VfwTS0GTaflj1Tw83IhA== react-native-svg@^12.4.0: - version "12.5.0" - resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-12.5.0.tgz#61e4395eb5eb52bbd0511b3c227473f7dbf98a16" - integrity sha512-xVMA6QjwU2E30DHLzjmewjHSmby4mMMlUaiKnFCYPWpsezsaB3zHS7eURwKNJLmcRYOPi3f6aBhxvFTNj/5j/A== + version "12.5.1" + resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-12.5.1.tgz#7dfd9daf2f8ed7843c0f3e7a16af193bd5f9b287" + integrity sha512-c374ENsq2MWCfr+7jC7TGwSeOAuC1Dp0osh2pw8PjpYFxmmB/toFIwcnCLz+SgBd6iLJClRhbATealqM05HOGg== dependencies: css-select "^5.1.0" css-tree "^1.1.3" react-native-tab-view@^3.3.0: - version "3.3.4" - resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-3.3.4.tgz#856d4527f3bbf05e2649302ec80abe9f2f004666" - integrity sha512-rceAYWpHa6knA7tsTnnjlcOxlCErR4F+yXQPpNm125IvYFyv09YRhE5uMU2IzyPIQ1CJvADCHurF3KySzVI+4Q== + version "3.5.0" + resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-3.5.0.tgz#eee4af05708578a1a0c6bcd2c98bf9f204778cc6" + integrity sha512-Dvvs54zPtI4f8x6GuTGy4VUMeMSnmjQFa0PsJkogWuRnxcYT+CunCDMOvxzSesh2AbL6UaTNPmTQ0Le9ybYsHg== dependencies: use-latest-callback "^0.1.5" @@ -11427,6 +11594,13 @@ react-native-web-linear-gradient@^1.1.2: resolved "https://registry.yarnpkg.com/react-native-web-linear-gradient/-/react-native-web-linear-gradient-1.1.2.tgz#33f85f7085a0bb5ffa5106faf02ed105b92a9ed7" integrity sha512-SmUnpwT49CEe78pXvIvYf72Es8Pv+ZYKCnEOgb2zAKpEUDMo0+xElfRJhwt5nfI8krJ5WbFPKnoDgD0uUjAN1A== +react-native-web-webview@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/react-native-web-webview/-/react-native-web-webview-1.0.2.tgz#c215efa70c17589f2c8d640b1f1dc669b18c6e02" + integrity sha512-oNAYNuqUqeqTuAAdIejzDqvUtYA+k5lrvhUYmASdUznZNmyIaoQFA6OKoA4K9F3wdMvark42vUXkUWIp875ewg== + dependencies: + qs "^6.5.1" + react-native-web@^0.18.11: version "0.18.12" resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.18.12.tgz#d4bb3a783ece2514ba0508d7805b09c0a98f5a8e" @@ -11440,15 +11614,30 @@ react-native-web@^0.18.11: postcss-value-parser "^4.2.0" styleq "^0.1.2" -react-native@0.71.0: - version "0.71.0" - resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.0.tgz#ce47c3cb6239484fcd686d1f45b363b7014b6c62" - integrity sha512-b5oCS/cPVqXT5E2K+0CfQMERAoRu6/6g1no9XRAcjQ4b5JG608WgDh5QgXPHaMSVhAvsJ1DuRoU8C/xqTjQITA== +react-native-webview@^11.26.1: + version "11.26.1" + resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-11.26.1.tgz#658c09ed5162dc170b361e48c2dd26c9712879da" + integrity sha512-hC7BkxOpf+z0UKhxFSFTPAM4shQzYmZHoELa6/8a/MspcjEP7ukYKpuSUTLDywQditT8yI9idfcKvfZDKQExGw== + dependencies: + escape-string-regexp "2.0.0" + invariant "2.2.4" + +react-native-youtube-iframe@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/react-native-youtube-iframe/-/react-native-youtube-iframe-2.2.2.tgz#ade1a2e4ead3d539fbb80463f45b59ff1b510b55" + integrity sha512-og2KW21kCwAHKcnWoyWWBYC6J2Xtqjjwpghhoy9G6zfwZkr8Ej27BbQIAKM/TheJJUZ5/YUrqsgqAdnFYDx5TQ== + dependencies: + events "^3.2.0" + +react-native@0.71.1: + version "0.71.1" + resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.1.tgz#72b45af2b29e3d5a660c63425ab5003bf2112f99" + integrity sha512-bLP5+IBj2IX6tgF9WnC/UL2ZPYkVUPsU4xqZV1jntTC2TH4xyLrvfKACjGlz5nQ3Mx4BmOFqsnMxithm53+6Aw== dependencies: "@jest/create-cache-key-function" "^29.2.1" - "@react-native-community/cli" "10.0.0" - "@react-native-community/cli-platform-android" "10.0.0" - "@react-native-community/cli-platform-ios" "10.0.0" + "@react-native-community/cli" "10.1.3" + "@react-native-community/cli-platform-android" "10.1.3" + "@react-native-community/cli-platform-ios" "10.1.1" "@react-native/assets" "1.0.0" "@react-native/normalize-color" "2.1.0" "@react-native/polyfills" "2.0.0" @@ -11461,16 +11650,16 @@ react-native@0.71.0: jest-environment-node "^29.2.1" jsc-android "^250230.2.1" memoize-one "^5.0.0" - metro-react-native-babel-transformer "0.73.5" - metro-runtime "0.73.5" - metro-source-map "0.73.5" + metro-react-native-babel-transformer "0.73.7" + metro-runtime "0.73.7" + metro-source-map "0.73.7" mkdirp "^0.5.1" nullthrows "^1.1.1" pretty-format "^26.5.2" promise "^8.3.0" react-devtools-core "^4.26.1" react-native-codegen "^0.71.3" - react-native-gradle-plugin "^0.71.12" + react-native-gradle-plugin "^0.71.13" react-refresh "^0.4.0" react-shallow-renderer "^16.15.0" regenerator-runtime "^0.13.2" @@ -11576,7 +11765,7 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -readable-stream@^2.0.1, readable-stream@~2.3.6: +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -11715,23 +11904,18 @@ regexpp@^3.2.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -regexpu-core@^5.2.1: - version "5.2.2" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.2.2.tgz#3e4e5d12103b64748711c3aad69934d7718e75fc" - integrity sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw== +regexpu-core@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.1.tgz#66900860f88def39a5cb79ebd9490e84f17bcdfb" + integrity sha512-nCOzW2V/X15XpLsK2rlgdwrysrBq+AauCn+omItIz4R1pIcmeot5zvjdmOBRLzEH/CkC6IxMJVmxDe3QcMuNVQ== dependencies: + "@babel/regjsgen" "^0.8.0" regenerate "^1.4.2" regenerate-unicode-properties "^10.1.0" - regjsgen "^0.7.1" regjsparser "^0.9.1" unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.1.0" -regjsgen@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.7.1.tgz#ee5ef30e18d3f09b7c369b76e7c2373ed25546f6" - integrity sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA== - regjsparser@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" @@ -11780,6 +11964,11 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== +requireindex@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.1.0.tgz#e5404b81557ef75db6e49c5a72004893fe03e162" + integrity sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg== + requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -11829,11 +12018,16 @@ resolve-url@^0.2.1: integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== resolve.exports@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" - integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== + version "1.1.1" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.1.tgz#05cfd5b3edf641571fd46fa608b610dda9ead999" + integrity sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ== -resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1: +resolve.exports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.0.tgz#c1a0028c2d166ec2fbf7d0644584927e76e7400e" + integrity sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg== + +resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.1: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -11842,7 +12036,7 @@ resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^2.0.0-next.3, resolve@^2.0.0-next.4: +resolve@^2.0.0-next.4: version "2.0.0-next.4" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660" integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== @@ -11864,6 +12058,11 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + retry@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" @@ -11891,6 +12090,13 @@ rimraf@~2.2.6: resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" integrity sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg== +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" + integrity sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ== + dependencies: + glob "^6.0.1" + rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -11959,6 +12165,11 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-json-stringify@~1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" @@ -11985,6 +12196,13 @@ safe-stable-stringify@^2.3.1, safe-stable-stringify@^2.4.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sanitize-filename@^1.6.1: + version "1.6.3" + resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" + integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg== + dependencies: + truncate-utf8-bytes "^1.0.0" + sanitize.css@*: version "13.0.0" resolved "https://registry.yarnpkg.com/sanitize.css/-/sanitize.css-13.0.0.tgz#2675553974b27964c75562ade3bd85d79879f173" @@ -12071,7 +12289,7 @@ semver-compare@^1.0.0: resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== -semver@7.3.8, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: +semver@7.3.8, semver@^7.0.0, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: version "7.3.8" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== @@ -12112,6 +12330,13 @@ serialize-error@^2.1.0: resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" integrity sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw== +serialize-error@^8.0.1: + version "8.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" + integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ== + dependencies: + type-fest "^0.20.2" + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -12120,9 +12345,9 @@ serialize-javascript@^4.0.0: randombytes "^2.1.0" serialize-javascript@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" - integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== dependencies: randombytes "^2.1.0" @@ -12229,10 +12454,10 @@ shell-quote@1.7.3: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== -shell-quote@^1.6.1, shell-quote@^1.7.3: - version "1.7.4" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.4.tgz#33fe15dee71ab2a81fcbd3a52106c5cfb9fb75d8" - integrity sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw== +shell-quote@^1.6.1, shell-quote@^1.7.2, shell-quote@^1.7.3: + version "1.8.0" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba" + integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ== side-channel@^1.0.4: version "1.0.4" @@ -12506,6 +12731,18 @@ stop-iteration-iterator@^1.0.0: dependencies: internal-slot "^1.0.4" +stream-chain@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/stream-chain/-/stream-chain-2.2.5.tgz#b30967e8f14ee033c5b9a19bbe8a2cba90ba0d09" + integrity sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA== + +stream-json@^1.7.4: + version "1.7.5" + resolved "https://registry.yarnpkg.com/stream-json/-/stream-json-1.7.5.tgz#2ff0563011f22cea4f6a28dbfc0344a53c761fe4" + integrity sha512-NSkoVduGakxZ8a+pTPUlcGEeAGQpWL9rKJhOFCV+J/QtdQUEU5vtBgVg6eJXn8JB8RZvpbJWZGvXkhz70MLWoA== + dependencies: + stream-chain "^2.2.5" + string-hash-64@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322" @@ -12664,6 +12901,11 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== + strtok3@^6.2.4: version "6.3.0" resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-6.3.0.tgz#358b80ffe6d5d5620e19a073aa78ce947a90f9a0" @@ -12772,9 +13014,9 @@ symbol-tree@^3.2.4: integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== tailwindcss@^3.0.2: - version "3.2.4" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.2.4.tgz#afe3477e7a19f3ceafb48e4b083e292ce0dc0250" - integrity sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ== + version "3.2.7" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.2.7.tgz#5936dd08c250b05180f0944500c01dce19188c07" + integrity sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ== dependencies: arg "^5.0.2" chokidar "^3.5.3" @@ -12790,12 +13032,12 @@ tailwindcss@^3.0.2: normalize-path "^3.0.0" object-hash "^3.0.0" picocolors "^1.0.0" - postcss "^8.4.18" + postcss "^8.0.9" postcss-import "^14.1.0" postcss-js "^4.0.0" postcss-load-config "^3.1.4" postcss-nested "6.0.0" - postcss-selector-parser "^6.0.10" + postcss-selector-parser "^6.0.11" postcss-value-parser "^4.2.0" quick-lru "^5.1.1" resolve "^1.22.1" @@ -12831,6 +13073,18 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +telnet-client@1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/telnet-client/-/telnet-client-1.2.8.tgz#946c0dadc8daa3f19bb40a3e898cb870403a4ca4" + integrity sha512-W+w4k3QAmULVNhBVT2Fei369kGZCh/TH25M7caJAXW+hLxwoQRuw0di3cX4l0S9fgH3Mvq7u+IFMoBDpEw/eIg== + dependencies: + bluebird "^3.5.4" + +temp-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" + integrity sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ== + temp-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" @@ -12851,6 +13105,14 @@ temp@^0.8.4: dependencies: rimraf "~2.6.2" +tempfile@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-2.0.0.tgz#6b0446856a9b1114d1856ffcbe509cccb0977265" + integrity sha512-ZOn6nJUgvgC09+doCEF3oB+r3ag7kUvlsXEGX069QRD60p+P3uP7XG9N2/at+EyIRGSN//ZY3LyEotA1YpmjuA== + dependencies: + temp-dir "^1.0.0" + uuid "^3.0.1" + tempy@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.6.0.tgz#65e2c35abc06f1124a97f387b08303442bde59f3" @@ -12881,9 +13143,9 @@ terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5: terser "^5.14.1" terser@^5.0.0, terser@^5.10.0, terser@^5.14.1, terser@^5.15.0: - version "5.16.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.1.tgz#5af3bc3d0f24241c7fb2024199d5c461a1075880" - integrity sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw== + version "5.16.4" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.4.tgz#51284b440b93242291a98f2a9903c024cfb70e6e" + integrity sha512-5yEGuZ3DZradbogeYQ1NaGz7rXVBDWujWlx1PT8efXO6Txn+eWbfKqB2bTDVmFXmePFkoLU6XI8UektMIEA0ug== dependencies: "@jridgewell/source-map" "^0.3.2" acorn "^8.5.0" @@ -13023,6 +13285,21 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +trace-event-lib@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/trace-event-lib/-/trace-event-lib-1.3.1.tgz#8113146caa30778f45d0ec479d899f9eda94d594" + integrity sha512-RO/TD5E9RNqU6MhOfi/njFWKYhrzOJCpRXlEQHgXwM+6boLSrQnOZ9xbHwOXzC+Luyixc7LNNSiTsqTVeF7I1g== + dependencies: + browser-process-hrtime "^1.0.0" + lodash "^4.17.21" + +truncate-utf8-bytes@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" + integrity sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ== + dependencies: + utf8-byte-length "^1.0.1" + tryer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" @@ -13044,9 +13321,9 @@ tslib@^1.8.1: integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" - integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== tsutils@^3.21.0: version "3.21.0" @@ -13114,6 +13391,15 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + typed-emitter@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/typed-emitter/-/typed-emitter-2.1.0.tgz#ca78e3d8ef1476f228f548d62e04e3d4d3fd77fb" @@ -13129,14 +13415,14 @@ typedarray-to-buffer@^3.1.5: is-typedarray "^1.0.0" typescript@^4.4.4: - version "4.9.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" - integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== ua-parser-js@^0.7.30: - version "0.7.32" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.32.tgz#cd8c639cdca949e30fa68c44b7813ef13e36d211" - integrity sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw== + version "0.7.33" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" + integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw== uglify-es@^3.1.9: version "3.3.9" @@ -13246,7 +13532,7 @@ upath@^1.2.0: resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== -update-browserslist-db@^1.0.9: +update-browserslist-db@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== @@ -13298,6 +13584,11 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +utf8-byte-length@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" + integrity sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA== + utf8@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" @@ -13328,6 +13619,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@^3.0.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -13343,9 +13639,9 @@ v8-to-istanbul@^8.1.0: source-map "^0.7.3" v8-to-istanbul@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" - integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w== + version "9.1.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265" + integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA== dependencies: "@jridgewell/trace-mapping" "^0.3.12" "@types/istanbul-lib-coverage" "^2.0.1" @@ -13643,11 +13939,33 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== +which-typed-array@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" + integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.10" + which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -13888,7 +14206,7 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -write-file-atomic@^4.0.1: +write-file-atomic@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== @@ -13903,15 +14221,15 @@ ws@^6.2.2: dependencies: async-limiter "~1.0.0" -ws@^7, ws@^7.4.6, ws@^7.5.1: +ws@^7, ws@^7.0.0, ws@^7.4.6, ws@^7.5.1: version "7.5.9" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== ws@^8.4.2: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" - integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + version "8.12.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.1.tgz#c51e583d79140b5e42e39be48c934131942d4a8f" + integrity sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew== xml-name-validator@^3.0.0: version "3.0.0" @@ -13943,6 +14261,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== + yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" @@ -13966,7 +14289,7 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2: +yargs-parser@^20.2.2, yargs-parser@^20.2.9: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== @@ -13976,6 +14299,16 @@ yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== +yargs-unparser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + yargs@^15.1.0: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" @@ -13993,7 +14326,7 @@ yargs@^15.1.0: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.2.0: +yargs@^16.0.3, yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== @@ -14007,9 +14340,9 @@ yargs@^16.2.0: yargs-parser "^20.2.2" yargs@^17.3.1, yargs@^17.5.1: - version "17.6.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" - integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== + version "17.7.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967" + integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw== dependencies: cliui "^8.0.1" escalade "^3.1.1" @@ -14025,6 +14358,6 @@ yocto-queue@^0.1.0: integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== zod@^3.14.2, zod@^3.20.2: - version "3.20.2" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.20.2.tgz#068606642c8f51b3333981f91c0a8ab37dfc2807" - integrity sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ== + version "3.20.6" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.20.6.tgz#2f2f08ff81291d47d99e86140fedb4e0db08361a" + integrity sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==